blob: 53918e8684f11a8e8f0874cb2c20087e4fa4b19a [file] [log] [blame]
// 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
* 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);