| // Copyright (c) 2013 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 Assertion support. |
| */ |
| |
| /** |
| * Verify |condition| is truthy and return |condition| if so. |
| * @template T |
| * @param {T} condition A condition to check for truthiness. Note that this |
| * may be used to test whether a value is defined or not, and we don't want |
| * to force a cast to Boolean. |
| * @param {string=} opt_message A message to show on failure. |
| * @return {T} A non-null |condition|. |
| */ |
| function assert(condition, opt_message) { |
| if (!condition) { |
| var message = 'Assertion failed'; |
| if (opt_message) |
| message = message + ': ' + opt_message; |
| var error = new Error(message); |
| var global = function() { return this; }(); |
| if (global.traceAssertionsForTesting) |
| console.warn(error.stack); |
| throw error; |
| } |
| return condition; |
| } |
| |
| /** |
| * Call this from places in the code that should never be reached. |
| * |
| * For example, handling all the values of enum with a switch() like this: |
| * |
| * function getValueFromEnum(enum) { |
| * switch (enum) { |
| * case ENUM_FIRST_OF_TWO: |
| * return first |
| * case ENUM_LAST_OF_TWO: |
| * return last; |
| * } |
| * assertNotReached(); |
| * return document; |
| * } |
| * |
| * This code should only be hit in the case of serious programmer error or |
| * unexpected input. |
| * |
| * @param {string=} opt_message A message to show when this is hit. |
| */ |
| function assertNotReached(opt_message) { |
| assert(false, opt_message || 'Unreachable code hit'); |
| } |
| |
| /** |
| * @param {*} value The value to check. |
| * @param {function(new: T, ...)} type A user-defined constructor. |
| * @param {string=} opt_message A message to show when this is hit. |
| * @return {T} |
| * @template T |
| */ |
| function assertInstanceof(value, type, opt_message) { |
| // We don't use assert immediately here so that we avoid constructing an error |
| // message if we don't have to. |
| if (!(value instanceof type)) { |
| assertNotReached(opt_message || 'Value ' + value + |
| ' is not a[n] ' + (type.name || typeof type)); |
| } |
| return value; |
| }; |
| // 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 PromiseResolver is a helper class that allows creating a |
| * Promise that will be fulfilled (resolved or rejected) some time later. |
| * |
| * Example: |
| * var resolver = new PromiseResolver(); |
| * resolver.promise.then(function(result) { |
| * console.log('resolved with', result); |
| * }); |
| * ... |
| * ... |
| * resolver.resolve({hello: 'world'}); |
| */ |
| |
| /** |
| * @constructor @struct |
| * @template T |
| */ |
| function PromiseResolver() { |
| /** @private {function(T=): void} */ |
| this.resolve_; |
| |
| /** @private {function(*=): void} */ |
| this.reject_; |
| |
| /** @private {!Promise<T>} */ |
| this.promise_ = new Promise(function(resolve, reject) { |
| this.resolve_ = resolve; |
| this.reject_ = reject; |
| }.bind(this)); |
| } |
| |
| PromiseResolver.prototype = { |
| /** @return {!Promise<T>} */ |
| get promise() { return this.promise_; }, |
| set promise(p) { assertNotReached(); }, |
| |
| /** @return {function(T=): void} */ |
| get resolve() { return this.resolve_; }, |
| set resolve(r) { assertNotReached(); }, |
| |
| /** @return {function(*=): void} */ |
| get reject() { return this.reject_; }, |
| set reject(s) { assertNotReached(); }, |
| }; |
| // 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. |
| |
| /** |
| * The global object. |
| * @type {!Object} |
| * @const |
| */ |
| var global = this; |
| |
| /** @typedef {{eventName: string, uid: number}} */ |
| var WebUIListener; |
| |
| /** Platform, package, object property, and Event support. **/ |
| var cr = cr || function() { |
| 'use strict'; |
| |
| /** |
| * Builds an object structure for the provided namespace path, |
| * ensuring that names that already exist are not overwritten. For |
| * example: |
| * "a.b.c" -> a = {};a.b={};a.b.c={}; |
| * @param {string} name Name of the object that this file defines. |
| * @param {*=} opt_object The object to expose at the end of the path. |
| * @param {Object=} opt_objectToExportTo The object to add the path to; |
| * default is {@code global}. |
| * @return {!Object} The last object exported (i.e. exportPath('cr.ui') |
| * returns a reference to the ui property of window.cr). |
| * @private |
| */ |
| function exportPath(name, opt_object, opt_objectToExportTo) { |
| var parts = name.split('.'); |
| var cur = opt_objectToExportTo || global; |
| |
| for (var part; parts.length && (part = parts.shift());) { |
| if (!parts.length && opt_object !== undefined) { |
| // last part and we have an object; use it |
| cur[part] = opt_object; |
| } else if (part in cur) { |
| cur = cur[part]; |
| } else { |
| cur = cur[part] = {}; |
| } |
| } |
| return cur; |
| } |
| |
| /** |
| * Fires a property change event on the target. |
| * @param {EventTarget} target The target to dispatch the event on. |
| * @param {string} propertyName The name of the property that changed. |
| * @param {*} newValue The new value for the property. |
| * @param {*} oldValue The old value for the property. |
| */ |
| function dispatchPropertyChange(target, propertyName, newValue, oldValue) { |
| var e = new Event(propertyName + 'Change'); |
| e.propertyName = propertyName; |
| e.newValue = newValue; |
| e.oldValue = oldValue; |
| target.dispatchEvent(e); |
| } |
| |
| /** |
| * Converts a camelCase javascript property name to a hyphenated-lower-case |
| * attribute name. |
| * @param {string} jsName The javascript camelCase property name. |
| * @return {string} The equivalent hyphenated-lower-case attribute name. |
| */ |
| function getAttributeName(jsName) { |
| return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); |
| } |
| |
| /** |
| * The kind of property to define in {@code defineProperty}. |
| * @enum {string} |
| * @const |
| */ |
| var PropertyKind = { |
| /** |
| * Plain old JS property where the backing data is stored as a "private" |
| * field on the object. |
| * Use for properties of any type. Type will not be checked. |
| */ |
| JS: 'js', |
| |
| /** |
| * The property backing data is stored as an attribute on an element. |
| * Use only for properties of type {string}. |
| */ |
| ATTR: 'attr', |
| |
| /** |
| * The property backing data is stored as an attribute on an element. If the |
| * element has the attribute then the value is true. |
| * Use only for properties of type {boolean}. |
| */ |
| BOOL_ATTR: 'boolAttr' |
| }; |
| |
| /** |
| * Helper function for defineProperty that returns the getter to use for the |
| * property. |
| * @param {string} name The name of the property. |
| * @param {PropertyKind} kind The kind of the property. |
| * @return {function():*} The getter for the property. |
| */ |
| function getGetter(name, kind) { |
| switch (kind) { |
| case PropertyKind.JS: |
| var privateName = name + '_'; |
| return function() { |
| return this[privateName]; |
| }; |
| case PropertyKind.ATTR: |
| var attributeName = getAttributeName(name); |
| return function() { |
| return this.getAttribute(attributeName); |
| }; |
| case PropertyKind.BOOL_ATTR: |
| var attributeName = getAttributeName(name); |
| return function() { |
| return this.hasAttribute(attributeName); |
| }; |
| } |
| |
| // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax |
| // the browser/unit tests to preprocess this file through grit. |
| throw 'not reached'; |
| } |
| |
| /** |
| * Helper function for defineProperty that returns the setter of the right |
| * kind. |
| * @param {string} name The name of the property we are defining the setter |
| * for. |
| * @param {PropertyKind} kind The kind of property we are getting the |
| * setter for. |
| * @param {function(*, *):void=} opt_setHook A function to run after the |
| * property is set, but before the propertyChange event is fired. |
| * @return {function(*):void} The function to use as a setter. |
| */ |
| function getSetter(name, kind, opt_setHook) { |
| switch (kind) { |
| case PropertyKind.JS: |
| var privateName = name + '_'; |
| return function(value) { |
| var oldValue = this[name]; |
| if (value !== oldValue) { |
| this[privateName] = value; |
| if (opt_setHook) |
| opt_setHook.call(this, value, oldValue); |
| dispatchPropertyChange(this, name, value, oldValue); |
| } |
| }; |
| |
| case PropertyKind.ATTR: |
| var attributeName = getAttributeName(name); |
| return function(value) { |
| var oldValue = this[name]; |
| if (value !== oldValue) { |
| if (value == undefined) |
| this.removeAttribute(attributeName); |
| else |
| this.setAttribute(attributeName, value); |
| if (opt_setHook) |
| opt_setHook.call(this, value, oldValue); |
| dispatchPropertyChange(this, name, value, oldValue); |
| } |
| }; |
| |
| case PropertyKind.BOOL_ATTR: |
| var attributeName = getAttributeName(name); |
| return function(value) { |
| var oldValue = this[name]; |
| if (value !== oldValue) { |
| if (value) |
| this.setAttribute(attributeName, name); |
| else |
| this.removeAttribute(attributeName); |
| if (opt_setHook) |
| opt_setHook.call(this, value, oldValue); |
| dispatchPropertyChange(this, name, value, oldValue); |
| } |
| }; |
| } |
| |
| // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax |
| // the browser/unit tests to preprocess this file through grit. |
| throw 'not reached'; |
| } |
| |
| /** |
| * Defines a property on an object. When the setter changes the value a |
| * property change event with the type {@code name + 'Change'} is fired. |
| * @param {!Object} obj The object to define the property for. |
| * @param {string} name The name of the property. |
| * @param {PropertyKind=} opt_kind What kind of underlying storage to use. |
| * @param {function(*, *):void=} opt_setHook A function to run after the |
| * property is set, but before the propertyChange event is fired. |
| */ |
| function defineProperty(obj, name, opt_kind, opt_setHook) { |
| if (typeof obj == 'function') |
| obj = obj.prototype; |
| |
| var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); |
| |
| if (!obj.__lookupGetter__(name)) |
| obj.__defineGetter__(name, getGetter(name, kind)); |
| |
| if (!obj.__lookupSetter__(name)) |
| obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); |
| } |
| |
| /** |
| * Counter for use with createUid |
| */ |
| var uidCounter = 1; |
| |
| /** |
| * @return {number} A new unique ID. |
| */ |
| function createUid() { |
| return uidCounter++; |
| } |
| |
| /** |
| * Returns a unique ID for the item. This mutates the item so it needs to be |
| * an object |
| * @param {!Object} item The item to get the unique ID for. |
| * @return {number} The unique ID for the item. |
| */ |
| function getUid(item) { |
| if (item.hasOwnProperty('uid')) |
| return item.uid; |
| return item.uid = createUid(); |
| } |
| |
| /** |
| * Dispatches a simple event on an event target. |
| * @param {!EventTarget} target The event target to dispatch the event on. |
| * @param {string} type The type of the event. |
| * @param {boolean=} opt_bubbles Whether the event bubbles or not. |
| * @param {boolean=} opt_cancelable Whether the default action of the event |
| * can be prevented. Default is true. |
| * @return {boolean} If any of the listeners called {@code preventDefault} |
| * during the dispatch this will return false. |
| */ |
| function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { |
| var e = new Event(type, { |
| bubbles: opt_bubbles, |
| cancelable: opt_cancelable === undefined || opt_cancelable |
| }); |
| return target.dispatchEvent(e); |
| } |
| |
| /** |
| * Calls |fun| and adds all the fields of the returned object to the object |
| * named by |name|. For example, cr.define('cr.ui', function() { |
| * function List() { |
| * ... |
| * } |
| * function ListItem() { |
| * ... |
| * } |
| * return { |
| * List: List, |
| * ListItem: ListItem, |
| * }; |
| * }); |
| * defines the functions cr.ui.List and cr.ui.ListItem. |
| * @param {string} name The name of the object that we are adding fields to. |
| * @param {!Function} fun The function that will return an object containing |
| * the names and values of the new fields. |
| */ |
| function define(name, fun) { |
| var obj = exportPath(name); |
| var exports = fun(); |
| for (var propertyName in exports) { |
| // Maybe we should check the prototype chain here? The current usage |
| // pattern is always using an object literal so we only care about own |
| // properties. |
| var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, |
| propertyName); |
| if (propertyDescriptor) |
| Object.defineProperty(obj, propertyName, propertyDescriptor); |
| } |
| } |
| |
| /** |
| * Adds a {@code getInstance} static method that always return the same |
| * instance object. |
| * @param {!Function} ctor The constructor for the class to add the static |
| * method to. |
| */ |
| function addSingletonGetter(ctor) { |
| ctor.getInstance = function() { |
| return ctor.instance_ || (ctor.instance_ = new ctor()); |
| }; |
| } |
| |
| /** |
| * Forwards public APIs to private implementations. |
| * @param {Function} ctor Constructor that have private implementations in its |
| * prototype. |
| * @param {Array<string>} methods List of public method names that have their |
| * underscored counterparts in constructor's prototype. |
| * @param {string=} opt_target Selector for target node. |
| */ |
| function makePublic(ctor, methods, opt_target) { |
| methods.forEach(function(method) { |
| ctor[method] = function() { |
| var target = opt_target ? document.getElementById(opt_target) : |
| ctor.getInstance(); |
| return target[method + '_'].apply(target, arguments); |
| }; |
| }); |
| } |
| |
| /** |
| * The mapping used by the sendWithPromise mechanism to tie the Promise |
| * returned to callers with the corresponding WebUI response. The mapping is |
| * from ID to the PromiseResolver helper; the ID is generated by |
| * sendWithPromise and is unique across all invocations of said method. |
| * @type {!Object<!PromiseResolver>} |
| */ |
| var chromeSendResolverMap = {}; |
| |
| /** |
| * The named method the WebUI handler calls directly in response to a |
| * chrome.send call that expects a response. The handler requires no knowledge |
| * of the specific name of this method, as the name is passed to the handler |
| * as the first argument in the arguments list of chrome.send. The handler |
| * must pass the ID, also sent via the chrome.send arguments list, as the |
| * first argument of the JS invocation; additionally, the handler may |
| * supply any number of other arguments that will be included in the response. |
| * @param {string} id The unique ID identifying the Promise this response is |
| * tied to. |
| * @param {boolean} isSuccess Whether the request was successful. |
| * @param {*} response The response as sent from C++. |
| */ |
| function webUIResponse(id, isSuccess, response) { |
| var resolver = chromeSendResolverMap[id]; |
| delete chromeSendResolverMap[id]; |
| |
| if (isSuccess) |
| resolver.resolve(response); |
| else |
| resolver.reject(response); |
| } |
| |
| /** |
| * A variation of chrome.send, suitable for messages that expect a single |
| * response from C++. |
| * @param {string} methodName The name of the WebUI handler API. |
| * @param {...*} var_args Varibale number of arguments to be forwarded to the |
| * C++ call. |
| * @return {!Promise} |
| */ |
| function sendWithPromise(methodName, var_args) { |
| var args = Array.prototype.slice.call(arguments, 1); |
| var promiseResolver = new PromiseResolver(); |
| var id = methodName + '_' + createUid(); |
| chromeSendResolverMap[id] = promiseResolver; |
| chrome.send(methodName, [id].concat(args)); |
| return promiseResolver.promise; |
| } |
| |
| /** |
| * A map of maps associating event names with listeners. The 2nd level map |
| * associates a listener ID with the callback function, such that individual |
| * listeners can be removed from an event without affecting other listeners of |
| * the same event. |
| * @type {!Object<!Object<!Function>>} |
| */ |
| var webUIListenerMap = {}; |
| |
| /** |
| * The named method the WebUI handler calls directly when an event occurs. |
| * The WebUI handler must supply the name of the event as the first argument |
| * of the JS invocation; additionally, the handler may supply any number of |
| * other arguments that will be forwarded to the listener callbacks. |
| * @param {string} event The name of the event that has occurred. |
| * @param {...*} var_args Additional arguments passed from C++. |
| */ |
| function webUIListenerCallback(event, var_args) { |
| var eventListenersMap = webUIListenerMap[event]; |
| if (!eventListenersMap) { |
| // C++ event sent for an event that has no listeners. |
| // TODO(dpapad): Should a warning be displayed here? |
| return; |
| } |
| |
| var args = Array.prototype.slice.call(arguments, 1); |
| for (var listenerId in eventListenersMap) { |
| eventListenersMap[listenerId].apply(null, args); |
| } |
| } |
| |
| /** |
| * Registers a listener for an event fired from WebUI handlers. Any number of |
| * listeners may register for a single event. |
| * @param {string} eventName The event to listen to. |
| * @param {!Function} callback The callback run when the event is fired. |
| * @return {!WebUIListener} An object to be used for removing a listener via |
| * cr.removeWebUIListener. Should be treated as read-only. |
| */ |
| function addWebUIListener(eventName, callback) { |
| webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; |
| var uid = createUid(); |
| webUIListenerMap[eventName][uid] = callback; |
| return {eventName: eventName, uid: uid}; |
| } |
| |
| /** |
| * Removes a listener. Does nothing if the specified listener is not found. |
| * @param {!WebUIListener} listener The listener to be removed (as returned by |
| * addWebUIListener). |
| * @return {boolean} Whether the given listener was found and actually |
| * removed. |
| */ |
| function removeWebUIListener(listener) { |
| var listenerExists = webUIListenerMap[listener.eventName] && |
| webUIListenerMap[listener.eventName][listener.uid]; |
| if (listenerExists) { |
| delete webUIListenerMap[listener.eventName][listener.uid]; |
| return true; |
| } |
| return false; |
| } |
| |
| return { |
| addSingletonGetter: addSingletonGetter, |
| createUid: createUid, |
| define: define, |
| defineProperty: defineProperty, |
| dispatchPropertyChange: dispatchPropertyChange, |
| dispatchSimpleEvent: dispatchSimpleEvent, |
| exportPath: exportPath, |
| getUid: getUid, |
| makePublic: makePublic, |
| PropertyKind: PropertyKind, |
| |
| // C++ <-> JS communication related methods. |
| addWebUIListener: addWebUIListener, |
| removeWebUIListener: removeWebUIListener, |
| sendWithPromise: sendWithPromise, |
| webUIListenerCallback: webUIListenerCallback, |
| webUIResponse: webUIResponse, |
| |
| get doc() { |
| return document; |
| }, |
| |
| /** Whether we are using a Mac or not. */ |
| get isMac() { |
| return /Mac/.test(navigator.platform); |
| }, |
| |
| /** Whether this is on the Windows platform or not. */ |
| get isWindows() { |
| return /Win/.test(navigator.platform); |
| }, |
| |
| /** Whether this is on chromeOS or not. */ |
| get isChromeOS() { |
| return /CrOS/.test(navigator.userAgent); |
| }, |
| |
| /** Whether this is on vanilla Linux (not chromeOS). */ |
| get isLinux() { |
| return /Linux/.test(navigator.userAgent); |
| }, |
| |
| /** Whether this is on Android. */ |
| get isAndroid() { |
| return /Android/.test(navigator.userAgent); |
| } |
| }; |
| }(); |
| // 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. |
| |
| 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); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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(); |
| }; |
| |
| return f; |
| } |
| |
| /** |
| * 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; |
| |
| // 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); |
| |
| // 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'; |
| } |
| } |
| } |
| |
| 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); |
| } |
| |
| 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. |
| |
| /** |
| * @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. |
| */ |
| |
| 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; |
| } |
| }); |
| |
| this.ident_ = ident; |
| this.mods_ = mods; |
| } |
| |
| 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.keyIdentifier == 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; |
| } |
| }; |
| |
| /** |
| * Creates a new command element. |
| * @constructor |
| * @extends {HTMLElement} |
| */ |
| var Command = cr.ui.define('command'); |
| |
| Command.prototype = { |
| __proto__: HTMLElement.prototype, |
| |
| /** |
| * Initializes the command. |
| */ |
| decorate: function() { |
| CommandManager.init(assert(this.ownerDocument)); |
| |
| if (this.hasAttribute('shortcut')) |
| this.shortcut = this.getAttribute('shortcut'); |
| }, |
| |
| /** |
| * 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; |
| |
| (opt_element || doc.activeElement).dispatchEvent(e); |
| } |
| }, |
| |
| /** |
| * 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 keyIdentifier (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" |
| * "U+0008-Meta" for Apple command backspace. |
| * "U+0041-Ctrl" for Control A |
| * "U+007F U+0008-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); |
| |
| /** |
| * Whether the command is hidden or not. |
| */ |
| cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); |
| |
| /** |
| * Whether the command is checked or not. |
| */ |
| cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); |
| |
| /** |
| * 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); |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * The command managers for different documents. |
| */ |
| var commandManagers = {}; |
| |
| /** |
| * 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); |
| } |
| }; |
| |
| CommandManager.prototype = { |
| |
| /** |
| * 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; |
| |
| // Ignore focus on a menu button or command item. |
| if (target.menu || target.command) |
| return; |
| |
| var commands = Array.prototype.slice.call( |
| target.ownerDocument.querySelectorAll('command')); |
| |
| commands.forEach(function(command) { |
| dispatchCanExecuteEvent(command, target); |
| }); |
| }, |
| |
| /** |
| * 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; |
| } |
| } |
| } |
| } |
| }; |
| |
| /** |
| * 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. |
| // 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; |
| } |
| |
| // 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; |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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 + '")'; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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); |
| |
| var newQuery = ''; |
| for (var q in query) { |
| newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; |
| } |
| |
| return location.origin + location.pathname + newQuery + location.hash; |
| } |
| |
| /** |
| * @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); |
| })); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| 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); |
| } |
| |
| /** |
| * 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(); |
| }; |
| |
| // Disable dragging. |
| document.ondragstart = function(e) { |
| if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) |
| e.preventDefault(); |
| }; |
| } |
| |
| /** |
| * 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(); |
| }); |
| } |
| |
| /** |
| * Check the directionality of the page. |
| * @return {boolean} True if Chrome is running an RTL UI. |
| */ |
| function isRTL() { |
| return document.documentElement.dir == 'rtl'; |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| // 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; |
| |
| var el = e.target; |
| if (el.nodeType == Node.ELEMENT_NODE && |
| el.webkitMatchesSelector('A, A *')) { |
| while (el.tagName != 'A') { |
| el = el.parentElement; |
| } |
| |
| 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(); |
| } |
| } |
| }); |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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; |
| |
| // 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); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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, '''); |
| } |
| |
| /** |
| * 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'; |
| } |
| |
| /** |
| * 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'); |
| }; |
| /** |
| * `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. |
| */ |
| notifyResize: function() { |
| if (!this.isAttached) { |
| return; |
| } |
| |
| this._interestedResizables.forEach(function(resizable) { |
| if (this.resizerShouldNotify(resizable)) { |
| this._notifyDescendant(resizable); |
| } |
| }, this); |
| |
| this._fireResize(); |
| }, |
| |
| /** |
| * Used to assign the closest resizable ancestor to this resizable |
| * if the ancestor detects a request for notifications. |
| */ |
| assignParentResizable: function(parentResizable) { |
| this._parentResizable = parentResizable; |
| }, |
| |
| /** |
| * 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'); |
| } |
| }, |
| |
| /** |
| * 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. |
| */ |
| 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(); |
| } |
| }, |
| |
| _fireResize: function() { |
| this.fire('iron-resize', null, { |
| node: this, |
| bubbles: false |
| }); |
| }, |
| |
| _onIronRequestResizeNotifications: function(event) { |
| var target = event.path ? event.path[0] : event.target; |
| |
| if (target === this) { |
| return; |
| } |
| |
| if (this._interestedResizables.indexOf(target) === -1) { |
| this._interestedResizables.push(target); |
| this.listen(target, 'iron-resize', '_onDescendantIronResize'); |
| } |
| |
| target.assignParentResizable(this); |
| this._notifyDescendant(target); |
| |
| event.stopPropagation(); |
| }, |
| |
| _parentResizableChanged: function(parentResizable) { |
| if (parentResizable) { |
| window.removeEventListener('resize', this._boundNotifyResize); |
| } |
| }, |
| |
| _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; |
| } |
| |
| this._notifyingDescendant = true; |
| descendant.notifyResize(); |
| this._notifyingDescendant = false; |
| } |
| }; |
| (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: '*' |
| }; |
| |
| /** |
| * 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' |
| }; |
| |
| /** |
| * 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*]/; |
| |
| /** |
| * Matches a keyIdentifier string. |
| */ |
| var IDENT_CHAR = /U\+/; |
| |
| /** |
| * Matches arrow keys in Gecko 27.0+ |
| */ |
| var ARROW_KEY = /^arrow/; |
| |
| /** |
| * Matches space keys everywhere (notably including IE10's exceptional name |
| * `spacebar`). |
| */ |
| var SPACE_KEY = /^space(bar)?/; |
| |
| /** |
| * Matches ESC key. |
| * |
| * Value from: http://w3c.github.io/uievents-key/#key-Escape |
| */ |
| var ESC_KEY = /^escape$/; |
| |
| /** |
| * 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; |
| } |
| } |
| 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; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * 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 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.key, noSpecialChars) || ''; |
| } |
| |
| 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) |
| ); |
| } |
| |
| 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); |
| }); |
| } |
| |
| /** |
| * `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 event to fire. |
| * |
| * Use the `key-event-target` attribute to set up event handlers on a specific |
| * node. |
| * The `keys-pressed` event will fire when one of the key combinations set with the |
| * `keys` property is pressed. |
| * |
| * @demo demo/index.html |
| * @polymerBehavior |
| */ |
| Polymer.IronA11yKeysBehavior = { |
| properties: { |
| /** |
| * The HTMLElement that will be firing relevant KeyboardEvents. |
| */ |
| keyEventTarget: { |
| type: Object, |
| value: function() { |
| return this; |
| } |
| }, |
| |
| /** |
| * If true, this property will cause the implementing element to |
| * automatically stop propagation on any handled KeyboardEvents. |
| */ |
| stopKeyboardEventPropagation: { |
| type: Boolean, |
| value: false |
| }, |
| |
| _boundKeyHandlers: { |
| type: Array, |
| value: function() { |
| return []; |
| } |
| }, |
| |
| // 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 {}; |
| } |
| } |
| }, |
| |
| observers: [ |
| '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' |
| ], |
| |
| keyBindings: {}, |
| |
| registered: function() { |
| this._prepKeyBindings(); |
| }, |
| |
| attached: function() { |
| this._listenKeyEventListeners(); |
| }, |
| |
| detached: function() { |
| this._unlistenKeyEventListeners(); |
| }, |
| |
| /** |
| * 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(); |
| }, |
| |
| /** |
| * When called, will remove all imperatively-added key bindings. |
| */ |
| removeOwnKeyBindings: function() { |
| this._imperativeKeyBindings = {}; |
| this._prepKeyBindings(); |
| this._resetKeyEventListeners(); |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| _collectKeyBindings: function() { |
| var keyBindings = this.behaviors.map(function(behavior) { |
| return behavior.keyBindings; |
| }); |
| |
| if (keyBindings.indexOf(this.keyBindings) === -1) { |
| keyBindings.push(this.keyBindings); |
| } |
| |
| return keyBindings; |
| }, |
| |
| _prepKeyBindings: function() { |
| this._keyBindings = {}; |
| |
| this._collectKeyBindings().forEach(function(keyBindings) { |
| for (var eventString in keyBindings) { |
| this._addKeyBinding(eventString, keyBindings[eventString]); |
| } |
| }, this); |
| |
| for (var eventString in this._imperativeKeyBindings) { |
| this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]); |
| } |
| |
| // 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; |
| }) |
| } |
| }, |
| |
| _addKeyBinding: function(eventString, handlerName) { |
| parseEventString(eventString).forEach(function(keyCombo) { |
| this._keyBindings[keyCombo.event] = |
| this._keyBindings[keyCombo.event] || []; |
| |
| this._keyBindings[keyCombo.event].push([ |
| keyCombo, |
| handlerName |
| ]); |
| }, this); |
| }, |
| |
| _resetKeyEventListeners: function() { |
| this._unlistenKeyEventListeners(); |
| |
| if (this.isAttached) { |
| this._listenKeyEventListeners(); |
| } |
| }, |
| |
| _listenKeyEventListeners: function() { |
| 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]); |
| |
| this.keyEventTarget.addEventListener(eventName, boundKeyHandler); |
| }, this); |
| }, |
| |
| _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]; |
| |
| keyEventTarget.removeEventListener(eventName, boundKeyHandler); |
| } |
| }, |
| |
| _onKeyBindingEvent: function(keyBindings, event) { |
| if (this.stopKeyboardEventPropagation) { |
| event.stopPropagation(); |
| } |
| |
| // if event has been already prevented, don't do anything |
| if (event.defaultPrevented) { |
| 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; |
| } |
| } |
| } |
| }, |
| |
| _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(); |
| } |
| } |
| }; |
| })(); |
| /** |
| * `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: { |
| |
| /** |
| * 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; |
| } |
| } |
| }, |
| |
| observers: [ |
| '_scrollTargetChanged(scrollTarget, isAttached)' |
| ], |
| |
| _scrollTargetChanged: function(scrollTarget, isAttached) { |
| var eventTarget; |
| |
| if (this._oldScrollTarget) { |
| eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScrollTarget; |
| eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
| this._oldScrollTarget = null; |
| } |
| |
| if (!isAttached) { |
| return; |
| } |
| // Support element id references |
| if (scrollTarget === 'document') { |
| |
| this.scrollTarget = this._doc; |
| |
| } else if (typeof scrollTarget === 'string') { |
| |
| this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : |
| Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); |
| |
| } 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); |
| } |
| }, |
| |
| /** |
| * Runs on every scroll event. Consumer of this behavior may override this method. |
| * |
| * @protected |
| */ |
| _scrollHandler: function scrollHandler() {}, |
| |
| /** |
| * The default scroll target. Consumers of this behavior may want to customize |
| * the default scroll target. |
| * |
| * @type {Element} |
| */ |
| get _defaultScrollTarget() { |
| return this._doc; |
| }, |
| |
| /** |
| * Shortcut for the document element |
| * |
| * @type {Element} |
| */ |
| get _doc() { |
| return this.ownerDocument.documentElement; |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * 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; |
| } |
| }, |
| |
| /** |
| * 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; |
| } |
| }, |
| |
| /** |
| * 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; |
| } |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * Returns true if the scroll target is a valid HTMLElement. |
| * |
| * @return {boolean} |
| */ |
| _isValidScrollTarget: function() { |
| return this.scrollTarget instanceof HTMLElement; |
| } |
| }; |
| (function() { |
| |
| 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; |
| |
| Polymer({ |
| |
| is: 'iron-list', |
| |
| properties: { |
| |
| /** |
| * 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 |
| }, |
| |
| /** |
| * 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, |
| 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' |
| }, |
| |
| /** |
| * 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. |
| */ |
| _ratio: 0.5, |
| |
| /** |
| * The padding-top value for the list. |
| */ |
| _scrollerPaddingTop: 0, |
| |
| /** |
| * This value is the same as `scrollTop`. |
| */ |
| _scrollPosition: 0, |
| |
| /** |
| * The sum of the heights of all the tiles in the DOM. |
| */ |
| _physicalSize: 0, |
| |
| /** |
| * The average `F` of the tiles observed till now. |
| */ |
| _physicalAverage: 0, |
| |
| /** |
| * The number of tiles which `offsetHeight` > 0 observed until now. |
| */ |
| _physicalAverageCount: 0, |
| |
| /** |
| * The Y position of the item rendered in the `_physicalStart` |
| * tile relative to the scrolling list. |
| */ |
| _physicalTop: 0, |
| |
| /** |
| * The number of items in the list. |
| */ |
| _virtualCount: 0, |
| |
| /** |
| * A map between an item key and its physical item index |
| */ |
| _physicalIndexForKey: null, |
| |
| /** |
| * The estimated scroll height based on `_physicalAverage` |
| */ |
| _estScrollHeight: 0, |
| |
| /** |
| * The scroll height of the dom node |
| */ |
| _scrollHeight: 0, |
| |
| /** |
| * The height of the list. This is referred as the viewport in the context of list. |
| */ |
| _viewportHeight: 0, |
| |
| /** |
| * The width of the list. This is referred as the viewport in the context of list. |
| */ |
| _viewportWidth: 0, |
| |
| /** |
| * An array of DOM nodes that are currently in the tree |
| * @type {?Array<!TemplatizerNode>} |
| */ |
| _physicalItems: null, |
| |
| /** |
| * An array of heights for each item in `_physicalItems` |
| * @type {?Array<number>} |
| */ |
| _physicalSizes: null, |
| |
| /** |
| * A cached value for the first visible index. |
| * See `firstVisibleIndex` |
| * @type {?number} |
| */ |
| _firstVisibleIndexVal: null, |
| |
| /** |
| * A cached value for the last visible index. |
| * See `lastVisibleIndex` |
| * @type {?number} |
| */ |
| _lastVisibleIndexVal: null, |
| |
| /** |
| * A Polymer collection for the items. |
| * @type {?Polymer.Collection} |
| */ |
| _collection: null, |
| |
| /** |
| * True if the current item list was rendered for the first time |
| * after attached. |
| */ |
| _itemsRendered: false, |
| |
| /** |
| * The page that is currently rendered. |
| */ |
| _lastPage: null, |
| |
| /** |
| * The max number of pages to render. One page is equivalent to the height of the list. |
| */ |
| _maxPages: 3, |
| |
| /** |
| * The currently focused physical item. |
| */ |
| _focusedItem: null, |
| |
| /** |
| * The index of the `_focusedItem`. |
| */ |
| _focusedIndex: -1, |
| |
| /** |
| * The the item that is focused if it is moved offscreen. |
| * @private {?TemplatizerNode} |
| */ |
| _offscreenFocusedItem: null, |
| |
| /** |
| * The item that backfills the `_offscreenFocusedItem` in the physical items |
| * list when that item is moved offscreen. |
| */ |
| _focusBackfillItem: null, |
| |
| /** |
| * The maximum items per row |
| */ |
| _itemsPerRow: 1, |
| |
| /** |
| * The width of each grid item |
| */ |
| _itemWidth: 0, |
| |
| /** |
| * The height of the row in grid layout. |
| */ |
| _rowHeight: 0, |
| |
| /** |
| * The bottom of the physical content. |
| */ |
| get _physicalBottom() { |
| return this._physicalTop + this._physicalSize; |
| }, |
| |
| /** |
| * The bottom of the scroll. |
| */ |
| get _scrollBottom() { |
| return this._scrollPosition + this._viewportHeight; |
| }, |
| |
| /** |
| * The n-th item rendered in the last physical item. |
| */ |
| get _virtualEnd() { |
| return this._virtualStart + this._physicalCount - 1; |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * The maximum scroll top value. |
| */ |
| get _maxScrollTop() { |
| return this._estScrollHeight - this._viewportHeight + this._scrollerPaddingTop; |
| }, |
| |
| /** |
| * The lowest n-th value for an item such that it can be rendered in `_physicalStart`. |
| */ |
| _minVirtualStart: 0, |
| |
| /** |
| * 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); |
| }, |
| |
| /** |
| * The n-th item rendered in the `_physicalStart` tile. |
| */ |
| _virtualStartVal: 0, |
| |
| set _virtualStart(val) { |
| this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val)); |
| }, |
| |
| get _virtualStart() { |
| return this._virtualStartVal || 0; |
| }, |
| |
| /** |
| * The k-th tile that is at the top of the scrolling list. |
| */ |
| _physicalStartVal: 0, |
| |
| 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; |
| }, |
| |
| 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; |
| }, |
| |
| get _physicalCount() { |
| return this._physicalCountVal; |
| }, |
| |
| /** |
| * The k-th tile that is at the bottom of the scrolling list. |
| */ |
| _physicalEnd: 0, |
| |
| /** |
| * 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; |
| }, |
| |
| get _optPhysicalCount() { |
| return this._estRowsInView * this._itemsPerRow * this._maxPages; |
| }, |
| |
| /** |
| * True if the current list is visible. |
| */ |
| get _isVisible() { |
| return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.scrollTarget.offsetHeight); |
| }, |
| |
| /** |
| * 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); |
| |
| 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; |
| } |
| return this._firstVisibleIndexVal; |
| }, |
| |
| /** |
| * 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 = lastIndex > this._virtualCount ? this._virtualCount : lastIndex; |
| } else { |
| var physicalOffset = this._physicalTop; |
| |
| this._iterateItems(function(pidx, vidx) { |
| physicalOffset += this._getPhysicalSizeIncrement(pidx); |
| |
| if(physicalOffset <= this._scrollBottom) { |
| if (this.grid) { |
| var lastIndex = vidx - vidx % this._itemsPerRow + this._itemsPerRow - 1; |
| this._lastVisibleIndexVal = lastIndex > this._virtualCount ? this._virtualCount : lastIndex; |
| } else { |
| this._lastVisibleIndexVal = vidx; |
| } |
| } |
| }); |
| } |
| } |
| return this._lastVisibleIndexVal; |
| }, |
| |
| get _defaultScrollTarget() { |
| return this; |
| }, |
| get _virtualRowCount() { |
| return Math.ceil(this._virtualCount / this._itemsPerRow); |
| }, |
| |
| get _estRowsInView() { |
| return Math.ceil(this._viewportHeight / this._rowHeight); |
| }, |
| |
| get _physicalRows() { |
| return Math.ceil(this._physicalCount / this._itemsPerRow); |
| }, |
| |
| ready: function() { |
| this.addEventListener('focus', this._didFocus.bind(this), true); |
| }, |
| |
| 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'); |
| }, |
| |
| detached: function() { |
| this._itemsRendered = false; |
| this.unlisten(this, 'iron-resize', '_resizeHandler'); |
| }, |
| |
| /** |
| * 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' : ''; |
| }, |
| |
| /** |
| * 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(); |
| } |
| }, |
| |
| /** |
| * 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); |
| } |
| // 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); |
| } |
| }, |
| |
| /** |
| * 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(); |
| }, |
| |
| /** |
| * Creates a pool of DOM elements and attaches them to the local dom. |
| */ |
| _createPool: function(size) { |
| var physicalItems = new Array(size); |
| |
| this._ensureTemplatized(); |
| |
| 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; |
| }, |
| |
| /** |
| * 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); |
| |
| 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)); |
| } |
| |
| this._lastPage = currentPage; |
| |
| return true; |
| }, |
| |
| /** |
| * 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; |
| |
| if (delta <= 0) { |
| return; |
| } |
| |
| [].push.apply(this._physicalItems, this._createPool(delta)); |
| [].push.apply(this._physicalSizes, new Array(delta)); |
| |
| this._physicalCount = prevPhysicalCount + delta; |
| |
| // 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(); |
| }, |
| |
| /** |
| * 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; |
| } |
| }, |
| |
| /** |
| * 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'); |
| } |
| } |
| }, |
| |
| /** |
| * Implements extension point from Templatizer mixin. |
| */ |
| _getStampedChildren: function() { |
| return this._physicalItems; |
| }, |
| |
| /** |
| * 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); |
| } |
| }, |
| |
| /** |
| * 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); |
| } |
| }, |
| |
| /** |
| * 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); |
| } |
| }, |
| |
| /** |
| * 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 inst; |
| var dot = path.indexOf('.'); |
| var key = path.substring(0, dot < 0 ? path.length : dot); |
| var idx = this._physicalIndexForKey[key]; |
| var el = this._physicalItems[idx]; |
| |
| |
| if (idx === this._focusedIndex && this._offscreenFocusedItem) { |
| el = this._offscreenFocusedItem; |
| } |
| if (!el) { |
| return; |
| } |
| |
| inst = el._templateInstance; |
| |
| if (inst.__key__ !== key) { |
| return; |
| } |
| if (dot >= 0) { |
| path = this.as + '.' + path.substring(dot+1); |
| inst.notifyPath(path, value, true); |
| } else { |
| inst[this.as] = value; |
| } |
| }, |
| |
| /** |
| * 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._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; |
| } |
| |
| this._itemsRendered = false; |
| this._debounceTemplate(this._render); |
| }, |
| |
| /** |
| * @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); |
| |
| this._virtualStart = this._virtualStart + delta; |
| |
| if (this._focusedIndex >= 0) { |
| this._focusedIndex = this._focusedIndex + delta; |
| } |
| } |
| }, this); |
| }, |
| |
| _removeItem: function(item) { |
| this.$.selector.deselect(item); |
| // remove the current focused item |
| if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) { |
| this._removeFocusedItem(); |
| } |
| }, |
| |
| /** |
| * 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; |
| |
| 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; |
| |
| 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; |
| } |
| } |
| } |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * 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]; |
| |
| 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); |
| }, |
| |
| /** |
| * 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 newPhysicalSize = 0; |
| var oldPhysicalSize = 0; |
| var prevAvgCount = this._physicalAverageCount; |
| var prevPhysicalAvg = this._physicalAverage; |
| |
| this._iterateItems(function(pidx, vidx) { |
| |
| oldPhysicalSize += this._physicalSizes[pidx] || 0; |
| this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
| newPhysicalSize += this._physicalSizes[pidx]; |
| this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
| |
| }, itemSet); |
| |
| 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; |
| } |
| |
| // update the average if we measured something |
| if (this._physicalAverageCount !== prevAvgCount) { |
| this._physicalAverage = Math.round( |
| ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / |
| this._physicalAverageCount); |
| } |
| }, |
| |
| _updateGridMetrics: function() { |
| this._viewportWidth = this._scrollTargetWidth; |
| // Set item width to the value of the _physicalItems offsetWidth |
| this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].offsetWidth : 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; |
| }, |
| |
| /** |
| * Updates the position of the physical items. |
| */ |
| _positionItems: function() { |
| this._adjustScrollPosition(); |
| |
| 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; |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * Adjusts the scroll position when it was overestimated. |
| */ |
| _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._resetScrollPosition(this._scrollTop - deltaHeight); |
| } |
| } |
| }, |
| |
| /** |
| * Sets the position of the scroll. |
| */ |
| _resetScrollPosition: function(pos) { |
| if (this.scrollTarget) { |
| this._scrollTop = pos; |
| this._scrollPosition = this._scrollTop; |
| } |
| }, |
| |
| /** |
| * 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); |
| } |
| |
| 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; |
| } |
| }, |
| /** |
| * Scroll to a specific item 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') { |
| return; |
| } |
| |
| Polymer.dom.flush(); |
| |
| 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(); |
| |
| // estimate new physical offset |
| var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) * this._physicalAverage; |
| this._physicalTop = estPhysicalTop; |
| |
| var currentTopItem = this._physicalStart; |
| var currentVirtualItem = this._virtualStart; |
| var targetOffsetTop = 0; |
| var hiddenContentSize = this._hiddenContentSize; |
| |
| // 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; |
| }, |
| |
| /** |
| * 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 (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]; |
| |
| if (pidx != null) { |
| return this._physicalItems[pidx]._templateInstance; |
| } |
| return null; |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * 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; |
| } |
| 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); |
| |
| if (model) { |
| model[this.selectedAs] = false; |
| } |
| this.$.selector.deselect(item); |
| this.updateSizeForItem(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); |
| } |
| }, |
| |
| /** |
| * 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; |
| } |
| } |
| |
| if (Array.isArray(this.selectedItems)) { |
| this.selectedItems.forEach(unselect, this); |
| } else if (this.selectedItem) { |
| unselect.call(this, this.selectedItem); |
| } |
| |
| /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); |
| }, |
| |
| /** |
| * 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'); |
| }, |
| |
| /** |
| * Select an item from an event object. |
| */ |
| _selectionHandler: function(e) { |
| if (this.selectionEnabled) { |
| var model = this.modelForElement(e.target); |
| if (model) { |
| this.toggleSelectionForItem(model[this.as]); |
| } |
| } |
| }, |
| |
| _multiSelectionChanged: function(multiSelection) { |
| this.clearSelection(); |
| this.$.selector.multi = multiSelection; |
| }, |
| |
| /** |
| * 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]; |
| |
| if (pidx != null) { |
| this._updateMetrics([pidx]); |
| this._positionItems(); |
| } |
| }, |
| |
| /** |
| * 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; |
| |
| 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]; |
| } |
| }, |
| |
| _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))]; |
| }, |
| |
| _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); |
| } |
| |
| var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
| var SECRET = ~(Math.random() * 100); |
| var model = physicalItem._templateInstance; |
| var focusable; |
| |
| // set a secret tab index |
| model.tabIndex = SECRET; |
| // check if focusable element is the physical item |
| if (physicalItem.tabIndex === SECRET) { |
| focusable = physicalItem; |
| } |
| // search for the element which tabindex is bound to the secret tab index |
| if (!focusable) { |
| focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET + '"]'); |
| } |
| // restore the tab index |
| model.tabIndex = 0; |
| // focus the focusable element |
| this._focusedIndex = idx; |
| focusable && focusable.focus(); |
| }, |
| |
| _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', |
| |
| properties: { |
| |
| /** |
| * The type of meta-data. All meta-data of the same type is stored |
| * together. |
| */ |
| type: { |
| type: String, |
| value: 'default', |
| observer: '_typeChanged' |
| }, |
| |
| /** |
| * The key used to store `value` under the `type` namespace. |
| */ |
| key: { |
| type: String, |
| observer: '_keyChanged' |
| }, |
| |
| /** |
| * The meta-data to store or retrieve. |
| */ |
| value: { |
| type: Object, |
| notify: true, |
| observer: '_valueChanged' |
| }, |
| |
| /** |
| * If true, `value` is set to the iron-meta instance itself. |
| */ |
| self: { |
| type: Boolean, |
| observer: '_selfChanged' |
| }, |
| |
| /** |
| * Array of all meta-data values for the given type. |
| */ |
| list: { |
| type: Array, |
| notify: true |
| } |
| |
| }, |
| |
| hostAttributes: { |
| hidden: true |
| }, |
| |
| /** |
| * Only runs if someone invokes the factory/constructor directly |
| * e.g. `new Polymer.IronMeta()` |
| * |
| * @param {{type: (string|undefined), key: (string|undefined), value}=} config |
| */ |
| factoryImpl: function(config) { |
| if (config) { |
| for (var n in config) { |
| switch(n) { |
| case 'type': |
| case 'key': |
| case 'value': |
| this[n] = config[n]; |
| break; |
| } |
| } |
| } |
| }, |
| |
| created: function() { |
| // TODO(sjmiles): good for debugging? |
| this._metaDatas = metaDatas; |
| this._metaArrays = metaArrays; |
| }, |
| |
| _keyChanged: function(key, old) { |
| this._resetRegistration(old); |
| }, |
| |
| _valueChanged: function(value) { |
| this._resetRegistration(this.key); |
| }, |
| |
| _selfChanged: function(self) { |
| if (self) { |
| this.value = this; |
| } |
| }, |
| |
| _typeChanged: function(type) { |
| this._unregisterKey(this.key); |
| if (!metaDatas[type]) { |
| metaDatas[type] = {}; |
| } |
| this._metaData = metaDatas[type]; |
| if (!metaArrays[type]) { |
| metaArrays[type] = []; |
| } |
| this.list = metaArrays[type]; |
| this._registerKeyValue(this.key, this.value); |
| }, |
| |
| /** |
| * Retrieves meta data value by key. |
| * |
| * @method byKey |
| * @param {string} key The key of the meta-data to be returned. |
| * @return {*} |
| */ |
| byKey: function(key) { |
| return this._metaData && this._metaData[key]; |
| }, |
| |
| _resetRegistration: function(oldKey) { |
| this._unregisterKey(oldKey); |
| this._registerKeyValue(this.key, this.value); |
| }, |
| |
| _unregisterKey: function(key) { |
| this._unregister(key, this._metaData, this.list); |
| }, |
| |
| _registerKeyValue: function(key, value) { |
| this._register(key, value, this._metaData, this.list); |
| }, |
| |
| _register: function(key, value, data, list) { |
| if (key && data && value !== undefined) { |
| data[key] = value; |
| list.push(value); |
| } |
| }, |
| |
| _unregister: function(key, data, list) { |
| if (key && data) { |
| if (key in data) { |
| var value = data[key]; |
| delete data[key]; |
| this.arrayDelete(list, value); |
| } |
| } |
| } |
| |
| }); |
| |
| Polymer.IronMeta.getIronMeta = function getIronMeta() { |
| if (singleton === null) { |
| singleton = new Polymer.IronMeta(); |
| } |
| return singleton; |
| }; |
| |
| /** |
| `iron-meta-query` can be used to access infomation stored in `iron-meta`. |
| |
| Examples: |
| |
| If I create an instance like this: |
| |
| <iron-meta key="info" value="foo/bar"></iron-meta> |
| |
| Note that value="foo/bar" is the metadata I've defined. I could define more |
| attributes or use child nodes to define additional metadata. |
| |
| Now I can access that element (and it's metadata) from any `iron-meta-query` instance: |
| |
| var value = new Polymer.IronMetaQuery({key: 'info'}).value; |
| |
| @group Polymer Iron Elements |
| @element iron-meta-query |
| */ |
| Polymer.IronMetaQuery = Polymer({ |
| |
| is: 'iron-meta-query', |
| |
| properties: { |
| |
| /** |
| * The type of meta-data. All meta-data of the same type is stored |
| * together. |
| */ |
| type: { |
| type: String, |
| value: 'default', |
| observer: '_typeChanged' |
| }, |
| |
| /** |
| * Specifies a key to use for retrieving `value` from the `type` |
| * namespace. |
| */ |
| key: { |
| type: String, |
| observer: '_keyChanged' |
| }, |
| |
| /** |
| * The meta-data to store or retrieve. |
| */ |
| value: { |
| type: Object, |
| notify: true, |
| readOnly: true |
| }, |
| |
| /** |
| * Array of all meta-data values for the given type. |
| */ |
| list: { |
| type: Array, |
| notify: true |
| } |
| |
| }, |
| |
| /** |
| * Actually a factory method, not a true constructor. Only runs if |
| * someone invokes it directly (via `new Polymer.IronMeta()`); |
| * |
| * @param {{type: (string|undefined), key: (string|undefined)}=} config |
| */ |
| factoryImpl: function(config) { |
| if (config) { |
| for (var n in config) { |
| switch(n) { |
| case 'type': |
| case 'key': |
| this[n] = config[n]; |
| break; |
| } |
| } |
| } |
| }, |
| |
| created: function() { |
| // TODO(sjmiles): good for debugging? |
| this._metaDatas = metaDatas; |
| this._metaArrays = metaArrays; |
| }, |
| |
| _keyChanged: function(key) { |
| this._setValue(this._metaData && this._metaData[key]); |
| }, |
| |
| _typeChanged: function(type) { |
| this._metaData = metaDatas[type]; |
| this.list = metaArrays[type]; |
| if (this.key) { |
| this._keyChanged(this.key); |
| } |
| }, |
| |
| /** |
| * Retrieves meta data value by key. |
| * @param {string} key The key of the meta-data to be returned. |
| * @return {*} |
| */ |
| byKey: function(key) { |
| return this._metaData && this._metaData[key]; |
| } |
| |
| }); |
| |
| })(); |
| Polymer({ |
| |
| is: 'iron-icon', |
| |
| properties: { |
| |
| /** |
| * The name of the icon to use. The name should be of the form: |
| * `iconset_name:icon_name`. |
| */ |
| icon: { |
| type: String, |
| observer: '_iconChanged' |
| }, |
| |
| /** |
| * The name of the theme to used, if one is specified by the |
| * iconset. |
| */ |
| theme: { |
| type: String, |
| observer: '_updateIcon' |
| }, |
| |
| /** |
| * If using iron-icon without an iconset, you can set the src to be |
| * the URL of an individual icon image file. Note that this will take |
| * precedence over a given icon attribute. |
| */ |
| src: { |
| type: String, |
| observer: '_srcChanged' |
| }, |
| |
| /** |
| * @type {!Polymer.IronMeta} |
| */ |
| _meta: { |
| value: Polymer.Base.create('iron-meta', {type: 'iconset'}), |
| observer: '_updateIcon' |
| } |
| |
| }, |
| |
| _DEFAULT_ICONSET: 'icons', |
| |
| _iconChanged: function(icon) { |
| var parts = (icon || '').split(':'); |
| this._iconName = parts.pop(); |
| this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; |
| this._updateIcon(); |
| }, |
| |
| _srcChanged: function(src) { |
| this._updateIcon(); |
| }, |
| |
| _usesIconset: function() { |
| return this.icon || !this.src; |
| }, |
| |
| /** @suppress {visibility} */ |
| _updateIcon: function() { |
| if (this._usesIconset()) { |
| if (this._img && this._img.parentNode) { |
| Polymer.dom(this.root).removeChild(this._img); |
| } |
| if (this._iconName === "") { |
| if (this._iconset) { |
| this._iconset.removeIcon(this); |
| } |
| } else if (this._iconsetName && this._meta) { |
| this._iconset = /** @type {?Polymer.Iconset} */ ( |
| this._meta.byKey(this._iconsetName)); |
| if (this._iconset) { |
| this._iconset.applyIcon(this, this._iconName, this.theme); |
| this.unlisten(window, 'iron-iconset-added', '_updateIcon'); |
| } else { |
| this.listen(window, 'iron-iconset-added', '_updateIcon'); |
| } |
| } |
| } else { |
| if (this._iconset) { |
| this._iconset.removeIcon(this); |
| } |
| if (!this._img) { |
| this._img = document.createElement('img'); |
| this._img.style.width = '100%'; |
| this._img.style.height = '100%'; |
| this._img.draggable = false; |
| } |
| this._img.src = this.src; |
| Polymer.dom(this.root).appendChild(this._img); |
| } |
| } |
| |
| }); |
| /** |
| * @demo demo/index.html |
| * @polymerBehavior |
| */ |
| Polymer.IronControlState = { |
| |
| properties: { |
| |
| /** |
| * If true, the element currently has focus. |
| */ |
| focused: { |
| type: Boolean, |
| value: false, |
| notify: true, |
| readOnly: true, |
| reflectToAttribute: true |
| }, |
| |
| /** |
| * If true, the user cannot interact with this element. |
| */ |
| disabled: { |
| type: Boolean, |
| value: false, |
| notify: true, |
| observer: '_disabledChanged', |
| reflectToAttribute: true |
| }, |
| |
| _oldTabIndex: { |
| type: Number |
| }, |
| |
| _boundFocusBlurHandler: { |
| type: Function, |
| value: function() { |
| return this._focusBlurHandler.bind(this); |
| } |
| } |
| |
| }, |
| |
| observers: [ |
| '_changedControlState(focused, disabled)' |
| ], |
| |
| ready: function() { |
| this.addEventListener('focus', this._boundFocusBlurHandler, true); |
| this.addEventListener('blur', this._boundFocusBlurHandler, true); |
| }, |
| |
| _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 |
| }); |
| } |
| } |
| }, |
| |
| _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; |
| } |
| }, |
| |
| _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' |
| } |
| }, |
| |
| 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; |
| } |
| }, |
| |
| _detectKeyboardFocus: function(focused) { |
| this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); |
| }, |
| |
| // 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); |
| }, |
| |
| /** |
| * @param {!KeyboardEvent} event . |
| */ |
| _spaceKeyDownHandler: function(event) { |
| var keyboardEvent = event.detail.keyboardEvent; |
| var target = Polymer.dom(keyboardEvent).localTarget; |
| |
| // 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; |
| |
| keyboardEvent.preventDefault(); |
| keyboardEvent.stopImmediatePropagation(); |
| this._setPressed(true); |
| }, |
| |
| /** |
| * @param {!KeyboardEvent} event . |
| */ |
| _spaceKeyUpHandler: function(event) { |
| var keyboardEvent = event.detail.keyboardEvent; |
| var target = Polymer.dom(keyboardEvent).localTarget; |
| |
| // 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.pressed) { |
| this._asyncClick(); |
| } |
| this._setPressed(false); |
| }, |
| |
| // 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); |
| }, |
| |
| // any of these changes are considered a change to button state |
| |
| _pressedChanged: function(pressed) { |
| this._changedButtonState(); |
| }, |
| |
| _ariaActiveAttributeChanged: function(value, oldValue) { |
| if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { |
| this.removeAttribute(oldValue); |
| } |
| }, |
| |
| _activeChanged: function(active, ariaActiveAttribute) { |
| if (this.toggles) { |
| this.setAttribute(this.ariaActiveAttribute, |
| active ? 'true' : 'false'); |
| } else { |
| this.removeAttribute(this.ariaActiveAttribute); |
| } |
| this._changedButtonState(); |
| }, |
| |
| _controlStateChanged: function() { |
| if (this.disabled) { |
| this._setPressed(false); |
| } else { |
| this._changedButtonState(); |
| } |
| }, |
| |
| // provide hook for follow-on behaviors to react to button-state |
| |
| _changedButtonState: function() { |
| if (this._buttonStateChanged) { |
| this._buttonStateChanged(); // abstract |
| } |
| } |
| |
| }; |
| |
| /** @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 |
| */ |
| function ElementMetrics(element) { |
| this.element = element; |
| this.width = this.boundingRect.width; |
| this.height = this.boundingRect.height; |
| |
| this.size = Math.max(this.width, this.height); |
| } |
| |
| 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); |
| |
| return Math.max(topLeft, topRight, bottomLeft, bottomRight); |
| } |
| }; |
| |
| /** |
| * @param {HTMLElement} element |
| * @constructor |
| */ |
| function Ripple(element) { |
| this.element = element; |
| this.color = window.getComputedStyle(element).color; |
| |
| 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); |
| |
| this.resetInteractionState(); |
| } |
| |
| Ripple.MAX_RADIUS = 300; |
| |
| Ripple.prototype = { |
| get recenters() { |
| return this.element.recenters; |
| }, |
| |
| get center() { |
| return this.element.center; |
| }, |
| |
| get mouseDownElapsed() { |
| var elapsed; |
| |
| if (!this.mouseDownStart) { |
| return 0; |
| } |
| |
| elapsed = Utility.now() - this.mouseDownStart; |
| |
| if (this.mouseUpStart) { |
| elapsed -= this.mouseUpElapsed; |
| } |
| |
| return elapsed; |
| }, |
| |
| get mouseUpElapsed() { |
| return this.mouseUpStart ? |
| Utility.now () - this.mouseUpStart : 0; |
| }, |
| |
| get mouseDownElapsedSeconds() { |
| return this.mouseDownElapsed / 1000; |
| }, |
| |
| get mouseUpElapsedSeconds() { |
| return this.mouseUpElapsed / 1000; |
| }, |
| |
| get mouseInteractionSeconds() { |
| return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; |
| }, |
| |
| get initialOpacity() { |
| return this.element.initialOpacity; |
| }, |
| |
| get opacityDecayVelocity() { |
| return this.element.opacityDecayVelocity; |
| }, |
| |
| 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); |
| }, |
| |
| get opacity() { |
| if (!this.mouseUpStart) { |
| return this.initialOpacity; |
| } |
| |
| return Math.max( |
| 0, |
| this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVelocity |
| ); |
| }, |
| |
| 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; |
| |
| return Math.max( |
| 0, |
| Math.min(outerOpacity, waveOpacity) |
| ); |
| }, |
| |
| get isOpacityFullyDecayed() { |
| return this.opacity < 0.01 && |
| this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); |
| }, |
| |
| get isRestingAtMaxRadius() { |
| return this.opacity >= this.initialOpacity && |
| this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); |
| }, |
| |
| get isAnimationComplete() { |
| return this.mouseUpStart ? |
| this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; |
| }, |
| |
| get translationFraction() { |
| return Math.min( |
| 1, |
| this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) |
| ); |
| }, |
| |
| get xNow() { |
| if (this.xEnd) { |
| return this.xStart + this.translationFraction * (this.xEnd - this.xStart); |
| } |
| |
| return this.xStart; |
| }, |
| |
| get yNow() { |
| if (this.yEnd) { |
| return this.yStart + this.translationFraction * (this.yEnd - this.yStart); |
| } |
| |
| return this.yStart; |
| }, |
| |
| get isMouseDown() { |
| return this.mouseDownStart && !this.mouseUpStart; |
| }, |
| |
| resetInteractionState: function() { |
| this.maxRadius = 0; |
| this.mouseDownStart = 0; |
| this.mouseUpStart = 0; |
| |
| this.xStart = 0; |
| this.yStart = 0; |
| this.xEnd = 0; |
| this.yEnd = 0; |
| this.slideDistance = 0; |
| |
| this.containerMetrics = new ElementMetrics(this.element); |
| }, |
| |
| 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); |
| |
| |
| // 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)'; |
| }, |
| |
| /** @param {Event=} event */ |
| downAction: function(event) { |
| var xCenter = this.containerMetrics.width / 2; |
| var yCenter = this.containerMetrics.height / 2; |
| |
| this.resetInteractionState(); |
| this.mouseDownStart = Utility.now(); |
| |
| 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 (this.recenters) { |
| this.xEnd = xCenter; |
| this.yEnd = yCenter; |
| this.slideDistance = Utility.distance( |
| this.xStart, this.yStart, this.xEnd, this.yEnd |
| ); |
| } |
| |
| this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( |
| this.xStart, |
| this.yStart |
| ); |
| |
| this.waveContainer.style.top = |
| (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'; |
| this.waveContainer.style.left = |
| (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; |
| |
| this.waveContainer.style.width = this.containerMetrics.size + 'px'; |
| this.waveContainer.style.height = this.containerMetrics.size + 'px'; |
| }, |
| |
| /** @param {Event=} event */ |
| upAction: function(event) { |
| if (!this.isMouseDown) { |
| return; |
| } |
| |
| this.mouseUpStart = Utility.now(); |
| }, |
| |
| remove: function() { |
| Polymer.dom(this.waveContainer.parentNode).removeChild( |
| this.waveContainer |
| ); |
| } |
| }; |
| |
| Polymer({ |
| is: 'paper-ripple', |
| |
| behaviors: [ |
| Polymer.IronA11yKeysBehavior |
| ], |
| |
| properties: { |
| /** |
| * The initial opacity set on the wave. |
| * |
| * @attribute initialOpacity |
| * @type number |
| * @default 0.25 |
| */ |
| initialOpacity: { |
| type: Number, |
| value: 0.25 |
| }, |
| |
| /** |
| * 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 |
| }, |
| |
| _boundAnimate: { |
| type: Function, |
| value: function() { |
| return this.animate.bind(this); |
| } |
| } |
| }, |
| |
| get target () { |
| var ownerRoot = Polymer.dom(this).getOwnerRoot(); |
| var target; |
| |
| if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE |
| target = ownerRoot.host; |
| } else { |
| target = this.parentNode; |
| } |
| |
| return target; |
| }, |
| |
| keyBindings: { |
| 'enter:keydown': '_onEnterKeydown', |
| 'space:keydown': '_onSpaceKeydown', |
| 'space:keyup': '_onSpaceKeyup' |
| }, |
| |
| 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. |
| this.keyEventTarget = this.target; |
| this.listen(this.target, 'up', 'uiUpAction'); |
| this.listen(this.target, 'down', 'uiDownAction'); |
| }, |
| |
| detached: function() { |
| this.unlisten(this.target, 'up', 'uiUpAction'); |
| this.unlisten(this.target, 'down', 'uiDownAction'); |
| }, |
| |
| get shouldKeepAnimating () { |
| for (var index = 0; index < this.ripples.length; ++index) { |
| if (!this.ripples[index].isAnimationComplete) { |
| return true; |
| } |
| } |
| |
| return false; |
| }, |
| |
| simulatedRipple: function() { |
| this.downAction(null); |
| |
| // Please see polymer/polymer#1305 |
| this.async(function() { |
| this.upAction(); |
| }, 1); |
| }, |
| |
| /** |
| * 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); |
| } |
| }, |
| |
| /** |
| * 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; |
| } |
| |
| var ripple = this.addRipple(); |
| |
| ripple.downAction(event); |
| |
| if (!this._animating) { |
| this.animate(); |
| } |
| }, |
| |
| /** |
| * 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); |
| } |
| }, |
| |
| /** |
| * Provokes a ripple up effect via a UI event, |
| * *not* respecting the `noink` property. |
| * @param {Event=} event |
| */ |
| upAction: function(event) { |
| if (this.holdDown) { |
| return; |
| } |
| |
| this.ripples.forEach(function(ripple) { |
| ripple.upAction(event); |
| }); |
| |
| this.animate(); |
| }, |
| |
| onAnimationComplete: function() { |
| this._animating = false; |
| this.$.background.style.backgroundColor = null; |
| this.fire('transitionend'); |
| }, |
| |
| addRipple: function() { |
| var ripple = new Ripple(this); |
| |
| Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); |
| this.$.background.style.backgroundColor = ripple.color; |
| this.ripples.push(ripple); |
| |
| this._setAnimating(true); |
| |
| return ripple; |
| }, |
| |
| removeRipple: function(ripple) { |
| var rippleIndex = this.ripples.indexOf(ripple); |
| |
| if (rippleIndex < 0) { |
| return; |
| } |
| |
| this.ripples.splice(rippleIndex, 1); |
| |
| ripple.remove(); |
| |
| if (!this.ripples.length) { |
| this._setAnimating(false); |
| } |
| }, |
| |
| animate: function() { |
| var index; |
| var ripple; |
| |
| this._animating = true; |
| |
| for (index = 0; index < this.ripples.length; ++index) { |
| ripple = this.ripples[index]; |
| |
| ripple.draw(); |
| |
| this.$.background.style.opacity = ripple.outerOpacity; |
| |
| if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { |
| this.removeRipple(ripple); |
| } |
| } |
| |
| if (!this.shouldKeepAnimating && this.ripples.length === 0) { |
| this.onAnimationComplete(); |
| } else { |
| window.requestAnimationFrame(this._boundAnimate); |
| } |
| }, |
| |
| _onEnterKeydown: function() { |
| this.uiDownAction(); |
| this.async(this.uiUpAction, 1); |
| }, |
| |
| _onSpaceKeydown: function() { |
| this.uiDownAction(); |
| }, |
| |
| _onSpaceKeyup: function() { |
| this.uiUpAction(); |
| }, |
| |
| // 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(); |
| } |
| } |
| }); |
| })(); |
| /** |
| * `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' |
| }, |
| |
| /** |
| * @type {Element|undefined} |
| */ |
| _rippleContainer: { |
| type: Object, |
| } |
| }, |
| |
| /** |
| * Ensures a `<paper-ripple>` element is available when the element is |
| * focused. |
| */ |
| _buttonStateChanged: function() { |
| if (this.focused) { |
| this.ensureRipple(); |
| } |
| }, |
| |
| /** |
| * 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); |
| } |
| }, |
| |
| /** |
| * 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); |
| } |
| } |
| } |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * Returns true if this element currently contains a ripple effect. |
| * @return {boolean} |
| */ |
| hasRipple: function() { |
| return Boolean(this._ripple); |
| }, |
| |
| /** |
| * 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')); |
| }, |
| |
| _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 |
| } |
| |
| }, |
| |
| observers: [ |
| '_calculateElevation(focused, disabled, active, pressed, receivedFocusFromKeyboard)', |
| '_computeKeyboardClass(receivedFocusFromKeyboard)' |
| ], |
| |
| hostAttributes: { |
| role: 'button', |
| tabindex: '0', |
| animated: true |
| }, |
| |
| _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); |
| }, |
| |
| _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
| this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
| }, |
| |
| /** |
| * 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(); |
| } |
| }, |
| |
| /** |
| * 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(); |
| } |
| } |
| |
| }; |
| |
| /** @polymerBehavior */ |
| Polymer.PaperButtonBehavior = [ |
| Polymer.IronButtonState, |
| Polymer.IronControlState, |
| Polymer.PaperRippleBehavior, |
| Polymer.PaperButtonBehaviorImpl |
| ]; |
| Polymer({ |
| is: 'paper-material', |
| |
| 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, |
| value: 1 |
| }, |
| |
| /** |
| * Set this to true to animate the shadow when setting a new |
| * `elevation` value. |
| * |
| * @attribute animated |
| * @type boolean |
| * @default false |
| */ |
| animated: { |
| type: Boolean, |
| reflectToAttribute: true, |
| value: false |
| } |
| } |
| }); |
| Polymer({ |
| is: 'paper-button', |
| |
| behaviors: [ |
| Polymer.PaperButtonBehavior |
| ], |
| |
| 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 animation finishes. |
| This is useful if you want to wait until |
| the ripple animation finishes to perform some action. |
| |
| @event transitionend |
| Event param: {{node: Object}} detail Contains the animated node. |
| */ |
| }); |
| 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'); |
| } |
| } |
| }); |
| /** |
| * `iron-range-behavior` provides the behavior for something with a minimum to maximum range. |
| * |
| * @demo demo/index.html |
| * @polymerBehavior |
| */ |
| Polymer.IronRangeBehavior = { |
| |
| properties: { |
| |
| /** |
| * The number that represents the current value. |
| */ |
| value: { |
| type: Number, |
| value: 0, |
| notify: true, |
| reflectToAttribute: true |
| }, |
| |
| /** |
| * The number that indicates the minimum value of the range. |
| */ |
| min: { |
| type: Number, |
| value: 0, |
| notify: true |
| }, |
| |
| /** |
| * The number that indicates the maximum value of the range. |
| */ |
| max: { |
| type: Number, |
| value: 100, |
| notify: true |
| }, |
| |
| /** |
| * Specifies the value granularity of the range's value. |
| */ |
| step: { |
| type: Number, |
| value: 1, |
| notify: true |
| }, |
| |
| /** |
| * Returns the ratio of the value. |
| */ |
| ratio: { |
| type: Number, |
| value: 0, |
| readOnly: true, |
| notify: true |
| }, |
| }, |
| |
| observers: [ |
| '_update(value, min, max, step)' |
| ], |
| |
| _calcRatio: function(value) { |
| return (this._clampValue(value) - this.min) / (this.max - this.min); |
| }, |
| |
| _clampValue: function(value) { |
| return Math.min(this.max, Math.max(this.min, this._calcStep(value))); |
| }, |
| |
| _calcStep: function(value) { |
| // polymer/issues/2493 |
| value = parseFloat(value); |
| |
| if (!this.step) { |
| return value; |
| } |
| |
| 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; |
| } |
| }, |
| |
| _validateValue: function() { |
| var v = this._clampValue(this.value); |
| this.value = this.oldValue = isNaN(v) ? this.oldValue : v; |
| return this.value !== v; |
| }, |
| |
| _update: function() { |
| this._validateValue(); |
| this._setRatio(this._calcRatio(this.value) * 100); |
| } |
| |
| }; |
| Polymer({ |
| is: 'paper-progress', |
| |
| behaviors: [ |
| Polymer.IronRangeBehavior |
| ], |
| |
| properties: { |
| /** |
| * The number that represents the current secondary progress. |
| */ |
| secondaryProgress: { |
| type: Number, |
| value: 0 |
| }, |
| |
| /** |
| * The secondary ratio |
| */ |
| secondaryRatio: { |
| type: Number, |
| value: 0, |
| readOnly: true |
| }, |
| |
| /** |
| * Use an indeterminate progress indicator. |
| */ |
| indeterminate: { |
| type: Boolean, |
| value: false, |
| observer: '_toggleIndeterminate' |
| }, |
| |
| /** |
| * True if the progress is disabled. |
| */ |
| disabled: { |
| type: Boolean, |
| value: false, |
| reflectToAttribute: true, |
| observer: '_disabledChanged' |
| } |
| }, |
| |
| observers: [ |
| '_progressChanged(secondaryProgress, value, min, max)' |
| ], |
| |
| hostAttributes: { |
| role: 'progressbar' |
| }, |
| |
| _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); |
| }, |
| |
| _transformProgress: function(progress, ratio) { |
| var transform = 'scaleX(' + (ratio / 100) + ')'; |
| progress.style.transform = progress.style.webkitTransform = transform; |
| }, |
| |
| _mainRatioChanged: function(ratio) { |
| this._transformProgress(this.$.primaryProgress, ratio); |
| }, |
| |
| _progressChanged: function(secondaryProgress, value, min, max) { |
| secondaryProgress = this._clampValue(secondaryProgress); |
| value = this._clampValue(value); |
| |
| var secondaryRatio = this._calcRatio(secondaryProgress) * 100; |
| var mainRatio = this._calcRatio(value) * 100; |
| |
| this._setSecondaryRatio(secondaryRatio); |
| this._transformProgress(this.$.secondaryProgress, secondaryRatio); |
| this._transformProgress(this.$.primaryProgress, mainRatio); |
| |
| this.secondaryProgress = secondaryProgress; |
| |
| this.setAttribute('aria-valuenow', value); |
| this.setAttribute('aria-valuemin', min); |
| this.setAttribute('aria-valuemax', max); |
| }, |
| |
| _disabledChanged: function(disabled) { |
| this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| }, |
| |
| _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; |
| }, |
| |
| /** |
| * 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; |
| } |
| }, |
| |
| /** |
| * |
| * 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}); |
| }); |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * 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 {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; |
| } |
| |
| }); |
| // 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() { |
| var Item = Polymer({ |
| is: 'downloads-item', |
| |
| properties: { |
| data: { |
| type: Object, |
| }, |
| |
| completelyOnDisk_: { |
| computed: 'computeCompletelyOnDisk_(' + |
| 'data.state, data.file_externally_removed)', |
| type: Boolean, |
| value: true, |
| }, |
| |
| controlledBy_: { |
| computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)', |
| type: String, |
| value: '', |
| }, |
| |
| isActive_: { |
| computed: 'computeIsActive_(' + |
| 'data.state, data.file_externally_removed)', |
| type: Boolean, |
| value: true, |
| }, |
| |
| isDangerous_: { |
| computed: 'computeIsDangerous_(data.state)', |
| type: Boolean, |
| value: false, |
| }, |
| |
| isInProgress_: { |
| computed: 'computeIsInProgress_(data.state)', |
| type: Boolean, |
| value: false, |
| }, |
| |
| showCancel_: { |
| computed: 'computeShowCancel_(data.state)', |
| type: Boolean, |
| value: false, |
| }, |
| |
| showProgress_: { |
| computed: 'computeShowProgress_(showCancel_, data.percent)', |
| type: Boolean, |
| value: false, |
| }, |
| |
| isMalware_: { |
| computed: 'computeIsMalware_(isDangerous_, data.danger_type)', |
| type: Boolean, |
| value: false, |
| }, |
| }, |
| |
| 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.file_path)', |
| ], |
| |
| ready: function() { |
| this.content = this.$.content; |
| }, |
| |
| /** @private */ |
| computeClass_: function() { |
| var classes = []; |
| |
| if (this.isActive_) |
| classes.push('is-active'); |
| |
| if (this.isDangerous_) |
| classes.push('dangerous'); |
| |
| if (this.showProgress_) |
| classes.push('show-progress'); |
| |
| return classes.join(' '); |
| }, |
| |
| /** @private */ |
| computeCompletelyOnDisk_: function() { |
| return this.data.state == downloads.States.COMPLETE && |
| !this.data.file_externally_removed; |
| }, |
| |
| /** @private */ |
| computeControlledBy_: function() { |
| if (!this.data.by_ext_id || !this.data.by_ext_name) |
| return ''; |
| |
| var url = 'chrome://extensions#' + this.data.by_ext_id; |
| var name = this.data.by_ext_name; |
| return loadTimeData.getStringF('controlledByUrl', url, name); |
| }, |
| |
| /** @private */ |
| computeDangerIcon_: function() { |
| if (!this.isDangerous_) |
| return ''; |
| |
| 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'; |
| } |
| }, |
| |
| /** @private */ |
| computeDate_: function() { |
| assert(typeof this.data.hideDate == 'boolean'); |
| if (this.data.hideDate) |
| return ''; |
| return assert(this.data.since_string || this.data.date_string); |
| }, |
| |
| /** @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; |
| |
| case downloads.States.IN_PROGRESS: |
| case downloads.States.PAUSED: // Fallthrough. |
| return data.progress_status_text; |
| } |
| |
| return ''; |
| }, |
| |
| /** @private */ |
| computeIsActive_: function() { |
| return this.data.state != downloads.States.CANCELLED && |
| this.data.state != downloads.States.INTERRUPTED && |
| !this.data.file_externally_removed; |
| }, |
| |
| /** @private */ |
| computeIsDangerous_: function() { |
| return this.data.state == downloads.States.DANGEROUS; |
| }, |
| |
| /** @private */ |
| computeIsInProgress_: function() { |
| return this.data.state == downloads.States.IN_PROGRESS; |
| }, |
| |
| /** @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); |
| }, |
| |
| /** @private */ |
| computeRemoveStyle_: function() { |
| var canDelete = loadTimeData.getBoolean('allowDeletingHistory'); |
| var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete; |
| return hideRemove ? 'visibility: hidden' : ''; |
| }, |
| |
| /** @private */ |
| computeShowCancel_: function() { |
| return this.data.state == downloads.States.IN_PROGRESS || |
| this.data.state == downloads.States.PAUSED; |
| }, |
| |
| /** @private */ |
| computeShowProgress_: function() { |
| return this.showCancel_ && this.data.percent >= -1; |
| }, |
| |
| /** @private */ |
| computeTag_: function() { |
| switch (this.data.state) { |
| case downloads.States.CANCELLED: |
| return loadTimeData.getString('statusCancelled'); |
| |
| case downloads.States.INTERRUPTED: |
| return this.data.last_reason_text; |
| |
| case downloads.States.COMPLETE: |
| return this.data.file_externally_removed ? |
| loadTimeData.getString('statusRemoved') : ''; |
| } |
| |
| return ''; |
| }, |
| |
| /** @private */ |
| isIndeterminate_: function() { |
| return this.data.percent == -1; |
| }, |
| |
| /** @private */ |
| observeControlledBy_: function() { |
| this.$['controlled-by'].innerHTML = this.controlledBy_; |
| }, |
| |
| /** @private */ |
| observeIsDangerous_: function() { |
| if (this.data && !this.isDangerous_) { |
| var filePath = encodeURIComponent(this.data.file_path); |
| var scaleFactor = '?scale=' + window.devicePixelRatio + 'x'; |
| this.$['file-icon'].src = 'chrome://fileicon/' + filePath + scaleFactor; |
| } |
| }, |
| |
| /** @private */ |
| onCancelTap_: function() { |
| downloads.ActionService.getInstance().cancel(this.data.id); |
| }, |
| |
| /** @private */ |
| onDiscardDangerousTap_: function() { |
| downloads.ActionService.getInstance().discardDangerous(this.data.id); |
| }, |
| |
| /** |
| * @private |
| * @param {Event} e |
| */ |
| onDragStart_: function(e) { |
| e.preventDefault(); |
| downloads.ActionService.getInstance().drag(this.data.id); |
| }, |
| |
| /** |
| * @param {Event} e |
| * @private |
| */ |
| onFileLinkTap_: function(e) { |
| e.preventDefault(); |
| downloads.ActionService.getInstance().openFile(this.data.id); |
| }, |
| |
| /** @private */ |
| onPauseTap_: function() { |
| downloads.ActionService.getInstance().pause(this.data.id); |
| }, |
| |
| /** @private */ |
| onRemoveTap_: function() { |
| downloads.ActionService.getInstance().remove(this.data.id); |
| }, |
| |
| /** @private */ |
| onResumeTap_: function() { |
| downloads.ActionService.getInstance().resume(this.data.id); |
| }, |
| |
| /** @private */ |
| onRetryTap_: function() { |
| downloads.ActionService.getInstance().download(this.data.url); |
| }, |
| |
| /** @private */ |
| onSaveDangerousTap_: function() { |
| downloads.ActionService.getInstance().saveDangerous(this.data.id); |
| }, |
| |
| /** @private */ |
| onShowTap_: function() { |
| downloads.ActionService.getInstance().show(this.data.id); |
| }, |
| }); |
| |
| return {Item: Item}; |
| }); |
| /** @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 |
| ] |
| }); |
| /** |
| * @param {!Function} selectCallback |
| * @constructor |
| */ |
| Polymer.IronSelection = function(selectCallback) { |
| this.selection = []; |
| this.selectCallback = selectCallback; |
| }; |
| |
| Polymer.IronSelection.prototype = { |
| |
| /** |
| * 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]; |
| }, |
| |
| /** |
| * 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); |
| }, |
| |
| /** |
| * 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; |
| }, |
| |
| /** |
| * 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. |
| */ |
| 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); |
| } |
| } |
| } |
| }, |
| |
| /** |
| * 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. |
| */ |
| select: function(item) { |
| if (this.multi) { |
| this.toggle(item); |
| } else if (this.get() !== item) { |
| this.setItemSelected(this.get(), false); |
| this.setItemSelected(item, true); |
| } |
| }, |
| |
| /** |
| * 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 |
| */ |
| |
| /** |
| * Fired when an item is selected |
| * |
| * @event iron-select |
| */ |
| |
| /** |
| * Fired when an item is deselected |
| * |
| * @event iron-deselect |
| */ |
| |
| /** |
| * 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 |
| */ |
| |
| properties: { |
| |
| /** |
| * 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 |
| }, |
| |
| /** |
| * Gets or sets the selected element. The default is to use the index of the item. |
| * @type {string|number} |
| */ |
| selected: { |
| type: String, |
| notify: true |
| }, |
| |
| /** |
| * Returns the currently selected item. |
| * |
| * @type {?Object} |
| */ |
| selectedItem: { |
| type: Object, |
| readOnly: true, |
| notify: 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' |
| }, |
| |
| /** |
| * This is a CSS selector string. If this is set, only items that match the CSS selector |
| * are selectable. |
| */ |
| selectable: String, |
| |
| /** |
| * The class to set on elements when selected. |
| */ |
| selectedClass: { |
| type: String, |
| value: 'iron-selected' |
| }, |
| |
| /** |
| * The attribute to set on elements when selected. |
| */ |
| selectedAttribute: { |
| type: String, |
| value: null |
| }, |
| |
| /** |
| * Default fallback if the selection based on selected with `attrForSelected` |
| * is not found. |
| */ |
| fallbackSelection: { |
| type: String, |
| value: null |
| }, |
| |
| /** |
| * 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 |
| }; |
| } |
| } |
| }, |
| |
| 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)' |
| ], |
| |
| /** |
| * Selects the given value. If the `multi` property is true, then the selected state of the |
| * `value` will be toggled; otherwise the `value` will be selected. |
| * |
| * @method select |
| * @param {string|number} value the value to select. |
| */ |
| select: function(value) { |
| if (this.multi) { |
| if (this.selectedValues) { |
| this._toggleSelected(value); |
| } else { |
| this.selectedValues = [value]; |
| } |
| } else { |
| this.selected = value; |
| } |
| }, |
| |
| multiChanged: function(multi) { |
| this._selection.multi = multi; |
| }, |
| |
| get _shouldUpdateSelection() { |
| return this.selected != null || |
| (this.selectedValues != null && this.selectedValues.length); |
| }, |
| |
| _updateAttrForSelected: function() { |
| if (!this.multi) { |
| Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); |
| } else if (this._shouldUpdateSelection) { |
| this.selectedValues = this.selectedItems.map(function(selectedItem) { |
| return this._indexToValue(this.indexOf(selectedItem)); |
| }, this).filter(function(unfilteredValue) { |
| return unfilteredValue != null; |
| }, this); |
| } |
| }, |
| |
| _updateSelected: function() { |
| if (this.multi) { |
| this._selectMulti(this.selectedValues); |
| } else { |
| this._selectSelected(this.selected); |
| } |
| }, |
| |
| _selectMulti: function(values) { |
| if (values) { |
| var selectedItems = this._valuesToItems(values); |
| // clear all but the current selected items |
| this._selection.clear(selectedItems); |
| // select only those not selected yet |
| for (var i = 0; i < selectedItems.length; i++) { |
| this._selection.setItemSelected(selectedItems[i], true); |
| } |
| // Check for items, since this array is populated only when attached |
| if (this.fallbackSelection && this.items.length && !this._selection.get().length) { |
| var fallback = this._valueToItem(this.fallbackSelection); |
| if (fallback) { |
| this.selectedValues = [this.fallbackSelection]; |
| } |
| } |
| } else { |
| this._selection.clear(); |
| } |
| }, |
| |
| _selectionChange: function() { |
| var s = this._selection.get(); |
| if (this.multi) { |
| this._setSelectedItems(s); |
| } else { |
| this._setSelectedItems([s]); |
| this._setSelectedItem(s); |
| } |
| }, |
| |
| _toggleSelected: function(value) { |
| var i = this.selectedValues.indexOf(value); |
| var unselected = i < 0; |
| if (unselected) { |
| this.push('selectedValues',value); |
| } else { |
| this.splice('selectedValues',i,1); |
| } |
| }, |
| |
| _valuesToItems: function(values) { |
| return (values == null) ? null : values.map(function(value) { |
| return this._valueToItem(value); |
| }, this); |
| } |
| }; |
| |
| /** @polymerBehavior */ |
| Polymer.IronMultiSelectableBehavior = [ |
| Polymer.IronSelectableBehavior, |
| Polymer.IronMultiSelectableBehaviorImpl |
| ]; |
| /** |
| * `Polymer.IronMenuBehavior` implements accessible menu behavior. |
| * |
| * @demo demo/index.html |
| * @polymerBehavior Polymer.IronMenuBehavior |
| */ |
| Polymer.IronMenuBehaviorImpl = { |
| |
| properties: { |
| |
| /** |
| * Returns the currently focused item. |
| * @type {?Object} |
| */ |
| focusedItem: { |
| observer: '_focusedItemChanged', |
| readOnly: true, |
| type: Object |
| }, |
| |
| /** |
| * The attribute to use on menu items to look up the item title. Typing the first |
| * letter of an item when the menu is open focuses that item. If unset, `textContent` |
| * will be used. |
| */ |
| attrForItemTitle: { |
| type: String |
| } |
| }, |
| |
| hostAttributes: { |
| 'role': 'menu', |
| 'tabindex': '0' |
| }, |
| |
| observers: [ |
| '_updateMultiselectable(multi)' |
| ], |
| |
| listeners: { |
| 'focus': '_onFocus', |
| 'keydown': '_onKeydown', |
| 'iron-items-changed': '_onIronItemsChanged' |
| }, |
| |
| keyBindings: { |
| 'up': '_onUpKey', |
| 'down': '_onDownKey', |
| 'esc': '_onEscKey', |
| 'shift+tab:keydown': '_onShiftTabDown' |
| }, |
| |
| attached: function() { |
| this._resetTabindices(); |
| }, |
| |
| /** |
| * Selects the given value. If the `multi` property is true, then the selected state of the |
| * `value` will be toggled; otherwise the `value` will be selected. |
| * |
| * @param {string|number} value the value to select. |
| */ |
| select: function(value) { |
| // Cancel automatically focusing a default item if the menu received focus |
| // through a user action selecting a particular item. |
| if (this._defaultFocusAsync) { |
| this.cancelAsync(this._defaultFocusAsync); |
| this._defaultFocusAsync = null; |
| } |
| var item = this._valueToItem(value); |
| if (item && item.hasAttribute('disabled')) return; |
| this._setFocusedItem(item); |
| Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
| }, |
| |
| /** |
| * Resets all tabindex attributes to the appropriate value based on the |
| * current selection state. The appropriate value is `0` (focusable) for |
| * the default selected item, and `-1` (not keyboard focusable) for all |
| * other items. |
| */ |
| _resetTabindices: function() { |
| var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[0]) : this.selectedItem; |
| |
| this.items.forEach(function(item) { |
| item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
| }, this); |
| }, |
| |
| /** |
| * Sets appropriate ARIA based on whether or not the menu is meant to be |
| * multi-selectable. |
| * |
| * @param {boolean} multi True if the menu should be multi-selectable. |
| */ |
| _updateMultiselectable: function(multi) { |
| if (multi) { |
| this.setAttribute('aria-multiselectable', 'true'); |
| } else { |
| this.removeAttribute('aria-multiselectable'); |
| } |
| }, |
| |
| /** |
| * Given a KeyboardEvent, this method will focus the appropriate item in the |
| * menu (if there is a relevant item, and it is possible to focus it). |
| * |
| * @param {KeyboardEvent} event A KeyboardEvent. |
| */ |
| _focusWithKeyboardEvent: function(event) { |
| for (var i = 0, item; item = this.items[i]; i++) { |
| var attr = this.attrForItemTitle || 'textContent'; |
| var title = item[attr] || item.getAttribute(attr); |
| |
| if (!item.hasAttribute('disabled') && title && |
| title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.keyCode).toLowerCase()) { |
| this._setFocusedItem(item); |
| break; |
| } |
| } |
| }, |
| |
| /** |
| * Focuses the previous item (relative to the currently focused item) in the |
| * menu, disabled items will be skipped. |
| */ |
| _focusPrevious: function() { |
| var length = this.items.length; |
| var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| for (var i = 1; i < length; i++) { |
| var item = this.items[(curFocusIndex - i + length) % length]; |
| if (!item.hasAttribute('disabled')) { |
| this._setFocusedItem(item); |
| return; |
| } |
| } |
| }, |
| |
| /** |
| * Focuses the next item (relative to the currently focused item) in the |
| * menu, disabled items will be skipped. |
| */ |
| _focusNext: function() { |
| var length = this.items.length; |
| var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
| for (var i = 1; i < length; i++) { |
| var item = this.items[(curFocusIndex + i) % length]; |
| if (!item.hasAttribute('disabled')) { |
| this._setFocusedItem(item); |
| return; |
| } |
| } |
| }, |
| |
| /** |
| * Mutates items in the menu based on provided selection details, so that |
| * all items correctly reflect selection state. |
| * |
| * @param {Element} item An item in the menu. |
| * @param {boolean} isSelected True if the item should be shown in a |
| * selected state, otherwise false. |
| */ |
| _applySelection: function(item, isSelected) { |
| if (isSelected) { |
| item.setAttribute('aria-selected', 'true'); |
| } else { |
| item.removeAttribute('aria-selected'); |
| } |
| Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
| }, |
| |
| /** |
| * Discretely updates tabindex values among menu items as the focused item |
| * changes. |
| * |
| * @param {Element} focusedItem The element that is currently focused. |
| * @param {?Element} old The last element that was considered focused, if |
| * applicable. |
| */ |
| _focusedItemChanged: function(focusedItem, old) { |
| old && old.setAttribute('tabindex', '-1'); |
| if (focusedItem) { |
| focusedItem.setAttribute('tabindex', '0'); |
| focusedItem.focus(); |
| } |
| }, |
| |
| /** |
| * A handler that responds to mutation changes related to the list of items |
| * in the menu. |
| * |
| * @param {CustomEvent} event An event containing mutation records as its |
| * detail. |
| */ |
| _onIronItemsChanged: function(event) { |
| if (event.detail.addedNodes.length) { |
| this._resetTabindices(); |
| } |
| }, |
| |
| /** |
| * Handler that is called when a shift+tab keypress is detected by the menu. |
| * |
| * @param {CustomEvent} event A key combination event. |
| */ |
| _onShiftTabDown: function(event) { |
| var oldTabIndex = this.getAttribute('tabindex'); |
| |
| Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
| |
| this._setFocusedItem(null); |
| |
| this.setAttribute('tabindex', '-1'); |
| |
| this.async(function() { |
| this.setAttribute('tabindex', oldTabIndex); |
| Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| // NOTE(cdata): polymer/polymer#1305 |
| }, 1); |
| }, |
| |
| /** |
| * Handler that is called when the menu receives focus. |
| * |
| * @param {FocusEvent} event A focus event. |
| */ |
| _onFocus: function(event) { |
| if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
| // do not focus the menu itself |
| return; |
| } |
| |
| // Do not focus the selected tab if the deepest target is part of the |
| // menu element's local DOM and is focusable. |
| var rootTarget = /** @type {?HTMLElement} */( |
| Polymer.dom(event).rootTarget); |
| if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !this.isLightDescendant(rootTarget)) { |
| return; |
| } |
| |
| // clear the cached focus item |
| this._defaultFocusAsync = this.async(function() { |
| // focus the selected item when the menu receives focus, or the first item |
| // if no item is selected |
| var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[0]) : this.selectedItem; |
| |
| this._setFocusedItem(null); |
| |
| if (selectedItem) { |
| this._setFocusedItem(selectedItem); |
| } else if (this.items[0]) { |
| // We find the first none-disabled item (if one exists) |
| this._focusNext(); |
| } |
| }); |
| }, |
| |
| /** |
| * Handler that is called when the up key is pressed. |
| * |
| * @param {CustomEvent} event A key combination event. |
| */ |
| _onUpKey: function(event) { |
| // up and down arrows moves the focus |
| this._focusPrevious(); |
| event.detail.keyboardEvent.preventDefault(); |
| }, |
| |
| /** |
| * Handler that is called when the down key is pressed. |
| * |
| * @param {CustomEvent} event A key combination event. |
| */ |
| _onDownKey: function(event) { |
| this._focusNext(); |
| event.detail.keyboardEvent.preventDefault(); |
| }, |
| |
| /** |
| * Handler that is called when the esc key is pressed. |
| * |
| * @param {CustomEvent} event A key combination event. |
| */ |
| _onEscKey: function(event) { |
| // esc blurs the control |
| this.focusedItem.blur(); |
| }, |
| |
| /** |
| * Handler that is called when a keydown event is detected. |
| * |
| * @param {KeyboardEvent} event A keyboard event. |
| */ |
| _onKeydown: function(event) { |
| if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
| // all other keys focus the menu item starting with that character |
| this._focusWithKeyboardEvent(event); |
| } |
| event.stopPropagation(); |
| }, |
| |
| // override _activateHandler |
| _activateHandler: function(event) { |
| Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
| event.stopPropagation(); |
| } |
| }; |
| |
| Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
| |
| /** @polymerBehavior Polymer.IronMenuBehavior */ |
| Polymer.IronMenuBehavior = [ |
| Polymer.IronMultiSelectableBehavior, |
| 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. |
| |
| 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._discoverInfo(); |
| this.position(); |
| this.constrain(); |
| this.center(); |
| }, |
| |
| /** |
| * Memoize information needed to position and size the target element. |
| */ |
| _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.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; |
| } |
| 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; |
| } |
| 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', null); |
| // Need to have useCapture=true, Polymer.Gestures doesn't offer that. |
| document.addEventListener('tap', this._onCaptureClick.bind(this), true); |
| 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); |
| |
| // Get focused node. |
| var element = this.deepActiveElement; |
| overlay.restoreFocusNode = this._overlayParent(element) ? null : element; |
| this.trackBackdrop(); |
| }, |
| |
| /** |
| * @param {!Element} overlay |
| */ |
| removeOverlay: function(overlay) { |
| var i = this._overlays.indexOf(overlay); |
| if (i === -1) { |
| return; |
| } |
| this._overlays.splice(i, 1); |
| |
| var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null; |
| overlay.restoreFocusNode = null; |
| // Focus back only if still contained in document.body |
| if (node && Polymer.dom(document.body).deepContains(node)) { |
| node.focus(); |
| } |
| 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()); |
| // We have to be careful to focus the next overlay _after_ any current |
| // transitions are complete (due to the state being toggled prior to the |
| // transition). Otherwise, we risk infinite recursion when a transitioning |
| // (closed) overlay becomes the current overlay. |
| // |
| // NOTE: We make the assumption that any overlay that completes a transition |
| // will call into focusOverlay to kick the process back off. Currently: |
| // transitionend -> _applyFocus -> focusOverlay. |
| if (current && !current.transitioning) { |
| 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 overlay containing the provided node. If the node is an overlay, |
| * it returns the node. |
| * @param {Element=} node |
| * @return {Element|undefined} |
| * @private |
| */ |
| _overlayParent: function(node) { |
| while (node && node !== document.body) { |
| // Check if it is an overlay. |
| if (node._manager === this) { |
| return node; |
| } |
| // Use logical parentNode, or native ShadowRoot host. |
| node = Polymer.dom(node).parentNode || node.host; |
| } |
| }, |
| |
| /** |
| * Returns the deepest overlay in the path. |
| * @param {Array<Element>=} path |
| * @return {Element|undefined} |
| * @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} |
| * @private |
| */ |
| _shouldBeBehindOverlay: function(overlay1, overlay2) { |
| var o1 = /** @type {?} */ (overlay1); |
| var o2 = /** @type {?} */ (overlay2); |
| return !o1.alwaysOnTop && o2.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. |
| |
| ### Closing and canceling |
| |
| A dialog 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. |
| |
| ### 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. |
| */ |
| 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 for requestAnimationFrame when opened changes. |
| this.__openChangedAsync = null; |
| // Used for requestAnimationFrame when iron-resize is fired. |
| this.__onIronResizeAsync = null; |
| this._ensureSetup(); |
| }, |
| |
| attached: function() { |
| // Call _openedChanged here so that position can be computed correctly. |
| if (this.opened) { |
| this._openedChanged(); |
| } |
| this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); |
| }, |
| |
| detached: function() { |
| Polymer.dom(this).unobserveNodes(this._observer); |
| this._observer = null; |
| this.opened = false; |
| }, |
| |
| /** |
| * 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'; |
| }, |
| |
| _openedChanged: function() { |
| if (this.opened) { |
| this.removeAttribute('aria-hidden'); |
| } else { |
| this.setAttribute('aria-hidden', 'true'); |
| } |
| |
| // wait to call after ready only if we're initially open |
| if (!this._overlaySetup) { |
| return; |
| } |
| |
| if (this.__openChangedAsync) { |
| window.cancelAnimationFrame(this.__openChangedAsync); |
| } |
| |
| // Synchronously remove the overlay. |
| // The adding is done asynchronously to go out of the scope of the event |
| // which might have generated the opening. |
| if (!this.opened) { |
| this._manager.removeOverlay(this); |
| } |
| |
| // Defer any animation-related code on attached |
| // (_openedChanged gets called again on attached). |
| if (!this.isAttached) { |
| return; |
| } |
| |
| this.__isAnimating = true; |
| |
| // requestAnimationFrame for non-blocking rendering |
| this.__openChangedAsync = window.requestAnimationFrame(function() { |
| this.__openChangedAsync = null; |
| if (this.opened) { |
| this._manager.addOverlay(this); |
| this._prepareRenderOpened(); |
| this._renderOpened(); |
| } else { |
| this._renderClosed(); |
| } |
| }.bind(this)); |
| }, |
| |
| _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() { |
| |
| // 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 blur it. Later, _applyFocus will set the focus if necessary. |
| if (this.noAutoFocus && document.activeElement === this._focusNode) { |
| this._focusNode.blur(); |
| } |
| }, |
| |
| /** |
| * 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() { |
| // Focus the child node with [autofocus] |
| this._applyFocus(); |
| |
| 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 and remove the backdrop. |
| this.style.display = 'none'; |
| // Reset z-index only at the end of the animation. |
| this.style.zIndex = ''; |
| |
| this._applyFocus(); |
| |
| 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; |
| this._manager.focusOverlay(); |
| } |
| }, |
| |
| /** |
| * 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.__onIronResizeAsync) { |
| window.cancelAnimationFrame(this.__onIronResizeAsync); |
| this.__onIronResizeAsync = null; |
| } |
| if (this.opened && !this.__isAnimating) { |
| this.__onIronResizeAsync = window.requestAnimationFrame(function() { |
| this.__onIronResizeAsync = null; |
| this.refit(); |
| }.bind(this)); |
| } |
| }, |
| |
| /** |
| * 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(); |
| } |
| } |
| }; |
| |
| /** @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 = { |
| |
| properties: { |
| |
| /** @type {?Object} */ |
| _player: { |
| type: Object |
| } |
| |
| }, |
| |
| _configureAnimationEffects: function(allConfigs) { |
| var allAnimations = []; |
| if (allConfigs.length > 0) { |
| for (var config, index = 0; config = allConfigs[index]; index++) { |
| var animation = document.createElement(config.name); |
| // is this element actually a neon animation? |
| if (animation.isNeonAnimation) { |
| var effect = animation.configure(config); |
| if (effect) { |
| allAnimations.push({ |
| animation: animation, |
| config: config, |
| effect: effect |
| }); |
| } |
| } else { |
| console.warn(this.is + ':', config.name, 'not found!'); |
| } |
| } |
| } |
| return allAnimations; |
| }, |
| |
| _runAnimationEffects: function(allEffects) { |
| return document.timeline.play(new GroupEffect(allEffects)); |
| }, |
| |
| _completeAnimations: function(allAnimations) { |
| for (var animation, index = 0; animation = allAnimations[index]; index++) { |
| animation.animation.complete(animation.config); |
| } |
| }, |
| |
| /** |
| * Plays an animation with an optional `type`. |
| * @param {string=} type |
| * @param {!Object=} cookie |
| */ |
| playAnimation: function(type, cookie) { |
| var allConfigs = this.getAnimationConfig(type); |
| if (!allConfigs) { |
| return; |
| } |
| try { |
| var allAnimations = this._configureAnimationEffects(allConfigs); |
| var allEffects = allAnimations.map(function(animation) { |
| return animation.effect; |
| }); |
| |
| if (allEffects.length > 0) { |
| this._player = this._runAnimationEffects(allEffects); |
| this._player.onfinish = function() { |
| this._completeAnimations(allAnimations); |
| |
| if (this._player) { |
| this._player.cancel(); |
| this._player = null; |
| } |
| |
| this.fire('neon-animation-finish', cookie, {bubbles: false}); |
| }.bind(this); |
| return; |
| } |
| } catch (e) { |
| console.warn('Couldnt play', '(', type, allConfigs, ').', e); |
| } |
| this.fire('neon-animation-finish', cookie, {bubbles: false}); |
| }, |
| |
| /** |
| * Cancels the currently running animation. |
| */ |
| cancelAnimation: function() { |
| if (this._player) { |
| this._player.cancel(); |
| } |
| } |
| }; |
| |
| /** @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'; |
| |
| /** |
| * 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, |
| |
| _originalBodyStyles: {}, |
| |
| _isScrollingKeypress: function(event) { |
| return Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys( |
| event, 'pageup pagedown home end up left down right'); |
| }, |
| |
| _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) { |
| var scrolledElement = |
| /** @type {HTMLElement} */(Polymer.dom(event).rootTarget); |
| if (Polymer |
| .IronDropdownScrollManager |
| .elementIsScrollLocked(scrolledElement)) { |
| if (event.type === 'keydown' && |
| !Polymer.IronDropdownScrollManager._isScrollingKeypress(event)) { |
| return; |
| } |
| |
| event.preventDefault(); |
| } |
| }, |
| |
| _lockScrollInteractions: function() { |
| // Memoize body inline styles: |
| this._originalBodyStyles.overflow = document.body.style.overflow; |
| this._originalBodyStyles.overflowX = document.body.style.overflowX; |
| this._originalBodyStyles.overflowY = document.body.style.overflowY; |
| |
| // Disable overflow scrolling on body: |
| // TODO(cdata): It is technically not sufficient to hide overflow on |
| // body alone. A better solution might be to traverse all ancestors of |
| // the current scroll locking element and hide overflow on them. This |
| // becomes expensive, though, as it would have to be redone every time |
| // a new scroll locking element is added. |
| document.body.style.overflow = 'hidden'; |
| document.body.style.overflowX = 'hidden'; |
| document.body.style.overflowY = 'hidden'; |
| |
| // Modern `wheel` event for mouse wheel scrolling: |
| document.addEventListener('wheel', this._scrollInteractionHandler, true); |
| // Older, non-standard `mousewheel` event for some FF: |
| document.addEventListener('mousewheel', this._scrollInteractionHandler, true); |
| // IE: |
| document.addEventListener('DOMMouseScroll', this._scrollInteractionHandler, true); |
| // Mobile devices can scroll on touch move: |
| document.addEventListener('touchmove', this._scrollInteractionHandler, true); |
| // Capture keydown to prevent scrolling keys (pageup, pagedown etc.) |
| document.addEventListener('keydown', this._scrollInteractionHandler, true); |
| }, |
| |
| _unlockScrollInteractions: function() { |
| document.body.style.overflow = this._originalBodyStyles.overflow; |
| document.body.style.overflowX = this._originalBodyStyles.overflowX; |
| document.body.style.overflowY = this._originalBodyStyles.overflowY; |
| |
| document.removeEventListener('wheel', this._scrollInteractionHandler, true); |
| document.removeEventListener('mousewheel', this._scrollInteractionHandler, true); |
| document.removeEventListener('DOMMouseScroll', this._scrollInteractionHandler, true); |
| document.removeEventListener('touchmove', this._scrollInteractionHandler, true); |
| document.removeEventListener('keydown', this._scrollInteractionHandler, true); |
| } |
| }; |
| })(); |
| (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 |
| } |
| }, |
| |
| 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; |
| }, |
| |
| 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(); |
| if (this.opened && !this.allowOutsideScroll) { |
| Polymer.IronDropdownScrollManager.pushScrollLock(this); |
| } else { |
| 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(); |
| } |
| }, |
| |
| /** |
| * 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({ |
| |
| is: 'fade-in-animation', |
| |
| behaviors: [ |
| Polymer.NeonAnimationBehavior |
| ], |
| |
| configure: function(config) { |
| var node = config.node; |
| this._effect = new KeyframeEffect(node, [ |
| {'opacity': '0'}, |
| {'opacity': '1'} |
| ], this.timingFromConfig(config)); |
| return this._effect; |
| } |
| |
| }); |
| Polymer({ |
| |
| is: 'fade-out-animation', |
| |
| behaviors: [ |
| Polymer.NeonAnimationBehavior |
| ], |
| |
| configure: function(config) { |
| var node = config.node; |
| this._effect = new KeyframeEffect(node, [ |
| {'opacity': '1'}, |
| {'opacity': '0'} |
| ], this.timingFromConfig(config)); |
| return this._effect; |
| } |
| |
| }); |
| Polymer({ |
| is: 'paper-menu-grow-height-animation', |
| |
| behaviors: [ |
| Polymer.NeonAnimationBehavior |
| ], |
| |
| configure: function(config) { |
| var node = config.node; |
| var rect = node.getBoundingClientRect(); |
| var height = rect.height; |
| |
| this._effect = new KeyframeEffect(node, [{ |
| height: (height / 2) + 'px' |
| }, { |
| height: height + 'px' |
| }], this.timingFromConfig(config)); |
| |
| return this._effect; |
| } |
| }); |
| |
| 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', |
| |
| 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 + 'px' |
| }, { |
| width: width - (width / 20) + 'px' |
| }], this.timingFromConfig(config)); |
| |
| return this._effect; |
| } |
| }); |
| |
| Polymer({ |
| is: 'paper-menu-shrink-height-animation', |
| |
| behaviors: [ |
| Polymer.NeonAnimationBehavior |
| ], |
| |
| configure: function(config) { |
| var node = config.node; |
| var rect = node.getBoundingClientRect(); |
| var height = rect.height; |
| var top = rect.top; |
| |
| this.setPrefixedProperty(node, 'transformOrigin', '0 0'); |
| |
| this._effect = new KeyframeEffect(node, [{ |
| height: height + 'px', |
| transform: 'translateY(0)' |
| }, { |
| height: height / 2 + 'px', |
| transform: 'translateY(-20px)' |
| }], this.timingFromConfig(config)); |
| |
| return this._effect; |
| } |
| }); |
| (function() { |
| 'use strict'; |
| |
| var PaperMenuButton = Polymer({ |
| is: 'paper-menu-button', |
| |
| /** |
| * Fired when the dropdown opens. |
| * |
| * @event paper-dropdown-open |
| */ |
| |
| /** |
| * Fired when the dropdown closes. |
| * |
| * @event paper-dropdown-close |
| */ |
| |
| behaviors: [ |
| Polymer.IronA11yKeysBehavior, |
| Polymer.IronControlState |
| ], |
| |
| properties: { |
| /** |
| * True if the content is currently displayed. |
| */ |
| opened: { |
| type: Boolean, |
| value: false, |
| notify: true, |
| observer: '_openedChanged' |
| }, |
| |
| /** |
| * The orientation against which to align the menu dropdown |
| * horizontally relative to the dropdown trigger. |
| */ |
| horizontalAlign: { |
| type: String, |
| value: 'left', |
| reflectToAttribute: true |
| }, |
| |
| /** |
| * The orientation against which to align the menu dropdown |
| * vertically relative to the dropdown trigger. |
| */ |
| verticalAlign: { |
| type: String, |
| value: 'top', |
| reflectToAttribute: true |
| }, |
| |
| /** |
| * 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 |
| }, |
| |
| /** |
| * 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 |
| }, |
| |
| /** |
| * Set to true to disable animations when opening and closing the |
| * dropdown. |
| */ |
| noAnimations: { |
| type: Boolean, |
| value: false |
| }, |
| |
| /** |
| * Set to true to disable automatically closing the dropdown after |
| * a selection has been made. |
| */ |
| ignoreSelect: { |
| type: Boolean, |
| value: false |
| }, |
| |
| /** |
| * 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: PaperMenuButton.ANIMATION_CUBIC_BEZIER |
| } |
| }, { |
| name: 'paper-menu-grow-height-animation', |
| timing: { |
| delay: 100, |
| duration: 275, |
| easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER |
| } |
| }]; |
| } |
| }, |
| |
| /** |
| * 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: PaperMenuButton.ANIMATION_CUBIC_BEZIER |
| } |
| }, { |
| name: 'paper-menu-shrink-height-animation', |
| timing: { |
| duration: 200, |
| easing: 'ease-in' |
| } |
| }]; |
| } |
| }, |
| |
| /** |
| * This is the element intended to be bound as the focus target |
| * for the `iron-dropdown` contained by `paper-menu-button`. |
| */ |
| _dropdownContent: { |
| type: Object |
| } |
| }, |
| |
| hostAttributes: { |
| role: 'group', |
| 'aria-haspopup': 'true' |
| }, |
| |
| listeners: { |
| 'iron-select': '_onIronSelect' |
| }, |
| |
| /** |
| * The content element that is contained by the menu button, if any. |
| */ |
| get contentElement() { |
| return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| }, |
| |
| /** |
| * Toggles the drowpdown content between opened and closed. |
| */ |
| toggle: function() { |
| if (this.opened) { |
| this.close(); |
| } else { |
| this.open(); |
| } |
| }, |
| |
| /** |
| * Make the dropdown content appear as an overlay positioned relative |
| * to the dropdown trigger. |
| */ |
| open: function() { |
| if (this.disabled) { |
| return; |
| } |
| |
| this.$.dropdown.open(); |
| }, |
| |
| /** |
| * Hide the dropdown content. |
| */ |
| close: function() { |
| this.$.dropdown.close(); |
| }, |
| |
| /** |
| * 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(); |
| } |
| }, |
| |
| /** |
| * 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'); |
| } |
| }, |
| |
| /** |
| * 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(); |
| } |
| }, |
| |
| __onIronOverlayCanceled: function(event) { |
| var uiEvent = event.detail; |
| var target = Polymer.dom(uiEvent).rootTarget; |
| var trigger = this.$.trigger; |
| var path = Polymer.dom(uiEvent).path; |
| |
| if (path.indexOf(trigger) > -1) { |
| event.preventDefault(); |
| } |
| } |
| }); |
| |
| PaperMenuButton.ANIMATION_CUBIC_BEZIER = 'cubic-bezier(.3,.95,.5,1)'; |
| PaperMenuButton.MAX_ANIMATION_TIME_MS = 400; |
| |
| Polymer.PaperMenuButton = PaperMenuButton; |
| })(); |
| /** |
| * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has keyboard focus. |
| * |
| * @polymerBehavior Polymer.PaperInkyFocusBehavior |
| */ |
| Polymer.PaperInkyFocusBehaviorImpl = { |
| |
| observers: [ |
| '_focusedChanged(receivedFocusFromKeyboard)' |
| ], |
| |
| _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' |
| }, |
| |
| 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" |
| } |
| }, |
| |
| _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); |
| } |
| } |
| }); |
| // 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. |
| |
| /** @interface */ |
| var SearchFieldDelegate = function() {}; |
| |
| SearchFieldDelegate.prototype = { |
| /** |
| * @param {string} value |
| */ |
| onSearchTermSearch: assertNotReached, |
| }; |
| |
| /** |
| * 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 |
| }, |
| |
| hasSearchText: Boolean, |
| }, |
| |
| /** |
| * @return {string} The value of the search field. |
| */ |
| getValue: function() { |
| return this.$.searchInput.value; |
| }, |
| |
| /** |
| * Sets the value of the search field, if it exists. |
| * @param {string} value |
| */ |
| setValue: function(value) { |
| // Use bindValue when setting the input value so that changes propagate |
| // correctly. |
| this.$.searchInput.bindValue = value; |
| this.hasSearchText = value != ''; |
| }, |
| |
| /** @param {SearchFieldDelegate} delegate */ |
| setDelegate: function(delegate) { |
| this.delegate_ = delegate; |
| }, |
| |
| showAndFocus: function() { |
| this.showingSearch = true; |
| this.focus_(); |
| }, |
| |
| /** @private */ |
| focus_: function() { |
| this.$.searchInput.focus(); |
| }, |
| |
| /** @private */ |
| onSearchTermSearch_: function() { |
| this.hasSearchText = this.getValue() != ''; |
| if (this.delegate_) |
| this.delegate_.onSearchTermSearch(this.getValue()); |
| }, |
| |
| /** @private */ |
| onSearchTermKeydown_: function(e) { |
| if (e.key == 'Escape') |
| this.showingSearch = false; |
| }, |
| |
| /** @private */ |
| showingSearchChanged_: function() { |
| if (this.showingSearch) { |
| this.focus_(); |
| return; |
| } |
| |
| this.setValue(''); |
| this.$.searchInput.blur(); |
| this.onSearchTermSearch_(); |
| }, |
| |
| /** @private */ |
| toggleShowingSearch_: function() { |
| this.showingSearch = !this.showingSearch; |
| }, |
| }; |
| (function() { |
| 'use strict'; |
| |
| Polymer.IronA11yAnnouncer = Polymer({ |
| is: 'iron-a11y-announcer', |
| |
| properties: { |
| |
| /** |
| * 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' |
| }, |
| |
| _text: { |
| type: String, |
| value: '' |
| } |
| }, |
| |
| 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)' |
| } |
| }, |
| |
| 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'); |
| } |
| }, |
| |
| /** |
| * @return {boolean} True if the validator `validator` exists. |
| */ |
| hasValidator: function() { |
| return this._validator != null; |
| }, |
| |
| /** |
| * 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. |
| */ |
| 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. |
| * |
| * @param {Object} value The value to be validated. |
| * @return {boolean} True if `value` is valid. |
| */ |
| |
| _getValidity: function(value) { |
| if (this.hasValidator()) { |
| return this._validator.validate(value); |
| } |
| return true; |
| }, |
| |
| __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; |
| } |
| }); |
| // 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. |
| |
| var SearchField = Polymer({ |
| is: 'cr-search-field', |
| behaviors: [CrSearchFieldBehavior] |
| }); |
| // 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() { |
| 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'; |
| |
| /** @private {!SearchFieldDelegate} */ |
| this.searchFieldDelegate_ = new ToolbarSearchFieldDelegate(this); |
| this.$['search-input'].setDelegate(this.searchFieldDelegate_); |
| }, |
| |
| properties: { |
| downloadsShowing: { |
| reflectToAttribute: true, |
| type: Boolean, |
| value: false, |
| observer: 'downloadsShowingChanged_', |
| }, |
| |
| overflowAlign_: { |
| type: String, |
| value: 'right', |
| }, |
| }, |
| |
| /** @return {boolean} Whether removal can be undone. */ |
| canUndo: function() { |
| return this.$['search-input'] != this.shadowRoot.activeElement; |
| }, |
| |
| /** @return {boolean} Whether "Clear all" should be allowed. */ |
| canClearAll: function() { |
| return !this.$['search-input'].getValue() && this.downloadsShowing; |
| }, |
| |
| onFindCommand: function() { |
| this.$['search-input'].showAndFocus(); |
| }, |
| |
| /** @private */ |
| onClearAllTap_: function() { |
| assert(this.canClearAll()); |
| downloads.ActionService.getInstance().clearAll(); |
| }, |
| |
| /** @private */ |
| downloadsShowingChanged_: function() { |
| this.updateClearAll_(); |
| }, |
| |
| /** @param {string} searchTerm */ |
| onSearchTermSearch: function(searchTerm) { |
| downloads.ActionService.getInstance().search(searchTerm); |
| this.updateClearAll_(); |
| }, |
| |
| /** @private */ |
| onOpenDownloadsFolderTap_: function() { |
| downloads.ActionService.getInstance().openDownloadsFolder(); |
| }, |
| |
| /** @private */ |
| updateClearAll_: function() { |
| this.$$('#actions .clear-all').hidden = !this.canClearAll(); |
| this.$$('paper-menu .clear-all').hidden = !this.canClearAll(); |
| }, |
| }); |
| |
| /** |
| * @constructor |
| * @implements {SearchFieldDelegate} |
| */ |
| // TODO(devlin): This is a bit excessive, and it would be better to just have |
| // Toolbar implement SearchFieldDelegate. But for now, we don't know how to |
| // make that happen with closure compiler. |
| function ToolbarSearchFieldDelegate(toolbar) { |
| this.toolbar_ = toolbar; |
| } |
| |
| ToolbarSearchFieldDelegate.prototype = { |
| /** @override */ |
| onSearchTermSearch: function(searchTerm) { |
| this.toolbar_.onSearchTermSearch(searchTerm); |
| } |
| }; |
| |
| return {Toolbar: Toolbar}; |
| }); |
| |
| // TODO(dbeam): https://github.com/PolymerElements/iron-dropdown/pull/16/files |
| /** @suppress {checkTypes} */ |
| (function() { |
| Polymer.IronDropdownScrollManager.pushScrollLock = function() {}; |
| })(); |
| // 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() { |
| var Manager = Polymer({ |
| is: 'downloads-manager', |
| |
| properties: { |
| hasDownloads_: { |
| observer: 'hasDownloadsChanged_', |
| type: Boolean, |
| }, |
| |
| items_: { |
| type: Array, |
| value: function() { return []; }, |
| }, |
| }, |
| |
| hostAttributes: { |
| loading: true, |
| }, |
| |
| listeners: { |
| 'downloads-list.scroll': 'onListScroll_', |
| }, |
| |
| observers: [ |
| 'itemsChanged_(items_.*)', |
| ], |
| |
| /** @private */ |
| clearAll_: function() { |
| this.set('items_', []); |
| }, |
| |
| /** @private */ |
| hasDownloadsChanged_: function() { |
| if (loadTimeData.getBoolean('allowDeletingHistory')) |
| this.$.toolbar.downloadsShowing = this.hasDownloads_; |
| |
| 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 {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'); |
| }, |
| |
| /** @private */ |
| itemsChanged_: function() { |
| this.hasDownloads_ = this.items_.length > 0; |
| }, |
| |
| /** |
| * @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; |
| } |
| }, |
| |
| /** |
| * @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(); |
| }, |
| |
| /** @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(); |
| } |
| }, |
| |
| /** @private */ |
| onLoad_: function() { |
| cr.ui.decorate('command', cr.ui.Command); |
| document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
| document.addEventListener('command', this.onCommand_.bind(this)); |
| |
| downloads.ActionService.getInstance().loadMore(); |
| }, |
| |
| /** |
| * @param {number} index |
| * @private |
| */ |
| removeItem_: function(index) { |
| this.splice('items_', index, 1); |
| this.updateHideDates_(index, index); |
| this.onListScroll_(); |
| }, |
| |
| /** |
| * @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; |
| } |
| }, |
| |
| /** |
| * @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); |
| }, |
| }); |
| |
| Manager.clearAll = function() { |
| Manager.get().clearAll_(); |
| }; |
| |
| /** @return {!downloads.Manager} */ |
| Manager.get = function() { |
| return /** @type {!downloads.Manager} */( |
| queryRequiredElement('downloads-manager')); |
| }; |
| |
| Manager.insertItems = function(index, list) { |
| Manager.get().insertItems_(index, list); |
| }; |
| |
| Manager.onLoad = function() { |
| Manager.get().onLoad_(); |
| }; |
| |
| Manager.removeItem = function(index) { |
| Manager.get().removeItem_(index); |
| }; |
| |
| Manager.updateItem = function(index, data) { |
| Manager.get().updateItem_(index, data); |
| }; |
| |
| 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. |
| |
| window.addEventListener('load', downloads.Manager.onLoad); |