OLD | NEW |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | |
5 /** | |
6 * @fileoverview PromiseResolver is a helper class that allows creating a | |
7 * Promise that will be fulfilled (resolved or rejected) some time later. | |
8 * | |
9 * Example: | |
10 * var resolver = new PromiseResolver(); | |
11 * resolver.promise.then(function(result) { | |
12 * console.log('resolved with', result); | |
13 * }); | |
14 * ... | |
15 * ... | |
16 * resolver.resolve({hello: 'world'}); | |
17 */ | |
18 | |
19 /** | |
20 * @constructor @struct | |
21 * @template T | |
22 */ | |
23 function PromiseResolver() { | 4 function PromiseResolver() { |
24 /** @private {function(T=): void} */ | |
25 this.resolve_; | 5 this.resolve_; |
26 | |
27 /** @private {function(*=): void} */ | |
28 this.reject_; | 6 this.reject_; |
29 | |
30 /** @private {!Promise<T>} */ | |
31 this.promise_ = new Promise(function(resolve, reject) { | 7 this.promise_ = new Promise(function(resolve, reject) { |
32 this.resolve_ = resolve; | 8 this.resolve_ = resolve; |
33 this.reject_ = reject; | 9 this.reject_ = reject; |
34 }.bind(this)); | 10 }.bind(this)); |
35 } | 11 } |
36 | 12 |
37 PromiseResolver.prototype = { | 13 PromiseResolver.prototype = { |
38 /** @return {!Promise<T>} */ | 14 get promise() { |
39 get promise() { return this.promise_; }, | 15 return this.promise_; |
40 set promise(p) { assertNotReached(); }, | 16 }, |
| 17 set promise(p) { |
| 18 assertNotReached(); |
| 19 }, |
| 20 get resolve() { |
| 21 return this.resolve_; |
| 22 }, |
| 23 set resolve(r) { |
| 24 assertNotReached(); |
| 25 }, |
| 26 get reject() { |
| 27 return this.reject_; |
| 28 }, |
| 29 set reject(s) { |
| 30 assertNotReached(); |
| 31 } |
| 32 }; |
41 | 33 |
42 /** @return {function(T=): void} */ | |
43 get resolve() { return this.resolve_; }, | |
44 set resolve(r) { assertNotReached(); }, | |
45 | |
46 /** @return {function(*=): void} */ | |
47 get reject() { return this.reject_; }, | |
48 set reject(s) { assertNotReached(); }, | |
49 }; | |
50 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 34 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
51 // Use of this source code is governed by a BSD-style license that can be | 35 // Use of this source code is governed by a BSD-style license that can be |
52 // found in the LICENSE file. | 36 // found in the LICENSE file. |
53 | |
54 /** | |
55 * The global object. | |
56 * @type {!Object} | |
57 * @const | |
58 */ | |
59 var global = this; | 37 var global = this; |
60 | 38 |
61 /** @typedef {{eventName: string, uid: number}} */ | |
62 var WebUIListener; | 39 var WebUIListener; |
63 | 40 |
64 /** Platform, package, object property, and Event support. **/ | |
65 var cr = cr || function() { | 41 var cr = cr || function() { |
66 'use strict'; | 42 'use strict'; |
67 | |
68 /** | |
69 * Builds an object structure for the provided namespace path, | |
70 * ensuring that names that already exist are not overwritten. For | |
71 * example: | |
72 * "a.b.c" -> a = {};a.b={};a.b.c={}; | |
73 * @param {string} name Name of the object that this file defines. | |
74 * @param {*=} opt_object The object to expose at the end of the path. | |
75 * @param {Object=} opt_objectToExportTo The object to add the path to; | |
76 * default is {@code global}. | |
77 * @return {!Object} The last object exported (i.e. exportPath('cr.ui') | |
78 * returns a reference to the ui property of window.cr). | |
79 * @private | |
80 */ | |
81 function exportPath(name, opt_object, opt_objectToExportTo) { | 43 function exportPath(name, opt_object, opt_objectToExportTo) { |
82 var parts = name.split('.'); | 44 var parts = name.split('.'); |
83 var cur = opt_objectToExportTo || global; | 45 var cur = opt_objectToExportTo || global; |
84 | 46 for (var part; parts.length && (part = parts.shift()); ) { |
85 for (var part; parts.length && (part = parts.shift());) { | |
86 if (!parts.length && opt_object !== undefined) { | 47 if (!parts.length && opt_object !== undefined) { |
87 // last part and we have an object; use it | |
88 cur[part] = opt_object; | 48 cur[part] = opt_object; |
89 } else if (part in cur) { | 49 } else if (part in cur) { |
90 cur = cur[part]; | 50 cur = cur[part]; |
91 } else { | 51 } else { |
92 cur = cur[part] = {}; | 52 cur = cur[part] = {}; |
93 } | 53 } |
94 } | 54 } |
95 return cur; | 55 return cur; |
96 } | 56 } |
97 | |
98 /** | |
99 * Fires a property change event on the target. | |
100 * @param {EventTarget} target The target to dispatch the event on. | |
101 * @param {string} propertyName The name of the property that changed. | |
102 * @param {*} newValue The new value for the property. | |
103 * @param {*} oldValue The old value for the property. | |
104 */ | |
105 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { | 57 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { |
106 var e = new Event(propertyName + 'Change'); | 58 var e = new Event(propertyName + 'Change'); |
107 e.propertyName = propertyName; | 59 e.propertyName = propertyName; |
108 e.newValue = newValue; | 60 e.newValue = newValue; |
109 e.oldValue = oldValue; | 61 e.oldValue = oldValue; |
110 target.dispatchEvent(e); | 62 target.dispatchEvent(e); |
111 } | 63 } |
112 | |
113 /** | |
114 * Converts a camelCase javascript property name to a hyphenated-lower-case | |
115 * attribute name. | |
116 * @param {string} jsName The javascript camelCase property name. | |
117 * @return {string} The equivalent hyphenated-lower-case attribute name. | |
118 */ | |
119 function getAttributeName(jsName) { | 64 function getAttributeName(jsName) { |
120 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); | 65 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); |
121 } | 66 } |
122 | |
123 /** | |
124 * The kind of property to define in {@code defineProperty}. | |
125 * @enum {string} | |
126 * @const | |
127 */ | |
128 var PropertyKind = { | 67 var PropertyKind = { |
129 /** | |
130 * Plain old JS property where the backing data is stored as a "private" | |
131 * field on the object. | |
132 * Use for properties of any type. Type will not be checked. | |
133 */ | |
134 JS: 'js', | 68 JS: 'js', |
135 | |
136 /** | |
137 * The property backing data is stored as an attribute on an element. | |
138 * Use only for properties of type {string}. | |
139 */ | |
140 ATTR: 'attr', | 69 ATTR: 'attr', |
141 | |
142 /** | |
143 * The property backing data is stored as an attribute on an element. If the | |
144 * element has the attribute then the value is true. | |
145 * Use only for properties of type {boolean}. | |
146 */ | |
147 BOOL_ATTR: 'boolAttr' | 70 BOOL_ATTR: 'boolAttr' |
148 }; | 71 }; |
149 | |
150 /** | |
151 * Helper function for defineProperty that returns the getter to use for the | |
152 * property. | |
153 * @param {string} name The name of the property. | |
154 * @param {PropertyKind} kind The kind of the property. | |
155 * @return {function():*} The getter for the property. | |
156 */ | |
157 function getGetter(name, kind) { | 72 function getGetter(name, kind) { |
158 switch (kind) { | 73 switch (kind) { |
159 case PropertyKind.JS: | 74 case PropertyKind.JS: |
160 var privateName = name + '_'; | 75 var privateName = name + '_'; |
161 return function() { | 76 return function() { |
162 return this[privateName]; | 77 return this[privateName]; |
163 }; | 78 }; |
164 case PropertyKind.ATTR: | 79 |
165 var attributeName = getAttributeName(name); | 80 case PropertyKind.ATTR: |
166 return function() { | 81 var attributeName = getAttributeName(name); |
167 return this.getAttribute(attributeName); | 82 return function() { |
168 }; | 83 return this.getAttribute(attributeName); |
169 case PropertyKind.BOOL_ATTR: | 84 }; |
170 var attributeName = getAttributeName(name); | 85 |
171 return function() { | 86 case PropertyKind.BOOL_ATTR: |
172 return this.hasAttribute(attributeName); | 87 var attributeName = getAttributeName(name); |
173 }; | 88 return function() { |
| 89 return this.hasAttribute(attributeName); |
| 90 }; |
174 } | 91 } |
175 | |
176 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax | |
177 // the browser/unit tests to preprocess this file through grit. | |
178 throw 'not reached'; | 92 throw 'not reached'; |
179 } | 93 } |
180 | |
181 /** | |
182 * Helper function for defineProperty that returns the setter of the right | |
183 * kind. | |
184 * @param {string} name The name of the property we are defining the setter | |
185 * for. | |
186 * @param {PropertyKind} kind The kind of property we are getting the | |
187 * setter for. | |
188 * @param {function(*, *):void=} opt_setHook A function to run after the | |
189 * property is set, but before the propertyChange event is fired. | |
190 * @return {function(*):void} The function to use as a setter. | |
191 */ | |
192 function getSetter(name, kind, opt_setHook) { | 94 function getSetter(name, kind, opt_setHook) { |
193 switch (kind) { | 95 switch (kind) { |
194 case PropertyKind.JS: | 96 case PropertyKind.JS: |
195 var privateName = name + '_'; | 97 var privateName = name + '_'; |
196 return function(value) { | 98 return function(value) { |
197 var oldValue = this[name]; | 99 var oldValue = this[name]; |
198 if (value !== oldValue) { | 100 if (value !== oldValue) { |
199 this[privateName] = value; | 101 this[privateName] = value; |
200 if (opt_setHook) | 102 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
201 opt_setHook.call(this, value, oldValue); | 103 dispatchPropertyChange(this, name, value, oldValue); |
202 dispatchPropertyChange(this, name, value, oldValue); | 104 } |
203 } | 105 }; |
204 }; | |
205 | 106 |
206 case PropertyKind.ATTR: | 107 case PropertyKind.ATTR: |
207 var attributeName = getAttributeName(name); | 108 var attributeName = getAttributeName(name); |
208 return function(value) { | 109 return function(value) { |
209 var oldValue = this[name]; | 110 var oldValue = this[name]; |
210 if (value !== oldValue) { | 111 if (value !== oldValue) { |
211 if (value == undefined) | 112 if (value == undefined) this.removeAttribute(attributeName); else this
.setAttribute(attributeName, value); |
212 this.removeAttribute(attributeName); | 113 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
213 else | 114 dispatchPropertyChange(this, name, value, oldValue); |
214 this.setAttribute(attributeName, value); | 115 } |
215 if (opt_setHook) | 116 }; |
216 opt_setHook.call(this, value, oldValue); | |
217 dispatchPropertyChange(this, name, value, oldValue); | |
218 } | |
219 }; | |
220 | 117 |
221 case PropertyKind.BOOL_ATTR: | 118 case PropertyKind.BOOL_ATTR: |
222 var attributeName = getAttributeName(name); | 119 var attributeName = getAttributeName(name); |
223 return function(value) { | 120 return function(value) { |
224 var oldValue = this[name]; | 121 var oldValue = this[name]; |
225 if (value !== oldValue) { | 122 if (value !== oldValue) { |
226 if (value) | 123 if (value) this.setAttribute(attributeName, name); else this.removeAtt
ribute(attributeName); |
227 this.setAttribute(attributeName, name); | 124 if (opt_setHook) opt_setHook.call(this, value, oldValue); |
228 else | 125 dispatchPropertyChange(this, name, value, oldValue); |
229 this.removeAttribute(attributeName); | 126 } |
230 if (opt_setHook) | 127 }; |
231 opt_setHook.call(this, value, oldValue); | |
232 dispatchPropertyChange(this, name, value, oldValue); | |
233 } | |
234 }; | |
235 } | 128 } |
236 | |
237 // TODO(dbeam): replace with assertNotReached() in assert.js when I can coax | |
238 // the browser/unit tests to preprocess this file through grit. | |
239 throw 'not reached'; | 129 throw 'not reached'; |
240 } | 130 } |
241 | |
242 /** | |
243 * Defines a property on an object. When the setter changes the value a | |
244 * property change event with the type {@code name + 'Change'} is fired. | |
245 * @param {!Object} obj The object to define the property for. | |
246 * @param {string} name The name of the property. | |
247 * @param {PropertyKind=} opt_kind What kind of underlying storage to use. | |
248 * @param {function(*, *):void=} opt_setHook A function to run after the | |
249 * property is set, but before the propertyChange event is fired. | |
250 */ | |
251 function defineProperty(obj, name, opt_kind, opt_setHook) { | 131 function defineProperty(obj, name, opt_kind, opt_setHook) { |
252 if (typeof obj == 'function') | 132 if (typeof obj == 'function') obj = obj.prototype; |
253 obj = obj.prototype; | 133 var kind = opt_kind || PropertyKind.JS; |
254 | 134 if (!obj.__lookupGetter__(name)) obj.__defineGetter__(name, getGetter(name,
kind)); |
255 var kind = /** @type {PropertyKind} */ (opt_kind || PropertyKind.JS); | 135 if (!obj.__lookupSetter__(name)) obj.__defineSetter__(name, getSetter(name,
kind, opt_setHook)); |
256 | |
257 if (!obj.__lookupGetter__(name)) | |
258 obj.__defineGetter__(name, getGetter(name, kind)); | |
259 | |
260 if (!obj.__lookupSetter__(name)) | |
261 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); | |
262 } | 136 } |
263 | |
264 /** | |
265 * Counter for use with createUid | |
266 */ | |
267 var uidCounter = 1; | 137 var uidCounter = 1; |
268 | |
269 /** | |
270 * @return {number} A new unique ID. | |
271 */ | |
272 function createUid() { | 138 function createUid() { |
273 return uidCounter++; | 139 return uidCounter++; |
274 } | 140 } |
275 | |
276 /** | |
277 * Returns a unique ID for the item. This mutates the item so it needs to be | |
278 * an object | |
279 * @param {!Object} item The item to get the unique ID for. | |
280 * @return {number} The unique ID for the item. | |
281 */ | |
282 function getUid(item) { | 141 function getUid(item) { |
283 if (item.hasOwnProperty('uid')) | 142 if (item.hasOwnProperty('uid')) return item.uid; |
284 return item.uid; | |
285 return item.uid = createUid(); | 143 return item.uid = createUid(); |
286 } | 144 } |
287 | |
288 /** | |
289 * Dispatches a simple event on an event target. | |
290 * @param {!EventTarget} target The event target to dispatch the event on. | |
291 * @param {string} type The type of the event. | |
292 * @param {boolean=} opt_bubbles Whether the event bubbles or not. | |
293 * @param {boolean=} opt_cancelable Whether the default action of the event | |
294 * can be prevented. Default is true. | |
295 * @return {boolean} If any of the listeners called {@code preventDefault} | |
296 * during the dispatch this will return false. | |
297 */ | |
298 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { | 145 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { |
299 var e = new Event(type, { | 146 var e = new Event(type, { |
300 bubbles: opt_bubbles, | 147 bubbles: opt_bubbles, |
301 cancelable: opt_cancelable === undefined || opt_cancelable | 148 cancelable: opt_cancelable === undefined || opt_cancelable |
302 }); | 149 }); |
303 return target.dispatchEvent(e); | 150 return target.dispatchEvent(e); |
304 } | 151 } |
305 | |
306 /** | |
307 * Calls |fun| and adds all the fields of the returned object to the object | |
308 * named by |name|. For example, cr.define('cr.ui', function() { | |
309 * function List() { | |
310 * ... | |
311 * } | |
312 * function ListItem() { | |
313 * ... | |
314 * } | |
315 * return { | |
316 * List: List, | |
317 * ListItem: ListItem, | |
318 * }; | |
319 * }); | |
320 * defines the functions cr.ui.List and cr.ui.ListItem. | |
321 * @param {string} name The name of the object that we are adding fields to. | |
322 * @param {!Function} fun The function that will return an object containing | |
323 * the names and values of the new fields. | |
324 */ | |
325 function define(name, fun) { | 152 function define(name, fun) { |
326 var obj = exportPath(name); | 153 var obj = exportPath(name); |
327 var exports = fun(); | 154 var exports = fun(); |
328 for (var propertyName in exports) { | 155 for (var propertyName in exports) { |
329 // Maybe we should check the prototype chain here? The current usage | 156 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, property
Name); |
330 // pattern is always using an object literal so we only care about own | 157 if (propertyDescriptor) Object.defineProperty(obj, propertyName, propertyD
escriptor); |
331 // properties. | |
332 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, | |
333 propertyName); | |
334 if (propertyDescriptor) | |
335 Object.defineProperty(obj, propertyName, propertyDescriptor); | |
336 } | 158 } |
337 } | 159 } |
338 | |
339 /** | |
340 * Adds a {@code getInstance} static method that always return the same | |
341 * instance object. | |
342 * @param {!Function} ctor The constructor for the class to add the static | |
343 * method to. | |
344 */ | |
345 function addSingletonGetter(ctor) { | 160 function addSingletonGetter(ctor) { |
346 ctor.getInstance = function() { | 161 ctor.getInstance = function() { |
347 return ctor.instance_ || (ctor.instance_ = new ctor()); | 162 return ctor.instance_ || (ctor.instance_ = new ctor()); |
348 }; | 163 }; |
349 } | 164 } |
350 | |
351 /** | |
352 * Forwards public APIs to private implementations. | |
353 * @param {Function} ctor Constructor that have private implementations in its | |
354 * prototype. | |
355 * @param {Array<string>} methods List of public method names that have their | |
356 * underscored counterparts in constructor's prototype. | |
357 * @param {string=} opt_target Selector for target node. | |
358 */ | |
359 function makePublic(ctor, methods, opt_target) { | 165 function makePublic(ctor, methods, opt_target) { |
360 methods.forEach(function(method) { | 166 methods.forEach(function(method) { |
361 ctor[method] = function() { | 167 ctor[method] = function() { |
362 var target = opt_target ? document.getElementById(opt_target) : | 168 var target = opt_target ? document.getElementById(opt_target) : ctor.get
Instance(); |
363 ctor.getInstance(); | |
364 return target[method + '_'].apply(target, arguments); | 169 return target[method + '_'].apply(target, arguments); |
365 }; | 170 }; |
366 }); | 171 }); |
367 } | 172 } |
368 | |
369 /** | |
370 * The mapping used by the sendWithPromise mechanism to tie the Promise | |
371 * returned to callers with the corresponding WebUI response. The mapping is | |
372 * from ID to the PromiseResolver helper; the ID is generated by | |
373 * sendWithPromise and is unique across all invocations of said method. | |
374 * @type {!Object<!PromiseResolver>} | |
375 */ | |
376 var chromeSendResolverMap = {}; | 173 var chromeSendResolverMap = {}; |
377 | |
378 /** | |
379 * The named method the WebUI handler calls directly in response to a | |
380 * chrome.send call that expects a response. The handler requires no knowledge | |
381 * of the specific name of this method, as the name is passed to the handler | |
382 * as the first argument in the arguments list of chrome.send. The handler | |
383 * must pass the ID, also sent via the chrome.send arguments list, as the | |
384 * first argument of the JS invocation; additionally, the handler may | |
385 * supply any number of other arguments that will be included in the response. | |
386 * @param {string} id The unique ID identifying the Promise this response is | |
387 * tied to. | |
388 * @param {boolean} isSuccess Whether the request was successful. | |
389 * @param {*} response The response as sent from C++. | |
390 */ | |
391 function webUIResponse(id, isSuccess, response) { | 174 function webUIResponse(id, isSuccess, response) { |
392 var resolver = chromeSendResolverMap[id]; | 175 var resolver = chromeSendResolverMap[id]; |
393 delete chromeSendResolverMap[id]; | 176 delete chromeSendResolverMap[id]; |
394 | 177 if (isSuccess) resolver.resolve(response); else resolver.reject(response); |
395 if (isSuccess) | |
396 resolver.resolve(response); | |
397 else | |
398 resolver.reject(response); | |
399 } | 178 } |
400 | |
401 /** | |
402 * A variation of chrome.send, suitable for messages that expect a single | |
403 * response from C++. | |
404 * @param {string} methodName The name of the WebUI handler API. | |
405 * @param {...*} var_args Varibale number of arguments to be forwarded to the | |
406 * C++ call. | |
407 * @return {!Promise} | |
408 */ | |
409 function sendWithPromise(methodName, var_args) { | 179 function sendWithPromise(methodName, var_args) { |
410 var args = Array.prototype.slice.call(arguments, 1); | 180 var args = Array.prototype.slice.call(arguments, 1); |
411 var promiseResolver = new PromiseResolver(); | 181 var promiseResolver = new PromiseResolver(); |
412 var id = methodName + '_' + createUid(); | 182 var id = methodName + '_' + createUid(); |
413 chromeSendResolverMap[id] = promiseResolver; | 183 chromeSendResolverMap[id] = promiseResolver; |
414 chrome.send(methodName, [id].concat(args)); | 184 chrome.send(methodName, [ id ].concat(args)); |
415 return promiseResolver.promise; | 185 return promiseResolver.promise; |
416 } | 186 } |
417 | |
418 /** | |
419 * A map of maps associating event names with listeners. The 2nd level map | |
420 * associates a listener ID with the callback function, such that individual | |
421 * listeners can be removed from an event without affecting other listeners of | |
422 * the same event. | |
423 * @type {!Object<!Object<!Function>>} | |
424 */ | |
425 var webUIListenerMap = {}; | 187 var webUIListenerMap = {}; |
426 | |
427 /** | |
428 * The named method the WebUI handler calls directly when an event occurs. | |
429 * The WebUI handler must supply the name of the event as the first argument | |
430 * of the JS invocation; additionally, the handler may supply any number of | |
431 * other arguments that will be forwarded to the listener callbacks. | |
432 * @param {string} event The name of the event that has occurred. | |
433 * @param {...*} var_args Additional arguments passed from C++. | |
434 */ | |
435 function webUIListenerCallback(event, var_args) { | 188 function webUIListenerCallback(event, var_args) { |
436 var eventListenersMap = webUIListenerMap[event]; | 189 var eventListenersMap = webUIListenerMap[event]; |
437 if (!eventListenersMap) { | 190 if (!eventListenersMap) { |
438 // C++ event sent for an event that has no listeners. | |
439 // TODO(dpapad): Should a warning be displayed here? | |
440 return; | 191 return; |
441 } | 192 } |
442 | |
443 var args = Array.prototype.slice.call(arguments, 1); | 193 var args = Array.prototype.slice.call(arguments, 1); |
444 for (var listenerId in eventListenersMap) { | 194 for (var listenerId in eventListenersMap) { |
445 eventListenersMap[listenerId].apply(null, args); | 195 eventListenersMap[listenerId].apply(null, args); |
446 } | 196 } |
447 } | 197 } |
448 | |
449 /** | |
450 * Registers a listener for an event fired from WebUI handlers. Any number of | |
451 * listeners may register for a single event. | |
452 * @param {string} eventName The event to listen to. | |
453 * @param {!Function} callback The callback run when the event is fired. | |
454 * @return {!WebUIListener} An object to be used for removing a listener via | |
455 * cr.removeWebUIListener. Should be treated as read-only. | |
456 */ | |
457 function addWebUIListener(eventName, callback) { | 198 function addWebUIListener(eventName, callback) { |
458 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; | 199 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; |
459 var uid = createUid(); | 200 var uid = createUid(); |
460 webUIListenerMap[eventName][uid] = callback; | 201 webUIListenerMap[eventName][uid] = callback; |
461 return {eventName: eventName, uid: uid}; | 202 return { |
| 203 eventName: eventName, |
| 204 uid: uid |
| 205 }; |
462 } | 206 } |
463 | |
464 /** | |
465 * Removes a listener. Does nothing if the specified listener is not found. | |
466 * @param {!WebUIListener} listener The listener to be removed (as returned by | |
467 * addWebUIListener). | |
468 * @return {boolean} Whether the given listener was found and actually | |
469 * removed. | |
470 */ | |
471 function removeWebUIListener(listener) { | 207 function removeWebUIListener(listener) { |
472 var listenerExists = webUIListenerMap[listener.eventName] && | 208 var listenerExists = webUIListenerMap[listener.eventName] && webUIListenerMa
p[listener.eventName][listener.uid]; |
473 webUIListenerMap[listener.eventName][listener.uid]; | |
474 if (listenerExists) { | 209 if (listenerExists) { |
475 delete webUIListenerMap[listener.eventName][listener.uid]; | 210 delete webUIListenerMap[listener.eventName][listener.uid]; |
476 return true; | 211 return true; |
477 } | 212 } |
478 return false; | 213 return false; |
479 } | 214 } |
480 | |
481 return { | 215 return { |
482 addSingletonGetter: addSingletonGetter, | 216 addSingletonGetter: addSingletonGetter, |
483 createUid: createUid, | 217 createUid: createUid, |
484 define: define, | 218 define: define, |
485 defineProperty: defineProperty, | 219 defineProperty: defineProperty, |
486 dispatchPropertyChange: dispatchPropertyChange, | 220 dispatchPropertyChange: dispatchPropertyChange, |
487 dispatchSimpleEvent: dispatchSimpleEvent, | 221 dispatchSimpleEvent: dispatchSimpleEvent, |
488 exportPath: exportPath, | 222 exportPath: exportPath, |
489 getUid: getUid, | 223 getUid: getUid, |
490 makePublic: makePublic, | 224 makePublic: makePublic, |
491 PropertyKind: PropertyKind, | 225 PropertyKind: PropertyKind, |
492 | |
493 // C++ <-> JS communication related methods. | |
494 addWebUIListener: addWebUIListener, | 226 addWebUIListener: addWebUIListener, |
495 removeWebUIListener: removeWebUIListener, | 227 removeWebUIListener: removeWebUIListener, |
496 sendWithPromise: sendWithPromise, | 228 sendWithPromise: sendWithPromise, |
497 webUIListenerCallback: webUIListenerCallback, | 229 webUIListenerCallback: webUIListenerCallback, |
498 webUIResponse: webUIResponse, | 230 webUIResponse: webUIResponse, |
499 | |
500 get doc() { | 231 get doc() { |
501 return document; | 232 return document; |
502 }, | 233 }, |
503 | |
504 /** Whether we are using a Mac or not. */ | |
505 get isMac() { | 234 get isMac() { |
506 return /Mac/.test(navigator.platform); | 235 return /Mac/.test(navigator.platform); |
507 }, | 236 }, |
508 | |
509 /** Whether this is on the Windows platform or not. */ | |
510 get isWindows() { | 237 get isWindows() { |
511 return /Win/.test(navigator.platform); | 238 return /Win/.test(navigator.platform); |
512 }, | 239 }, |
513 | |
514 /** Whether this is on chromeOS or not. */ | |
515 get isChromeOS() { | 240 get isChromeOS() { |
516 return /CrOS/.test(navigator.userAgent); | 241 return /CrOS/.test(navigator.userAgent); |
517 }, | 242 }, |
518 | |
519 /** Whether this is on vanilla Linux (not chromeOS). */ | |
520 get isLinux() { | 243 get isLinux() { |
521 return /Linux/.test(navigator.userAgent); | 244 return /Linux/.test(navigator.userAgent); |
522 }, | 245 }, |
523 | |
524 /** Whether this is on Android. */ | |
525 get isAndroid() { | 246 get isAndroid() { |
526 return /Android/.test(navigator.userAgent); | 247 return /Android/.test(navigator.userAgent); |
527 }, | 248 }, |
528 | |
529 /** Whether this is on iOS. */ | |
530 get isIOS() { | 249 get isIOS() { |
531 return /iPad|iPhone|iPod/.test(navigator.platform); | 250 return /iPad|iPhone|iPod/.test(navigator.platform); |
532 } | 251 } |
533 }; | 252 }; |
534 }(); | 253 }(); |
| 254 |
535 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 255 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
536 // Use of this source code is governed by a BSD-style license that can be | 256 // Use of this source code is governed by a BSD-style license that can be |
537 // found in the LICENSE file. | 257 // found in the LICENSE file. |
538 | |
539 cr.define('cr.ui', function() { | 258 cr.define('cr.ui', function() { |
540 | |
541 /** | |
542 * Decorates elements as an instance of a class. | |
543 * @param {string|!Element} source The way to find the element(s) to decorate. | |
544 * If this is a string then {@code querySeletorAll} is used to find the | |
545 * elements to decorate. | |
546 * @param {!Function} constr The constructor to decorate with. The constr | |
547 * needs to have a {@code decorate} function. | |
548 */ | |
549 function decorate(source, constr) { | 259 function decorate(source, constr) { |
550 var elements; | 260 var elements; |
551 if (typeof source == 'string') | 261 if (typeof source == 'string') elements = cr.doc.querySelectorAll(source); e
lse elements = [ source ]; |
552 elements = cr.doc.querySelectorAll(source); | |
553 else | |
554 elements = [source]; | |
555 | |
556 for (var i = 0, el; el = elements[i]; i++) { | 262 for (var i = 0, el; el = elements[i]; i++) { |
557 if (!(el instanceof constr)) | 263 if (!(el instanceof constr)) constr.decorate(el); |
558 constr.decorate(el); | |
559 } | 264 } |
560 } | 265 } |
561 | |
562 /** | |
563 * Helper function for creating new element for define. | |
564 */ | |
565 function createElementHelper(tagName, opt_bag) { | 266 function createElementHelper(tagName, opt_bag) { |
566 // Allow passing in ownerDocument to create in a different document. | |
567 var doc; | 267 var doc; |
568 if (opt_bag && opt_bag.ownerDocument) | 268 if (opt_bag && opt_bag.ownerDocument) doc = opt_bag.ownerDocument; else doc
= cr.doc; |
569 doc = opt_bag.ownerDocument; | |
570 else | |
571 doc = cr.doc; | |
572 return doc.createElement(tagName); | 269 return doc.createElement(tagName); |
573 } | 270 } |
574 | |
575 /** | |
576 * Creates the constructor for a UI element class. | |
577 * | |
578 * Usage: | |
579 * <pre> | |
580 * var List = cr.ui.define('list'); | |
581 * List.prototype = { | |
582 * __proto__: HTMLUListElement.prototype, | |
583 * decorate: function() { | |
584 * ... | |
585 * }, | |
586 * ... | |
587 * }; | |
588 * </pre> | |
589 * | |
590 * @param {string|Function} tagNameOrFunction The tagName or | |
591 * function to use for newly created elements. If this is a function it | |
592 * needs to return a new element when called. | |
593 * @return {function(Object=):Element} The constructor function which takes | |
594 * an optional property bag. The function also has a static | |
595 * {@code decorate} method added to it. | |
596 */ | |
597 function define(tagNameOrFunction) { | 271 function define(tagNameOrFunction) { |
598 var createFunction, tagName; | 272 var createFunction, tagName; |
599 if (typeof tagNameOrFunction == 'function') { | 273 if (typeof tagNameOrFunction == 'function') { |
600 createFunction = tagNameOrFunction; | 274 createFunction = tagNameOrFunction; |
601 tagName = ''; | 275 tagName = ''; |
602 } else { | 276 } else { |
603 createFunction = createElementHelper; | 277 createFunction = createElementHelper; |
604 tagName = tagNameOrFunction; | 278 tagName = tagNameOrFunction; |
605 } | 279 } |
606 | |
607 /** | |
608 * Creates a new UI element constructor. | |
609 * @param {Object=} opt_propertyBag Optional bag of properties to set on the | |
610 * object after created. The property {@code ownerDocument} is special | |
611 * cased and it allows you to create the element in a different | |
612 * document than the default. | |
613 * @constructor | |
614 */ | |
615 function f(opt_propertyBag) { | 280 function f(opt_propertyBag) { |
616 var el = createFunction(tagName, opt_propertyBag); | 281 var el = createFunction(tagName, opt_propertyBag); |
617 f.decorate(el); | 282 f.decorate(el); |
618 for (var propertyName in opt_propertyBag) { | 283 for (var propertyName in opt_propertyBag) { |
619 el[propertyName] = opt_propertyBag[propertyName]; | 284 el[propertyName] = opt_propertyBag[propertyName]; |
620 } | 285 } |
621 return el; | 286 return el; |
622 } | 287 } |
623 | |
624 /** | |
625 * Decorates an element as a UI element class. | |
626 * @param {!Element} el The element to decorate. | |
627 */ | |
628 f.decorate = function(el) { | 288 f.decorate = function(el) { |
629 el.__proto__ = f.prototype; | 289 el.__proto__ = f.prototype; |
630 el.decorate(); | 290 el.decorate(); |
631 }; | 291 }; |
632 | |
633 return f; | 292 return f; |
634 } | 293 } |
635 | |
636 /** | |
637 * Input elements do not grow and shrink with their content. This is a simple | |
638 * (and not very efficient) way of handling shrinking to content with support | |
639 * for min width and limited by the width of the parent element. | |
640 * @param {!HTMLElement} el The element to limit the width for. | |
641 * @param {!HTMLElement} parentEl The parent element that should limit the | |
642 * size. | |
643 * @param {number} min The minimum width. | |
644 * @param {number=} opt_scale Optional scale factor to apply to the width. | |
645 */ | |
646 function limitInputWidth(el, parentEl, min, opt_scale) { | 294 function limitInputWidth(el, parentEl, min, opt_scale) { |
647 // Needs a size larger than borders | |
648 el.style.width = '10px'; | 295 el.style.width = '10px'; |
649 var doc = el.ownerDocument; | 296 var doc = el.ownerDocument; |
650 var win = doc.defaultView; | 297 var win = doc.defaultView; |
651 var computedStyle = win.getComputedStyle(el); | 298 var computedStyle = win.getComputedStyle(el); |
652 var parentComputedStyle = win.getComputedStyle(parentEl); | 299 var parentComputedStyle = win.getComputedStyle(parentEl); |
653 var rtl = computedStyle.direction == 'rtl'; | 300 var rtl = computedStyle.direction == 'rtl'; |
654 | 301 var inputRect = el.getBoundingClientRect(); |
655 // To get the max width we get the width of the treeItem minus the position | |
656 // of the input. | |
657 var inputRect = el.getBoundingClientRect(); // box-sizing | |
658 var parentRect = parentEl.getBoundingClientRect(); | 302 var parentRect = parentEl.getBoundingClientRect(); |
659 var startPos = rtl ? parentRect.right - inputRect.right : | 303 var startPos = rtl ? parentRect.right - inputRect.right : inputRect.left - p
arentRect.left; |
660 inputRect.left - parentRect.left; | 304 var inner = parseInt(computedStyle.borderLeftWidth, 10) + parseInt(computedS
tyle.paddingLeft, 10) + parseInt(computedStyle.paddingRight, 10) + parseInt(comp
utedStyle.borderRightWidth, 10); |
661 | 305 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : pa
rseInt(parentComputedStyle.paddingRight, 10); |
662 // Add up border and padding of the input. | |
663 var inner = parseInt(computedStyle.borderLeftWidth, 10) + | |
664 parseInt(computedStyle.paddingLeft, 10) + | |
665 parseInt(computedStyle.paddingRight, 10) + | |
666 parseInt(computedStyle.borderRightWidth, 10); | |
667 | |
668 // We also need to subtract the padding of parent to prevent it to overflow. | |
669 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : | |
670 parseInt(parentComputedStyle.paddingRight, 10); | |
671 | |
672 var max = parentEl.clientWidth - startPos - inner - parentPadding; | 306 var max = parentEl.clientWidth - startPos - inner - parentPadding; |
673 if (opt_scale) | 307 if (opt_scale) max *= opt_scale; |
674 max *= opt_scale; | |
675 | |
676 function limit() { | 308 function limit() { |
677 if (el.scrollWidth > max) { | 309 if (el.scrollWidth > max) { |
678 el.style.width = max + 'px'; | 310 el.style.width = max + 'px'; |
679 } else { | 311 } else { |
680 el.style.width = 0; | 312 el.style.width = 0; |
681 var sw = el.scrollWidth; | 313 var sw = el.scrollWidth; |
682 if (sw < min) { | 314 if (sw < min) { |
683 el.style.width = min + 'px'; | 315 el.style.width = min + 'px'; |
684 } else { | 316 } else { |
685 el.style.width = sw + 'px'; | 317 el.style.width = sw + 'px'; |
686 } | 318 } |
687 } | 319 } |
688 } | 320 } |
689 | |
690 el.addEventListener('input', limit); | 321 el.addEventListener('input', limit); |
691 limit(); | 322 limit(); |
692 } | 323 } |
693 | |
694 /** | |
695 * Takes a number and spits out a value CSS will be happy with. To avoid | |
696 * subpixel layout issues, the value is rounded to the nearest integral value. | |
697 * @param {number} pixels The number of pixels. | |
698 * @return {string} e.g. '16px'. | |
699 */ | |
700 function toCssPx(pixels) { | 324 function toCssPx(pixels) { |
701 if (!window.isFinite(pixels)) | 325 if (!window.isFinite(pixels)) console.error('Pixel value is not a number: '
+ pixels); |
702 console.error('Pixel value is not a number: ' + pixels); | |
703 return Math.round(pixels) + 'px'; | 326 return Math.round(pixels) + 'px'; |
704 } | 327 } |
705 | |
706 /** | |
707 * Users complain they occasionaly use doubleclicks instead of clicks | |
708 * (http://crbug.com/140364). To fix it we freeze click handling for | |
709 * the doubleclick time interval. | |
710 * @param {MouseEvent} e Initial click event. | |
711 */ | |
712 function swallowDoubleClick(e) { | 328 function swallowDoubleClick(e) { |
713 var doc = e.target.ownerDocument; | 329 var doc = e.target.ownerDocument; |
714 var counter = Math.min(1, e.detail); | 330 var counter = Math.min(1, e.detail); |
715 function swallow(e) { | 331 function swallow(e) { |
716 e.stopPropagation(); | 332 e.stopPropagation(); |
717 e.preventDefault(); | 333 e.preventDefault(); |
718 } | 334 } |
719 function onclick(e) { | 335 function onclick(e) { |
720 if (e.detail > counter) { | 336 if (e.detail > counter) { |
721 counter = e.detail; | 337 counter = e.detail; |
722 // Swallow the click since it's a click inside the doubleclick timeout. | |
723 swallow(e); | 338 swallow(e); |
724 } else { | 339 } else { |
725 // Stop tracking clicks and let regular handling. | |
726 doc.removeEventListener('dblclick', swallow, true); | 340 doc.removeEventListener('dblclick', swallow, true); |
727 doc.removeEventListener('click', onclick, true); | 341 doc.removeEventListener('click', onclick, true); |
728 } | 342 } |
729 } | 343 } |
730 // The following 'click' event (if e.type == 'mouseup') mustn't be taken | |
731 // into account (it mustn't stop tracking clicks). Start event listening | |
732 // after zero timeout. | |
733 setTimeout(function() { | 344 setTimeout(function() { |
734 doc.addEventListener('click', onclick, true); | 345 doc.addEventListener('click', onclick, true); |
735 doc.addEventListener('dblclick', swallow, true); | 346 doc.addEventListener('dblclick', swallow, true); |
736 }, 0); | 347 }, 0); |
737 } | 348 } |
738 | |
739 return { | 349 return { |
740 decorate: decorate, | 350 decorate: decorate, |
741 define: define, | 351 define: define, |
742 limitInputWidth: limitInputWidth, | 352 limitInputWidth: limitInputWidth, |
743 toCssPx: toCssPx, | 353 toCssPx: toCssPx, |
744 swallowDoubleClick: swallowDoubleClick | 354 swallowDoubleClick: swallowDoubleClick |
745 }; | 355 }; |
746 }); | 356 }); |
| 357 |
747 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 358 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
748 // Use of this source code is governed by a BSD-style license that can be | 359 // Use of this source code is governed by a BSD-style license that can be |
749 // found in the LICENSE file. | 360 // found in the LICENSE file. |
750 | |
751 /** | |
752 * @fileoverview A command is an abstraction of an action a user can do in the | |
753 * UI. | |
754 * | |
755 * When the focus changes in the document for each command a canExecute event | |
756 * is dispatched on the active element. By listening to this event you can | |
757 * enable and disable the command by setting the event.canExecute property. | |
758 * | |
759 * When a command is executed a command event is dispatched on the active | |
760 * element. Note that you should stop the propagation after you have handled the | |
761 * command if there might be other command listeners higher up in the DOM tree. | |
762 */ | |
763 | |
764 cr.define('cr.ui', function() { | 361 cr.define('cr.ui', function() { |
765 | |
766 /** | |
767 * This is used to identify keyboard shortcuts. | |
768 * @param {string} shortcut The text used to describe the keys for this | |
769 * keyboard shortcut. | |
770 * @constructor | |
771 */ | |
772 function KeyboardShortcut(shortcut) { | 362 function KeyboardShortcut(shortcut) { |
773 var mods = {}; | 363 var mods = {}; |
774 var ident = ''; | 364 var ident = ''; |
775 shortcut.split('|').forEach(function(part) { | 365 shortcut.split('|').forEach(function(part) { |
776 var partLc = part.toLowerCase(); | 366 var partLc = part.toLowerCase(); |
777 switch (partLc) { | 367 switch (partLc) { |
778 case 'alt': | 368 case 'alt': |
779 case 'ctrl': | 369 case 'ctrl': |
780 case 'meta': | 370 case 'meta': |
781 case 'shift': | 371 case 'shift': |
782 mods[partLc + 'Key'] = true; | 372 mods[partLc + 'Key'] = true; |
783 break; | 373 break; |
784 default: | 374 |
785 if (ident) | 375 default: |
786 throw Error('Invalid shortcut'); | 376 if (ident) throw Error('Invalid shortcut'); |
787 ident = part; | 377 ident = part; |
788 } | 378 } |
789 }); | 379 }); |
790 | |
791 this.ident_ = ident; | 380 this.ident_ = ident; |
792 this.mods_ = mods; | 381 this.mods_ = mods; |
793 } | 382 } |
794 | |
795 KeyboardShortcut.prototype = { | 383 KeyboardShortcut.prototype = { |
796 /** | |
797 * Whether the keyboard shortcut object matches a keyboard event. | |
798 * @param {!Event} e The keyboard event object. | |
799 * @return {boolean} Whether we found a match or not. | |
800 */ | |
801 matchesEvent: function(e) { | 384 matchesEvent: function(e) { |
802 if (e.key == this.ident_) { | 385 if (e.key == this.ident_) { |
803 // All keyboard modifiers needs to match. | |
804 var mods = this.mods_; | 386 var mods = this.mods_; |
805 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { | 387 return [ 'altKey', 'ctrlKey', 'metaKey', 'shiftKey' ].every(function(k)
{ |
806 return e[k] == !!mods[k]; | 388 return e[k] == !!mods[k]; |
807 }); | 389 }); |
808 } | 390 } |
809 return false; | 391 return false; |
810 } | 392 } |
811 }; | 393 }; |
812 | |
813 /** | |
814 * Creates a new command element. | |
815 * @constructor | |
816 * @extends {HTMLElement} | |
817 */ | |
818 var Command = cr.ui.define('command'); | 394 var Command = cr.ui.define('command'); |
819 | |
820 Command.prototype = { | 395 Command.prototype = { |
821 __proto__: HTMLElement.prototype, | 396 __proto__: HTMLElement.prototype, |
822 | |
823 /** | |
824 * Initializes the command. | |
825 */ | |
826 decorate: function() { | 397 decorate: function() { |
827 CommandManager.init(assert(this.ownerDocument)); | 398 CommandManager.init(assert(this.ownerDocument)); |
828 | 399 if (this.hasAttribute('shortcut')) this.shortcut = this.getAttribute('shor
tcut'); |
829 if (this.hasAttribute('shortcut')) | |
830 this.shortcut = this.getAttribute('shortcut'); | |
831 }, | 400 }, |
832 | |
833 /** | |
834 * Executes the command by dispatching a command event on the given element. | |
835 * If |element| isn't given, the active element is used instead. | |
836 * If the command is {@code disabled} this does nothing. | |
837 * @param {HTMLElement=} opt_element Optional element to dispatch event on. | |
838 */ | |
839 execute: function(opt_element) { | 401 execute: function(opt_element) { |
840 if (this.disabled) | 402 if (this.disabled) return; |
841 return; | |
842 var doc = this.ownerDocument; | 403 var doc = this.ownerDocument; |
843 if (doc.activeElement) { | 404 if (doc.activeElement) { |
844 var e = new Event('command', {bubbles: true}); | 405 var e = new Event('command', { |
| 406 bubbles: true |
| 407 }); |
845 e.command = this; | 408 e.command = this; |
846 | |
847 (opt_element || doc.activeElement).dispatchEvent(e); | 409 (opt_element || doc.activeElement).dispatchEvent(e); |
848 } | 410 } |
849 }, | 411 }, |
850 | |
851 /** | |
852 * Call this when there have been changes that might change whether the | |
853 * command can be executed or not. | |
854 * @param {Node=} opt_node Node for which to actuate command state. | |
855 */ | |
856 canExecuteChange: function(opt_node) { | 412 canExecuteChange: function(opt_node) { |
857 dispatchCanExecuteEvent(this, | 413 dispatchCanExecuteEvent(this, opt_node || this.ownerDocument.activeElement
); |
858 opt_node || this.ownerDocument.activeElement); | |
859 }, | 414 }, |
860 | |
861 /** | |
862 * The keyboard shortcut that triggers the command. This is a string | |
863 * consisting of a key (as reported by WebKit in keydown) as | |
864 * well as optional key modifiers joinded with a '|'. | |
865 * | |
866 * Multiple keyboard shortcuts can be provided by separating them by | |
867 * whitespace. | |
868 * | |
869 * For example: | |
870 * "F1" | |
871 * "Backspace|Meta" for Apple command backspace. | |
872 * "a|Ctrl" for Control A | |
873 * "Delete Backspace|Meta" for Delete and Command Backspace | |
874 * | |
875 * @type {string} | |
876 */ | |
877 shortcut_: '', | 415 shortcut_: '', |
878 get shortcut() { | 416 get shortcut() { |
879 return this.shortcut_; | 417 return this.shortcut_; |
880 }, | 418 }, |
881 set shortcut(shortcut) { | 419 set shortcut(shortcut) { |
882 var oldShortcut = this.shortcut_; | 420 var oldShortcut = this.shortcut_; |
883 if (shortcut !== oldShortcut) { | 421 if (shortcut !== oldShortcut) { |
884 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { | 422 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { |
885 return new KeyboardShortcut(shortcut); | 423 return new KeyboardShortcut(shortcut); |
886 }); | 424 }); |
887 | |
888 // Set this after the keyboardShortcuts_ since that might throw. | |
889 this.shortcut_ = shortcut; | 425 this.shortcut_ = shortcut; |
890 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, | 426 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, oldShortcut)
; |
891 oldShortcut); | |
892 } | 427 } |
893 }, | 428 }, |
894 | |
895 /** | |
896 * Whether the event object matches the shortcut for this command. | |
897 * @param {!Event} e The key event object. | |
898 * @return {boolean} Whether it matched or not. | |
899 */ | |
900 matchesEvent: function(e) { | 429 matchesEvent: function(e) { |
901 if (!this.keyboardShortcuts_) | 430 if (!this.keyboardShortcuts_) return false; |
902 return false; | |
903 | |
904 return this.keyboardShortcuts_.some(function(keyboardShortcut) { | 431 return this.keyboardShortcuts_.some(function(keyboardShortcut) { |
905 return keyboardShortcut.matchesEvent(e); | 432 return keyboardShortcut.matchesEvent(e); |
906 }); | 433 }); |
907 }, | 434 } |
908 }; | 435 }; |
909 | |
910 /** | |
911 * The label of the command. | |
912 */ | |
913 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); | 436 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); |
914 | |
915 /** | |
916 * Whether the command is disabled or not. | |
917 */ | |
918 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); | 437 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); |
919 | |
920 /** | |
921 * Whether the command is hidden or not. | |
922 */ | |
923 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); | 438 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); |
924 | |
925 /** | |
926 * Whether the command is checked or not. | |
927 */ | |
928 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); | 439 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); |
929 | |
930 /** | |
931 * The flag that prevents the shortcut text from being displayed on menu. | |
932 * | |
933 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command) | |
934 * is displayed in menu when the command is assosiated with a menu item. | |
935 * Otherwise, no text is displayed. | |
936 */ | |
937 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); | 440 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); |
938 | |
939 /** | |
940 * Dispatches a canExecute event on the target. | |
941 * @param {!cr.ui.Command} command The command that we are testing for. | |
942 * @param {EventTarget} target The target element to dispatch the event on. | |
943 */ | |
944 function dispatchCanExecuteEvent(command, target) { | 441 function dispatchCanExecuteEvent(command, target) { |
945 var e = new CanExecuteEvent(command); | 442 var e = new CanExecuteEvent(command); |
946 target.dispatchEvent(e); | 443 target.dispatchEvent(e); |
947 command.disabled = !e.canExecute; | 444 command.disabled = !e.canExecute; |
948 } | 445 } |
949 | |
950 /** | |
951 * The command managers for different documents. | |
952 */ | |
953 var commandManagers = {}; | 446 var commandManagers = {}; |
954 | |
955 /** | |
956 * Keeps track of the focused element and updates the commands when the focus | |
957 * changes. | |
958 * @param {!Document} doc The document that we are managing the commands for. | |
959 * @constructor | |
960 */ | |
961 function CommandManager(doc) { | 447 function CommandManager(doc) { |
962 doc.addEventListener('focus', this.handleFocus_.bind(this), true); | 448 doc.addEventListener('focus', this.handleFocus_.bind(this), true); |
963 // Make sure we add the listener to the bubbling phase so that elements can | |
964 // prevent the command. | |
965 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); | 449 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); |
966 } | 450 } |
967 | |
968 /** | |
969 * Initializes a command manager for the document as needed. | |
970 * @param {!Document} doc The document to manage the commands for. | |
971 */ | |
972 CommandManager.init = function(doc) { | 451 CommandManager.init = function(doc) { |
973 var uid = cr.getUid(doc); | 452 var uid = cr.getUid(doc); |
974 if (!(uid in commandManagers)) { | 453 if (!(uid in commandManagers)) { |
975 commandManagers[uid] = new CommandManager(doc); | 454 commandManagers[uid] = new CommandManager(doc); |
976 } | 455 } |
977 }; | 456 }; |
978 | |
979 CommandManager.prototype = { | 457 CommandManager.prototype = { |
980 | |
981 /** | |
982 * Handles focus changes on the document. | |
983 * @param {Event} e The focus event object. | |
984 * @private | |
985 * @suppress {checkTypes} | |
986 * TODO(vitalyp): remove the suppression. | |
987 */ | |
988 handleFocus_: function(e) { | 458 handleFocus_: function(e) { |
989 var target = e.target; | 459 var target = e.target; |
990 | 460 if (target.menu || target.command) return; |
991 // Ignore focus on a menu button or command item. | 461 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect
orAll('command')); |
992 if (target.menu || target.command) | |
993 return; | |
994 | |
995 var commands = Array.prototype.slice.call( | |
996 target.ownerDocument.querySelectorAll('command')); | |
997 | |
998 commands.forEach(function(command) { | 462 commands.forEach(function(command) { |
999 dispatchCanExecuteEvent(command, target); | 463 dispatchCanExecuteEvent(command, target); |
1000 }); | 464 }); |
1001 }, | 465 }, |
1002 | |
1003 /** | |
1004 * Handles the keydown event and routes it to the right command. | |
1005 * @param {!Event} e The keydown event. | |
1006 */ | |
1007 handleKeyDown_: function(e) { | 466 handleKeyDown_: function(e) { |
1008 var target = e.target; | 467 var target = e.target; |
1009 var commands = Array.prototype.slice.call( | 468 var commands = Array.prototype.slice.call(target.ownerDocument.querySelect
orAll('command')); |
1010 target.ownerDocument.querySelectorAll('command')); | |
1011 | |
1012 for (var i = 0, command; command = commands[i]; i++) { | 469 for (var i = 0, command; command = commands[i]; i++) { |
1013 if (command.matchesEvent(e)) { | 470 if (command.matchesEvent(e)) { |
1014 // When invoking a command via a shortcut, we have to manually check | |
1015 // if it can be executed, since focus might not have been changed | |
1016 // what would have updated the command's state. | |
1017 command.canExecuteChange(); | 471 command.canExecuteChange(); |
1018 | |
1019 if (!command.disabled) { | 472 if (!command.disabled) { |
1020 e.preventDefault(); | 473 e.preventDefault(); |
1021 // We do not want any other element to handle this. | |
1022 e.stopPropagation(); | 474 e.stopPropagation(); |
1023 command.execute(); | 475 command.execute(); |
1024 return; | 476 return; |
1025 } | 477 } |
1026 } | 478 } |
1027 } | 479 } |
1028 } | 480 } |
1029 }; | 481 }; |
1030 | |
1031 /** | |
1032 * The event type used for canExecute events. | |
1033 * @param {!cr.ui.Command} command The command that we are evaluating. | |
1034 * @extends {Event} | |
1035 * @constructor | |
1036 * @class | |
1037 */ | |
1038 function CanExecuteEvent(command) { | 482 function CanExecuteEvent(command) { |
1039 var e = new Event('canExecute', {bubbles: true, cancelable: true}); | 483 var e = new Event('canExecute', { |
| 484 bubbles: true, |
| 485 cancelable: true |
| 486 }); |
1040 e.__proto__ = CanExecuteEvent.prototype; | 487 e.__proto__ = CanExecuteEvent.prototype; |
1041 e.command = command; | 488 e.command = command; |
1042 return e; | 489 return e; |
1043 } | 490 } |
1044 | |
1045 CanExecuteEvent.prototype = { | 491 CanExecuteEvent.prototype = { |
1046 __proto__: Event.prototype, | 492 __proto__: Event.prototype, |
1047 | |
1048 /** | |
1049 * The current command | |
1050 * @type {cr.ui.Command} | |
1051 */ | |
1052 command: null, | 493 command: null, |
1053 | |
1054 /** | |
1055 * Whether the target can execute the command. Setting this also stops the | |
1056 * propagation and prevents the default. Callers can tell if an event has | |
1057 * been handled via |this.defaultPrevented|. | |
1058 * @type {boolean} | |
1059 */ | |
1060 canExecute_: false, | 494 canExecute_: false, |
1061 get canExecute() { | 495 get canExecute() { |
1062 return this.canExecute_; | 496 return this.canExecute_; |
1063 }, | 497 }, |
1064 set canExecute(canExecute) { | 498 set canExecute(canExecute) { |
1065 this.canExecute_ = !!canExecute; | 499 this.canExecute_ = !!canExecute; |
1066 this.stopPropagation(); | 500 this.stopPropagation(); |
1067 this.preventDefault(); | 501 this.preventDefault(); |
1068 } | 502 } |
1069 }; | 503 }; |
1070 | |
1071 // Export | |
1072 return { | 504 return { |
1073 Command: Command, | 505 Command: Command, |
1074 CanExecuteEvent: CanExecuteEvent | 506 CanExecuteEvent: CanExecuteEvent |
1075 }; | 507 }; |
1076 }); | 508 }); |
| 509 |
1077 Polymer({ | 510 Polymer({ |
1078 is: 'app-drawer', | 511 is: 'app-drawer', |
1079 | 512 properties: { |
1080 properties: { | 513 opened: { |
1081 /** | 514 type: Boolean, |
1082 * The opened state of the drawer. | 515 value: false, |
1083 */ | 516 notify: true, |
1084 opened: { | 517 reflectToAttribute: true |
1085 type: Boolean, | 518 }, |
1086 value: false, | 519 persistent: { |
1087 notify: true, | 520 type: Boolean, |
1088 reflectToAttribute: true | 521 value: false, |
1089 }, | 522 reflectToAttribute: true |
1090 | 523 }, |
1091 /** | 524 align: { |
1092 * The drawer does not have a scrim and cannot be swiped close. | 525 type: String, |
1093 */ | 526 value: 'left' |
1094 persistent: { | 527 }, |
1095 type: Boolean, | 528 position: { |
1096 value: false, | 529 type: String, |
1097 reflectToAttribute: true | 530 readOnly: true, |
1098 }, | 531 value: 'left', |
1099 | 532 reflectToAttribute: true |
1100 /** | 533 }, |
1101 * The alignment of the drawer on the screen ('left', 'right', 'start' o
r 'end'). | 534 swipeOpen: { |
1102 * 'start' computes to left and 'end' to right in LTR layout and vice ve
rsa in RTL | 535 type: Boolean, |
1103 * layout. | 536 value: false, |
1104 */ | 537 reflectToAttribute: true |
1105 align: { | 538 }, |
1106 type: String, | 539 noFocusTrap: { |
1107 value: 'left' | 540 type: Boolean, |
1108 }, | 541 value: false |
1109 | 542 } |
1110 /** | 543 }, |
1111 * The computed, read-only position of the drawer on the screen ('left'
or 'right'). | 544 observers: [ 'resetLayout(position)', '_resetPosition(align, isAttached)' ], |
1112 */ | 545 _translateOffset: 0, |
1113 position: { | 546 _trackDetails: null, |
1114 type: String, | 547 _drawerState: 0, |
1115 readOnly: true, | 548 _boundEscKeydownHandler: null, |
1116 value: 'left', | 549 _firstTabStop: null, |
1117 reflectToAttribute: true | 550 _lastTabStop: null, |
1118 }, | 551 ready: function() { |
1119 | 552 this.setScrollDirection('y'); |
1120 /** | 553 this._setTransitionDuration('0s'); |
1121 * Create an area at the edge of the screen to swipe open the drawer. | 554 }, |
1122 */ | 555 attached: function() { |
1123 swipeOpen: { | 556 Polymer.RenderStatus.afterNextRender(this, function() { |
1124 type: Boolean, | 557 this._setTransitionDuration(''); |
1125 value: false, | 558 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this); |
1126 reflectToAttribute: true | 559 this._resetDrawerState(); |
1127 }, | 560 this.listen(this, 'track', '_track'); |
1128 | 561 this.addEventListener('transitionend', this._transitionend.bind(this)); |
1129 /** | 562 this.addEventListener('keydown', this._tabKeydownHandler.bind(this)); |
1130 * Trap keyboard focus when the drawer is opened and not persistent. | 563 }); |
1131 */ | 564 }, |
1132 noFocusTrap: { | 565 detached: function() { |
1133 type: Boolean, | 566 document.removeEventListener('keydown', this._boundEscKeydownHandler); |
1134 value: false | 567 }, |
1135 } | 568 open: function() { |
1136 }, | 569 this.opened = true; |
1137 | 570 }, |
1138 observers: [ | 571 close: function() { |
1139 'resetLayout(position)', | 572 this.opened = false; |
1140 '_resetPosition(align, isAttached)' | 573 }, |
1141 ], | 574 toggle: function() { |
1142 | 575 this.opened = !this.opened; |
1143 _translateOffset: 0, | 576 }, |
1144 | 577 getWidth: function() { |
1145 _trackDetails: null, | 578 return this.$.contentContainer.offsetWidth; |
1146 | 579 }, |
1147 _drawerState: 0, | 580 resetLayout: function() { |
1148 | 581 this.debounce('_resetLayout', function() { |
1149 _boundEscKeydownHandler: null, | 582 this.fire('app-drawer-reset-layout'); |
1150 | 583 }, 1); |
1151 _firstTabStop: null, | 584 }, |
1152 | 585 _isRTL: function() { |
1153 _lastTabStop: null, | 586 return window.getComputedStyle(this).direction === 'rtl'; |
1154 | 587 }, |
1155 ready: function() { | 588 _resetPosition: function() { |
1156 // Set the scroll direction so you can vertically scroll inside the draw
er. | 589 switch (this.align) { |
1157 this.setScrollDirection('y'); | 590 case 'start': |
1158 | 591 this._setPosition(this._isRTL() ? 'right' : 'left'); |
1159 // Only transition the drawer after its first render (e.g. app-drawer-la
yout | 592 return; |
1160 // may need to set the initial opened state which should not be transiti
oned). | 593 |
1161 this._setTransitionDuration('0s'); | 594 case 'end': |
1162 }, | 595 this._setPosition(this._isRTL() ? 'left' : 'right'); |
1163 | 596 return; |
1164 attached: function() { | 597 } |
1165 // Only transition the drawer after its first render (e.g. app-drawer-la
yout | 598 this._setPosition(this.align); |
1166 // may need to set the initial opened state which should not be transiti
oned). | 599 }, |
1167 Polymer.RenderStatus.afterNextRender(this, function() { | 600 _escKeydownHandler: function(event) { |
1168 this._setTransitionDuration(''); | 601 var ESC_KEYCODE = 27; |
1169 this._boundEscKeydownHandler = this._escKeydownHandler.bind(this); | 602 if (event.keyCode === ESC_KEYCODE) { |
1170 this._resetDrawerState(); | 603 event.preventDefault(); |
1171 | 604 this.close(); |
1172 this.listen(this, 'track', '_track'); | 605 } |
1173 this.addEventListener('transitionend', this._transitionend.bind(this))
; | 606 }, |
1174 this.addEventListener('keydown', this._tabKeydownHandler.bind(this)) | 607 _track: function(event) { |
1175 }); | 608 if (this.persistent) { |
1176 }, | 609 return; |
1177 | 610 } |
1178 detached: function() { | 611 event.preventDefault(); |
| 612 switch (event.detail.state) { |
| 613 case 'start': |
| 614 this._trackStart(event); |
| 615 break; |
| 616 |
| 617 case 'track': |
| 618 this._trackMove(event); |
| 619 break; |
| 620 |
| 621 case 'end': |
| 622 this._trackEnd(event); |
| 623 break; |
| 624 } |
| 625 }, |
| 626 _trackStart: function(event) { |
| 627 this._drawerState = this._DRAWER_STATE.TRACKING; |
| 628 this._setTransitionDuration('0s'); |
| 629 this.style.visibility = 'visible'; |
| 630 var rect = this.$.contentContainer.getBoundingClientRect(); |
| 631 if (this.position === 'left') { |
| 632 this._translateOffset = rect.left; |
| 633 } else { |
| 634 this._translateOffset = rect.right - window.innerWidth; |
| 635 } |
| 636 this._trackDetails = []; |
| 637 }, |
| 638 _trackMove: function(event) { |
| 639 this._translateDrawer(event.detail.dx + this._translateOffset); |
| 640 this._trackDetails.push({ |
| 641 dx: event.detail.dx, |
| 642 timeStamp: Date.now() |
| 643 }); |
| 644 }, |
| 645 _trackEnd: function(event) { |
| 646 var x = event.detail.dx + this._translateOffset; |
| 647 var drawerWidth = this.getWidth(); |
| 648 var isPositionLeft = this.position === 'left'; |
| 649 var isInEndState = isPositionLeft ? x >= 0 || x <= -drawerWidth : x <= 0 ||
x >= drawerWidth; |
| 650 if (!isInEndState) { |
| 651 var trackDetails = this._trackDetails; |
| 652 this._trackDetails = null; |
| 653 this._flingDrawer(event, trackDetails); |
| 654 if (this._drawerState === this._DRAWER_STATE.FLINGING) { |
| 655 return; |
| 656 } |
| 657 } |
| 658 var halfWidth = drawerWidth / 2; |
| 659 if (event.detail.dx < -halfWidth) { |
| 660 this.opened = this.position === 'right'; |
| 661 } else if (event.detail.dx > halfWidth) { |
| 662 this.opened = this.position === 'left'; |
| 663 } |
| 664 if (isInEndState) { |
| 665 this._resetDrawerState(); |
| 666 } |
| 667 this._setTransitionDuration(''); |
| 668 this._resetDrawerTranslate(); |
| 669 this.style.visibility = ''; |
| 670 }, |
| 671 _calculateVelocity: function(event, trackDetails) { |
| 672 var now = Date.now(); |
| 673 var timeLowerBound = now - 100; |
| 674 var trackDetail; |
| 675 var min = 0; |
| 676 var max = trackDetails.length - 1; |
| 677 while (min <= max) { |
| 678 var mid = min + max >> 1; |
| 679 var d = trackDetails[mid]; |
| 680 if (d.timeStamp >= timeLowerBound) { |
| 681 trackDetail = d; |
| 682 max = mid - 1; |
| 683 } else { |
| 684 min = mid + 1; |
| 685 } |
| 686 } |
| 687 if (trackDetail) { |
| 688 var dx = event.detail.dx - trackDetail.dx; |
| 689 var dt = now - trackDetail.timeStamp || 1; |
| 690 return dx / dt; |
| 691 } |
| 692 return 0; |
| 693 }, |
| 694 _flingDrawer: function(event, trackDetails) { |
| 695 var velocity = this._calculateVelocity(event, trackDetails); |
| 696 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) { |
| 697 return; |
| 698 } |
| 699 this._drawerState = this._DRAWER_STATE.FLINGING; |
| 700 var x = event.detail.dx + this._translateOffset; |
| 701 var drawerWidth = this.getWidth(); |
| 702 var isPositionLeft = this.position === 'left'; |
| 703 var isVelocityPositive = velocity > 0; |
| 704 var isClosingLeft = !isVelocityPositive && isPositionLeft; |
| 705 var isClosingRight = isVelocityPositive && !isPositionLeft; |
| 706 var dx; |
| 707 if (isClosingLeft) { |
| 708 dx = -(x + drawerWidth); |
| 709 } else if (isClosingRight) { |
| 710 dx = drawerWidth - x; |
| 711 } else { |
| 712 dx = -x; |
| 713 } |
| 714 if (isVelocityPositive) { |
| 715 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY); |
| 716 this.opened = this.position === 'left'; |
| 717 } else { |
| 718 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY); |
| 719 this.opened = this.position === 'right'; |
| 720 } |
| 721 this._setTransitionDuration(this._FLING_INITIAL_SLOPE * dx / velocity + 'ms'
); |
| 722 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION); |
| 723 this._resetDrawerTranslate(); |
| 724 }, |
| 725 _transitionend: function(event) { |
| 726 var target = Polymer.dom(event).rootTarget; |
| 727 if (target === this.$.contentContainer || target === this.$.scrim) { |
| 728 if (this._drawerState === this._DRAWER_STATE.FLINGING) { |
| 729 this._setTransitionDuration(''); |
| 730 this._setTransitionTimingFunction(''); |
| 731 this.style.visibility = ''; |
| 732 } |
| 733 this._resetDrawerState(); |
| 734 } |
| 735 }, |
| 736 _setTransitionDuration: function(duration) { |
| 737 this.$.contentContainer.style.transitionDuration = duration; |
| 738 this.$.scrim.style.transitionDuration = duration; |
| 739 }, |
| 740 _setTransitionTimingFunction: function(timingFunction) { |
| 741 this.$.contentContainer.style.transitionTimingFunction = timingFunction; |
| 742 this.$.scrim.style.transitionTimingFunction = timingFunction; |
| 743 }, |
| 744 _translateDrawer: function(x) { |
| 745 var drawerWidth = this.getWidth(); |
| 746 if (this.position === 'left') { |
| 747 x = Math.max(-drawerWidth, Math.min(x, 0)); |
| 748 this.$.scrim.style.opacity = 1 + x / drawerWidth; |
| 749 } else { |
| 750 x = Math.max(0, Math.min(x, drawerWidth)); |
| 751 this.$.scrim.style.opacity = 1 - x / drawerWidth; |
| 752 } |
| 753 this.translate3d(x + 'px', '0', '0', this.$.contentContainer); |
| 754 }, |
| 755 _resetDrawerTranslate: function() { |
| 756 this.$.scrim.style.opacity = ''; |
| 757 this.transform('', this.$.contentContainer); |
| 758 }, |
| 759 _resetDrawerState: function() { |
| 760 var oldState = this._drawerState; |
| 761 if (this.opened) { |
| 762 this._drawerState = this.persistent ? this._DRAWER_STATE.OPENED_PERSISTENT
: this._DRAWER_STATE.OPENED; |
| 763 } else { |
| 764 this._drawerState = this._DRAWER_STATE.CLOSED; |
| 765 } |
| 766 if (oldState !== this._drawerState) { |
| 767 if (this._drawerState === this._DRAWER_STATE.OPENED) { |
| 768 this._setKeyboardFocusTrap(); |
| 769 document.addEventListener('keydown', this._boundEscKeydownHandler); |
| 770 document.body.style.overflow = 'hidden'; |
| 771 } else { |
1179 document.removeEventListener('keydown', this._boundEscKeydownHandler); | 772 document.removeEventListener('keydown', this._boundEscKeydownHandler); |
1180 }, | 773 document.body.style.overflow = ''; |
1181 | 774 } |
1182 /** | 775 if (oldState !== this._DRAWER_STATE.INIT) { |
1183 * Opens the drawer. | 776 this.fire('app-drawer-transitioned'); |
1184 */ | 777 } |
1185 open: function() { | 778 } |
1186 this.opened = true; | 779 }, |
1187 }, | 780 _setKeyboardFocusTrap: function() { |
1188 | 781 if (this.noFocusTrap) { |
1189 /** | 782 return; |
1190 * Closes the drawer. | 783 } |
1191 */ | 784 var focusableElementsSelector = [ 'a[href]:not([tabindex="-1"])', 'area[href
]:not([tabindex="-1"])', 'input:not([disabled]):not([tabindex="-1"])', 'select:n
ot([disabled]):not([tabindex="-1"])', 'textarea:not([disabled]):not([tabindex="-
1"])', 'button:not([disabled]):not([tabindex="-1"])', 'iframe:not([tabindex="-1"
])', '[tabindex]:not([tabindex="-1"])', '[contentEditable=true]:not([tabindex="-
1"])' ].join(','); |
1192 close: function() { | 785 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElements
Selector); |
1193 this.opened = false; | 786 if (focusableElements.length > 0) { |
1194 }, | 787 this._firstTabStop = focusableElements[0]; |
1195 | 788 this._lastTabStop = focusableElements[focusableElements.length - 1]; |
1196 /** | 789 } else { |
1197 * Toggles the drawer open and close. | 790 this._firstTabStop = null; |
1198 */ | 791 this._lastTabStop = null; |
1199 toggle: function() { | 792 } |
1200 this.opened = !this.opened; | 793 var tabindex = this.getAttribute('tabindex'); |
1201 }, | 794 if (tabindex && parseInt(tabindex, 10) > -1) { |
1202 | 795 this.focus(); |
1203 /** | 796 } else if (this._firstTabStop) { |
1204 * Gets the width of the drawer. | 797 this._firstTabStop.focus(); |
1205 * | 798 } |
1206 * @return {number} The width of the drawer in pixels. | 799 }, |
1207 */ | 800 _tabKeydownHandler: function(event) { |
1208 getWidth: function() { | 801 if (this.noFocusTrap) { |
1209 return this.$.contentContainer.offsetWidth; | 802 return; |
1210 }, | 803 } |
1211 | 804 var TAB_KEYCODE = 9; |
1212 /** | 805 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode === TAB
_KEYCODE) { |
1213 * Resets the layout. If you changed the size of app-header via CSS | 806 if (event.shiftKey) { |
1214 * you can notify the changes by either firing the `iron-resize` event | 807 if (this._firstTabStop && Polymer.dom(event).localTarget === this._first
TabStop) { |
1215 * or calling `resetLayout` directly. | |
1216 * | |
1217 * @method resetLayout | |
1218 */ | |
1219 resetLayout: function() { | |
1220 this.debounce('_resetLayout', function() { | |
1221 this.fire('app-drawer-reset-layout'); | |
1222 }, 1); | |
1223 }, | |
1224 | |
1225 _isRTL: function() { | |
1226 return window.getComputedStyle(this).direction === 'rtl'; | |
1227 }, | |
1228 | |
1229 _resetPosition: function() { | |
1230 switch (this.align) { | |
1231 case 'start': | |
1232 this._setPosition(this._isRTL() ? 'right' : 'left'); | |
1233 return; | |
1234 case 'end': | |
1235 this._setPosition(this._isRTL() ? 'left' : 'right'); | |
1236 return; | |
1237 } | |
1238 this._setPosition(this.align); | |
1239 }, | |
1240 | |
1241 _escKeydownHandler: function(event) { | |
1242 var ESC_KEYCODE = 27; | |
1243 if (event.keyCode === ESC_KEYCODE) { | |
1244 // Prevent any side effects if app-drawer closes. | |
1245 event.preventDefault(); | 808 event.preventDefault(); |
1246 this.close(); | 809 this._lastTabStop.focus(); |
1247 } | 810 } |
1248 }, | 811 } else { |
1249 | 812 if (this._lastTabStop && Polymer.dom(event).localTarget === this._lastTa
bStop) { |
1250 _track: function(event) { | 813 event.preventDefault(); |
1251 if (this.persistent) { | |
1252 return; | |
1253 } | |
1254 | |
1255 // Disable user selection on desktop. | |
1256 event.preventDefault(); | |
1257 | |
1258 switch (event.detail.state) { | |
1259 case 'start': | |
1260 this._trackStart(event); | |
1261 break; | |
1262 case 'track': | |
1263 this._trackMove(event); | |
1264 break; | |
1265 case 'end': | |
1266 this._trackEnd(event); | |
1267 break; | |
1268 } | |
1269 }, | |
1270 | |
1271 _trackStart: function(event) { | |
1272 this._drawerState = this._DRAWER_STATE.TRACKING; | |
1273 | |
1274 // Disable transitions since style attributes will reflect user track ev
ents. | |
1275 this._setTransitionDuration('0s'); | |
1276 this.style.visibility = 'visible'; | |
1277 | |
1278 var rect = this.$.contentContainer.getBoundingClientRect(); | |
1279 if (this.position === 'left') { | |
1280 this._translateOffset = rect.left; | |
1281 } else { | |
1282 this._translateOffset = rect.right - window.innerWidth; | |
1283 } | |
1284 | |
1285 this._trackDetails = []; | |
1286 }, | |
1287 | |
1288 _trackMove: function(event) { | |
1289 this._translateDrawer(event.detail.dx + this._translateOffset); | |
1290 | |
1291 // Use Date.now() since event.timeStamp is inconsistent across browsers
(e.g. most | |
1292 // browsers use milliseconds but FF 44 uses microseconds). | |
1293 this._trackDetails.push({ | |
1294 dx: event.detail.dx, | |
1295 timeStamp: Date.now() | |
1296 }); | |
1297 }, | |
1298 | |
1299 _trackEnd: function(event) { | |
1300 var x = event.detail.dx + this._translateOffset; | |
1301 var drawerWidth = this.getWidth(); | |
1302 var isPositionLeft = this.position === 'left'; | |
1303 var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) : | |
1304 (x <= 0 || x >= drawerWidth); | |
1305 | |
1306 if (!isInEndState) { | |
1307 // No longer need the track events after this method returns - allow t
hem to be GC'd. | |
1308 var trackDetails = this._trackDetails; | |
1309 this._trackDetails = null; | |
1310 | |
1311 this._flingDrawer(event, trackDetails); | |
1312 if (this._drawerState === this._DRAWER_STATE.FLINGING) { | |
1313 return; | |
1314 } | |
1315 } | |
1316 | |
1317 // If the drawer is not flinging, toggle the opened state based on the p
osition of | |
1318 // the drawer. | |
1319 var halfWidth = drawerWidth / 2; | |
1320 if (event.detail.dx < -halfWidth) { | |
1321 this.opened = this.position === 'right'; | |
1322 } else if (event.detail.dx > halfWidth) { | |
1323 this.opened = this.position === 'left'; | |
1324 } | |
1325 | |
1326 // Trigger app-drawer-transitioned now since there will be no transition
end event. | |
1327 if (isInEndState) { | |
1328 this._resetDrawerState(); | |
1329 } | |
1330 | |
1331 this._setTransitionDuration(''); | |
1332 this._resetDrawerTranslate(); | |
1333 this.style.visibility = ''; | |
1334 }, | |
1335 | |
1336 _calculateVelocity: function(event, trackDetails) { | |
1337 // Find the oldest track event that is within 100ms using binary search. | |
1338 var now = Date.now(); | |
1339 var timeLowerBound = now - 100; | |
1340 var trackDetail; | |
1341 var min = 0; | |
1342 var max = trackDetails.length - 1; | |
1343 | |
1344 while (min <= max) { | |
1345 // Floor of average of min and max. | |
1346 var mid = (min + max) >> 1; | |
1347 var d = trackDetails[mid]; | |
1348 if (d.timeStamp >= timeLowerBound) { | |
1349 trackDetail = d; | |
1350 max = mid - 1; | |
1351 } else { | |
1352 min = mid + 1; | |
1353 } | |
1354 } | |
1355 | |
1356 if (trackDetail) { | |
1357 var dx = event.detail.dx - trackDetail.dx; | |
1358 var dt = (now - trackDetail.timeStamp) || 1; | |
1359 return dx / dt; | |
1360 } | |
1361 return 0; | |
1362 }, | |
1363 | |
1364 _flingDrawer: function(event, trackDetails) { | |
1365 var velocity = this._calculateVelocity(event, trackDetails); | |
1366 | |
1367 // Do not fling if velocity is not above a threshold. | |
1368 if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) { | |
1369 return; | |
1370 } | |
1371 | |
1372 this._drawerState = this._DRAWER_STATE.FLINGING; | |
1373 | |
1374 var x = event.detail.dx + this._translateOffset; | |
1375 var drawerWidth = this.getWidth(); | |
1376 var isPositionLeft = this.position === 'left'; | |
1377 var isVelocityPositive = velocity > 0; | |
1378 var isClosingLeft = !isVelocityPositive && isPositionLeft; | |
1379 var isClosingRight = isVelocityPositive && !isPositionLeft; | |
1380 var dx; | |
1381 if (isClosingLeft) { | |
1382 dx = -(x + drawerWidth); | |
1383 } else if (isClosingRight) { | |
1384 dx = (drawerWidth - x); | |
1385 } else { | |
1386 dx = -x; | |
1387 } | |
1388 | |
1389 // Enforce a minimum transition velocity to make the drawer feel snappy. | |
1390 if (isVelocityPositive) { | |
1391 velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY); | |
1392 this.opened = this.position === 'left'; | |
1393 } else { | |
1394 velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY); | |
1395 this.opened = this.position === 'right'; | |
1396 } | |
1397 | |
1398 // Calculate the amount of time needed to finish the transition based on
the | |
1399 // initial slope of the timing function. | |
1400 this._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity)
+ 'ms'); | |
1401 this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION); | |
1402 | |
1403 this._resetDrawerTranslate(); | |
1404 }, | |
1405 | |
1406 _transitionend: function(event) { | |
1407 // contentContainer will transition on opened state changed, and scrim w
ill | |
1408 // transition on persistent state changed when opened - these are the | |
1409 // transitions we are interested in. | |
1410 var target = Polymer.dom(event).rootTarget; | |
1411 if (target === this.$.contentContainer || target === this.$.scrim) { | |
1412 | |
1413 // If the drawer was flinging, we need to reset the style attributes. | |
1414 if (this._drawerState === this._DRAWER_STATE.FLINGING) { | |
1415 this._setTransitionDuration(''); | |
1416 this._setTransitionTimingFunction(''); | |
1417 this.style.visibility = ''; | |
1418 } | |
1419 | |
1420 this._resetDrawerState(); | |
1421 } | |
1422 }, | |
1423 | |
1424 _setTransitionDuration: function(duration) { | |
1425 this.$.contentContainer.style.transitionDuration = duration; | |
1426 this.$.scrim.style.transitionDuration = duration; | |
1427 }, | |
1428 | |
1429 _setTransitionTimingFunction: function(timingFunction) { | |
1430 this.$.contentContainer.style.transitionTimingFunction = timingFunction; | |
1431 this.$.scrim.style.transitionTimingFunction = timingFunction; | |
1432 }, | |
1433 | |
1434 _translateDrawer: function(x) { | |
1435 var drawerWidth = this.getWidth(); | |
1436 | |
1437 if (this.position === 'left') { | |
1438 x = Math.max(-drawerWidth, Math.min(x, 0)); | |
1439 this.$.scrim.style.opacity = 1 + x / drawerWidth; | |
1440 } else { | |
1441 x = Math.max(0, Math.min(x, drawerWidth)); | |
1442 this.$.scrim.style.opacity = 1 - x / drawerWidth; | |
1443 } | |
1444 | |
1445 this.translate3d(x + 'px', '0', '0', this.$.contentContainer); | |
1446 }, | |
1447 | |
1448 _resetDrawerTranslate: function() { | |
1449 this.$.scrim.style.opacity = ''; | |
1450 this.transform('', this.$.contentContainer); | |
1451 }, | |
1452 | |
1453 _resetDrawerState: function() { | |
1454 var oldState = this._drawerState; | |
1455 if (this.opened) { | |
1456 this._drawerState = this.persistent ? | |
1457 this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED; | |
1458 } else { | |
1459 this._drawerState = this._DRAWER_STATE.CLOSED; | |
1460 } | |
1461 | |
1462 if (oldState !== this._drawerState) { | |
1463 if (this._drawerState === this._DRAWER_STATE.OPENED) { | |
1464 this._setKeyboardFocusTrap(); | |
1465 document.addEventListener('keydown', this._boundEscKeydownHandler); | |
1466 document.body.style.overflow = 'hidden'; | |
1467 } else { | |
1468 document.removeEventListener('keydown', this._boundEscKeydownHandler
); | |
1469 document.body.style.overflow = ''; | |
1470 } | |
1471 | |
1472 // Don't fire the event on initial load. | |
1473 if (oldState !== this._DRAWER_STATE.INIT) { | |
1474 this.fire('app-drawer-transitioned'); | |
1475 } | |
1476 } | |
1477 }, | |
1478 | |
1479 _setKeyboardFocusTrap: function() { | |
1480 if (this.noFocusTrap) { | |
1481 return; | |
1482 } | |
1483 | |
1484 // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated)
, this will | |
1485 // not select focusable elements inside shadow roots. | |
1486 var focusableElementsSelector = [ | |
1487 'a[href]:not([tabindex="-1"])', | |
1488 'area[href]:not([tabindex="-1"])', | |
1489 'input:not([disabled]):not([tabindex="-1"])', | |
1490 'select:not([disabled]):not([tabindex="-1"])', | |
1491 'textarea:not([disabled]):not([tabindex="-1"])', | |
1492 'button:not([disabled]):not([tabindex="-1"])', | |
1493 'iframe:not([tabindex="-1"])', | |
1494 '[tabindex]:not([tabindex="-1"])', | |
1495 '[contentEditable=true]:not([tabindex="-1"])' | |
1496 ].join(','); | |
1497 var focusableElements = Polymer.dom(this).querySelectorAll(focusableElem
entsSelector); | |
1498 | |
1499 if (focusableElements.length > 0) { | |
1500 this._firstTabStop = focusableElements[0]; | |
1501 this._lastTabStop = focusableElements[focusableElements.length - 1]; | |
1502 } else { | |
1503 // Reset saved tab stops when there are no focusable elements in the d
rawer. | |
1504 this._firstTabStop = null; | |
1505 this._lastTabStop = null; | |
1506 } | |
1507 | |
1508 // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the
first focusable | |
1509 // element in the drawer, if it exists. Use the tabindex attribute since
the this.tabIndex | |
1510 // property in IE/Edge returns 0 (instead of -1) when the attribute is n
ot set. | |
1511 var tabindex = this.getAttribute('tabindex'); | |
1512 if (tabindex && parseInt(tabindex, 10) > -1) { | |
1513 this.focus(); | |
1514 } else if (this._firstTabStop) { | |
1515 this._firstTabStop.focus(); | 814 this._firstTabStop.focus(); |
1516 } | 815 } |
1517 }, | 816 } |
1518 | 817 } |
1519 _tabKeydownHandler: function(event) { | 818 }, |
1520 if (this.noFocusTrap) { | 819 _MIN_FLING_THRESHOLD: .2, |
1521 return; | 820 _MIN_TRANSITION_VELOCITY: 1.2, |
1522 } | 821 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)', |
1523 | 822 _FLING_INITIAL_SLOPE: 1.5, |
1524 var TAB_KEYCODE = 9; | 823 _DRAWER_STATE: { |
1525 if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode ===
TAB_KEYCODE) { | 824 INIT: 0, |
1526 if (event.shiftKey) { | 825 OPENED: 1, |
1527 if (this._firstTabStop && Polymer.dom(event).localTarget === this._f
irstTabStop) { | 826 OPENED_PERSISTENT: 2, |
1528 event.preventDefault(); | 827 CLOSED: 3, |
1529 this._lastTabStop.focus(); | 828 TRACKING: 4, |
1530 } | 829 FLINGING: 5 |
1531 } else { | 830 } |
1532 if (this._lastTabStop && Polymer.dom(event).localTarget === this._la
stTabStop) { | 831 }); |
1533 event.preventDefault(); | 832 |
1534 this._firstTabStop.focus(); | |
1535 } | |
1536 } | |
1537 } | |
1538 }, | |
1539 | |
1540 _MIN_FLING_THRESHOLD: 0.2, | |
1541 | |
1542 _MIN_TRANSITION_VELOCITY: 1.2, | |
1543 | |
1544 _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)', | |
1545 | |
1546 _FLING_INITIAL_SLOPE: 1.5, | |
1547 | |
1548 _DRAWER_STATE: { | |
1549 INIT: 0, | |
1550 OPENED: 1, | |
1551 OPENED_PERSISTENT: 2, | |
1552 CLOSED: 3, | |
1553 TRACKING: 4, | |
1554 FLINGING: 5 | |
1555 } | |
1556 | |
1557 /** | |
1558 * Fired when the layout of app-drawer has changed. | |
1559 * | |
1560 * @event app-drawer-reset-layout | |
1561 */ | |
1562 | |
1563 /** | |
1564 * Fired when app-drawer has finished transitioning. | |
1565 * | |
1566 * @event app-drawer-transitioned | |
1567 */ | |
1568 }); | |
1569 (function() { | 833 (function() { |
1570 'use strict'; | 834 'use strict'; |
1571 | |
1572 Polymer({ | |
1573 is: 'iron-location', | |
1574 properties: { | |
1575 /** | |
1576 * The pathname component of the URL. | |
1577 */ | |
1578 path: { | |
1579 type: String, | |
1580 notify: true, | |
1581 value: function() { | |
1582 return window.decodeURIComponent(window.location.pathname); | |
1583 } | |
1584 }, | |
1585 /** | |
1586 * The query string portion of the URL. | |
1587 */ | |
1588 query: { | |
1589 type: String, | |
1590 notify: true, | |
1591 value: function() { | |
1592 return window.decodeURIComponent(window.location.search.slice(1)); | |
1593 } | |
1594 }, | |
1595 /** | |
1596 * The hash component of the URL. | |
1597 */ | |
1598 hash: { | |
1599 type: String, | |
1600 notify: true, | |
1601 value: function() { | |
1602 return window.decodeURIComponent(window.location.hash.slice(1)); | |
1603 } | |
1604 }, | |
1605 /** | |
1606 * If the user was on a URL for less than `dwellTime` milliseconds, it | |
1607 * won't be added to the browser's history, but instead will be replaced | |
1608 * by the next entry. | |
1609 * | |
1610 * This is to prevent large numbers of entries from clogging up the user
's | |
1611 * browser history. Disable by setting to a negative number. | |
1612 */ | |
1613 dwellTime: { | |
1614 type: Number, | |
1615 value: 2000 | |
1616 }, | |
1617 | |
1618 /** | |
1619 * A regexp that defines the set of URLs that should be considered part | |
1620 * of this web app. | |
1621 * | |
1622 * Clicking on a link that matches this regex won't result in a full pag
e | |
1623 * navigation, but will instead just update the URL state in place. | |
1624 * | |
1625 * This regexp is given everything after the origin in an absolute | |
1626 * URL. So to match just URLs that start with /search/ do: | |
1627 * url-space-regex="^/search/" | |
1628 * | |
1629 * @type {string|RegExp} | |
1630 */ | |
1631 urlSpaceRegex: { | |
1632 type: String, | |
1633 value: '' | |
1634 }, | |
1635 | |
1636 /** | |
1637 * urlSpaceRegex, but coerced into a regexp. | |
1638 * | |
1639 * @type {RegExp} | |
1640 */ | |
1641 _urlSpaceRegExp: { | |
1642 computed: '_makeRegExp(urlSpaceRegex)' | |
1643 }, | |
1644 | |
1645 _lastChangedAt: { | |
1646 type: Number | |
1647 }, | |
1648 | |
1649 _initialized: { | |
1650 type: Boolean, | |
1651 value: false | |
1652 } | |
1653 }, | |
1654 hostAttributes: { | |
1655 hidden: true | |
1656 }, | |
1657 observers: [ | |
1658 '_updateUrl(path, query, hash)' | |
1659 ], | |
1660 attached: function() { | |
1661 this.listen(window, 'hashchange', '_hashChanged'); | |
1662 this.listen(window, 'location-changed', '_urlChanged'); | |
1663 this.listen(window, 'popstate', '_urlChanged'); | |
1664 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_g
lobalOnClick'); | |
1665 // Give a 200ms grace period to make initial redirects without any | |
1666 // additions to the user's history. | |
1667 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200); | |
1668 | |
1669 this._initialized = true; | |
1670 this._urlChanged(); | |
1671 }, | |
1672 detached: function() { | |
1673 this.unlisten(window, 'hashchange', '_hashChanged'); | |
1674 this.unlisten(window, 'location-changed', '_urlChanged'); | |
1675 this.unlisten(window, 'popstate', '_urlChanged'); | |
1676 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', '
_globalOnClick'); | |
1677 this._initialized = false; | |
1678 }, | |
1679 _hashChanged: function() { | |
1680 this.hash = window.decodeURIComponent(window.location.hash.substring(1))
; | |
1681 }, | |
1682 _urlChanged: function() { | |
1683 // We want to extract all info out of the updated URL before we | |
1684 // try to write anything back into it. | |
1685 // | |
1686 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old | |
1687 // one when we set this.hash. Likewise for query. | |
1688 this._dontUpdateUrl = true; | |
1689 this._hashChanged(); | |
1690 this.path = window.decodeURIComponent(window.location.pathname); | |
1691 this.query = window.decodeURIComponent( | |
1692 window.location.search.substring(1)); | |
1693 this._dontUpdateUrl = false; | |
1694 this._updateUrl(); | |
1695 }, | |
1696 _getUrl: function() { | |
1697 var partiallyEncodedPath = window.encodeURI( | |
1698 this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F'); | |
1699 var partiallyEncodedQuery = ''; | |
1700 if (this.query) { | |
1701 partiallyEncodedQuery = '?' + window.encodeURI( | |
1702 this.query).replace(/\#/g, '%23'); | |
1703 } | |
1704 var partiallyEncodedHash = ''; | |
1705 if (this.hash) { | |
1706 partiallyEncodedHash = '#' + window.encodeURI(this.hash); | |
1707 } | |
1708 return ( | |
1709 partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash)
; | |
1710 }, | |
1711 _updateUrl: function() { | |
1712 if (this._dontUpdateUrl || !this._initialized) { | |
1713 return; | |
1714 } | |
1715 if (this.path === window.decodeURIComponent(window.location.pathname) && | |
1716 this.query === window.decodeURIComponent( | |
1717 window.location.search.substring(1)) && | |
1718 this.hash === window.decodeURIComponent( | |
1719 window.location.hash.substring(1))) { | |
1720 // Nothing to do, the current URL is a representation of our propertie
s. | |
1721 return; | |
1722 } | |
1723 var newUrl = this._getUrl(); | |
1724 // Need to use a full URL in case the containing page has a base URI. | |
1725 var fullNewUrl = new URL( | |
1726 newUrl, window.location.protocol + '//' + window.location.host).href
; | |
1727 var now = window.performance.now(); | |
1728 var shouldReplace = | |
1729 this._lastChangedAt + this.dwellTime > now; | |
1730 this._lastChangedAt = now; | |
1731 if (shouldReplace) { | |
1732 window.history.replaceState({}, '', fullNewUrl); | |
1733 } else { | |
1734 window.history.pushState({}, '', fullNewUrl); | |
1735 } | |
1736 this.fire('location-changed', {}, {node: window}); | |
1737 }, | |
1738 /** | |
1739 * A necessary evil so that links work as expected. Does its best to | |
1740 * bail out early if possible. | |
1741 * | |
1742 * @param {MouseEvent} event . | |
1743 */ | |
1744 _globalOnClick: function(event) { | |
1745 // If another event handler has stopped this event then there's nothing | |
1746 // for us to do. This can happen e.g. when there are multiple | |
1747 // iron-location elements in a page. | |
1748 if (event.defaultPrevented) { | |
1749 return; | |
1750 } | |
1751 var href = this._getSameOriginLinkHref(event); | |
1752 if (!href) { | |
1753 return; | |
1754 } | |
1755 event.preventDefault(); | |
1756 // If the navigation is to the current page we shouldn't add a history | |
1757 // entry or fire a change event. | |
1758 if (href === window.location.href) { | |
1759 return; | |
1760 } | |
1761 window.history.pushState({}, '', href); | |
1762 this.fire('location-changed', {}, {node: window}); | |
1763 }, | |
1764 /** | |
1765 * Returns the absolute URL of the link (if any) that this click event | |
1766 * is clicking on, if we can and should override the resulting full | |
1767 * page navigation. Returns null otherwise. | |
1768 * | |
1769 * @param {MouseEvent} event . | |
1770 * @return {string?} . | |
1771 */ | |
1772 _getSameOriginLinkHref: function(event) { | |
1773 // We only care about left-clicks. | |
1774 if (event.button !== 0) { | |
1775 return null; | |
1776 } | |
1777 // We don't want modified clicks, where the intent is to open the page | |
1778 // in a new tab. | |
1779 if (event.metaKey || event.ctrlKey) { | |
1780 return null; | |
1781 } | |
1782 var eventPath = Polymer.dom(event).path; | |
1783 var anchor = null; | |
1784 for (var i = 0; i < eventPath.length; i++) { | |
1785 var element = eventPath[i]; | |
1786 if (element.tagName === 'A' && element.href) { | |
1787 anchor = element; | |
1788 break; | |
1789 } | |
1790 } | |
1791 | |
1792 // If there's no link there's nothing to do. | |
1793 if (!anchor) { | |
1794 return null; | |
1795 } | |
1796 | |
1797 // Target blank is a new tab, don't intercept. | |
1798 if (anchor.target === '_blank') { | |
1799 return null; | |
1800 } | |
1801 // If the link is for an existing parent frame, don't intercept. | |
1802 if ((anchor.target === '_top' || | |
1803 anchor.target === '_parent') && | |
1804 window.top !== window) { | |
1805 return null; | |
1806 } | |
1807 | |
1808 var href = anchor.href; | |
1809 | |
1810 // It only makes sense for us to intercept same-origin navigations. | |
1811 // pushState/replaceState don't work with cross-origin links. | |
1812 var url; | |
1813 if (document.baseURI != null) { | |
1814 url = new URL(href, /** @type {string} */(document.baseURI)); | |
1815 } else { | |
1816 url = new URL(href); | |
1817 } | |
1818 | |
1819 var origin; | |
1820 | |
1821 // IE Polyfill | |
1822 if (window.location.origin) { | |
1823 origin = window.location.origin; | |
1824 } else { | |
1825 origin = window.location.protocol + '//' + window.location.hostname; | |
1826 | |
1827 if (window.location.port) { | |
1828 origin += ':' + window.location.port; | |
1829 } | |
1830 } | |
1831 | |
1832 if (url.origin !== origin) { | |
1833 return null; | |
1834 } | |
1835 var normalizedHref = url.pathname + url.search + url.hash; | |
1836 | |
1837 // If we've been configured not to handle this url... don't handle it! | |
1838 if (this._urlSpaceRegExp && | |
1839 !this._urlSpaceRegExp.test(normalizedHref)) { | |
1840 return null; | |
1841 } | |
1842 // Need to use a full URL in case the containing page has a base URI. | |
1843 var fullNormalizedHref = new URL( | |
1844 normalizedHref, window.location.href).href; | |
1845 return fullNormalizedHref; | |
1846 }, | |
1847 _makeRegExp: function(urlSpaceRegex) { | |
1848 return RegExp(urlSpaceRegex); | |
1849 } | |
1850 }); | |
1851 })(); | |
1852 'use strict'; | |
1853 | |
1854 Polymer({ | 835 Polymer({ |
1855 is: 'iron-query-params', | 836 is: 'iron-location', |
1856 properties: { | 837 properties: { |
1857 paramsString: { | 838 path: { |
1858 type: String, | 839 type: String, |
1859 notify: true, | 840 notify: true, |
1860 observer: 'paramsStringChanged', | 841 value: function() { |
1861 }, | 842 return window.decodeURIComponent(window.location.pathname); |
1862 paramsObject: { | 843 } |
1863 type: Object, | 844 }, |
| 845 query: { |
| 846 type: String, |
1864 notify: true, | 847 notify: true, |
1865 value: function() { | 848 value: function() { |
1866 return {}; | 849 return window.decodeURIComponent(window.location.search.slice(1)); |
1867 } | 850 } |
1868 }, | 851 }, |
1869 _dontReact: { | 852 hash: { |
| 853 type: String, |
| 854 notify: true, |
| 855 value: function() { |
| 856 return window.decodeURIComponent(window.location.hash.slice(1)); |
| 857 } |
| 858 }, |
| 859 dwellTime: { |
| 860 type: Number, |
| 861 value: 2e3 |
| 862 }, |
| 863 urlSpaceRegex: { |
| 864 type: String, |
| 865 value: '' |
| 866 }, |
| 867 _urlSpaceRegExp: { |
| 868 computed: '_makeRegExp(urlSpaceRegex)' |
| 869 }, |
| 870 _lastChangedAt: { |
| 871 type: Number |
| 872 }, |
| 873 _initialized: { |
1870 type: Boolean, | 874 type: Boolean, |
1871 value: false | 875 value: false |
1872 } | 876 } |
1873 }, | 877 }, |
1874 hostAttributes: { | 878 hostAttributes: { |
1875 hidden: true | 879 hidden: true |
1876 }, | 880 }, |
1877 observers: [ | 881 observers: [ '_updateUrl(path, query, hash)' ], |
1878 'paramsObjectChanged(paramsObject.*)' | 882 attached: function() { |
1879 ], | 883 this.listen(window, 'hashchange', '_hashChanged'); |
1880 paramsStringChanged: function() { | 884 this.listen(window, 'location-changed', '_urlChanged'); |
1881 this._dontReact = true; | 885 this.listen(window, 'popstate', '_urlChanged'); |
1882 this.paramsObject = this._decodeParams(this.paramsString); | 886 this.listen(document.body, 'click', '_globalOnClick'); |
1883 this._dontReact = false; | 887 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200); |
1884 }, | 888 this._initialized = true; |
1885 paramsObjectChanged: function() { | 889 this._urlChanged(); |
1886 if (this._dontReact) { | 890 }, |
| 891 detached: function() { |
| 892 this.unlisten(window, 'hashchange', '_hashChanged'); |
| 893 this.unlisten(window, 'location-changed', '_urlChanged'); |
| 894 this.unlisten(window, 'popstate', '_urlChanged'); |
| 895 this.unlisten(document.body, 'click', '_globalOnClick'); |
| 896 this._initialized = false; |
| 897 }, |
| 898 _hashChanged: function() { |
| 899 this.hash = window.decodeURIComponent(window.location.hash.substring(1)); |
| 900 }, |
| 901 _urlChanged: function() { |
| 902 this._dontUpdateUrl = true; |
| 903 this._hashChanged(); |
| 904 this.path = window.decodeURIComponent(window.location.pathname); |
| 905 this.query = window.decodeURIComponent(window.location.search.substring(1)
); |
| 906 this._dontUpdateUrl = false; |
| 907 this._updateUrl(); |
| 908 }, |
| 909 _getUrl: function() { |
| 910 var partiallyEncodedPath = window.encodeURI(this.path).replace(/\#/g, '%23
').replace(/\?/g, '%3F'); |
| 911 var partiallyEncodedQuery = ''; |
| 912 if (this.query) { |
| 913 partiallyEncodedQuery = '?' + window.encodeURI(this.query).replace(/\#/g
, '%23'); |
| 914 } |
| 915 var partiallyEncodedHash = ''; |
| 916 if (this.hash) { |
| 917 partiallyEncodedHash = '#' + window.encodeURI(this.hash); |
| 918 } |
| 919 return partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash
; |
| 920 }, |
| 921 _updateUrl: function() { |
| 922 if (this._dontUpdateUrl || !this._initialized) { |
1887 return; | 923 return; |
1888 } | 924 } |
1889 this.paramsString = this._encodeParams(this.paramsObject); | 925 if (this.path === window.decodeURIComponent(window.location.pathname) && t
his.query === window.decodeURIComponent(window.location.search.substring(1)) &&
this.hash === window.decodeURIComponent(window.location.hash.substring(1))) { |
1890 }, | 926 return; |
1891 _encodeParams: function(params) { | 927 } |
1892 var encodedParams = []; | 928 var newUrl = this._getUrl(); |
1893 for (var key in params) { | 929 var fullNewUrl = new URL(newUrl, window.location.protocol + '//' + window.
location.host).href; |
1894 var value = params[key]; | 930 var now = window.performance.now(); |
1895 if (value === '') { | 931 var shouldReplace = this._lastChangedAt + this.dwellTime > now; |
1896 encodedParams.push(encodeURIComponent(key)); | 932 this._lastChangedAt = now; |
1897 } else if (value) { | 933 if (shouldReplace) { |
1898 encodedParams.push( | 934 window.history.replaceState({}, '', fullNewUrl); |
1899 encodeURIComponent(key) + | 935 } else { |
1900 '=' + | 936 window.history.pushState({}, '', fullNewUrl); |
1901 encodeURIComponent(value.toString()) | 937 } |
1902 ); | 938 this.fire('location-changed', {}, { |
| 939 node: window |
| 940 }); |
| 941 }, |
| 942 _globalOnClick: function(event) { |
| 943 if (event.defaultPrevented) { |
| 944 return; |
| 945 } |
| 946 var href = this._getSameOriginLinkHref(event); |
| 947 if (!href) { |
| 948 return; |
| 949 } |
| 950 event.preventDefault(); |
| 951 if (href === window.location.href) { |
| 952 return; |
| 953 } |
| 954 window.history.pushState({}, '', href); |
| 955 this.fire('location-changed', {}, { |
| 956 node: window |
| 957 }); |
| 958 }, |
| 959 _getSameOriginLinkHref: function(event) { |
| 960 if (event.button !== 0) { |
| 961 return null; |
| 962 } |
| 963 if (event.metaKey || event.ctrlKey) { |
| 964 return null; |
| 965 } |
| 966 var eventPath = Polymer.dom(event).path; |
| 967 var anchor = null; |
| 968 for (var i = 0; i < eventPath.length; i++) { |
| 969 var element = eventPath[i]; |
| 970 if (element.tagName === 'A' && element.href) { |
| 971 anchor = element; |
| 972 break; |
1903 } | 973 } |
1904 } | 974 } |
1905 return encodedParams.join('&'); | 975 if (!anchor) { |
1906 }, | 976 return null; |
1907 _decodeParams: function(paramString) { | 977 } |
1908 var params = {}; | 978 if (anchor.target === '_blank') { |
1909 | 979 return null; |
1910 // Work around a bug in decodeURIComponent where + is not | 980 } |
1911 // converted to spaces: | 981 if ((anchor.target === '_top' || anchor.target === '_parent') && window.to
p !== window) { |
1912 paramString = (paramString || '').replace(/\+/g, '%20'); | 982 return null; |
1913 | 983 } |
1914 var paramList = paramString.split('&'); | 984 var href = anchor.href; |
1915 for (var i = 0; i < paramList.length; i++) { | 985 var url; |
1916 var param = paramList[i].split('='); | 986 if (document.baseURI != null) { |
1917 if (param[0]) { | 987 url = new URL(href, document.baseURI); |
1918 params[decodeURIComponent(param[0])] = | 988 } else { |
1919 decodeURIComponent(param[1] || ''); | 989 url = new URL(href); |
| 990 } |
| 991 var origin; |
| 992 if (window.location.origin) { |
| 993 origin = window.location.origin; |
| 994 } else { |
| 995 origin = window.location.protocol + '//' + window.location.hostname; |
| 996 if (window.location.port) { |
| 997 origin += ':' + window.location.port; |
1920 } | 998 } |
1921 } | 999 } |
1922 return params; | 1000 if (url.origin !== origin) { |
| 1001 return null; |
| 1002 } |
| 1003 var normalizedHref = url.pathname + url.search + url.hash; |
| 1004 if (this._urlSpaceRegExp && !this._urlSpaceRegExp.test(normalizedHref)) { |
| 1005 return null; |
| 1006 } |
| 1007 var fullNormalizedHref = new URL(normalizedHref, window.location.href).hre
f; |
| 1008 return fullNormalizedHref; |
| 1009 }, |
| 1010 _makeRegExp: function(urlSpaceRegex) { |
| 1011 return RegExp(urlSpaceRegex); |
1923 } | 1012 } |
1924 }); | 1013 }); |
| 1014 })(); |
| 1015 |
1925 'use strict'; | 1016 'use strict'; |
1926 | 1017 |
1927 /** | 1018 Polymer({ |
1928 * Provides bidirectional mapping between `path` and `queryParams` and a | 1019 is: 'iron-query-params', |
1929 * app-route compatible `route` object. | 1020 properties: { |
1930 * | 1021 paramsString: { |
1931 * For more information, see the docs for `app-route-converter`. | 1022 type: String, |
1932 * | 1023 notify: true, |
1933 * @polymerBehavior | 1024 observer: 'paramsStringChanged' |
1934 */ | 1025 }, |
1935 Polymer.AppRouteConverterBehavior = { | 1026 paramsObject: { |
| 1027 type: Object, |
| 1028 notify: true, |
| 1029 value: function() { |
| 1030 return {}; |
| 1031 } |
| 1032 }, |
| 1033 _dontReact: { |
| 1034 type: Boolean, |
| 1035 value: false |
| 1036 } |
| 1037 }, |
| 1038 hostAttributes: { |
| 1039 hidden: true |
| 1040 }, |
| 1041 observers: [ 'paramsObjectChanged(paramsObject.*)' ], |
| 1042 paramsStringChanged: function() { |
| 1043 this._dontReact = true; |
| 1044 this.paramsObject = this._decodeParams(this.paramsString); |
| 1045 this._dontReact = false; |
| 1046 }, |
| 1047 paramsObjectChanged: function() { |
| 1048 if (this._dontReact) { |
| 1049 return; |
| 1050 } |
| 1051 this.paramsString = this._encodeParams(this.paramsObject); |
| 1052 }, |
| 1053 _encodeParams: function(params) { |
| 1054 var encodedParams = []; |
| 1055 for (var key in params) { |
| 1056 var value = params[key]; |
| 1057 if (value === '') { |
| 1058 encodedParams.push(encodeURIComponent(key)); |
| 1059 } else if (value) { |
| 1060 encodedParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(va
lue.toString())); |
| 1061 } |
| 1062 } |
| 1063 return encodedParams.join('&'); |
| 1064 }, |
| 1065 _decodeParams: function(paramString) { |
| 1066 var params = {}; |
| 1067 paramString = (paramString || '').replace(/\+/g, '%20'); |
| 1068 var paramList = paramString.split('&'); |
| 1069 for (var i = 0; i < paramList.length; i++) { |
| 1070 var param = paramList[i].split('='); |
| 1071 if (param[0]) { |
| 1072 params[decodeURIComponent(param[0])] = decodeURIComponent(param[1] || ''
); |
| 1073 } |
| 1074 } |
| 1075 return params; |
| 1076 } |
| 1077 }); |
| 1078 |
| 1079 'use strict'; |
| 1080 |
| 1081 Polymer.AppRouteConverterBehavior = { |
| 1082 properties: { |
| 1083 route: { |
| 1084 type: Object, |
| 1085 notify: true |
| 1086 }, |
| 1087 queryParams: { |
| 1088 type: Object, |
| 1089 notify: true |
| 1090 }, |
| 1091 path: { |
| 1092 type: String, |
| 1093 notify: true |
| 1094 } |
| 1095 }, |
| 1096 observers: [ '_locationChanged(path, queryParams)', '_routeChanged(route.prefi
x, route.path)', '_routeQueryParamsChanged(route.__queryParams)' ], |
| 1097 created: function() { |
| 1098 this.linkPaths('route.__queryParams', 'queryParams'); |
| 1099 this.linkPaths('queryParams', 'route.__queryParams'); |
| 1100 }, |
| 1101 _locationChanged: function() { |
| 1102 if (this.route && this.route.path === this.path && this.queryParams === this
.route.__queryParams) { |
| 1103 return; |
| 1104 } |
| 1105 this.route = { |
| 1106 prefix: '', |
| 1107 path: this.path, |
| 1108 __queryParams: this.queryParams |
| 1109 }; |
| 1110 }, |
| 1111 _routeChanged: function() { |
| 1112 if (!this.route) { |
| 1113 return; |
| 1114 } |
| 1115 this.path = this.route.prefix + this.route.path; |
| 1116 }, |
| 1117 _routeQueryParamsChanged: function(queryParams) { |
| 1118 if (!this.route) { |
| 1119 return; |
| 1120 } |
| 1121 this.queryParams = queryParams; |
| 1122 } |
| 1123 }; |
| 1124 |
| 1125 'use strict'; |
| 1126 |
| 1127 Polymer({ |
| 1128 is: 'app-location', |
| 1129 properties: { |
| 1130 route: { |
| 1131 type: Object, |
| 1132 notify: true |
| 1133 }, |
| 1134 useHashAsPath: { |
| 1135 type: Boolean, |
| 1136 value: false |
| 1137 }, |
| 1138 urlSpaceRegex: { |
| 1139 type: String, |
| 1140 notify: true |
| 1141 }, |
| 1142 __queryParams: { |
| 1143 type: Object |
| 1144 }, |
| 1145 __path: { |
| 1146 type: String |
| 1147 }, |
| 1148 __query: { |
| 1149 type: String |
| 1150 }, |
| 1151 __hash: { |
| 1152 type: String |
| 1153 }, |
| 1154 path: { |
| 1155 type: String, |
| 1156 observer: '__onPathChanged' |
| 1157 } |
| 1158 }, |
| 1159 behaviors: [ Polymer.AppRouteConverterBehavior ], |
| 1160 observers: [ '__computeRoutePath(useHashAsPath, __hash, __path)' ], |
| 1161 __computeRoutePath: function() { |
| 1162 this.path = this.useHashAsPath ? this.__hash : this.__path; |
| 1163 }, |
| 1164 __onPathChanged: function() { |
| 1165 if (!this._readied) { |
| 1166 return; |
| 1167 } |
| 1168 if (this.useHashAsPath) { |
| 1169 this.__hash = this.path; |
| 1170 } else { |
| 1171 this.__path = this.path; |
| 1172 } |
| 1173 } |
| 1174 }); |
| 1175 |
| 1176 'use strict'; |
| 1177 |
| 1178 Polymer({ |
| 1179 is: 'app-route', |
| 1180 properties: { |
| 1181 route: { |
| 1182 type: Object, |
| 1183 notify: true |
| 1184 }, |
| 1185 pattern: { |
| 1186 type: String |
| 1187 }, |
| 1188 data: { |
| 1189 type: Object, |
| 1190 value: function() { |
| 1191 return {}; |
| 1192 }, |
| 1193 notify: true |
| 1194 }, |
| 1195 queryParams: { |
| 1196 type: Object, |
| 1197 value: function() { |
| 1198 return {}; |
| 1199 }, |
| 1200 notify: true |
| 1201 }, |
| 1202 tail: { |
| 1203 type: Object, |
| 1204 value: function() { |
| 1205 return { |
| 1206 path: null, |
| 1207 prefix: null, |
| 1208 __queryParams: null |
| 1209 }; |
| 1210 }, |
| 1211 notify: true |
| 1212 }, |
| 1213 active: { |
| 1214 type: Boolean, |
| 1215 notify: true, |
| 1216 readOnly: true |
| 1217 }, |
| 1218 _queryParamsUpdating: { |
| 1219 type: Boolean, |
| 1220 value: false |
| 1221 }, |
| 1222 _matched: { |
| 1223 type: String, |
| 1224 value: '' |
| 1225 } |
| 1226 }, |
| 1227 observers: [ '__tryToMatch(route.path, pattern)', '__updatePathOnDataChange(da
ta.*)', '__tailPathChanged(tail.path)', '__routeQueryParamsChanged(route.__query
Params)', '__tailQueryParamsChanged(tail.__queryParams)', '__queryParamsChanged(
queryParams.*)' ], |
| 1228 created: function() { |
| 1229 this.linkPaths('route.__queryParams', 'tail.__queryParams'); |
| 1230 this.linkPaths('tail.__queryParams', 'route.__queryParams'); |
| 1231 }, |
| 1232 __routeQueryParamsChanged: function(queryParams) { |
| 1233 if (queryParams && this.tail) { |
| 1234 this.set('tail.__queryParams', queryParams); |
| 1235 if (!this.active || this._queryParamsUpdating) { |
| 1236 return; |
| 1237 } |
| 1238 var copyOfQueryParams = {}; |
| 1239 var anythingChanged = false; |
| 1240 for (var key in queryParams) { |
| 1241 copyOfQueryParams[key] = queryParams[key]; |
| 1242 if (anythingChanged || !this.queryParams || queryParams[key] !== this.qu
eryParams[key]) { |
| 1243 anythingChanged = true; |
| 1244 } |
| 1245 } |
| 1246 for (var key in this.queryParams) { |
| 1247 if (anythingChanged || !(key in queryParams)) { |
| 1248 anythingChanged = true; |
| 1249 break; |
| 1250 } |
| 1251 } |
| 1252 if (!anythingChanged) { |
| 1253 return; |
| 1254 } |
| 1255 this._queryParamsUpdating = true; |
| 1256 this.set('queryParams', copyOfQueryParams); |
| 1257 this._queryParamsUpdating = false; |
| 1258 } |
| 1259 }, |
| 1260 __tailQueryParamsChanged: function(queryParams) { |
| 1261 if (queryParams && this.route) { |
| 1262 this.set('route.__queryParams', queryParams); |
| 1263 } |
| 1264 }, |
| 1265 __queryParamsChanged: function(changes) { |
| 1266 if (!this.active || this._queryParamsUpdating) { |
| 1267 return; |
| 1268 } |
| 1269 this.set('route.__' + changes.path, changes.value); |
| 1270 }, |
| 1271 __resetProperties: function() { |
| 1272 this._setActive(false); |
| 1273 this._matched = null; |
| 1274 }, |
| 1275 __tryToMatch: function() { |
| 1276 if (!this.route) { |
| 1277 return; |
| 1278 } |
| 1279 var path = this.route.path; |
| 1280 var pattern = this.pattern; |
| 1281 if (!pattern) { |
| 1282 return; |
| 1283 } |
| 1284 if (!path) { |
| 1285 this.__resetProperties(); |
| 1286 return; |
| 1287 } |
| 1288 var remainingPieces = path.split('/'); |
| 1289 var patternPieces = pattern.split('/'); |
| 1290 var matched = []; |
| 1291 var namedMatches = {}; |
| 1292 for (var i = 0; i < patternPieces.length; i++) { |
| 1293 var patternPiece = patternPieces[i]; |
| 1294 if (!patternPiece && patternPiece !== '') { |
| 1295 break; |
| 1296 } |
| 1297 var pathPiece = remainingPieces.shift(); |
| 1298 if (!pathPiece && pathPiece !== '') { |
| 1299 this.__resetProperties(); |
| 1300 return; |
| 1301 } |
| 1302 matched.push(pathPiece); |
| 1303 if (patternPiece.charAt(0) == ':') { |
| 1304 namedMatches[patternPiece.slice(1)] = pathPiece; |
| 1305 } else if (patternPiece !== pathPiece) { |
| 1306 this.__resetProperties(); |
| 1307 return; |
| 1308 } |
| 1309 } |
| 1310 this._matched = matched.join('/'); |
| 1311 var propertyUpdates = {}; |
| 1312 if (!this.active) { |
| 1313 propertyUpdates.active = true; |
| 1314 } |
| 1315 var tailPrefix = this.route.prefix + this._matched; |
| 1316 var tailPath = remainingPieces.join('/'); |
| 1317 if (remainingPieces.length > 0) { |
| 1318 tailPath = '/' + tailPath; |
| 1319 } |
| 1320 if (!this.tail || this.tail.prefix !== tailPrefix || this.tail.path !== tail
Path) { |
| 1321 propertyUpdates.tail = { |
| 1322 prefix: tailPrefix, |
| 1323 path: tailPath, |
| 1324 __queryParams: this.route.__queryParams |
| 1325 }; |
| 1326 } |
| 1327 propertyUpdates.data = namedMatches; |
| 1328 this._dataInUrl = {}; |
| 1329 for (var key in namedMatches) { |
| 1330 this._dataInUrl[key] = namedMatches[key]; |
| 1331 } |
| 1332 this.__setMulti(propertyUpdates); |
| 1333 }, |
| 1334 __tailPathChanged: function() { |
| 1335 if (!this.active) { |
| 1336 return; |
| 1337 } |
| 1338 var tailPath = this.tail.path; |
| 1339 var newPath = this._matched; |
| 1340 if (tailPath) { |
| 1341 if (tailPath.charAt(0) !== '/') { |
| 1342 tailPath = '/' + tailPath; |
| 1343 } |
| 1344 newPath += tailPath; |
| 1345 } |
| 1346 this.set('route.path', newPath); |
| 1347 }, |
| 1348 __updatePathOnDataChange: function() { |
| 1349 if (!this.route || !this.active) { |
| 1350 return; |
| 1351 } |
| 1352 var newPath = this.__getLink({}); |
| 1353 var oldPath = this.__getLink(this._dataInUrl); |
| 1354 if (newPath === oldPath) { |
| 1355 return; |
| 1356 } |
| 1357 this.set('route.path', newPath); |
| 1358 }, |
| 1359 __getLink: function(overrideValues) { |
| 1360 var values = { |
| 1361 tail: null |
| 1362 }; |
| 1363 for (var key in this.data) { |
| 1364 values[key] = this.data[key]; |
| 1365 } |
| 1366 for (var key in overrideValues) { |
| 1367 values[key] = overrideValues[key]; |
| 1368 } |
| 1369 var patternPieces = this.pattern.split('/'); |
| 1370 var interp = patternPieces.map(function(value) { |
| 1371 if (value[0] == ':') { |
| 1372 value = values[value.slice(1)]; |
| 1373 } |
| 1374 return value; |
| 1375 }, this); |
| 1376 if (values.tail && values.tail.path) { |
| 1377 if (interp.length > 0 && values.tail.path.charAt(0) === '/') { |
| 1378 interp.push(values.tail.path.slice(1)); |
| 1379 } else { |
| 1380 interp.push(values.tail.path); |
| 1381 } |
| 1382 } |
| 1383 return interp.join('/'); |
| 1384 }, |
| 1385 __setMulti: function(setObj) { |
| 1386 for (var property in setObj) { |
| 1387 this._propertySetter(property, setObj[property]); |
| 1388 } |
| 1389 for (var property in setObj) { |
| 1390 this._pathEffector(property, this[property]); |
| 1391 this._notifyPathUp(property, this[property]); |
| 1392 } |
| 1393 } |
| 1394 }); |
| 1395 |
| 1396 Polymer({ |
| 1397 is: 'iron-media-query', |
| 1398 properties: { |
| 1399 queryMatches: { |
| 1400 type: Boolean, |
| 1401 value: false, |
| 1402 readOnly: true, |
| 1403 notify: true |
| 1404 }, |
| 1405 query: { |
| 1406 type: String, |
| 1407 observer: 'queryChanged' |
| 1408 }, |
| 1409 full: { |
| 1410 type: Boolean, |
| 1411 value: false |
| 1412 }, |
| 1413 _boundMQHandler: { |
| 1414 value: function() { |
| 1415 return this.queryHandler.bind(this); |
| 1416 } |
| 1417 }, |
| 1418 _mq: { |
| 1419 value: null |
| 1420 } |
| 1421 }, |
| 1422 attached: function() { |
| 1423 this.style.display = 'none'; |
| 1424 this.queryChanged(); |
| 1425 }, |
| 1426 detached: function() { |
| 1427 this._remove(); |
| 1428 }, |
| 1429 _add: function() { |
| 1430 if (this._mq) { |
| 1431 this._mq.addListener(this._boundMQHandler); |
| 1432 } |
| 1433 }, |
| 1434 _remove: function() { |
| 1435 if (this._mq) { |
| 1436 this._mq.removeListener(this._boundMQHandler); |
| 1437 } |
| 1438 this._mq = null; |
| 1439 }, |
| 1440 queryChanged: function() { |
| 1441 this._remove(); |
| 1442 var query = this.query; |
| 1443 if (!query) { |
| 1444 return; |
| 1445 } |
| 1446 if (!this.full && query[0] !== '(') { |
| 1447 query = '(' + query + ')'; |
| 1448 } |
| 1449 this._mq = window.matchMedia(query); |
| 1450 this._add(); |
| 1451 this.queryHandler(this._mq); |
| 1452 }, |
| 1453 queryHandler: function(mq) { |
| 1454 this._setQueryMatches(mq.matches); |
| 1455 } |
| 1456 }); |
| 1457 |
| 1458 Polymer.IronResizableBehavior = { |
| 1459 properties: { |
| 1460 _parentResizable: { |
| 1461 type: Object, |
| 1462 observer: '_parentResizableChanged' |
| 1463 }, |
| 1464 _notifyingDescendant: { |
| 1465 type: Boolean, |
| 1466 value: false |
| 1467 } |
| 1468 }, |
| 1469 listeners: { |
| 1470 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' |
| 1471 }, |
| 1472 created: function() { |
| 1473 this._interestedResizables = []; |
| 1474 this._boundNotifyResize = this.notifyResize.bind(this); |
| 1475 }, |
| 1476 attached: function() { |
| 1477 this.fire('iron-request-resize-notifications', null, { |
| 1478 node: this, |
| 1479 bubbles: true, |
| 1480 cancelable: true |
| 1481 }); |
| 1482 if (!this._parentResizable) { |
| 1483 window.addEventListener('resize', this._boundNotifyResize); |
| 1484 this.notifyResize(); |
| 1485 } |
| 1486 }, |
| 1487 detached: function() { |
| 1488 if (this._parentResizable) { |
| 1489 this._parentResizable.stopResizeNotificationsFor(this); |
| 1490 } else { |
| 1491 window.removeEventListener('resize', this._boundNotifyResize); |
| 1492 } |
| 1493 this._parentResizable = null; |
| 1494 }, |
| 1495 notifyResize: function() { |
| 1496 if (!this.isAttached) { |
| 1497 return; |
| 1498 } |
| 1499 this._interestedResizables.forEach(function(resizable) { |
| 1500 if (this.resizerShouldNotify(resizable)) { |
| 1501 this._notifyDescendant(resizable); |
| 1502 } |
| 1503 }, this); |
| 1504 this._fireResize(); |
| 1505 }, |
| 1506 assignParentResizable: function(parentResizable) { |
| 1507 this._parentResizable = parentResizable; |
| 1508 }, |
| 1509 stopResizeNotificationsFor: function(target) { |
| 1510 var index = this._interestedResizables.indexOf(target); |
| 1511 if (index > -1) { |
| 1512 this._interestedResizables.splice(index, 1); |
| 1513 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); |
| 1514 } |
| 1515 }, |
| 1516 resizerShouldNotify: function(element) { |
| 1517 return true; |
| 1518 }, |
| 1519 _onDescendantIronResize: function(event) { |
| 1520 if (this._notifyingDescendant) { |
| 1521 event.stopPropagation(); |
| 1522 return; |
| 1523 } |
| 1524 if (!Polymer.Settings.useShadow) { |
| 1525 this._fireResize(); |
| 1526 } |
| 1527 }, |
| 1528 _fireResize: function() { |
| 1529 this.fire('iron-resize', null, { |
| 1530 node: this, |
| 1531 bubbles: false |
| 1532 }); |
| 1533 }, |
| 1534 _onIronRequestResizeNotifications: function(event) { |
| 1535 var target = event.path ? event.path[0] : event.target; |
| 1536 if (target === this) { |
| 1537 return; |
| 1538 } |
| 1539 if (this._interestedResizables.indexOf(target) === -1) { |
| 1540 this._interestedResizables.push(target); |
| 1541 this.listen(target, 'iron-resize', '_onDescendantIronResize'); |
| 1542 } |
| 1543 target.assignParentResizable(this); |
| 1544 this._notifyDescendant(target); |
| 1545 event.stopPropagation(); |
| 1546 }, |
| 1547 _parentResizableChanged: function(parentResizable) { |
| 1548 if (parentResizable) { |
| 1549 window.removeEventListener('resize', this._boundNotifyResize); |
| 1550 } |
| 1551 }, |
| 1552 _notifyDescendant: function(descendant) { |
| 1553 if (!this.isAttached) { |
| 1554 return; |
| 1555 } |
| 1556 this._notifyingDescendant = true; |
| 1557 descendant.notifyResize(); |
| 1558 this._notifyingDescendant = false; |
| 1559 } |
| 1560 }; |
| 1561 |
| 1562 Polymer.IronSelection = function(selectCallback) { |
| 1563 this.selection = []; |
| 1564 this.selectCallback = selectCallback; |
| 1565 }; |
| 1566 |
| 1567 Polymer.IronSelection.prototype = { |
| 1568 get: function() { |
| 1569 return this.multi ? this.selection.slice() : this.selection[0]; |
| 1570 }, |
| 1571 clear: function(excludes) { |
| 1572 this.selection.slice().forEach(function(item) { |
| 1573 if (!excludes || excludes.indexOf(item) < 0) { |
| 1574 this.setItemSelected(item, false); |
| 1575 } |
| 1576 }, this); |
| 1577 }, |
| 1578 isSelected: function(item) { |
| 1579 return this.selection.indexOf(item) >= 0; |
| 1580 }, |
| 1581 setItemSelected: function(item, isSelected) { |
| 1582 if (item != null) { |
| 1583 if (isSelected !== this.isSelected(item)) { |
| 1584 if (isSelected) { |
| 1585 this.selection.push(item); |
| 1586 } else { |
| 1587 var i = this.selection.indexOf(item); |
| 1588 if (i >= 0) { |
| 1589 this.selection.splice(i, 1); |
| 1590 } |
| 1591 } |
| 1592 if (this.selectCallback) { |
| 1593 this.selectCallback(item, isSelected); |
| 1594 } |
| 1595 } |
| 1596 } |
| 1597 }, |
| 1598 select: function(item) { |
| 1599 if (this.multi) { |
| 1600 this.toggle(item); |
| 1601 } else if (this.get() !== item) { |
| 1602 this.setItemSelected(this.get(), false); |
| 1603 this.setItemSelected(item, true); |
| 1604 } |
| 1605 }, |
| 1606 toggle: function(item) { |
| 1607 this.setItemSelected(item, !this.isSelected(item)); |
| 1608 } |
| 1609 }; |
| 1610 |
| 1611 Polymer.IronSelectableBehavior = { |
| 1612 properties: { |
| 1613 attrForSelected: { |
| 1614 type: String, |
| 1615 value: null |
| 1616 }, |
| 1617 selected: { |
| 1618 type: String, |
| 1619 notify: true |
| 1620 }, |
| 1621 selectedItem: { |
| 1622 type: Object, |
| 1623 readOnly: true, |
| 1624 notify: true |
| 1625 }, |
| 1626 activateEvent: { |
| 1627 type: String, |
| 1628 value: 'tap', |
| 1629 observer: '_activateEventChanged' |
| 1630 }, |
| 1631 selectable: String, |
| 1632 selectedClass: { |
| 1633 type: String, |
| 1634 value: 'iron-selected' |
| 1635 }, |
| 1636 selectedAttribute: { |
| 1637 type: String, |
| 1638 value: null |
| 1639 }, |
| 1640 fallbackSelection: { |
| 1641 type: String, |
| 1642 value: null |
| 1643 }, |
| 1644 items: { |
| 1645 type: Array, |
| 1646 readOnly: true, |
| 1647 notify: true, |
| 1648 value: function() { |
| 1649 return []; |
| 1650 } |
| 1651 }, |
| 1652 _excludedLocalNames: { |
| 1653 type: Object, |
| 1654 value: function() { |
| 1655 return { |
| 1656 template: 1 |
| 1657 }; |
| 1658 } |
| 1659 } |
| 1660 }, |
| 1661 observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selec
ted)', '_checkFallback(fallbackSelection)' ], |
| 1662 created: function() { |
| 1663 this._bindFilterItem = this._filterItem.bind(this); |
| 1664 this._selection = new Polymer.IronSelection(this._applySelection.bind(this))
; |
| 1665 }, |
| 1666 attached: function() { |
| 1667 this._observer = this._observeItems(this); |
| 1668 this._updateItems(); |
| 1669 if (!this._shouldUpdateSelection) { |
| 1670 this._updateSelected(); |
| 1671 } |
| 1672 this._addListener(this.activateEvent); |
| 1673 }, |
| 1674 detached: function() { |
| 1675 if (this._observer) { |
| 1676 Polymer.dom(this).unobserveNodes(this._observer); |
| 1677 } |
| 1678 this._removeListener(this.activateEvent); |
| 1679 }, |
| 1680 indexOf: function(item) { |
| 1681 return this.items.indexOf(item); |
| 1682 }, |
| 1683 select: function(value) { |
| 1684 this.selected = value; |
| 1685 }, |
| 1686 selectPrevious: function() { |
| 1687 var length = this.items.length; |
| 1688 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % lengt
h; |
| 1689 this.selected = this._indexToValue(index); |
| 1690 }, |
| 1691 selectNext: function() { |
| 1692 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.len
gth; |
| 1693 this.selected = this._indexToValue(index); |
| 1694 }, |
| 1695 selectIndex: function(index) { |
| 1696 this.select(this._indexToValue(index)); |
| 1697 }, |
| 1698 forceSynchronousItemUpdate: function() { |
| 1699 this._updateItems(); |
| 1700 }, |
| 1701 get _shouldUpdateSelection() { |
| 1702 return this.selected != null; |
| 1703 }, |
| 1704 _checkFallback: function() { |
| 1705 if (this._shouldUpdateSelection) { |
| 1706 this._updateSelected(); |
| 1707 } |
| 1708 }, |
| 1709 _addListener: function(eventName) { |
| 1710 this.listen(this, eventName, '_activateHandler'); |
| 1711 }, |
| 1712 _removeListener: function(eventName) { |
| 1713 this.unlisten(this, eventName, '_activateHandler'); |
| 1714 }, |
| 1715 _activateEventChanged: function(eventName, old) { |
| 1716 this._removeListener(old); |
| 1717 this._addListener(eventName); |
| 1718 }, |
| 1719 _updateItems: function() { |
| 1720 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*
'); |
| 1721 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); |
| 1722 this._setItems(nodes); |
| 1723 }, |
| 1724 _updateAttrForSelected: function() { |
| 1725 if (this._shouldUpdateSelection) { |
| 1726 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); |
| 1727 } |
| 1728 }, |
| 1729 _updateSelected: function() { |
| 1730 this._selectSelected(this.selected); |
| 1731 }, |
| 1732 _selectSelected: function(selected) { |
| 1733 this._selection.select(this._valueToItem(this.selected)); |
| 1734 if (this.fallbackSelection && this.items.length && this._selection.get() ===
undefined) { |
| 1735 this.selected = this.fallbackSelection; |
| 1736 } |
| 1737 }, |
| 1738 _filterItem: function(node) { |
| 1739 return !this._excludedLocalNames[node.localName]; |
| 1740 }, |
| 1741 _valueToItem: function(value) { |
| 1742 return value == null ? null : this.items[this._valueToIndex(value)]; |
| 1743 }, |
| 1744 _valueToIndex: function(value) { |
| 1745 if (this.attrForSelected) { |
| 1746 for (var i = 0, item; item = this.items[i]; i++) { |
| 1747 if (this._valueForItem(item) == value) { |
| 1748 return i; |
| 1749 } |
| 1750 } |
| 1751 } else { |
| 1752 return Number(value); |
| 1753 } |
| 1754 }, |
| 1755 _indexToValue: function(index) { |
| 1756 if (this.attrForSelected) { |
| 1757 var item = this.items[index]; |
| 1758 if (item) { |
| 1759 return this._valueForItem(item); |
| 1760 } |
| 1761 } else { |
| 1762 return index; |
| 1763 } |
| 1764 }, |
| 1765 _valueForItem: function(item) { |
| 1766 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)]; |
| 1767 return propValue != undefined ? propValue : item.getAttribute(this.attrForSe
lected); |
| 1768 }, |
| 1769 _applySelection: function(item, isSelected) { |
| 1770 if (this.selectedClass) { |
| 1771 this.toggleClass(this.selectedClass, isSelected, item); |
| 1772 } |
| 1773 if (this.selectedAttribute) { |
| 1774 this.toggleAttribute(this.selectedAttribute, isSelected, item); |
| 1775 } |
| 1776 this._selectionChange(); |
| 1777 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), { |
| 1778 item: item |
| 1779 }); |
| 1780 }, |
| 1781 _selectionChange: function() { |
| 1782 this._setSelectedItem(this._selection.get()); |
| 1783 }, |
| 1784 _observeItems: function(node) { |
| 1785 return Polymer.dom(node).observeNodes(function(mutation) { |
| 1786 this._updateItems(); |
| 1787 if (this._shouldUpdateSelection) { |
| 1788 this._updateSelected(); |
| 1789 } |
| 1790 this.fire('iron-items-changed', mutation, { |
| 1791 bubbles: false, |
| 1792 cancelable: false |
| 1793 }); |
| 1794 }); |
| 1795 }, |
| 1796 _activateHandler: function(e) { |
| 1797 var t = e.target; |
| 1798 var items = this.items; |
| 1799 while (t && t != this) { |
| 1800 var i = items.indexOf(t); |
| 1801 if (i >= 0) { |
| 1802 var value = this._indexToValue(i); |
| 1803 this._itemActivate(value, t); |
| 1804 return; |
| 1805 } |
| 1806 t = t.parentNode; |
| 1807 } |
| 1808 }, |
| 1809 _itemActivate: function(value, item) { |
| 1810 if (!this.fire('iron-activate', { |
| 1811 selected: value, |
| 1812 item: item |
| 1813 }, { |
| 1814 cancelable: true |
| 1815 }).defaultPrevented) { |
| 1816 this.select(value); |
| 1817 } |
| 1818 } |
| 1819 }; |
| 1820 |
| 1821 Polymer({ |
| 1822 is: 'iron-pages', |
| 1823 behaviors: [ Polymer.IronResizableBehavior, Polymer.IronSelectableBehavior ], |
| 1824 properties: { |
| 1825 activateEvent: { |
| 1826 type: String, |
| 1827 value: null |
| 1828 } |
| 1829 }, |
| 1830 observers: [ '_selectedPageChanged(selected)' ], |
| 1831 _selectedPageChanged: function(selected, old) { |
| 1832 this.async(this.notifyResize); |
| 1833 } |
| 1834 }); |
| 1835 |
| 1836 (function() { |
| 1837 'use strict'; |
| 1838 var KEY_IDENTIFIER = { |
| 1839 'U+0008': 'backspace', |
| 1840 'U+0009': 'tab', |
| 1841 'U+001B': 'esc', |
| 1842 'U+0020': 'space', |
| 1843 'U+007F': 'del' |
| 1844 }; |
| 1845 var KEY_CODE = { |
| 1846 8: 'backspace', |
| 1847 9: 'tab', |
| 1848 13: 'enter', |
| 1849 27: 'esc', |
| 1850 33: 'pageup', |
| 1851 34: 'pagedown', |
| 1852 35: 'end', |
| 1853 36: 'home', |
| 1854 32: 'space', |
| 1855 37: 'left', |
| 1856 38: 'up', |
| 1857 39: 'right', |
| 1858 40: 'down', |
| 1859 46: 'del', |
| 1860 106: '*' |
| 1861 }; |
| 1862 var MODIFIER_KEYS = { |
| 1863 shift: 'shiftKey', |
| 1864 ctrl: 'ctrlKey', |
| 1865 alt: 'altKey', |
| 1866 meta: 'metaKey' |
| 1867 }; |
| 1868 var KEY_CHAR = /[a-z0-9*]/; |
| 1869 var IDENT_CHAR = /U\+/; |
| 1870 var ARROW_KEY = /^arrow/; |
| 1871 var SPACE_KEY = /^space(bar)?/; |
| 1872 var ESC_KEY = /^escape$/; |
| 1873 function transformKey(key, noSpecialChars) { |
| 1874 var validKey = ''; |
| 1875 if (key) { |
| 1876 var lKey = key.toLowerCase(); |
| 1877 if (lKey === ' ' || SPACE_KEY.test(lKey)) { |
| 1878 validKey = 'space'; |
| 1879 } else if (ESC_KEY.test(lKey)) { |
| 1880 validKey = 'esc'; |
| 1881 } else if (lKey.length == 1) { |
| 1882 if (!noSpecialChars || KEY_CHAR.test(lKey)) { |
| 1883 validKey = lKey; |
| 1884 } |
| 1885 } else if (ARROW_KEY.test(lKey)) { |
| 1886 validKey = lKey.replace('arrow', ''); |
| 1887 } else if (lKey == 'multiply') { |
| 1888 validKey = '*'; |
| 1889 } else { |
| 1890 validKey = lKey; |
| 1891 } |
| 1892 } |
| 1893 return validKey; |
| 1894 } |
| 1895 function transformKeyIdentifier(keyIdent) { |
| 1896 var validKey = ''; |
| 1897 if (keyIdent) { |
| 1898 if (keyIdent in KEY_IDENTIFIER) { |
| 1899 validKey = KEY_IDENTIFIER[keyIdent]; |
| 1900 } else if (IDENT_CHAR.test(keyIdent)) { |
| 1901 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); |
| 1902 validKey = String.fromCharCode(keyIdent).toLowerCase(); |
| 1903 } else { |
| 1904 validKey = keyIdent.toLowerCase(); |
| 1905 } |
| 1906 } |
| 1907 return validKey; |
| 1908 } |
| 1909 function transformKeyCode(keyCode) { |
| 1910 var validKey = ''; |
| 1911 if (Number(keyCode)) { |
| 1912 if (keyCode >= 65 && keyCode <= 90) { |
| 1913 validKey = String.fromCharCode(32 + keyCode); |
| 1914 } else if (keyCode >= 112 && keyCode <= 123) { |
| 1915 validKey = 'f' + (keyCode - 112); |
| 1916 } else if (keyCode >= 48 && keyCode <= 57) { |
| 1917 validKey = String(keyCode - 48); |
| 1918 } else if (keyCode >= 96 && keyCode <= 105) { |
| 1919 validKey = String(keyCode - 96); |
| 1920 } else { |
| 1921 validKey = KEY_CODE[keyCode]; |
| 1922 } |
| 1923 } |
| 1924 return validKey; |
| 1925 } |
| 1926 function normalizedKeyForEvent(keyEvent, noSpecialChars) { |
| 1927 return transformKey(keyEvent.key, noSpecialChars) || transformKeyIdentifier(
keyEvent.keyIdentifier) || transformKeyCode(keyEvent.keyCode) || transformKey(ke
yEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || ''; |
| 1928 } |
| 1929 function keyComboMatchesEvent(keyCombo, event) { |
| 1930 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); |
| 1931 return keyEvent === keyCombo.key && (!keyCombo.hasModifiers || !!event.shift
Key === !!keyCombo.shiftKey && !!event.ctrlKey === !!keyCombo.ctrlKey && !!event
.altKey === !!keyCombo.altKey && !!event.metaKey === !!keyCombo.metaKey); |
| 1932 } |
| 1933 function parseKeyComboString(keyComboString) { |
| 1934 if (keyComboString.length === 1) { |
| 1935 return { |
| 1936 combo: keyComboString, |
| 1937 key: keyComboString, |
| 1938 event: 'keydown' |
| 1939 }; |
| 1940 } |
| 1941 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPar
t) { |
| 1942 var eventParts = keyComboPart.split(':'); |
| 1943 var keyName = eventParts[0]; |
| 1944 var event = eventParts[1]; |
| 1945 if (keyName in MODIFIER_KEYS) { |
| 1946 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; |
| 1947 parsedKeyCombo.hasModifiers = true; |
| 1948 } else { |
| 1949 parsedKeyCombo.key = keyName; |
| 1950 parsedKeyCombo.event = event || 'keydown'; |
| 1951 } |
| 1952 return parsedKeyCombo; |
| 1953 }, { |
| 1954 combo: keyComboString.split(':').shift() |
| 1955 }); |
| 1956 } |
| 1957 function parseEventString(eventString) { |
| 1958 return eventString.trim().split(' ').map(function(keyComboString) { |
| 1959 return parseKeyComboString(keyComboString); |
| 1960 }); |
| 1961 } |
| 1962 Polymer.IronA11yKeysBehavior = { |
1936 properties: { | 1963 properties: { |
1937 /** | 1964 keyEventTarget: { |
1938 * A model representing the deserialized path through the route tree, as | |
1939 * well as the current queryParams. | |
1940 * | |
1941 * A route object is the kernel of the routing system. It is intended to | |
1942 * be fed into consuming elements such as `app-route`. | |
1943 * | |
1944 * @type {?Object} | |
1945 */ | |
1946 route: { | |
1947 type: Object, | 1965 type: Object, |
1948 notify: true | 1966 value: function() { |
1949 }, | 1967 return this; |
1950 | |
1951 /** | |
1952 * A set of key/value pairs that are universally accessible to branches of | |
1953 * the route tree. | |
1954 * | |
1955 * @type {?Object} | |
1956 */ | |
1957 queryParams: { | |
1958 type: Object, | |
1959 notify: true | |
1960 }, | |
1961 | |
1962 /** | |
1963 * The serialized path through the route tree. This corresponds to the | |
1964 * `window.location.pathname` value, and will update to reflect changes | |
1965 * to that value. | |
1966 */ | |
1967 path: { | |
1968 type: String, | |
1969 notify: true, | |
1970 } | |
1971 }, | |
1972 | |
1973 observers: [ | |
1974 '_locationChanged(path, queryParams)', | |
1975 '_routeChanged(route.prefix, route.path)', | |
1976 '_routeQueryParamsChanged(route.__queryParams)' | |
1977 ], | |
1978 | |
1979 created: function() { | |
1980 this.linkPaths('route.__queryParams', 'queryParams'); | |
1981 this.linkPaths('queryParams', 'route.__queryParams'); | |
1982 }, | |
1983 | |
1984 /** | |
1985 * Handler called when the path or queryParams change. | |
1986 */ | |
1987 _locationChanged: function() { | |
1988 if (this.route && | |
1989 this.route.path === this.path && | |
1990 this.queryParams === this.route.__queryParams) { | |
1991 return; | |
1992 } | |
1993 this.route = { | |
1994 prefix: '', | |
1995 path: this.path, | |
1996 __queryParams: this.queryParams | |
1997 }; | |
1998 }, | |
1999 | |
2000 /** | |
2001 * Handler called when the route prefix and route path change. | |
2002 */ | |
2003 _routeChanged: function() { | |
2004 if (!this.route) { | |
2005 return; | |
2006 } | |
2007 | |
2008 this.path = this.route.prefix + this.route.path; | |
2009 }, | |
2010 | |
2011 /** | |
2012 * Handler called when the route queryParams change. | |
2013 * | |
2014 * @param {Object} queryParams A set of key/value pairs that are | |
2015 * universally accessible to branches of the route tree. | |
2016 */ | |
2017 _routeQueryParamsChanged: function(queryParams) { | |
2018 if (!this.route) { | |
2019 return; | |
2020 } | |
2021 this.queryParams = queryParams; | |
2022 } | |
2023 }; | |
2024 'use strict'; | |
2025 | |
2026 Polymer({ | |
2027 is: 'app-location', | |
2028 | |
2029 properties: { | |
2030 /** | |
2031 * A model representing the deserialized path through the route tree, as | |
2032 * well as the current queryParams. | |
2033 */ | |
2034 route: { | |
2035 type: Object, | |
2036 notify: true | |
2037 }, | |
2038 | |
2039 /** | |
2040 * In many scenarios, it is convenient to treat the `hash` as a stand-in | |
2041 * alternative to the `path`. For example, if deploying an app to a stat
ic | |
2042 * web server (e.g., Github Pages) - where one does not have control ove
r | |
2043 * server-side routing - it is usually a better experience to use the ha
sh | |
2044 * to represent paths through one's app. | |
2045 * | |
2046 * When this property is set to true, the `hash` will be used in place o
f | |
2047 | |
2048 * the `path` for generating a `route`. | |
2049 */ | |
2050 useHashAsPath: { | |
2051 type: Boolean, | |
2052 value: false | |
2053 }, | |
2054 | |
2055 /** | |
2056 * A regexp that defines the set of URLs that should be considered part | |
2057 * of this web app. | |
2058 * | |
2059 * Clicking on a link that matches this regex won't result in a full pag
e | |
2060 * navigation, but will instead just update the URL state in place. | |
2061 * | |
2062 * This regexp is given everything after the origin in an absolute | |
2063 * URL. So to match just URLs that start with /search/ do: | |
2064 * url-space-regex="^/search/" | |
2065 * | |
2066 * @type {string|RegExp} | |
2067 */ | |
2068 urlSpaceRegex: { | |
2069 type: String, | |
2070 notify: true | |
2071 }, | |
2072 | |
2073 /** | |
2074 * A set of key/value pairs that are universally accessible to branches | |
2075 * of the route tree. | |
2076 */ | |
2077 __queryParams: { | |
2078 type: Object | |
2079 }, | |
2080 | |
2081 /** | |
2082 * The pathname component of the current URL. | |
2083 */ | |
2084 __path: { | |
2085 type: String | |
2086 }, | |
2087 | |
2088 /** | |
2089 * The query string portion of the current URL. | |
2090 */ | |
2091 __query: { | |
2092 type: String | |
2093 }, | |
2094 | |
2095 /** | |
2096 * The hash portion of the current URL. | |
2097 */ | |
2098 __hash: { | |
2099 type: String | |
2100 }, | |
2101 | |
2102 /** | |
2103 * The route path, which will be either the hash or the path, depending | |
2104 * on useHashAsPath. | |
2105 */ | |
2106 path: { | |
2107 type: String, | |
2108 observer: '__onPathChanged' | |
2109 } | 1968 } |
2110 }, | 1969 }, |
2111 | 1970 stopKeyboardEventPropagation: { |
2112 behaviors: [Polymer.AppRouteConverterBehavior], | |
2113 | |
2114 observers: [ | |
2115 '__computeRoutePath(useHashAsPath, __hash, __path)' | |
2116 ], | |
2117 | |
2118 __computeRoutePath: function() { | |
2119 this.path = this.useHashAsPath ? this.__hash : this.__path; | |
2120 }, | |
2121 | |
2122 __onPathChanged: function() { | |
2123 if (!this._readied) { | |
2124 return; | |
2125 } | |
2126 | |
2127 if (this.useHashAsPath) { | |
2128 this.__hash = this.path; | |
2129 } else { | |
2130 this.__path = this.path; | |
2131 } | |
2132 } | |
2133 }); | |
2134 'use strict'; | |
2135 | |
2136 Polymer({ | |
2137 is: 'app-route', | |
2138 | |
2139 properties: { | |
2140 /** | |
2141 * The URL component managed by this element. | |
2142 */ | |
2143 route: { | |
2144 type: Object, | |
2145 notify: true | |
2146 }, | |
2147 | |
2148 /** | |
2149 * The pattern of slash-separated segments to match `path` against. | |
2150 * | |
2151 * For example the pattern "/foo" will match "/foo" or "/foo/bar" | |
2152 * but not "/foobar". | |
2153 * | |
2154 * Path segments like `/:named` are mapped to properties on the `data` obj
ect. | |
2155 */ | |
2156 pattern: { | |
2157 type: String | |
2158 }, | |
2159 | |
2160 /** | |
2161 * The parameterized values that are extracted from the route as | |
2162 * described by `pattern`. | |
2163 */ | |
2164 data: { | |
2165 type: Object, | |
2166 value: function() {return {};}, | |
2167 notify: true | |
2168 }, | |
2169 | |
2170 /** | |
2171 * @type {?Object} | |
2172 */ | |
2173 queryParams: { | |
2174 type: Object, | |
2175 value: function() { | |
2176 return {}; | |
2177 }, | |
2178 notify: true | |
2179 }, | |
2180 | |
2181 /** | |
2182 * The part of `path` NOT consumed by `pattern`. | |
2183 */ | |
2184 tail: { | |
2185 type: Object, | |
2186 value: function() {return {path: null, prefix: null, __queryParams: null
};}, | |
2187 notify: true | |
2188 }, | |
2189 | |
2190 active: { | |
2191 type: Boolean, | |
2192 notify: true, | |
2193 readOnly: true | |
2194 }, | |
2195 | |
2196 _queryParamsUpdating: { | |
2197 type: Boolean, | 1971 type: Boolean, |
2198 value: false | 1972 value: false |
2199 }, | 1973 }, |
2200 /** | 1974 _boundKeyHandlers: { |
2201 * @type {?string} | |
2202 */ | |
2203 _matched: { | |
2204 type: String, | |
2205 value: '' | |
2206 } | |
2207 }, | |
2208 | |
2209 observers: [ | |
2210 '__tryToMatch(route.path, pattern)', | |
2211 '__updatePathOnDataChange(data.*)', | |
2212 '__tailPathChanged(tail.path)', | |
2213 '__routeQueryParamsChanged(route.__queryParams)', | |
2214 '__tailQueryParamsChanged(tail.__queryParams)', | |
2215 '__queryParamsChanged(queryParams.*)' | |
2216 ], | |
2217 | |
2218 created: function() { | |
2219 this.linkPaths('route.__queryParams', 'tail.__queryParams'); | |
2220 this.linkPaths('tail.__queryParams', 'route.__queryParams'); | |
2221 }, | |
2222 | |
2223 /** | |
2224 * Deal with the query params object being assigned to wholesale. | |
2225 * @export | |
2226 */ | |
2227 __routeQueryParamsChanged: function(queryParams) { | |
2228 if (queryParams && this.tail) { | |
2229 this.set('tail.__queryParams', queryParams); | |
2230 | |
2231 if (!this.active || this._queryParamsUpdating) { | |
2232 return; | |
2233 } | |
2234 | |
2235 // Copy queryParams and track whether there are any differences compared | |
2236 // to the existing query params. | |
2237 var copyOfQueryParams = {}; | |
2238 var anythingChanged = false; | |
2239 for (var key in queryParams) { | |
2240 copyOfQueryParams[key] = queryParams[key]; | |
2241 if (anythingChanged || | |
2242 !this.queryParams || | |
2243 queryParams[key] !== this.queryParams[key]) { | |
2244 anythingChanged = true; | |
2245 } | |
2246 } | |
2247 // Need to check whether any keys were deleted | |
2248 for (var key in this.queryParams) { | |
2249 if (anythingChanged || !(key in queryParams)) { | |
2250 anythingChanged = true; | |
2251 break; | |
2252 } | |
2253 } | |
2254 | |
2255 if (!anythingChanged) { | |
2256 return; | |
2257 } | |
2258 this._queryParamsUpdating = true; | |
2259 this.set('queryParams', copyOfQueryParams); | |
2260 this._queryParamsUpdating = false; | |
2261 } | |
2262 }, | |
2263 | |
2264 /** | |
2265 * @export | |
2266 */ | |
2267 __tailQueryParamsChanged: function(queryParams) { | |
2268 if (queryParams && this.route) { | |
2269 this.set('route.__queryParams', queryParams); | |
2270 } | |
2271 }, | |
2272 | |
2273 /** | |
2274 * @export | |
2275 */ | |
2276 __queryParamsChanged: function(changes) { | |
2277 if (!this.active || this._queryParamsUpdating) { | |
2278 return; | |
2279 } | |
2280 | |
2281 this.set('route.__' + changes.path, changes.value); | |
2282 }, | |
2283 | |
2284 __resetProperties: function() { | |
2285 this._setActive(false); | |
2286 this._matched = null; | |
2287 //this.tail = { path: null, prefix: null, queryParams: null }; | |
2288 //this.data = {}; | |
2289 }, | |
2290 | |
2291 /** | |
2292 * @export | |
2293 */ | |
2294 __tryToMatch: function() { | |
2295 if (!this.route) { | |
2296 return; | |
2297 } | |
2298 var path = this.route.path; | |
2299 var pattern = this.pattern; | |
2300 if (!pattern) { | |
2301 return; | |
2302 } | |
2303 | |
2304 if (!path) { | |
2305 this.__resetProperties(); | |
2306 return; | |
2307 } | |
2308 | |
2309 var remainingPieces = path.split('/'); | |
2310 var patternPieces = pattern.split('/'); | |
2311 | |
2312 var matched = []; | |
2313 var namedMatches = {}; | |
2314 | |
2315 for (var i=0; i < patternPieces.length; i++) { | |
2316 var patternPiece = patternPieces[i]; | |
2317 if (!patternPiece && patternPiece !== '') { | |
2318 break; | |
2319 } | |
2320 var pathPiece = remainingPieces.shift(); | |
2321 | |
2322 // We don't match this path. | |
2323 if (!pathPiece && pathPiece !== '') { | |
2324 this.__resetProperties(); | |
2325 return; | |
2326 } | |
2327 matched.push(pathPiece); | |
2328 | |
2329 if (patternPiece.charAt(0) == ':') { | |
2330 namedMatches[patternPiece.slice(1)] = pathPiece; | |
2331 } else if (patternPiece !== pathPiece) { | |
2332 this.__resetProperties(); | |
2333 return; | |
2334 } | |
2335 } | |
2336 | |
2337 this._matched = matched.join('/'); | |
2338 | |
2339 // Properties that must be updated atomically. | |
2340 var propertyUpdates = {}; | |
2341 | |
2342 //this.active | |
2343 if (!this.active) { | |
2344 propertyUpdates.active = true; | |
2345 } | |
2346 | |
2347 // this.tail | |
2348 var tailPrefix = this.route.prefix + this._matched; | |
2349 var tailPath = remainingPieces.join('/'); | |
2350 if (remainingPieces.length > 0) { | |
2351 tailPath = '/' + tailPath; | |
2352 } | |
2353 if (!this.tail || | |
2354 this.tail.prefix !== tailPrefix || | |
2355 this.tail.path !== tailPath) { | |
2356 propertyUpdates.tail = { | |
2357 prefix: tailPrefix, | |
2358 path: tailPath, | |
2359 __queryParams: this.route.__queryParams | |
2360 }; | |
2361 } | |
2362 | |
2363 // this.data | |
2364 propertyUpdates.data = namedMatches; | |
2365 this._dataInUrl = {}; | |
2366 for (var key in namedMatches) { | |
2367 this._dataInUrl[key] = namedMatches[key]; | |
2368 } | |
2369 | |
2370 this.__setMulti(propertyUpdates); | |
2371 }, | |
2372 | |
2373 /** | |
2374 * @export | |
2375 */ | |
2376 __tailPathChanged: function() { | |
2377 if (!this.active) { | |
2378 return; | |
2379 } | |
2380 var tailPath = this.tail.path; | |
2381 var newPath = this._matched; | |
2382 if (tailPath) { | |
2383 if (tailPath.charAt(0) !== '/') { | |
2384 tailPath = '/' + tailPath; | |
2385 } | |
2386 newPath += tailPath; | |
2387 } | |
2388 this.set('route.path', newPath); | |
2389 }, | |
2390 | |
2391 /** | |
2392 * @export | |
2393 */ | |
2394 __updatePathOnDataChange: function() { | |
2395 if (!this.route || !this.active) { | |
2396 return; | |
2397 } | |
2398 var newPath = this.__getLink({}); | |
2399 var oldPath = this.__getLink(this._dataInUrl); | |
2400 if (newPath === oldPath) { | |
2401 return; | |
2402 } | |
2403 this.set('route.path', newPath); | |
2404 }, | |
2405 | |
2406 __getLink: function(overrideValues) { | |
2407 var values = {tail: null}; | |
2408 for (var key in this.data) { | |
2409 values[key] = this.data[key]; | |
2410 } | |
2411 for (var key in overrideValues) { | |
2412 values[key] = overrideValues[key]; | |
2413 } | |
2414 var patternPieces = this.pattern.split('/'); | |
2415 var interp = patternPieces.map(function(value) { | |
2416 if (value[0] == ':') { | |
2417 value = values[value.slice(1)]; | |
2418 } | |
2419 return value; | |
2420 }, this); | |
2421 if (values.tail && values.tail.path) { | |
2422 if (interp.length > 0 && values.tail.path.charAt(0) === '/') { | |
2423 interp.push(values.tail.path.slice(1)); | |
2424 } else { | |
2425 interp.push(values.tail.path); | |
2426 } | |
2427 } | |
2428 return interp.join('/'); | |
2429 }, | |
2430 | |
2431 __setMulti: function(setObj) { | |
2432 // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at | |
2433 // internal data structures. I would not advise that you copy this | |
2434 // example. | |
2435 // | |
2436 // In the future this will be a feature of Polymer itself. | |
2437 // See: https://github.com/Polymer/polymer/issues/3640 | |
2438 // | |
2439 // Hacking around with private methods like this is juggling footguns, | |
2440 // and is likely to have unexpected and unsupported rough edges. | |
2441 // | |
2442 // Be ye so warned. | |
2443 for (var property in setObj) { | |
2444 this._propertySetter(property, setObj[property]); | |
2445 } | |
2446 | |
2447 for (var property in setObj) { | |
2448 this._pathEffector(property, this[property]); | |
2449 this._notifyPathUp(property, this[property]); | |
2450 } | |
2451 } | |
2452 }); | |
2453 Polymer({ | |
2454 | |
2455 is: 'iron-media-query', | |
2456 | |
2457 properties: { | |
2458 | |
2459 /** | |
2460 * The Boolean return value of the media query. | |
2461 */ | |
2462 queryMatches: { | |
2463 type: Boolean, | |
2464 value: false, | |
2465 readOnly: true, | |
2466 notify: true | |
2467 }, | |
2468 | |
2469 /** | |
2470 * The CSS media query to evaluate. | |
2471 */ | |
2472 query: { | |
2473 type: String, | |
2474 observer: 'queryChanged' | |
2475 }, | |
2476 | |
2477 /** | |
2478 * If true, the query attribute is assumed to be a complete media query | |
2479 * string rather than a single media feature. | |
2480 */ | |
2481 full: { | |
2482 type: Boolean, | |
2483 value: false | |
2484 }, | |
2485 | |
2486 /** | |
2487 * @type {function(MediaQueryList)} | |
2488 */ | |
2489 _boundMQHandler: { | |
2490 value: function() { | |
2491 return this.queryHandler.bind(this); | |
2492 } | |
2493 }, | |
2494 | |
2495 /** | |
2496 * @type {MediaQueryList} | |
2497 */ | |
2498 _mq: { | |
2499 value: null | |
2500 } | |
2501 }, | |
2502 | |
2503 attached: function() { | |
2504 this.style.display = 'none'; | |
2505 this.queryChanged(); | |
2506 }, | |
2507 | |
2508 detached: function() { | |
2509 this._remove(); | |
2510 }, | |
2511 | |
2512 _add: function() { | |
2513 if (this._mq) { | |
2514 this._mq.addListener(this._boundMQHandler); | |
2515 } | |
2516 }, | |
2517 | |
2518 _remove: function() { | |
2519 if (this._mq) { | |
2520 this._mq.removeListener(this._boundMQHandler); | |
2521 } | |
2522 this._mq = null; | |
2523 }, | |
2524 | |
2525 queryChanged: function() { | |
2526 this._remove(); | |
2527 var query = this.query; | |
2528 if (!query) { | |
2529 return; | |
2530 } | |
2531 if (!this.full && query[0] !== '(') { | |
2532 query = '(' + query + ')'; | |
2533 } | |
2534 this._mq = window.matchMedia(query); | |
2535 this._add(); | |
2536 this.queryHandler(this._mq); | |
2537 }, | |
2538 | |
2539 queryHandler: function(mq) { | |
2540 this._setQueryMatches(mq.matches); | |
2541 } | |
2542 | |
2543 }); | |
2544 /** | |
2545 * `IronResizableBehavior` is a behavior that can be used in Polymer elements
to | |
2546 * coordinate the flow of resize events between "resizers" (elements that cont
rol the | |
2547 * size or hidden state of their children) and "resizables" (elements that nee
d to be | |
2548 * notified when they are resized or un-hidden by their parents in order to ta
ke | |
2549 * action on their new measurements). | |
2550 * | |
2551 * Elements that perform measurement should add the `IronResizableBehavior` be
havior to | |
2552 * their element definition and listen for the `iron-resize` event on themselv
es. | |
2553 * This event will be fired when they become showing after having been hidden, | |
2554 * when they are resized explicitly by another resizable, or when the window h
as been | |
2555 * resized. | |
2556 * | |
2557 * Note, the `iron-resize` event is non-bubbling. | |
2558 * | |
2559 * @polymerBehavior Polymer.IronResizableBehavior | |
2560 * @demo demo/index.html | |
2561 **/ | |
2562 Polymer.IronResizableBehavior = { | |
2563 properties: { | |
2564 /** | |
2565 * The closest ancestor element that implements `IronResizableBehavior`. | |
2566 */ | |
2567 _parentResizable: { | |
2568 type: Object, | |
2569 observer: '_parentResizableChanged' | |
2570 }, | |
2571 | |
2572 /** | |
2573 * True if this element is currently notifying its descedant elements of | |
2574 * resize. | |
2575 */ | |
2576 _notifyingDescendant: { | |
2577 type: Boolean, | |
2578 value: false | |
2579 } | |
2580 }, | |
2581 | |
2582 listeners: { | |
2583 'iron-request-resize-notifications': '_onIronRequestResizeNotifications' | |
2584 }, | |
2585 | |
2586 created: function() { | |
2587 // We don't really need property effects on these, and also we want them | |
2588 // to be created before the `_parentResizable` observer fires: | |
2589 this._interestedResizables = []; | |
2590 this._boundNotifyResize = this.notifyResize.bind(this); | |
2591 }, | |
2592 | |
2593 attached: function() { | |
2594 this.fire('iron-request-resize-notifications', null, { | |
2595 node: this, | |
2596 bubbles: true, | |
2597 cancelable: true | |
2598 }); | |
2599 | |
2600 if (!this._parentResizable) { | |
2601 window.addEventListener('resize', this._boundNotifyResize); | |
2602 this.notifyResize(); | |
2603 } | |
2604 }, | |
2605 | |
2606 detached: function() { | |
2607 if (this._parentResizable) { | |
2608 this._parentResizable.stopResizeNotificationsFor(this); | |
2609 } else { | |
2610 window.removeEventListener('resize', this._boundNotifyResize); | |
2611 } | |
2612 | |
2613 this._parentResizable = null; | |
2614 }, | |
2615 | |
2616 /** | |
2617 * Can be called to manually notify a resizable and its descendant | |
2618 * resizables of a resize change. | |
2619 */ | |
2620 notifyResize: function() { | |
2621 if (!this.isAttached) { | |
2622 return; | |
2623 } | |
2624 | |
2625 this._interestedResizables.forEach(function(resizable) { | |
2626 if (this.resizerShouldNotify(resizable)) { | |
2627 this._notifyDescendant(resizable); | |
2628 } | |
2629 }, this); | |
2630 | |
2631 this._fireResize(); | |
2632 }, | |
2633 | |
2634 /** | |
2635 * Used to assign the closest resizable ancestor to this resizable | |
2636 * if the ancestor detects a request for notifications. | |
2637 */ | |
2638 assignParentResizable: function(parentResizable) { | |
2639 this._parentResizable = parentResizable; | |
2640 }, | |
2641 | |
2642 /** | |
2643 * Used to remove a resizable descendant from the list of descendants | |
2644 * that should be notified of a resize change. | |
2645 */ | |
2646 stopResizeNotificationsFor: function(target) { | |
2647 var index = this._interestedResizables.indexOf(target); | |
2648 | |
2649 if (index > -1) { | |
2650 this._interestedResizables.splice(index, 1); | |
2651 this.unlisten(target, 'iron-resize', '_onDescendantIronResize'); | |
2652 } | |
2653 }, | |
2654 | |
2655 /** | |
2656 * This method can be overridden to filter nested elements that should or | |
2657 * should not be notified by the current element. Return true if an element | |
2658 * should be notified, or false if it should not be notified. | |
2659 * | |
2660 * @param {HTMLElement} element A candidate descendant element that | |
2661 * implements `IronResizableBehavior`. | |
2662 * @return {boolean} True if the `element` should be notified of resize. | |
2663 */ | |
2664 resizerShouldNotify: function(element) { return true; }, | |
2665 | |
2666 _onDescendantIronResize: function(event) { | |
2667 if (this._notifyingDescendant) { | |
2668 event.stopPropagation(); | |
2669 return; | |
2670 } | |
2671 | |
2672 // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the | |
2673 // otherwise non-bubbling event "just work." We do it manually here for | |
2674 // the case where Polymer is not using shadow roots for whatever reason: | |
2675 if (!Polymer.Settings.useShadow) { | |
2676 this._fireResize(); | |
2677 } | |
2678 }, | |
2679 | |
2680 _fireResize: function() { | |
2681 this.fire('iron-resize', null, { | |
2682 node: this, | |
2683 bubbles: false | |
2684 }); | |
2685 }, | |
2686 | |
2687 _onIronRequestResizeNotifications: function(event) { | |
2688 var target = event.path ? event.path[0] : event.target; | |
2689 | |
2690 if (target === this) { | |
2691 return; | |
2692 } | |
2693 | |
2694 if (this._interestedResizables.indexOf(target) === -1) { | |
2695 this._interestedResizables.push(target); | |
2696 this.listen(target, 'iron-resize', '_onDescendantIronResize'); | |
2697 } | |
2698 | |
2699 target.assignParentResizable(this); | |
2700 this._notifyDescendant(target); | |
2701 | |
2702 event.stopPropagation(); | |
2703 }, | |
2704 | |
2705 _parentResizableChanged: function(parentResizable) { | |
2706 if (parentResizable) { | |
2707 window.removeEventListener('resize', this._boundNotifyResize); | |
2708 } | |
2709 }, | |
2710 | |
2711 _notifyDescendant: function(descendant) { | |
2712 // NOTE(cdata): In IE10, attached is fired on children first, so it's | |
2713 // important not to notify them if the parent is not attached yet (or | |
2714 // else they will get redundantly notified when the parent attaches). | |
2715 if (!this.isAttached) { | |
2716 return; | |
2717 } | |
2718 | |
2719 this._notifyingDescendant = true; | |
2720 descendant.notifyResize(); | |
2721 this._notifyingDescendant = false; | |
2722 } | |
2723 }; | |
2724 /** | |
2725 * @param {!Function} selectCallback | |
2726 * @constructor | |
2727 */ | |
2728 Polymer.IronSelection = function(selectCallback) { | |
2729 this.selection = []; | |
2730 this.selectCallback = selectCallback; | |
2731 }; | |
2732 | |
2733 Polymer.IronSelection.prototype = { | |
2734 | |
2735 /** | |
2736 * Retrieves the selected item(s). | |
2737 * | |
2738 * @method get | |
2739 * @returns Returns the selected item(s). If the multi property is true, | |
2740 * `get` will return an array, otherwise it will return | |
2741 * the selected item or undefined if there is no selection. | |
2742 */ | |
2743 get: function() { | |
2744 return this.multi ? this.selection.slice() : this.selection[0]; | |
2745 }, | |
2746 | |
2747 /** | |
2748 * Clears all the selection except the ones indicated. | |
2749 * | |
2750 * @method clear | |
2751 * @param {Array} excludes items to be excluded. | |
2752 */ | |
2753 clear: function(excludes) { | |
2754 this.selection.slice().forEach(function(item) { | |
2755 if (!excludes || excludes.indexOf(item) < 0) { | |
2756 this.setItemSelected(item, false); | |
2757 } | |
2758 }, this); | |
2759 }, | |
2760 | |
2761 /** | |
2762 * Indicates if a given item is selected. | |
2763 * | |
2764 * @method isSelected | |
2765 * @param {*} item The item whose selection state should be checked. | |
2766 * @returns Returns true if `item` is selected. | |
2767 */ | |
2768 isSelected: function(item) { | |
2769 return this.selection.indexOf(item) >= 0; | |
2770 }, | |
2771 | |
2772 /** | |
2773 * Sets the selection state for a given item to either selected or deselecte
d. | |
2774 * | |
2775 * @method setItemSelected | |
2776 * @param {*} item The item to select. | |
2777 * @param {boolean} isSelected True for selected, false for deselected. | |
2778 */ | |
2779 setItemSelected: function(item, isSelected) { | |
2780 if (item != null) { | |
2781 if (isSelected !== this.isSelected(item)) { | |
2782 // proceed to update selection only if requested state differs from cu
rrent | |
2783 if (isSelected) { | |
2784 this.selection.push(item); | |
2785 } else { | |
2786 var i = this.selection.indexOf(item); | |
2787 if (i >= 0) { | |
2788 this.selection.splice(i, 1); | |
2789 } | |
2790 } | |
2791 if (this.selectCallback) { | |
2792 this.selectCallback(item, isSelected); | |
2793 } | |
2794 } | |
2795 } | |
2796 }, | |
2797 | |
2798 /** | |
2799 * Sets the selection state for a given item. If the `multi` property | |
2800 * is true, then the selected state of `item` will be toggled; otherwise | |
2801 * the `item` will be selected. | |
2802 * | |
2803 * @method select | |
2804 * @param {*} item The item to select. | |
2805 */ | |
2806 select: function(item) { | |
2807 if (this.multi) { | |
2808 this.toggle(item); | |
2809 } else if (this.get() !== item) { | |
2810 this.setItemSelected(this.get(), false); | |
2811 this.setItemSelected(item, true); | |
2812 } | |
2813 }, | |
2814 | |
2815 /** | |
2816 * Toggles the selection state for `item`. | |
2817 * | |
2818 * @method toggle | |
2819 * @param {*} item The item to toggle. | |
2820 */ | |
2821 toggle: function(item) { | |
2822 this.setItemSelected(item, !this.isSelected(item)); | |
2823 } | |
2824 | |
2825 }; | |
2826 /** @polymerBehavior */ | |
2827 Polymer.IronSelectableBehavior = { | |
2828 | |
2829 /** | |
2830 * Fired when iron-selector is activated (selected or deselected). | |
2831 * It is fired before the selected items are changed. | |
2832 * Cancel the event to abort selection. | |
2833 * | |
2834 * @event iron-activate | |
2835 */ | |
2836 | |
2837 /** | |
2838 * Fired when an item is selected | |
2839 * | |
2840 * @event iron-select | |
2841 */ | |
2842 | |
2843 /** | |
2844 * Fired when an item is deselected | |
2845 * | |
2846 * @event iron-deselect | |
2847 */ | |
2848 | |
2849 /** | |
2850 * Fired when the list of selectable items changes (e.g., items are | |
2851 * added or removed). The detail of the event is a mutation record that | |
2852 * describes what changed. | |
2853 * | |
2854 * @event iron-items-changed | |
2855 */ | |
2856 | |
2857 properties: { | |
2858 | |
2859 /** | |
2860 * If you want to use an attribute value or property of an element for | |
2861 * `selected` instead of the index, set this to the name of the attribute | |
2862 * or property. Hyphenated values are converted to camel case when used to | |
2863 * look up the property of a selectable element. Camel cased values are | |
2864 * *not* converted to hyphenated values for attribute lookup. It's | |
2865 * recommended that you provide the hyphenated form of the name so that | |
2866 * selection works in both cases. (Use `attr-or-property-name` instead of | |
2867 * `attrOrPropertyName`.) | |
2868 */ | |
2869 attrForSelected: { | |
2870 type: String, | |
2871 value: null | |
2872 }, | |
2873 | |
2874 /** | |
2875 * Gets or sets the selected element. The default is to use the index of t
he item. | |
2876 * @type {string|number} | |
2877 */ | |
2878 selected: { | |
2879 type: String, | |
2880 notify: true | |
2881 }, | |
2882 | |
2883 /** | |
2884 * Returns the currently selected item. | |
2885 * | |
2886 * @type {?Object} | |
2887 */ | |
2888 selectedItem: { | |
2889 type: Object, | |
2890 readOnly: true, | |
2891 notify: true | |
2892 }, | |
2893 | |
2894 /** | |
2895 * The event that fires from items when they are selected. Selectable | |
2896 * will listen for this event from items and update the selection state. | |
2897 * Set to empty string to listen to no events. | |
2898 */ | |
2899 activateEvent: { | |
2900 type: String, | |
2901 value: 'tap', | |
2902 observer: '_activateEventChanged' | |
2903 }, | |
2904 | |
2905 /** | |
2906 * This is a CSS selector string. If this is set, only items that match t
he CSS selector | |
2907 * are selectable. | |
2908 */ | |
2909 selectable: String, | |
2910 | |
2911 /** | |
2912 * The class to set on elements when selected. | |
2913 */ | |
2914 selectedClass: { | |
2915 type: String, | |
2916 value: 'iron-selected' | |
2917 }, | |
2918 | |
2919 /** | |
2920 * The attribute to set on elements when selected. | |
2921 */ | |
2922 selectedAttribute: { | |
2923 type: String, | |
2924 value: null | |
2925 }, | |
2926 | |
2927 /** | |
2928 * Default fallback if the selection based on selected with `attrForSelect
ed` | |
2929 * is not found. | |
2930 */ | |
2931 fallbackSelection: { | |
2932 type: String, | |
2933 value: null | |
2934 }, | |
2935 | |
2936 /** | |
2937 * The list of items from which a selection can be made. | |
2938 */ | |
2939 items: { | |
2940 type: Array, | 1975 type: Array, |
2941 readOnly: true, | |
2942 notify: true, | |
2943 value: function() { | 1976 value: function() { |
2944 return []; | 1977 return []; |
2945 } | 1978 } |
2946 }, | 1979 }, |
2947 | 1980 _imperativeKeyBindings: { |
2948 /** | |
2949 * The set of excluded elements where the key is the `localName` | |
2950 * of the element that will be ignored from the item list. | |
2951 * | |
2952 * @default {template: 1} | |
2953 */ | |
2954 _excludedLocalNames: { | |
2955 type: Object, | 1981 type: Object, |
2956 value: function() { | 1982 value: function() { |
2957 return { | 1983 return {}; |
2958 'template': 1 | |
2959 }; | |
2960 } | 1984 } |
2961 } | 1985 } |
2962 }, | 1986 }, |
2963 | 1987 observers: [ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' ], |
2964 observers: [ | 1988 keyBindings: {}, |
2965 '_updateAttrForSelected(attrForSelected)', | 1989 registered: function() { |
2966 '_updateSelected(selected)', | 1990 this._prepKeyBindings(); |
2967 '_checkFallback(fallbackSelection)' | 1991 }, |
2968 ], | |
2969 | |
2970 created: function() { | |
2971 this._bindFilterItem = this._filterItem.bind(this); | |
2972 this._selection = new Polymer.IronSelection(this._applySelection.bind(this
)); | |
2973 }, | |
2974 | |
2975 attached: function() { | 1992 attached: function() { |
2976 this._observer = this._observeItems(this); | 1993 this._listenKeyEventListeners(); |
2977 this._updateItems(); | 1994 }, |
2978 if (!this._shouldUpdateSelection) { | |
2979 this._updateSelected(); | |
2980 } | |
2981 this._addListener(this.activateEvent); | |
2982 }, | |
2983 | |
2984 detached: function() { | 1995 detached: function() { |
2985 if (this._observer) { | 1996 this._unlistenKeyEventListeners(); |
2986 Polymer.dom(this).unobserveNodes(this._observer); | 1997 }, |
2987 } | 1998 addOwnKeyBinding: function(eventString, handlerName) { |
2988 this._removeListener(this.activateEvent); | 1999 this._imperativeKeyBindings[eventString] = handlerName; |
2989 }, | 2000 this._prepKeyBindings(); |
2990 | 2001 this._resetKeyEventListeners(); |
2991 /** | 2002 }, |
2992 * Returns the index of the given item. | 2003 removeOwnKeyBindings: function() { |
2993 * | 2004 this._imperativeKeyBindings = {}; |
2994 * @method indexOf | 2005 this._prepKeyBindings(); |
2995 * @param {Object} item | 2006 this._resetKeyEventListeners(); |
2996 * @returns Returns the index of the item | 2007 }, |
2997 */ | 2008 keyboardEventMatchesKeys: function(event, eventString) { |
2998 indexOf: function(item) { | 2009 var keyCombos = parseEventString(eventString); |
2999 return this.items.indexOf(item); | 2010 for (var i = 0; i < keyCombos.length; ++i) { |
3000 }, | 2011 if (keyComboMatchesEvent(keyCombos[i], event)) { |
3001 | 2012 return true; |
3002 /** | 2013 } |
3003 * Selects the given value. | 2014 } |
3004 * | 2015 return false; |
3005 * @method select | 2016 }, |
3006 * @param {string|number} value the value to select. | 2017 _collectKeyBindings: function() { |
3007 */ | 2018 var keyBindings = this.behaviors.map(function(behavior) { |
3008 select: function(value) { | 2019 return behavior.keyBindings; |
3009 this.selected = value; | 2020 }); |
3010 }, | 2021 if (keyBindings.indexOf(this.keyBindings) === -1) { |
3011 | 2022 keyBindings.push(this.keyBindings); |
3012 /** | 2023 } |
3013 * Selects the previous item. | 2024 return keyBindings; |
3014 * | 2025 }, |
3015 * @method selectPrevious | 2026 _prepKeyBindings: function() { |
3016 */ | 2027 this._keyBindings = {}; |
3017 selectPrevious: function() { | 2028 this._collectKeyBindings().forEach(function(keyBindings) { |
3018 var length = this.items.length; | 2029 for (var eventString in keyBindings) { |
3019 var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % len
gth; | 2030 this._addKeyBinding(eventString, keyBindings[eventString]); |
3020 this.selected = this._indexToValue(index); | 2031 } |
3021 }, | 2032 }, this); |
3022 | 2033 for (var eventString in this._imperativeKeyBindings) { |
3023 /** | 2034 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString
]); |
3024 * Selects the next item. | 2035 } |
3025 * | 2036 for (var eventName in this._keyBindings) { |
3026 * @method selectNext | 2037 this._keyBindings[eventName].sort(function(kb1, kb2) { |
3027 */ | 2038 var b1 = kb1[0].hasModifiers; |
3028 selectNext: function() { | 2039 var b2 = kb2[0].hasModifiers; |
3029 var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.l
ength; | 2040 return b1 === b2 ? 0 : b1 ? -1 : 1; |
3030 this.selected = this._indexToValue(index); | 2041 }); |
3031 }, | 2042 } |
3032 | 2043 }, |
3033 /** | 2044 _addKeyBinding: function(eventString, handlerName) { |
3034 * Selects the item at the given index. | 2045 parseEventString(eventString).forEach(function(keyCombo) { |
3035 * | 2046 this._keyBindings[keyCombo.event] = this._keyBindings[keyCombo.event] ||
[]; |
3036 * @method selectIndex | 2047 this._keyBindings[keyCombo.event].push([ keyCombo, handlerName ]); |
3037 */ | 2048 }, this); |
3038 selectIndex: function(index) { | 2049 }, |
3039 this.select(this._indexToValue(index)); | 2050 _resetKeyEventListeners: function() { |
3040 }, | 2051 this._unlistenKeyEventListeners(); |
3041 | 2052 if (this.isAttached) { |
3042 /** | 2053 this._listenKeyEventListeners(); |
3043 * Force a synchronous update of the `items` property. | 2054 } |
3044 * | 2055 }, |
3045 * NOTE: Consider listening for the `iron-items-changed` event to respond to | 2056 _listenKeyEventListeners: function() { |
3046 * updates to the set of selectable items after updates to the DOM list and | 2057 if (!this.keyEventTarget) { |
3047 * selection state have been made. | 2058 return; |
3048 * | 2059 } |
3049 * WARNING: If you are using this method, you should probably consider an | 2060 Object.keys(this._keyBindings).forEach(function(eventName) { |
3050 * alternate approach. Synchronously querying for items is potentially | 2061 var keyBindings = this._keyBindings[eventName]; |
3051 * slow for many use cases. The `items` property will update asynchronously | 2062 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); |
3052 * on its own to reflect selectable items in the DOM. | 2063 this._boundKeyHandlers.push([ this.keyEventTarget, eventName, boundKeyHa
ndler ]); |
3053 */ | 2064 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); |
3054 forceSynchronousItemUpdate: function() { | 2065 }, this); |
3055 this._updateItems(); | 2066 }, |
3056 }, | 2067 _unlistenKeyEventListeners: function() { |
3057 | 2068 var keyHandlerTuple; |
3058 get _shouldUpdateSelection() { | 2069 var keyEventTarget; |
3059 return this.selected != null; | 2070 var eventName; |
3060 }, | 2071 var boundKeyHandler; |
3061 | 2072 while (this._boundKeyHandlers.length) { |
3062 _checkFallback: function() { | 2073 keyHandlerTuple = this._boundKeyHandlers.pop(); |
3063 if (this._shouldUpdateSelection) { | 2074 keyEventTarget = keyHandlerTuple[0]; |
3064 this._updateSelected(); | 2075 eventName = keyHandlerTuple[1]; |
3065 } | 2076 boundKeyHandler = keyHandlerTuple[2]; |
3066 }, | 2077 keyEventTarget.removeEventListener(eventName, boundKeyHandler); |
3067 | 2078 } |
3068 _addListener: function(eventName) { | 2079 }, |
3069 this.listen(this, eventName, '_activateHandler'); | 2080 _onKeyBindingEvent: function(keyBindings, event) { |
3070 }, | 2081 if (this.stopKeyboardEventPropagation) { |
3071 | 2082 event.stopPropagation(); |
3072 _removeListener: function(eventName) { | 2083 } |
3073 this.unlisten(this, eventName, '_activateHandler'); | 2084 if (event.defaultPrevented) { |
3074 }, | 2085 return; |
3075 | 2086 } |
3076 _activateEventChanged: function(eventName, old) { | 2087 for (var i = 0; i < keyBindings.length; i++) { |
3077 this._removeListener(old); | 2088 var keyCombo = keyBindings[i][0]; |
3078 this._addListener(eventName); | 2089 var handlerName = keyBindings[i][1]; |
3079 }, | 2090 if (keyComboMatchesEvent(keyCombo, event)) { |
3080 | 2091 this._triggerKeyHandler(keyCombo, handlerName, event); |
3081 _updateItems: function() { | 2092 if (event.defaultPrevented) { |
3082 var nodes = Polymer.dom(this).queryDistributedElements(this.selectable ||
'*'); | 2093 return; |
3083 nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); | |
3084 this._setItems(nodes); | |
3085 }, | |
3086 | |
3087 _updateAttrForSelected: function() { | |
3088 if (this._shouldUpdateSelection) { | |
3089 this.selected = this._indexToValue(this.indexOf(this.selectedItem)); | |
3090 } | |
3091 }, | |
3092 | |
3093 _updateSelected: function() { | |
3094 this._selectSelected(this.selected); | |
3095 }, | |
3096 | |
3097 _selectSelected: function(selected) { | |
3098 this._selection.select(this._valueToItem(this.selected)); | |
3099 // Check for items, since this array is populated only when attached | |
3100 // Since Number(0) is falsy, explicitly check for undefined | |
3101 if (this.fallbackSelection && this.items.length && (this._selection.get()
=== undefined)) { | |
3102 this.selected = this.fallbackSelection; | |
3103 } | |
3104 }, | |
3105 | |
3106 _filterItem: function(node) { | |
3107 return !this._excludedLocalNames[node.localName]; | |
3108 }, | |
3109 | |
3110 _valueToItem: function(value) { | |
3111 return (value == null) ? null : this.items[this._valueToIndex(value)]; | |
3112 }, | |
3113 | |
3114 _valueToIndex: function(value) { | |
3115 if (this.attrForSelected) { | |
3116 for (var i = 0, item; item = this.items[i]; i++) { | |
3117 if (this._valueForItem(item) == value) { | |
3118 return i; | |
3119 } | 2094 } |
3120 } | 2095 } |
| 2096 } |
| 2097 }, |
| 2098 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { |
| 2099 var detail = Object.create(keyCombo); |
| 2100 detail.keyboardEvent = keyboardEvent; |
| 2101 var event = new CustomEvent(keyCombo.event, { |
| 2102 detail: detail, |
| 2103 cancelable: true |
| 2104 }); |
| 2105 this[handlerName].call(this, event); |
| 2106 if (event.defaultPrevented) { |
| 2107 keyboardEvent.preventDefault(); |
| 2108 } |
| 2109 } |
| 2110 }; |
| 2111 })(); |
| 2112 |
| 2113 Polymer.IronControlState = { |
| 2114 properties: { |
| 2115 focused: { |
| 2116 type: Boolean, |
| 2117 value: false, |
| 2118 notify: true, |
| 2119 readOnly: true, |
| 2120 reflectToAttribute: true |
| 2121 }, |
| 2122 disabled: { |
| 2123 type: Boolean, |
| 2124 value: false, |
| 2125 notify: true, |
| 2126 observer: '_disabledChanged', |
| 2127 reflectToAttribute: true |
| 2128 }, |
| 2129 _oldTabIndex: { |
| 2130 type: Number |
| 2131 }, |
| 2132 _boundFocusBlurHandler: { |
| 2133 type: Function, |
| 2134 value: function() { |
| 2135 return this._focusBlurHandler.bind(this); |
| 2136 } |
| 2137 } |
| 2138 }, |
| 2139 observers: [ '_changedControlState(focused, disabled)' ], |
| 2140 ready: function() { |
| 2141 this.addEventListener('focus', this._boundFocusBlurHandler, true); |
| 2142 this.addEventListener('blur', this._boundFocusBlurHandler, true); |
| 2143 }, |
| 2144 _focusBlurHandler: function(event) { |
| 2145 if (event.target === this) { |
| 2146 this._setFocused(event.type === 'focus'); |
| 2147 } else if (!this.shadowRoot) { |
| 2148 var target = Polymer.dom(event).localTarget; |
| 2149 if (!this.isLightDescendant(target)) { |
| 2150 this.fire(event.type, { |
| 2151 sourceEvent: event |
| 2152 }, { |
| 2153 node: this, |
| 2154 bubbles: event.bubbles, |
| 2155 cancelable: event.cancelable |
| 2156 }); |
| 2157 } |
| 2158 } |
| 2159 }, |
| 2160 _disabledChanged: function(disabled, old) { |
| 2161 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); |
| 2162 this.style.pointerEvents = disabled ? 'none' : ''; |
| 2163 if (disabled) { |
| 2164 this._oldTabIndex = this.tabIndex; |
| 2165 this._setFocused(false); |
| 2166 this.tabIndex = -1; |
| 2167 this.blur(); |
| 2168 } else if (this._oldTabIndex !== undefined) { |
| 2169 this.tabIndex = this._oldTabIndex; |
| 2170 } |
| 2171 }, |
| 2172 _changedControlState: function() { |
| 2173 if (this._controlStateChanged) { |
| 2174 this._controlStateChanged(); |
| 2175 } |
| 2176 } |
| 2177 }; |
| 2178 |
| 2179 Polymer.IronButtonStateImpl = { |
| 2180 properties: { |
| 2181 pressed: { |
| 2182 type: Boolean, |
| 2183 readOnly: true, |
| 2184 value: false, |
| 2185 reflectToAttribute: true, |
| 2186 observer: '_pressedChanged' |
| 2187 }, |
| 2188 toggles: { |
| 2189 type: Boolean, |
| 2190 value: false, |
| 2191 reflectToAttribute: true |
| 2192 }, |
| 2193 active: { |
| 2194 type: Boolean, |
| 2195 value: false, |
| 2196 notify: true, |
| 2197 reflectToAttribute: true |
| 2198 }, |
| 2199 pointerDown: { |
| 2200 type: Boolean, |
| 2201 readOnly: true, |
| 2202 value: false |
| 2203 }, |
| 2204 receivedFocusFromKeyboard: { |
| 2205 type: Boolean, |
| 2206 readOnly: true |
| 2207 }, |
| 2208 ariaActiveAttribute: { |
| 2209 type: String, |
| 2210 value: 'aria-pressed', |
| 2211 observer: '_ariaActiveAttributeChanged' |
| 2212 } |
| 2213 }, |
| 2214 listeners: { |
| 2215 down: '_downHandler', |
| 2216 up: '_upHandler', |
| 2217 tap: '_tapHandler' |
| 2218 }, |
| 2219 observers: [ '_detectKeyboardFocus(focused)', '_activeChanged(active, ariaActi
veAttribute)' ], |
| 2220 keyBindings: { |
| 2221 'enter:keydown': '_asyncClick', |
| 2222 'space:keydown': '_spaceKeyDownHandler', |
| 2223 'space:keyup': '_spaceKeyUpHandler' |
| 2224 }, |
| 2225 _mouseEventRe: /^mouse/, |
| 2226 _tapHandler: function() { |
| 2227 if (this.toggles) { |
| 2228 this._userActivate(!this.active); |
| 2229 } else { |
| 2230 this.active = false; |
| 2231 } |
| 2232 }, |
| 2233 _detectKeyboardFocus: function(focused) { |
| 2234 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); |
| 2235 }, |
| 2236 _userActivate: function(active) { |
| 2237 if (this.active !== active) { |
| 2238 this.active = active; |
| 2239 this.fire('change'); |
| 2240 } |
| 2241 }, |
| 2242 _downHandler: function(event) { |
| 2243 this._setPointerDown(true); |
| 2244 this._setPressed(true); |
| 2245 this._setReceivedFocusFromKeyboard(false); |
| 2246 }, |
| 2247 _upHandler: function() { |
| 2248 this._setPointerDown(false); |
| 2249 this._setPressed(false); |
| 2250 }, |
| 2251 _spaceKeyDownHandler: function(event) { |
| 2252 var keyboardEvent = event.detail.keyboardEvent; |
| 2253 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2254 if (this.isLightDescendant(target)) return; |
| 2255 keyboardEvent.preventDefault(); |
| 2256 keyboardEvent.stopImmediatePropagation(); |
| 2257 this._setPressed(true); |
| 2258 }, |
| 2259 _spaceKeyUpHandler: function(event) { |
| 2260 var keyboardEvent = event.detail.keyboardEvent; |
| 2261 var target = Polymer.dom(keyboardEvent).localTarget; |
| 2262 if (this.isLightDescendant(target)) return; |
| 2263 if (this.pressed) { |
| 2264 this._asyncClick(); |
| 2265 } |
| 2266 this._setPressed(false); |
| 2267 }, |
| 2268 _asyncClick: function() { |
| 2269 this.async(function() { |
| 2270 this.click(); |
| 2271 }, 1); |
| 2272 }, |
| 2273 _pressedChanged: function(pressed) { |
| 2274 this._changedButtonState(); |
| 2275 }, |
| 2276 _ariaActiveAttributeChanged: function(value, oldValue) { |
| 2277 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { |
| 2278 this.removeAttribute(oldValue); |
| 2279 } |
| 2280 }, |
| 2281 _activeChanged: function(active, ariaActiveAttribute) { |
| 2282 if (this.toggles) { |
| 2283 this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false'); |
| 2284 } else { |
| 2285 this.removeAttribute(this.ariaActiveAttribute); |
| 2286 } |
| 2287 this._changedButtonState(); |
| 2288 }, |
| 2289 _controlStateChanged: function() { |
| 2290 if (this.disabled) { |
| 2291 this._setPressed(false); |
| 2292 } else { |
| 2293 this._changedButtonState(); |
| 2294 } |
| 2295 }, |
| 2296 _changedButtonState: function() { |
| 2297 if (this._buttonStateChanged) { |
| 2298 this._buttonStateChanged(); |
| 2299 } |
| 2300 } |
| 2301 }; |
| 2302 |
| 2303 Polymer.IronButtonState = [ Polymer.IronA11yKeysBehavior, Polymer.IronButtonStat
eImpl ]; |
| 2304 |
| 2305 (function() { |
| 2306 var Utility = { |
| 2307 distance: function(x1, y1, x2, y2) { |
| 2308 var xDelta = x1 - x2; |
| 2309 var yDelta = y1 - y2; |
| 2310 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); |
| 2311 }, |
| 2312 now: window.performance && window.performance.now ? window.performance.now.b
ind(window.performance) : Date.now |
| 2313 }; |
| 2314 function ElementMetrics(element) { |
| 2315 this.element = element; |
| 2316 this.width = this.boundingRect.width; |
| 2317 this.height = this.boundingRect.height; |
| 2318 this.size = Math.max(this.width, this.height); |
| 2319 } |
| 2320 ElementMetrics.prototype = { |
| 2321 get boundingRect() { |
| 2322 return this.element.getBoundingClientRect(); |
| 2323 }, |
| 2324 furthestCornerDistanceFrom: function(x, y) { |
| 2325 var topLeft = Utility.distance(x, y, 0, 0); |
| 2326 var topRight = Utility.distance(x, y, this.width, 0); |
| 2327 var bottomLeft = Utility.distance(x, y, 0, this.height); |
| 2328 var bottomRight = Utility.distance(x, y, this.width, this.height); |
| 2329 return Math.max(topLeft, topRight, bottomLeft, bottomRight); |
| 2330 } |
| 2331 }; |
| 2332 function Ripple(element) { |
| 2333 this.element = element; |
| 2334 this.color = window.getComputedStyle(element).color; |
| 2335 this.wave = document.createElement('div'); |
| 2336 this.waveContainer = document.createElement('div'); |
| 2337 this.wave.style.backgroundColor = this.color; |
| 2338 this.wave.classList.add('wave'); |
| 2339 this.waveContainer.classList.add('wave-container'); |
| 2340 Polymer.dom(this.waveContainer).appendChild(this.wave); |
| 2341 this.resetInteractionState(); |
| 2342 } |
| 2343 Ripple.MAX_RADIUS = 300; |
| 2344 Ripple.prototype = { |
| 2345 get recenters() { |
| 2346 return this.element.recenters; |
| 2347 }, |
| 2348 get center() { |
| 2349 return this.element.center; |
| 2350 }, |
| 2351 get mouseDownElapsed() { |
| 2352 var elapsed; |
| 2353 if (!this.mouseDownStart) { |
| 2354 return 0; |
| 2355 } |
| 2356 elapsed = Utility.now() - this.mouseDownStart; |
| 2357 if (this.mouseUpStart) { |
| 2358 elapsed -= this.mouseUpElapsed; |
| 2359 } |
| 2360 return elapsed; |
| 2361 }, |
| 2362 get mouseUpElapsed() { |
| 2363 return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0; |
| 2364 }, |
| 2365 get mouseDownElapsedSeconds() { |
| 2366 return this.mouseDownElapsed / 1e3; |
| 2367 }, |
| 2368 get mouseUpElapsedSeconds() { |
| 2369 return this.mouseUpElapsed / 1e3; |
| 2370 }, |
| 2371 get mouseInteractionSeconds() { |
| 2372 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; |
| 2373 }, |
| 2374 get initialOpacity() { |
| 2375 return this.element.initialOpacity; |
| 2376 }, |
| 2377 get opacityDecayVelocity() { |
| 2378 return this.element.opacityDecayVelocity; |
| 2379 }, |
| 2380 get radius() { |
| 2381 var width2 = this.containerMetrics.width * this.containerMetrics.width; |
| 2382 var height2 = this.containerMetrics.height * this.containerMetrics.height; |
| 2383 var waveRadius = Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS)
* 1.1 + 5; |
| 2384 var duration = 1.1 - .2 * (waveRadius / Ripple.MAX_RADIUS); |
| 2385 var timeNow = this.mouseInteractionSeconds / duration; |
| 2386 var size = waveRadius * (1 - Math.pow(80, -timeNow)); |
| 2387 return Math.abs(size); |
| 2388 }, |
| 2389 get opacity() { |
| 2390 if (!this.mouseUpStart) { |
| 2391 return this.initialOpacity; |
| 2392 } |
| 2393 return Math.max(0, this.initialOpacity - this.mouseUpElapsedSeconds * this
.opacityDecayVelocity); |
| 2394 }, |
| 2395 get outerOpacity() { |
| 2396 var outerOpacity = this.mouseUpElapsedSeconds * .3; |
| 2397 var waveOpacity = this.opacity; |
| 2398 return Math.max(0, Math.min(outerOpacity, waveOpacity)); |
| 2399 }, |
| 2400 get isOpacityFullyDecayed() { |
| 2401 return this.opacity < .01 && this.radius >= Math.min(this.maxRadius, Rippl
e.MAX_RADIUS); |
| 2402 }, |
| 2403 get isRestingAtMaxRadius() { |
| 2404 return this.opacity >= this.initialOpacity && this.radius >= Math.min(this
.maxRadius, Ripple.MAX_RADIUS); |
| 2405 }, |
| 2406 get isAnimationComplete() { |
| 2407 return this.mouseUpStart ? this.isOpacityFullyDecayed : this.isRestingAtMa
xRadius; |
| 2408 }, |
| 2409 get translationFraction() { |
| 2410 return Math.min(1, this.radius / this.containerMetrics.size * 2 / Math.sqr
t(2)); |
| 2411 }, |
| 2412 get xNow() { |
| 2413 if (this.xEnd) { |
| 2414 return this.xStart + this.translationFraction * (this.xEnd - this.xStart
); |
| 2415 } |
| 2416 return this.xStart; |
| 2417 }, |
| 2418 get yNow() { |
| 2419 if (this.yEnd) { |
| 2420 return this.yStart + this.translationFraction * (this.yEnd - this.yStart
); |
| 2421 } |
| 2422 return this.yStart; |
| 2423 }, |
| 2424 get isMouseDown() { |
| 2425 return this.mouseDownStart && !this.mouseUpStart; |
| 2426 }, |
| 2427 resetInteractionState: function() { |
| 2428 this.maxRadius = 0; |
| 2429 this.mouseDownStart = 0; |
| 2430 this.mouseUpStart = 0; |
| 2431 this.xStart = 0; |
| 2432 this.yStart = 0; |
| 2433 this.xEnd = 0; |
| 2434 this.yEnd = 0; |
| 2435 this.slideDistance = 0; |
| 2436 this.containerMetrics = new ElementMetrics(this.element); |
| 2437 }, |
| 2438 draw: function() { |
| 2439 var scale; |
| 2440 var translateString; |
| 2441 var dx; |
| 2442 var dy; |
| 2443 this.wave.style.opacity = this.opacity; |
| 2444 scale = this.radius / (this.containerMetrics.size / 2); |
| 2445 dx = this.xNow - this.containerMetrics.width / 2; |
| 2446 dy = this.yNow - this.containerMetrics.height / 2; |
| 2447 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy
+ 'px)'; |
| 2448 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + '
px, 0)'; |
| 2449 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; |
| 2450 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; |
| 2451 }, |
| 2452 downAction: function(event) { |
| 2453 var xCenter = this.containerMetrics.width / 2; |
| 2454 var yCenter = this.containerMetrics.height / 2; |
| 2455 this.resetInteractionState(); |
| 2456 this.mouseDownStart = Utility.now(); |
| 2457 if (this.center) { |
| 2458 this.xStart = xCenter; |
| 2459 this.yStart = yCenter; |
| 2460 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
3121 } else { | 2461 } else { |
3122 return Number(value); | 2462 this.xStart = event ? event.detail.x - this.containerMetrics.boundingRec
t.left : this.containerMetrics.width / 2; |
3123 } | 2463 this.yStart = event ? event.detail.y - this.containerMetrics.boundingRec
t.top : this.containerMetrics.height / 2; |
3124 }, | 2464 } |
3125 | 2465 if (this.recenters) { |
3126 _indexToValue: function(index) { | 2466 this.xEnd = xCenter; |
3127 if (this.attrForSelected) { | 2467 this.yEnd = yCenter; |
3128 var item = this.items[index]; | 2468 this.slideDistance = Utility.distance(this.xStart, this.yStart, this.xEn
d, this.yEnd); |
3129 if (item) { | 2469 } |
3130 return this._valueForItem(item); | 2470 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(this.xSt
art, this.yStart); |
| 2471 this.waveContainer.style.top = (this.containerMetrics.height - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2472 this.waveContainer.style.left = (this.containerMetrics.width - this.contai
nerMetrics.size) / 2 + 'px'; |
| 2473 this.waveContainer.style.width = this.containerMetrics.size + 'px'; |
| 2474 this.waveContainer.style.height = this.containerMetrics.size + 'px'; |
| 2475 }, |
| 2476 upAction: function(event) { |
| 2477 if (!this.isMouseDown) { |
| 2478 return; |
| 2479 } |
| 2480 this.mouseUpStart = Utility.now(); |
| 2481 }, |
| 2482 remove: function() { |
| 2483 Polymer.dom(this.waveContainer.parentNode).removeChild(this.waveContainer)
; |
| 2484 } |
| 2485 }; |
| 2486 Polymer({ |
| 2487 is: 'paper-ripple', |
| 2488 behaviors: [ Polymer.IronA11yKeysBehavior ], |
| 2489 properties: { |
| 2490 initialOpacity: { |
| 2491 type: Number, |
| 2492 value: .25 |
| 2493 }, |
| 2494 opacityDecayVelocity: { |
| 2495 type: Number, |
| 2496 value: .8 |
| 2497 }, |
| 2498 recenters: { |
| 2499 type: Boolean, |
| 2500 value: false |
| 2501 }, |
| 2502 center: { |
| 2503 type: Boolean, |
| 2504 value: false |
| 2505 }, |
| 2506 ripples: { |
| 2507 type: Array, |
| 2508 value: function() { |
| 2509 return []; |
3131 } | 2510 } |
| 2511 }, |
| 2512 animating: { |
| 2513 type: Boolean, |
| 2514 readOnly: true, |
| 2515 reflectToAttribute: true, |
| 2516 value: false |
| 2517 }, |
| 2518 holdDown: { |
| 2519 type: Boolean, |
| 2520 value: false, |
| 2521 observer: '_holdDownChanged' |
| 2522 }, |
| 2523 noink: { |
| 2524 type: Boolean, |
| 2525 value: false |
| 2526 }, |
| 2527 _animating: { |
| 2528 type: Boolean |
| 2529 }, |
| 2530 _boundAnimate: { |
| 2531 type: Function, |
| 2532 value: function() { |
| 2533 return this.animate.bind(this); |
| 2534 } |
| 2535 } |
| 2536 }, |
| 2537 get target() { |
| 2538 return this.keyEventTarget; |
| 2539 }, |
| 2540 keyBindings: { |
| 2541 'enter:keydown': '_onEnterKeydown', |
| 2542 'space:keydown': '_onSpaceKeydown', |
| 2543 'space:keyup': '_onSpaceKeyup' |
| 2544 }, |
| 2545 attached: function() { |
| 2546 if (this.parentNode.nodeType == 11) { |
| 2547 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; |
3132 } else { | 2548 } else { |
3133 return index; | 2549 this.keyEventTarget = this.parentNode; |
3134 } | 2550 } |
3135 }, | 2551 var keyEventTarget = this.keyEventTarget; |
3136 | 2552 this.listen(keyEventTarget, 'up', 'uiUpAction'); |
3137 _valueForItem: function(item) { | 2553 this.listen(keyEventTarget, 'down', 'uiDownAction'); |
3138 var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)
]; | 2554 }, |
3139 return propValue != undefined ? propValue : item.getAttribute(this.attrFor
Selected); | 2555 detached: function() { |
3140 }, | 2556 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); |
3141 | 2557 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); |
3142 _applySelection: function(item, isSelected) { | 2558 this.keyEventTarget = null; |
3143 if (this.selectedClass) { | 2559 }, |
3144 this.toggleClass(this.selectedClass, isSelected, item); | 2560 get shouldKeepAnimating() { |
3145 } | 2561 for (var index = 0; index < this.ripples.length; ++index) { |
3146 if (this.selectedAttribute) { | 2562 if (!this.ripples[index].isAnimationComplete) { |
3147 this.toggleAttribute(this.selectedAttribute, isSelected, item); | 2563 return true; |
3148 } | |
3149 this._selectionChange(); | |
3150 this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); | |
3151 }, | |
3152 | |
3153 _selectionChange: function() { | |
3154 this._setSelectedItem(this._selection.get()); | |
3155 }, | |
3156 | |
3157 // observe items change under the given node. | |
3158 _observeItems: function(node) { | |
3159 return Polymer.dom(node).observeNodes(function(mutation) { | |
3160 this._updateItems(); | |
3161 | |
3162 if (this._shouldUpdateSelection) { | |
3163 this._updateSelected(); | |
3164 } | 2564 } |
3165 | 2565 } |
3166 // Let other interested parties know about the change so that | 2566 return false; |
3167 // we don't have to recreate mutation observers everywhere. | 2567 }, |
3168 this.fire('iron-items-changed', mutation, { | 2568 simulatedRipple: function() { |
3169 bubbles: false, | 2569 this.downAction(null); |
3170 cancelable: false | 2570 this.async(function() { |
3171 }); | 2571 this.upAction(); |
| 2572 }, 1); |
| 2573 }, |
| 2574 uiDownAction: function(event) { |
| 2575 if (!this.noink) { |
| 2576 this.downAction(event); |
| 2577 } |
| 2578 }, |
| 2579 downAction: function(event) { |
| 2580 if (this.holdDown && this.ripples.length > 0) { |
| 2581 return; |
| 2582 } |
| 2583 var ripple = this.addRipple(); |
| 2584 ripple.downAction(event); |
| 2585 if (!this._animating) { |
| 2586 this._animating = true; |
| 2587 this.animate(); |
| 2588 } |
| 2589 }, |
| 2590 uiUpAction: function(event) { |
| 2591 if (!this.noink) { |
| 2592 this.upAction(event); |
| 2593 } |
| 2594 }, |
| 2595 upAction: function(event) { |
| 2596 if (this.holdDown) { |
| 2597 return; |
| 2598 } |
| 2599 this.ripples.forEach(function(ripple) { |
| 2600 ripple.upAction(event); |
3172 }); | 2601 }); |
3173 }, | 2602 this._animating = true; |
3174 | 2603 this.animate(); |
3175 _activateHandler: function(e) { | 2604 }, |
3176 var t = e.target; | 2605 onAnimationComplete: function() { |
3177 var items = this.items; | 2606 this._animating = false; |
3178 while (t && t != this) { | 2607 this.$.background.style.backgroundColor = null; |
3179 var i = items.indexOf(t); | 2608 this.fire('transitionend'); |
3180 if (i >= 0) { | 2609 }, |
3181 var value = this._indexToValue(i); | 2610 addRipple: function() { |
3182 this._itemActivate(value, t); | 2611 var ripple = new Ripple(this); |
3183 return; | 2612 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); |
| 2613 this.$.background.style.backgroundColor = ripple.color; |
| 2614 this.ripples.push(ripple); |
| 2615 this._setAnimating(true); |
| 2616 return ripple; |
| 2617 }, |
| 2618 removeRipple: function(ripple) { |
| 2619 var rippleIndex = this.ripples.indexOf(ripple); |
| 2620 if (rippleIndex < 0) { |
| 2621 return; |
| 2622 } |
| 2623 this.ripples.splice(rippleIndex, 1); |
| 2624 ripple.remove(); |
| 2625 if (!this.ripples.length) { |
| 2626 this._setAnimating(false); |
| 2627 } |
| 2628 }, |
| 2629 animate: function() { |
| 2630 if (!this._animating) { |
| 2631 return; |
| 2632 } |
| 2633 var index; |
| 2634 var ripple; |
| 2635 for (index = 0; index < this.ripples.length; ++index) { |
| 2636 ripple = this.ripples[index]; |
| 2637 ripple.draw(); |
| 2638 this.$.background.style.opacity = ripple.outerOpacity; |
| 2639 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { |
| 2640 this.removeRipple(ripple); |
3184 } | 2641 } |
3185 t = t.parentNode; | 2642 } |
3186 } | 2643 if (!this.shouldKeepAnimating && this.ripples.length === 0) { |
3187 }, | 2644 this.onAnimationComplete(); |
3188 | 2645 } else { |
3189 _itemActivate: function(value, item) { | 2646 window.requestAnimationFrame(this._boundAnimate); |
3190 if (!this.fire('iron-activate', | 2647 } |
3191 {selected: value, item: item}, {cancelable: true}).defaultPrevented) { | 2648 }, |
3192 this.select(value); | 2649 _onEnterKeydown: function() { |
3193 } | 2650 this.uiDownAction(); |
3194 } | 2651 this.async(this.uiUpAction, 1); |
3195 | 2652 }, |
3196 }; | 2653 _onSpaceKeydown: function() { |
| 2654 this.uiDownAction(); |
| 2655 }, |
| 2656 _onSpaceKeyup: function() { |
| 2657 this.uiUpAction(); |
| 2658 }, |
| 2659 _holdDownChanged: function(newVal, oldVal) { |
| 2660 if (oldVal === undefined) { |
| 2661 return; |
| 2662 } |
| 2663 if (newVal) { |
| 2664 this.downAction(); |
| 2665 } else { |
| 2666 this.upAction(); |
| 2667 } |
| 2668 } |
| 2669 }); |
| 2670 })(); |
| 2671 |
| 2672 Polymer.PaperRippleBehavior = { |
| 2673 properties: { |
| 2674 noink: { |
| 2675 type: Boolean, |
| 2676 observer: '_noinkChanged' |
| 2677 }, |
| 2678 _rippleContainer: { |
| 2679 type: Object |
| 2680 } |
| 2681 }, |
| 2682 _buttonStateChanged: function() { |
| 2683 if (this.focused) { |
| 2684 this.ensureRipple(); |
| 2685 } |
| 2686 }, |
| 2687 _downHandler: function(event) { |
| 2688 Polymer.IronButtonStateImpl._downHandler.call(this, event); |
| 2689 if (this.pressed) { |
| 2690 this.ensureRipple(event); |
| 2691 } |
| 2692 }, |
| 2693 ensureRipple: function(optTriggeringEvent) { |
| 2694 if (!this.hasRipple()) { |
| 2695 this._ripple = this._createRipple(); |
| 2696 this._ripple.noink = this.noink; |
| 2697 var rippleContainer = this._rippleContainer || this.root; |
| 2698 if (rippleContainer) { |
| 2699 Polymer.dom(rippleContainer).appendChild(this._ripple); |
| 2700 } |
| 2701 if (optTriggeringEvent) { |
| 2702 var domContainer = Polymer.dom(this._rippleContainer || this); |
| 2703 var target = Polymer.dom(optTriggeringEvent).rootTarget; |
| 2704 if (domContainer.deepContains(target)) { |
| 2705 this._ripple.uiDownAction(optTriggeringEvent); |
| 2706 } |
| 2707 } |
| 2708 } |
| 2709 }, |
| 2710 getRipple: function() { |
| 2711 this.ensureRipple(); |
| 2712 return this._ripple; |
| 2713 }, |
| 2714 hasRipple: function() { |
| 2715 return Boolean(this._ripple); |
| 2716 }, |
| 2717 _createRipple: function() { |
| 2718 return document.createElement('paper-ripple'); |
| 2719 }, |
| 2720 _noinkChanged: function(noink) { |
| 2721 if (this.hasRipple()) { |
| 2722 this._ripple.noink = noink; |
| 2723 } |
| 2724 } |
| 2725 }; |
| 2726 |
| 2727 Polymer.PaperButtonBehaviorImpl = { |
| 2728 properties: { |
| 2729 elevation: { |
| 2730 type: Number, |
| 2731 reflectToAttribute: true, |
| 2732 readOnly: true |
| 2733 } |
| 2734 }, |
| 2735 observers: [ '_calculateElevation(focused, disabled, active, pressed, received
FocusFromKeyboard)', '_computeKeyboardClass(receivedFocusFromKeyboard)' ], |
| 2736 hostAttributes: { |
| 2737 role: 'button', |
| 2738 tabindex: '0', |
| 2739 animated: true |
| 2740 }, |
| 2741 _calculateElevation: function() { |
| 2742 var e = 1; |
| 2743 if (this.disabled) { |
| 2744 e = 0; |
| 2745 } else if (this.active || this.pressed) { |
| 2746 e = 4; |
| 2747 } else if (this.receivedFocusFromKeyboard) { |
| 2748 e = 3; |
| 2749 } |
| 2750 this._setElevation(e); |
| 2751 }, |
| 2752 _computeKeyboardClass: function(receivedFocusFromKeyboard) { |
| 2753 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); |
| 2754 }, |
| 2755 _spaceKeyDownHandler: function(event) { |
| 2756 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); |
| 2757 if (this.hasRipple() && this.getRipple().ripples.length < 1) { |
| 2758 this._ripple.uiDownAction(); |
| 2759 } |
| 2760 }, |
| 2761 _spaceKeyUpHandler: function(event) { |
| 2762 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); |
| 2763 if (this.hasRipple()) { |
| 2764 this._ripple.uiUpAction(); |
| 2765 } |
| 2766 } |
| 2767 }; |
| 2768 |
| 2769 Polymer.PaperButtonBehavior = [ Polymer.IronButtonState, Polymer.IronControlStat
e, Polymer.PaperRippleBehavior, Polymer.PaperButtonBehaviorImpl ]; |
| 2770 |
3197 Polymer({ | 2771 Polymer({ |
3198 | 2772 is: 'paper-button', |
3199 is: 'iron-pages', | 2773 behaviors: [ Polymer.PaperButtonBehavior ], |
3200 | 2774 properties: { |
3201 behaviors: [ | 2775 raised: { |
3202 Polymer.IronResizableBehavior, | 2776 type: Boolean, |
3203 Polymer.IronSelectableBehavior | 2777 reflectToAttribute: true, |
3204 ], | 2778 value: false, |
3205 | 2779 observer: '_calculateElevation' |
3206 properties: { | 2780 } |
3207 | 2781 }, |
3208 // as the selected page is the only one visible, activateEvent | 2782 _calculateElevation: function() { |
3209 // is both non-sensical and problematic; e.g. in cases where a user | 2783 if (!this.raised) { |
3210 // handler attempts to change the page and the activateEvent | 2784 this._setElevation(0); |
3211 // handler immediately changes it back | 2785 } else { |
3212 activateEvent: { | 2786 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); |
3213 type: String, | 2787 } |
3214 value: null | 2788 } |
3215 } | 2789 }); |
3216 | 2790 |
| 2791 (function() { |
| 2792 var metaDatas = {}; |
| 2793 var metaArrays = {}; |
| 2794 var singleton = null; |
| 2795 Polymer.IronMeta = Polymer({ |
| 2796 is: 'iron-meta', |
| 2797 properties: { |
| 2798 type: { |
| 2799 type: String, |
| 2800 value: 'default', |
| 2801 observer: '_typeChanged' |
3217 }, | 2802 }, |
3218 | 2803 key: { |
3219 observers: [ | 2804 type: String, |
3220 '_selectedPageChanged(selected)' | 2805 observer: '_keyChanged' |
3221 ], | 2806 }, |
3222 | 2807 value: { |
3223 _selectedPageChanged: function(selected, old) { | 2808 type: Object, |
3224 this.async(this.notifyResize); | 2809 notify: true, |
3225 } | 2810 observer: '_valueChanged' |
3226 }); | 2811 }, |
3227 (function() { | 2812 self: { |
3228 'use strict'; | 2813 type: Boolean, |
3229 | 2814 observer: '_selfChanged' |
3230 /** | 2815 }, |
3231 * Chrome uses an older version of DOM Level 3 Keyboard Events | 2816 list: { |
3232 * | 2817 type: Array, |
3233 * Most keys are labeled as text, but some are Unicode codepoints. | 2818 notify: true |
3234 * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-200712
21/keyset.html#KeySet-Set | 2819 } |
3235 */ | 2820 }, |
3236 var KEY_IDENTIFIER = { | 2821 hostAttributes: { |
3237 'U+0008': 'backspace', | 2822 hidden: true |
3238 'U+0009': 'tab', | 2823 }, |
3239 'U+001B': 'esc', | 2824 factoryImpl: function(config) { |
3240 'U+0020': 'space', | 2825 if (config) { |
3241 'U+007F': 'del' | 2826 for (var n in config) { |
3242 }; | 2827 switch (n) { |
3243 | 2828 case 'type': |
3244 /** | 2829 case 'key': |
3245 * Special table for KeyboardEvent.keyCode. | 2830 case 'value': |
3246 * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even bett
er | 2831 this[n] = config[n]; |
3247 * than that. | 2832 break; |
3248 * | |
3249 * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEve
nt.keyCode#Value_of_keyCode | |
3250 */ | |
3251 var KEY_CODE = { | |
3252 8: 'backspace', | |
3253 9: 'tab', | |
3254 13: 'enter', | |
3255 27: 'esc', | |
3256 33: 'pageup', | |
3257 34: 'pagedown', | |
3258 35: 'end', | |
3259 36: 'home', | |
3260 32: 'space', | |
3261 37: 'left', | |
3262 38: 'up', | |
3263 39: 'right', | |
3264 40: 'down', | |
3265 46: 'del', | |
3266 106: '*' | |
3267 }; | |
3268 | |
3269 /** | |
3270 * MODIFIER_KEYS maps the short name for modifier keys used in a key | |
3271 * combo string to the property name that references those same keys | |
3272 * in a KeyboardEvent instance. | |
3273 */ | |
3274 var MODIFIER_KEYS = { | |
3275 'shift': 'shiftKey', | |
3276 'ctrl': 'ctrlKey', | |
3277 'alt': 'altKey', | |
3278 'meta': 'metaKey' | |
3279 }; | |
3280 | |
3281 /** | |
3282 * KeyboardEvent.key is mostly represented by printable character made by | |
3283 * the keyboard, with unprintable keys labeled nicely. | |
3284 * | |
3285 * However, on OS X, Alt+char can make a Unicode character that follows an | |
3286 * Apple-specific mapping. In this case, we fall back to .keyCode. | |
3287 */ | |
3288 var KEY_CHAR = /[a-z0-9*]/; | |
3289 | |
3290 /** | |
3291 * Matches a keyIdentifier string. | |
3292 */ | |
3293 var IDENT_CHAR = /U\+/; | |
3294 | |
3295 /** | |
3296 * Matches arrow keys in Gecko 27.0+ | |
3297 */ | |
3298 var ARROW_KEY = /^arrow/; | |
3299 | |
3300 /** | |
3301 * Matches space keys everywhere (notably including IE10's exceptional name | |
3302 * `spacebar`). | |
3303 */ | |
3304 var SPACE_KEY = /^space(bar)?/; | |
3305 | |
3306 /** | |
3307 * Matches ESC key. | |
3308 * | |
3309 * Value from: http://w3c.github.io/uievents-key/#key-Escape | |
3310 */ | |
3311 var ESC_KEY = /^escape$/; | |
3312 | |
3313 /** | |
3314 * Transforms the key. | |
3315 * @param {string} key The KeyBoardEvent.key | |
3316 * @param {Boolean} [noSpecialChars] Limits the transformation to | |
3317 * alpha-numeric characters. | |
3318 */ | |
3319 function transformKey(key, noSpecialChars) { | |
3320 var validKey = ''; | |
3321 if (key) { | |
3322 var lKey = key.toLowerCase(); | |
3323 if (lKey === ' ' || SPACE_KEY.test(lKey)) { | |
3324 validKey = 'space'; | |
3325 } else if (ESC_KEY.test(lKey)) { | |
3326 validKey = 'esc'; | |
3327 } else if (lKey.length == 1) { | |
3328 if (!noSpecialChars || KEY_CHAR.test(lKey)) { | |
3329 validKey = lKey; | |
3330 } | |
3331 } else if (ARROW_KEY.test(lKey)) { | |
3332 validKey = lKey.replace('arrow', ''); | |
3333 } else if (lKey == 'multiply') { | |
3334 // numpad '*' can map to Multiply on IE/Windows | |
3335 validKey = '*'; | |
3336 } else { | |
3337 validKey = lKey; | |
3338 } | |
3339 } | |
3340 return validKey; | |
3341 } | |
3342 | |
3343 function transformKeyIdentifier(keyIdent) { | |
3344 var validKey = ''; | |
3345 if (keyIdent) { | |
3346 if (keyIdent in KEY_IDENTIFIER) { | |
3347 validKey = KEY_IDENTIFIER[keyIdent]; | |
3348 } else if (IDENT_CHAR.test(keyIdent)) { | |
3349 keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); | |
3350 validKey = String.fromCharCode(keyIdent).toLowerCase(); | |
3351 } else { | |
3352 validKey = keyIdent.toLowerCase(); | |
3353 } | |
3354 } | |
3355 return validKey; | |
3356 } | |
3357 | |
3358 function transformKeyCode(keyCode) { | |
3359 var validKey = ''; | |
3360 if (Number(keyCode)) { | |
3361 if (keyCode >= 65 && keyCode <= 90) { | |
3362 // ascii a-z | |
3363 // lowercase is 32 offset from uppercase | |
3364 validKey = String.fromCharCode(32 + keyCode); | |
3365 } else if (keyCode >= 112 && keyCode <= 123) { | |
3366 // function keys f1-f12 | |
3367 validKey = 'f' + (keyCode - 112); | |
3368 } else if (keyCode >= 48 && keyCode <= 57) { | |
3369 // top 0-9 keys | |
3370 validKey = String(keyCode - 48); | |
3371 } else if (keyCode >= 96 && keyCode <= 105) { | |
3372 // num pad 0-9 | |
3373 validKey = String(keyCode - 96); | |
3374 } else { | |
3375 validKey = KEY_CODE[keyCode]; | |
3376 } | |
3377 } | |
3378 return validKey; | |
3379 } | |
3380 | |
3381 /** | |
3382 * Calculates the normalized key for a KeyboardEvent. | |
3383 * @param {KeyboardEvent} keyEvent | |
3384 * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key | |
3385 * transformation to alpha-numeric chars. This is useful with key | |
3386 * combinations like shift + 2, which on FF for MacOS produces | |
3387 * keyEvent.key = @ | |
3388 * To get 2 returned, set noSpecialChars = true | |
3389 * To get @ returned, set noSpecialChars = false | |
3390 */ | |
3391 function normalizedKeyForEvent(keyEvent, noSpecialChars) { | |
3392 // Fall back from .key, to .keyIdentifier, to .keyCode, and then to | |
3393 // .detail.key to support artificial keyboard events. | |
3394 return transformKey(keyEvent.key, noSpecialChars) || | |
3395 transformKeyIdentifier(keyEvent.keyIdentifier) || | |
3396 transformKeyCode(keyEvent.keyCode) || | |
3397 transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, no
SpecialChars) || ''; | |
3398 } | |
3399 | |
3400 function keyComboMatchesEvent(keyCombo, event) { | |
3401 // For combos with modifiers we support only alpha-numeric keys | |
3402 var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); | |
3403 return keyEvent === keyCombo.key && | |
3404 (!keyCombo.hasModifiers || ( | |
3405 !!event.shiftKey === !!keyCombo.shiftKey && | |
3406 !!event.ctrlKey === !!keyCombo.ctrlKey && | |
3407 !!event.altKey === !!keyCombo.altKey && | |
3408 !!event.metaKey === !!keyCombo.metaKey) | |
3409 ); | |
3410 } | |
3411 | |
3412 function parseKeyComboString(keyComboString) { | |
3413 if (keyComboString.length === 1) { | |
3414 return { | |
3415 combo: keyComboString, | |
3416 key: keyComboString, | |
3417 event: 'keydown' | |
3418 }; | |
3419 } | |
3420 return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboP
art) { | |
3421 var eventParts = keyComboPart.split(':'); | |
3422 var keyName = eventParts[0]; | |
3423 var event = eventParts[1]; | |
3424 | |
3425 if (keyName in MODIFIER_KEYS) { | |
3426 parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; | |
3427 parsedKeyCombo.hasModifiers = true; | |
3428 } else { | |
3429 parsedKeyCombo.key = keyName; | |
3430 parsedKeyCombo.event = event || 'keydown'; | |
3431 } | |
3432 | |
3433 return parsedKeyCombo; | |
3434 }, { | |
3435 combo: keyComboString.split(':').shift() | |
3436 }); | |
3437 } | |
3438 | |
3439 function parseEventString(eventString) { | |
3440 return eventString.trim().split(' ').map(function(keyComboString) { | |
3441 return parseKeyComboString(keyComboString); | |
3442 }); | |
3443 } | |
3444 | |
3445 /** | |
3446 * `Polymer.IronA11yKeysBehavior` provides a normalized interface for proces
sing | |
3447 * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3
.org/TR/wai-aria-practices/#kbd_general_binding). | |
3448 * The element takes care of browser differences with respect to Keyboard ev
ents | |
3449 * and uses an expressive syntax to filter key presses. | |
3450 * | |
3451 * Use the `keyBindings` prototype property to express what combination of k
eys | |
3452 * will trigger the callback. A key binding has the format | |
3453 * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or | |
3454 * `"KEY:EVENT": "callback"` are valid as well). Some examples: | |
3455 * | |
3456 * keyBindings: { | |
3457 * 'space': '_onKeydown', // same as 'space:keydown' | |
3458 * 'shift+tab': '_onKeydown', | |
3459 * 'enter:keypress': '_onKeypress', | |
3460 * 'esc:keyup': '_onKeyup' | |
3461 * } | |
3462 * | |
3463 * The callback will receive with an event containing the following informat
ion in `event.detail`: | |
3464 * | |
3465 * _onKeydown: function(event) { | |
3466 * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab" | |
3467 * console.log(event.detail.key); // KEY only, e.g. "tab" | |
3468 * console.log(event.detail.event); // EVENT, e.g. "keydown" | |
3469 * console.log(event.detail.keyboardEvent); // the original KeyboardE
vent | |
3470 * } | |
3471 * | |
3472 * Use the `keyEventTarget` attribute to set up event handlers on a specific | |
3473 * node. | |
3474 * | |
3475 * See the [demo source code](https://github.com/PolymerElements/iron-a11y-k
eys-behavior/blob/master/demo/x-key-aware.html) | |
3476 * for an example. | |
3477 * | |
3478 * @demo demo/index.html | |
3479 * @polymerBehavior | |
3480 */ | |
3481 Polymer.IronA11yKeysBehavior = { | |
3482 properties: { | |
3483 /** | |
3484 * The EventTarget that will be firing relevant KeyboardEvents. Set it t
o | |
3485 * `null` to disable the listeners. | |
3486 * @type {?EventTarget} | |
3487 */ | |
3488 keyEventTarget: { | |
3489 type: Object, | |
3490 value: function() { | |
3491 return this; | |
3492 } | |
3493 }, | |
3494 | |
3495 /** | |
3496 * If true, this property will cause the implementing element to | |
3497 * automatically stop propagation on any handled KeyboardEvents. | |
3498 */ | |
3499 stopKeyboardEventPropagation: { | |
3500 type: Boolean, | |
3501 value: false | |
3502 }, | |
3503 | |
3504 _boundKeyHandlers: { | |
3505 type: Array, | |
3506 value: function() { | |
3507 return []; | |
3508 } | |
3509 }, | |
3510 | |
3511 // We use this due to a limitation in IE10 where instances will have | |
3512 // own properties of everything on the "prototype". | |
3513 _imperativeKeyBindings: { | |
3514 type: Object, | |
3515 value: function() { | |
3516 return {}; | |
3517 } | 2833 } |
3518 } | 2834 } |
| 2835 } |
| 2836 }, |
| 2837 created: function() { |
| 2838 this._metaDatas = metaDatas; |
| 2839 this._metaArrays = metaArrays; |
| 2840 }, |
| 2841 _keyChanged: function(key, old) { |
| 2842 this._resetRegistration(old); |
| 2843 }, |
| 2844 _valueChanged: function(value) { |
| 2845 this._resetRegistration(this.key); |
| 2846 }, |
| 2847 _selfChanged: function(self) { |
| 2848 if (self) { |
| 2849 this.value = this; |
| 2850 } |
| 2851 }, |
| 2852 _typeChanged: function(type) { |
| 2853 this._unregisterKey(this.key); |
| 2854 if (!metaDatas[type]) { |
| 2855 metaDatas[type] = {}; |
| 2856 } |
| 2857 this._metaData = metaDatas[type]; |
| 2858 if (!metaArrays[type]) { |
| 2859 metaArrays[type] = []; |
| 2860 } |
| 2861 this.list = metaArrays[type]; |
| 2862 this._registerKeyValue(this.key, this.value); |
| 2863 }, |
| 2864 byKey: function(key) { |
| 2865 return this._metaData && this._metaData[key]; |
| 2866 }, |
| 2867 _resetRegistration: function(oldKey) { |
| 2868 this._unregisterKey(oldKey); |
| 2869 this._registerKeyValue(this.key, this.value); |
| 2870 }, |
| 2871 _unregisterKey: function(key) { |
| 2872 this._unregister(key, this._metaData, this.list); |
| 2873 }, |
| 2874 _registerKeyValue: function(key, value) { |
| 2875 this._register(key, value, this._metaData, this.list); |
| 2876 }, |
| 2877 _register: function(key, value, data, list) { |
| 2878 if (key && data && value !== undefined) { |
| 2879 data[key] = value; |
| 2880 list.push(value); |
| 2881 } |
| 2882 }, |
| 2883 _unregister: function(key, data, list) { |
| 2884 if (key && data) { |
| 2885 if (key in data) { |
| 2886 var value = data[key]; |
| 2887 delete data[key]; |
| 2888 this.arrayDelete(list, value); |
| 2889 } |
| 2890 } |
| 2891 } |
| 2892 }); |
| 2893 Polymer.IronMeta.getIronMeta = function getIronMeta() { |
| 2894 if (singleton === null) { |
| 2895 singleton = new Polymer.IronMeta(); |
| 2896 } |
| 2897 return singleton; |
| 2898 }; |
| 2899 Polymer.IronMetaQuery = Polymer({ |
| 2900 is: 'iron-meta-query', |
| 2901 properties: { |
| 2902 type: { |
| 2903 type: String, |
| 2904 value: 'default', |
| 2905 observer: '_typeChanged' |
3519 }, | 2906 }, |
3520 | 2907 key: { |
3521 observers: [ | 2908 type: String, |
3522 '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' | 2909 observer: '_keyChanged' |
3523 ], | |
3524 | |
3525 | |
3526 /** | |
3527 * To be used to express what combination of keys will trigger the relati
ve | |
3528 * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}` | |
3529 * @type {Object} | |
3530 */ | |
3531 keyBindings: {}, | |
3532 | |
3533 registered: function() { | |
3534 this._prepKeyBindings(); | |
3535 }, | 2910 }, |
3536 | 2911 value: { |
3537 attached: function() { | 2912 type: Object, |
3538 this._listenKeyEventListeners(); | 2913 notify: true, |
| 2914 readOnly: true |
3539 }, | 2915 }, |
3540 | 2916 list: { |
3541 detached: function() { | 2917 type: Array, |
3542 this._unlistenKeyEventListeners(); | 2918 notify: true |
3543 }, | 2919 } |
3544 | 2920 }, |
3545 /** | 2921 factoryImpl: function(config) { |
3546 * Can be used to imperatively add a key binding to the implementing | 2922 if (config) { |
3547 * element. This is the imperative equivalent of declaring a keybinding | 2923 for (var n in config) { |
3548 * in the `keyBindings` prototype property. | 2924 switch (n) { |
3549 */ | 2925 case 'type': |
3550 addOwnKeyBinding: function(eventString, handlerName) { | 2926 case 'key': |
3551 this._imperativeKeyBindings[eventString] = handlerName; | 2927 this[n] = config[n]; |
3552 this._prepKeyBindings(); | 2928 break; |
3553 this._resetKeyEventListeners(); | |
3554 }, | |
3555 | |
3556 /** | |
3557 * When called, will remove all imperatively-added key bindings. | |
3558 */ | |
3559 removeOwnKeyBindings: function() { | |
3560 this._imperativeKeyBindings = {}; | |
3561 this._prepKeyBindings(); | |
3562 this._resetKeyEventListeners(); | |
3563 }, | |
3564 | |
3565 /** | |
3566 * Returns true if a keyboard event matches `eventString`. | |
3567 * | |
3568 * @param {KeyboardEvent} event | |
3569 * @param {string} eventString | |
3570 * @return {boolean} | |
3571 */ | |
3572 keyboardEventMatchesKeys: function(event, eventString) { | |
3573 var keyCombos = parseEventString(eventString); | |
3574 for (var i = 0; i < keyCombos.length; ++i) { | |
3575 if (keyComboMatchesEvent(keyCombos[i], event)) { | |
3576 return true; | |
3577 } | 2929 } |
3578 } | 2930 } |
3579 return false; | 2931 } |
3580 }, | 2932 }, |
3581 | 2933 created: function() { |
3582 _collectKeyBindings: function() { | 2934 this._metaDatas = metaDatas; |
3583 var keyBindings = this.behaviors.map(function(behavior) { | 2935 this._metaArrays = metaArrays; |
3584 return behavior.keyBindings; | 2936 }, |
3585 }); | 2937 _keyChanged: function(key) { |
3586 | 2938 this._setValue(this._metaData && this._metaData[key]); |
3587 if (keyBindings.indexOf(this.keyBindings) === -1) { | 2939 }, |
3588 keyBindings.push(this.keyBindings); | 2940 _typeChanged: function(type) { |
| 2941 this._metaData = metaDatas[type]; |
| 2942 this.list = metaArrays[type]; |
| 2943 if (this.key) { |
| 2944 this._keyChanged(this.key); |
| 2945 } |
| 2946 }, |
| 2947 byKey: function(key) { |
| 2948 return this._metaData && this._metaData[key]; |
| 2949 } |
| 2950 }); |
| 2951 })(); |
| 2952 |
| 2953 Polymer({ |
| 2954 is: 'iron-icon', |
| 2955 properties: { |
| 2956 icon: { |
| 2957 type: String, |
| 2958 observer: '_iconChanged' |
| 2959 }, |
| 2960 theme: { |
| 2961 type: String, |
| 2962 observer: '_updateIcon' |
| 2963 }, |
| 2964 src: { |
| 2965 type: String, |
| 2966 observer: '_srcChanged' |
| 2967 }, |
| 2968 _meta: { |
| 2969 value: Polymer.Base.create('iron-meta', { |
| 2970 type: 'iconset' |
| 2971 }), |
| 2972 observer: '_updateIcon' |
| 2973 } |
| 2974 }, |
| 2975 _DEFAULT_ICONSET: 'icons', |
| 2976 _iconChanged: function(icon) { |
| 2977 var parts = (icon || '').split(':'); |
| 2978 this._iconName = parts.pop(); |
| 2979 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; |
| 2980 this._updateIcon(); |
| 2981 }, |
| 2982 _srcChanged: function(src) { |
| 2983 this._updateIcon(); |
| 2984 }, |
| 2985 _usesIconset: function() { |
| 2986 return this.icon || !this.src; |
| 2987 }, |
| 2988 _updateIcon: function() { |
| 2989 if (this._usesIconset()) { |
| 2990 if (this._img && this._img.parentNode) { |
| 2991 Polymer.dom(this.root).removeChild(this._img); |
| 2992 } |
| 2993 if (this._iconName === "") { |
| 2994 if (this._iconset) { |
| 2995 this._iconset.removeIcon(this); |
3589 } | 2996 } |
3590 | 2997 } else if (this._iconsetName && this._meta) { |
3591 return keyBindings; | 2998 this._iconset = this._meta.byKey(this._iconsetName); |
3592 }, | 2999 if (this._iconset) { |
3593 | 3000 this._iconset.applyIcon(this, this._iconName, this.theme); |
3594 _prepKeyBindings: function() { | 3001 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); |
3595 this._keyBindings = {}; | 3002 } else { |
3596 | 3003 this.listen(window, 'iron-iconset-added', '_updateIcon'); |
3597 this._collectKeyBindings().forEach(function(keyBindings) { | |
3598 for (var eventString in keyBindings) { | |
3599 this._addKeyBinding(eventString, keyBindings[eventString]); | |
3600 } | |
3601 }, this); | |
3602 | |
3603 for (var eventString in this._imperativeKeyBindings) { | |
3604 this._addKeyBinding(eventString, this._imperativeKeyBindings[eventStri
ng]); | |
3605 } | 3004 } |
3606 | 3005 } |
3607 // Give precedence to combos with modifiers to be checked first. | 3006 } else { |
3608 for (var eventName in this._keyBindings) { | 3007 if (this._iconset) { |
3609 this._keyBindings[eventName].sort(function (kb1, kb2) { | 3008 this._iconset.removeIcon(this); |
3610 var b1 = kb1[0].hasModifiers; | 3009 } |
3611 var b2 = kb2[0].hasModifiers; | 3010 if (!this._img) { |
3612 return (b1 === b2) ? 0 : b1 ? -1 : 1; | 3011 this._img = document.createElement('img'); |
3613 }) | 3012 this._img.style.width = '100%'; |
| 3013 this._img.style.height = '100%'; |
| 3014 this._img.draggable = false; |
| 3015 } |
| 3016 this._img.src = this.src; |
| 3017 Polymer.dom(this.root).appendChild(this._img); |
| 3018 } |
| 3019 } |
| 3020 }); |
| 3021 |
| 3022 Polymer.PaperInkyFocusBehaviorImpl = { |
| 3023 observers: [ '_focusedChanged(receivedFocusFromKeyboard)' ], |
| 3024 _focusedChanged: function(receivedFocusFromKeyboard) { |
| 3025 if (receivedFocusFromKeyboard) { |
| 3026 this.ensureRipple(); |
| 3027 } |
| 3028 if (this.hasRipple()) { |
| 3029 this._ripple.holdDown = receivedFocusFromKeyboard; |
| 3030 } |
| 3031 }, |
| 3032 _createRipple: function() { |
| 3033 var ripple = Polymer.PaperRippleBehavior._createRipple(); |
| 3034 ripple.id = 'ink'; |
| 3035 ripple.setAttribute('center', ''); |
| 3036 ripple.classList.add('circle'); |
| 3037 return ripple; |
| 3038 } |
| 3039 }; |
| 3040 |
| 3041 Polymer.PaperInkyFocusBehavior = [ Polymer.IronButtonState, Polymer.IronControlS
tate, Polymer.PaperRippleBehavior, Polymer.PaperInkyFocusBehaviorImpl ]; |
| 3042 |
| 3043 Polymer({ |
| 3044 is: 'paper-icon-button', |
| 3045 hostAttributes: { |
| 3046 role: 'button', |
| 3047 tabindex: '0' |
| 3048 }, |
| 3049 behaviors: [ Polymer.PaperInkyFocusBehavior ], |
| 3050 properties: { |
| 3051 src: { |
| 3052 type: String |
| 3053 }, |
| 3054 icon: { |
| 3055 type: String |
| 3056 }, |
| 3057 alt: { |
| 3058 type: String, |
| 3059 observer: "_altChanged" |
| 3060 } |
| 3061 }, |
| 3062 _altChanged: function(newValue, oldValue) { |
| 3063 var label = this.getAttribute('aria-label'); |
| 3064 if (!label || oldValue == label) { |
| 3065 this.setAttribute('aria-label', newValue); |
| 3066 } |
| 3067 } |
| 3068 }); |
| 3069 |
| 3070 Polymer({ |
| 3071 is: 'paper-tab', |
| 3072 behaviors: [ Polymer.IronControlState, Polymer.IronButtonState, Polymer.PaperR
ippleBehavior ], |
| 3073 properties: { |
| 3074 link: { |
| 3075 type: Boolean, |
| 3076 value: false, |
| 3077 reflectToAttribute: true |
| 3078 } |
| 3079 }, |
| 3080 hostAttributes: { |
| 3081 role: 'tab' |
| 3082 }, |
| 3083 listeners: { |
| 3084 down: '_updateNoink', |
| 3085 tap: '_onTap' |
| 3086 }, |
| 3087 attached: function() { |
| 3088 this._updateNoink(); |
| 3089 }, |
| 3090 get _parentNoink() { |
| 3091 var parent = Polymer.dom(this).parentNode; |
| 3092 return !!parent && !!parent.noink; |
| 3093 }, |
| 3094 _updateNoink: function() { |
| 3095 this.noink = !!this.noink || !!this._parentNoink; |
| 3096 }, |
| 3097 _onTap: function(event) { |
| 3098 if (this.link) { |
| 3099 var anchor = this.queryEffectiveChildren('a'); |
| 3100 if (!anchor) { |
| 3101 return; |
| 3102 } |
| 3103 if (event.target === anchor) { |
| 3104 return; |
| 3105 } |
| 3106 anchor.click(); |
| 3107 } |
| 3108 } |
| 3109 }); |
| 3110 |
| 3111 Polymer.IronMultiSelectableBehaviorImpl = { |
| 3112 properties: { |
| 3113 multi: { |
| 3114 type: Boolean, |
| 3115 value: false, |
| 3116 observer: 'multiChanged' |
| 3117 }, |
| 3118 selectedValues: { |
| 3119 type: Array, |
| 3120 notify: true |
| 3121 }, |
| 3122 selectedItems: { |
| 3123 type: Array, |
| 3124 readOnly: true, |
| 3125 notify: true |
| 3126 } |
| 3127 }, |
| 3128 observers: [ '_updateSelected(selectedValues.splices)' ], |
| 3129 select: function(value) { |
| 3130 if (this.multi) { |
| 3131 if (this.selectedValues) { |
| 3132 this._toggleSelected(value); |
| 3133 } else { |
| 3134 this.selectedValues = [ value ]; |
| 3135 } |
| 3136 } else { |
| 3137 this.selected = value; |
| 3138 } |
| 3139 }, |
| 3140 multiChanged: function(multi) { |
| 3141 this._selection.multi = multi; |
| 3142 }, |
| 3143 get _shouldUpdateSelection() { |
| 3144 return this.selected != null || this.selectedValues != null && this.selected
Values.length; |
| 3145 }, |
| 3146 _updateAttrForSelected: function() { |
| 3147 if (!this.multi) { |
| 3148 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); |
| 3149 } else if (this._shouldUpdateSelection) { |
| 3150 this.selectedValues = this.selectedItems.map(function(selectedItem) { |
| 3151 return this._indexToValue(this.indexOf(selectedItem)); |
| 3152 }, this).filter(function(unfilteredValue) { |
| 3153 return unfilteredValue != null; |
| 3154 }, this); |
| 3155 } |
| 3156 }, |
| 3157 _updateSelected: function() { |
| 3158 if (this.multi) { |
| 3159 this._selectMulti(this.selectedValues); |
| 3160 } else { |
| 3161 this._selectSelected(this.selected); |
| 3162 } |
| 3163 }, |
| 3164 _selectMulti: function(values) { |
| 3165 if (values) { |
| 3166 var selectedItems = this._valuesToItems(values); |
| 3167 this._selection.clear(selectedItems); |
| 3168 for (var i = 0; i < selectedItems.length; i++) { |
| 3169 this._selection.setItemSelected(selectedItems[i], true); |
| 3170 } |
| 3171 if (this.fallbackSelection && this.items.length && !this._selection.get().
length) { |
| 3172 var fallback = this._valueToItem(this.fallbackSelection); |
| 3173 if (fallback) { |
| 3174 this.selectedValues = [ this.fallbackSelection ]; |
3614 } | 3175 } |
3615 }, | 3176 } |
3616 | 3177 } else { |
3617 _addKeyBinding: function(eventString, handlerName) { | 3178 this._selection.clear(); |
3618 parseEventString(eventString).forEach(function(keyCombo) { | 3179 } |
3619 this._keyBindings[keyCombo.event] = | 3180 }, |
3620 this._keyBindings[keyCombo.event] || []; | 3181 _selectionChange: function() { |
3621 | 3182 var s = this._selection.get(); |
3622 this._keyBindings[keyCombo.event].push([ | 3183 if (this.multi) { |
3623 keyCombo, | 3184 this._setSelectedItems(s); |
3624 handlerName | 3185 } else { |
3625 ]); | 3186 this._setSelectedItems([ s ]); |
3626 }, this); | 3187 this._setSelectedItem(s); |
3627 }, | 3188 } |
3628 | 3189 }, |
3629 _resetKeyEventListeners: function() { | 3190 _toggleSelected: function(value) { |
3630 this._unlistenKeyEventListeners(); | 3191 var i = this.selectedValues.indexOf(value); |
3631 | 3192 var unselected = i < 0; |
3632 if (this.isAttached) { | 3193 if (unselected) { |
3633 this._listenKeyEventListeners(); | 3194 this.push('selectedValues', value); |
3634 } | 3195 } else { |
3635 }, | 3196 this.splice('selectedValues', i, 1); |
3636 | 3197 } |
3637 _listenKeyEventListeners: function() { | 3198 }, |
3638 if (!this.keyEventTarget) { | 3199 _valuesToItems: function(values) { |
3639 return; | 3200 return values == null ? null : values.map(function(value) { |
3640 } | 3201 return this._valueToItem(value); |
3641 Object.keys(this._keyBindings).forEach(function(eventName) { | 3202 }, this); |
3642 var keyBindings = this._keyBindings[eventName]; | 3203 } |
3643 var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); | 3204 }; |
3644 | 3205 |
3645 this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyH
andler]); | 3206 Polymer.IronMultiSelectableBehavior = [ Polymer.IronSelectableBehavior, Polymer.
IronMultiSelectableBehaviorImpl ]; |
3646 | 3207 |
3647 this.keyEventTarget.addEventListener(eventName, boundKeyHandler); | 3208 Polymer.IronMenuBehaviorImpl = { |
3648 }, this); | 3209 properties: { |
3649 }, | 3210 focusedItem: { |
3650 | 3211 observer: '_focusedItemChanged', |
3651 _unlistenKeyEventListeners: function() { | 3212 readOnly: true, |
3652 var keyHandlerTuple; | 3213 type: Object |
3653 var keyEventTarget; | 3214 }, |
3654 var eventName; | 3215 attrForItemTitle: { |
3655 var boundKeyHandler; | 3216 type: String |
3656 | 3217 } |
3657 while (this._boundKeyHandlers.length) { | 3218 }, |
3658 // My kingdom for block-scope binding and destructuring assignment.. | 3219 hostAttributes: { |
3659 keyHandlerTuple = this._boundKeyHandlers.pop(); | 3220 role: 'menu', |
3660 keyEventTarget = keyHandlerTuple[0]; | 3221 tabindex: '0' |
3661 eventName = keyHandlerTuple[1]; | 3222 }, |
3662 boundKeyHandler = keyHandlerTuple[2]; | 3223 observers: [ '_updateMultiselectable(multi)' ], |
3663 | 3224 listeners: { |
3664 keyEventTarget.removeEventListener(eventName, boundKeyHandler); | 3225 focus: '_onFocus', |
3665 } | 3226 keydown: '_onKeydown', |
3666 }, | 3227 'iron-items-changed': '_onIronItemsChanged' |
3667 | 3228 }, |
3668 _onKeyBindingEvent: function(keyBindings, event) { | 3229 keyBindings: { |
3669 if (this.stopKeyboardEventPropagation) { | 3230 up: '_onUpKey', |
3670 event.stopPropagation(); | 3231 down: '_onDownKey', |
3671 } | 3232 esc: '_onEscKey', |
3672 | 3233 'shift+tab:keydown': '_onShiftTabDown' |
3673 // if event has been already prevented, don't do anything | 3234 }, |
3674 if (event.defaultPrevented) { | 3235 attached: function() { |
3675 return; | 3236 this._resetTabindices(); |
3676 } | 3237 }, |
3677 | 3238 select: function(value) { |
3678 for (var i = 0; i < keyBindings.length; i++) { | 3239 if (this._defaultFocusAsync) { |
3679 var keyCombo = keyBindings[i][0]; | 3240 this.cancelAsync(this._defaultFocusAsync); |
3680 var handlerName = keyBindings[i][1]; | 3241 this._defaultFocusAsync = null; |
3681 if (keyComboMatchesEvent(keyCombo, event)) { | 3242 } |
3682 this._triggerKeyHandler(keyCombo, handlerName, event); | 3243 var item = this._valueToItem(value); |
3683 // exit the loop if eventDefault was prevented | 3244 if (item && item.hasAttribute('disabled')) return; |
3684 if (event.defaultPrevented) { | 3245 this._setFocusedItem(item); |
3685 return; | 3246 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); |
3686 } | 3247 }, |
3687 } | 3248 _resetTabindices: function() { |
3688 } | 3249 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0]
: this.selectedItem; |
3689 }, | 3250 this.items.forEach(function(item) { |
3690 | 3251 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); |
3691 _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { | 3252 }, this); |
3692 var detail = Object.create(keyCombo); | 3253 }, |
3693 detail.keyboardEvent = keyboardEvent; | 3254 _updateMultiselectable: function(multi) { |
3694 var event = new CustomEvent(keyCombo.event, { | 3255 if (multi) { |
3695 detail: detail, | 3256 this.setAttribute('aria-multiselectable', 'true'); |
3696 cancelable: true | 3257 } else { |
3697 }); | 3258 this.removeAttribute('aria-multiselectable'); |
3698 this[handlerName].call(this, event); | 3259 } |
3699 if (event.defaultPrevented) { | 3260 }, |
3700 keyboardEvent.preventDefault(); | 3261 _focusWithKeyboardEvent: function(event) { |
3701 } | 3262 for (var i = 0, item; item = this.items[i]; i++) { |
3702 } | 3263 var attr = this.attrForItemTitle || 'textContent'; |
3703 }; | 3264 var title = item[attr] || item.getAttribute(attr); |
3704 })(); | 3265 if (!item.hasAttribute('disabled') && title && title.trim().charAt(0).toLo
werCase() === String.fromCharCode(event.keyCode).toLowerCase()) { |
3705 /** | 3266 this._setFocusedItem(item); |
3706 * @demo demo/index.html | 3267 break; |
3707 * @polymerBehavior | 3268 } |
3708 */ | 3269 } |
3709 Polymer.IronControlState = { | 3270 }, |
3710 | 3271 _focusPrevious: function() { |
3711 properties: { | 3272 var length = this.items.length; |
3712 | 3273 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
3713 /** | 3274 for (var i = 1; i < length + 1; i++) { |
3714 * If true, the element currently has focus. | 3275 var item = this.items[(curFocusIndex - i + length) % length]; |
3715 */ | 3276 if (!item.hasAttribute('disabled')) { |
3716 focused: { | 3277 this._setFocusedItem(item); |
3717 type: Boolean, | |
3718 value: false, | |
3719 notify: true, | |
3720 readOnly: true, | |
3721 reflectToAttribute: true | |
3722 }, | |
3723 | |
3724 /** | |
3725 * If true, the user cannot interact with this element. | |
3726 */ | |
3727 disabled: { | |
3728 type: Boolean, | |
3729 value: false, | |
3730 notify: true, | |
3731 observer: '_disabledChanged', | |
3732 reflectToAttribute: true | |
3733 }, | |
3734 | |
3735 _oldTabIndex: { | |
3736 type: Number | |
3737 }, | |
3738 | |
3739 _boundFocusBlurHandler: { | |
3740 type: Function, | |
3741 value: function() { | |
3742 return this._focusBlurHandler.bind(this); | |
3743 } | |
3744 } | |
3745 | |
3746 }, | |
3747 | |
3748 observers: [ | |
3749 '_changedControlState(focused, disabled)' | |
3750 ], | |
3751 | |
3752 ready: function() { | |
3753 this.addEventListener('focus', this._boundFocusBlurHandler, true); | |
3754 this.addEventListener('blur', this._boundFocusBlurHandler, true); | |
3755 }, | |
3756 | |
3757 _focusBlurHandler: function(event) { | |
3758 // NOTE(cdata): if we are in ShadowDOM land, `event.target` will | |
3759 // eventually become `this` due to retargeting; if we are not in | |
3760 // ShadowDOM land, `event.target` will eventually become `this` due | |
3761 // to the second conditional which fires a synthetic event (that is also | |
3762 // handled). In either case, we can disregard `event.path`. | |
3763 | |
3764 if (event.target === this) { | |
3765 this._setFocused(event.type === 'focus'); | |
3766 } else if (!this.shadowRoot) { | |
3767 var target = /** @type {Node} */(Polymer.dom(event).localTarget); | |
3768 if (!this.isLightDescendant(target)) { | |
3769 this.fire(event.type, {sourceEvent: event}, { | |
3770 node: this, | |
3771 bubbles: event.bubbles, | |
3772 cancelable: event.cancelable | |
3773 }); | |
3774 } | |
3775 } | |
3776 }, | |
3777 | |
3778 _disabledChanged: function(disabled, old) { | |
3779 this.setAttribute('aria-disabled', disabled ? 'true' : 'false'); | |
3780 this.style.pointerEvents = disabled ? 'none' : ''; | |
3781 if (disabled) { | |
3782 this._oldTabIndex = this.tabIndex; | |
3783 this._setFocused(false); | |
3784 this.tabIndex = -1; | |
3785 this.blur(); | |
3786 } else if (this._oldTabIndex !== undefined) { | |
3787 this.tabIndex = this._oldTabIndex; | |
3788 } | |
3789 }, | |
3790 | |
3791 _changedControlState: function() { | |
3792 // _controlStateChanged is abstract, follow-on behaviors may implement it | |
3793 if (this._controlStateChanged) { | |
3794 this._controlStateChanged(); | |
3795 } | |
3796 } | |
3797 | |
3798 }; | |
3799 /** | |
3800 * @demo demo/index.html | |
3801 * @polymerBehavior Polymer.IronButtonState | |
3802 */ | |
3803 Polymer.IronButtonStateImpl = { | |
3804 | |
3805 properties: { | |
3806 | |
3807 /** | |
3808 * If true, the user is currently holding down the button. | |
3809 */ | |
3810 pressed: { | |
3811 type: Boolean, | |
3812 readOnly: true, | |
3813 value: false, | |
3814 reflectToAttribute: true, | |
3815 observer: '_pressedChanged' | |
3816 }, | |
3817 | |
3818 /** | |
3819 * If true, the button toggles the active state with each tap or press | |
3820 * of the spacebar. | |
3821 */ | |
3822 toggles: { | |
3823 type: Boolean, | |
3824 value: false, | |
3825 reflectToAttribute: true | |
3826 }, | |
3827 | |
3828 /** | |
3829 * If true, the button is a toggle and is currently in the active state. | |
3830 */ | |
3831 active: { | |
3832 type: Boolean, | |
3833 value: false, | |
3834 notify: true, | |
3835 reflectToAttribute: true | |
3836 }, | |
3837 | |
3838 /** | |
3839 * True if the element is currently being pressed by a "pointer," which | |
3840 * is loosely defined as mouse or touch input (but specifically excluding | |
3841 * keyboard input). | |
3842 */ | |
3843 pointerDown: { | |
3844 type: Boolean, | |
3845 readOnly: true, | |
3846 value: false | |
3847 }, | |
3848 | |
3849 /** | |
3850 * True if the input device that caused the element to receive focus | |
3851 * was a keyboard. | |
3852 */ | |
3853 receivedFocusFromKeyboard: { | |
3854 type: Boolean, | |
3855 readOnly: true | |
3856 }, | |
3857 | |
3858 /** | |
3859 * The aria attribute to be set if the button is a toggle and in the | |
3860 * active state. | |
3861 */ | |
3862 ariaActiveAttribute: { | |
3863 type: String, | |
3864 value: 'aria-pressed', | |
3865 observer: '_ariaActiveAttributeChanged' | |
3866 } | |
3867 }, | |
3868 | |
3869 listeners: { | |
3870 down: '_downHandler', | |
3871 up: '_upHandler', | |
3872 tap: '_tapHandler' | |
3873 }, | |
3874 | |
3875 observers: [ | |
3876 '_detectKeyboardFocus(focused)', | |
3877 '_activeChanged(active, ariaActiveAttribute)' | |
3878 ], | |
3879 | |
3880 keyBindings: { | |
3881 'enter:keydown': '_asyncClick', | |
3882 'space:keydown': '_spaceKeyDownHandler', | |
3883 'space:keyup': '_spaceKeyUpHandler', | |
3884 }, | |
3885 | |
3886 _mouseEventRe: /^mouse/, | |
3887 | |
3888 _tapHandler: function() { | |
3889 if (this.toggles) { | |
3890 // a tap is needed to toggle the active state | |
3891 this._userActivate(!this.active); | |
3892 } else { | |
3893 this.active = false; | |
3894 } | |
3895 }, | |
3896 | |
3897 _detectKeyboardFocus: function(focused) { | |
3898 this._setReceivedFocusFromKeyboard(!this.pointerDown && focused); | |
3899 }, | |
3900 | |
3901 // to emulate native checkbox, (de-)activations from a user interaction fire | |
3902 // 'change' events | |
3903 _userActivate: function(active) { | |
3904 if (this.active !== active) { | |
3905 this.active = active; | |
3906 this.fire('change'); | |
3907 } | |
3908 }, | |
3909 | |
3910 _downHandler: function(event) { | |
3911 this._setPointerDown(true); | |
3912 this._setPressed(true); | |
3913 this._setReceivedFocusFromKeyboard(false); | |
3914 }, | |
3915 | |
3916 _upHandler: function() { | |
3917 this._setPointerDown(false); | |
3918 this._setPressed(false); | |
3919 }, | |
3920 | |
3921 /** | |
3922 * @param {!KeyboardEvent} event . | |
3923 */ | |
3924 _spaceKeyDownHandler: function(event) { | |
3925 var keyboardEvent = event.detail.keyboardEvent; | |
3926 var target = Polymer.dom(keyboardEvent).localTarget; | |
3927 | |
3928 // Ignore the event if this is coming from a focused light child, since th
at | |
3929 // element will deal with it. | |
3930 if (this.isLightDescendant(/** @type {Node} */(target))) | |
3931 return; | 3278 return; |
3932 | 3279 } |
3933 keyboardEvent.preventDefault(); | 3280 } |
3934 keyboardEvent.stopImmediatePropagation(); | 3281 }, |
3935 this._setPressed(true); | 3282 _focusNext: function() { |
3936 }, | 3283 var length = this.items.length; |
3937 | 3284 var curFocusIndex = Number(this.indexOf(this.focusedItem)); |
3938 /** | 3285 for (var i = 1; i < length + 1; i++) { |
3939 * @param {!KeyboardEvent} event . | 3286 var item = this.items[(curFocusIndex + i) % length]; |
3940 */ | 3287 if (!item.hasAttribute('disabled')) { |
3941 _spaceKeyUpHandler: function(event) { | 3288 this._setFocusedItem(item); |
3942 var keyboardEvent = event.detail.keyboardEvent; | |
3943 var target = Polymer.dom(keyboardEvent).localTarget; | |
3944 | |
3945 // Ignore the event if this is coming from a focused light child, since th
at | |
3946 // element will deal with it. | |
3947 if (this.isLightDescendant(/** @type {Node} */(target))) | |
3948 return; | 3289 return; |
3949 | 3290 } |
3950 if (this.pressed) { | 3291 } |
3951 this._asyncClick(); | 3292 }, |
3952 } | 3293 _applySelection: function(item, isSelected) { |
3953 this._setPressed(false); | 3294 if (isSelected) { |
3954 }, | 3295 item.setAttribute('aria-selected', 'true'); |
3955 | 3296 } else { |
3956 // trigger click asynchronously, the asynchrony is useful to allow one | 3297 item.removeAttribute('aria-selected'); |
3957 // event handler to unwind before triggering another event | 3298 } |
3958 _asyncClick: function() { | 3299 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); |
3959 this.async(function() { | 3300 }, |
3960 this.click(); | 3301 _focusedItemChanged: function(focusedItem, old) { |
3961 }, 1); | 3302 old && old.setAttribute('tabindex', '-1'); |
3962 }, | 3303 if (focusedItem) { |
3963 | 3304 focusedItem.setAttribute('tabindex', '0'); |
3964 // any of these changes are considered a change to button state | 3305 focusedItem.focus(); |
3965 | 3306 } |
3966 _pressedChanged: function(pressed) { | 3307 }, |
3967 this._changedButtonState(); | 3308 _onIronItemsChanged: function(event) { |
3968 }, | 3309 if (event.detail.addedNodes.length) { |
3969 | 3310 this._resetTabindices(); |
3970 _ariaActiveAttributeChanged: function(value, oldValue) { | 3311 } |
3971 if (oldValue && oldValue != value && this.hasAttribute(oldValue)) { | 3312 }, |
3972 this.removeAttribute(oldValue); | 3313 _onShiftTabDown: function(event) { |
3973 } | 3314 var oldTabIndex = this.getAttribute('tabindex'); |
3974 }, | 3315 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; |
3975 | 3316 this._setFocusedItem(null); |
3976 _activeChanged: function(active, ariaActiveAttribute) { | 3317 this.setAttribute('tabindex', '-1'); |
3977 if (this.toggles) { | 3318 this.async(function() { |
3978 this.setAttribute(this.ariaActiveAttribute, | 3319 this.setAttribute('tabindex', oldTabIndex); |
3979 active ? 'true' : 'false'); | 3320 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
3980 } else { | 3321 }, 1); |
3981 this.removeAttribute(this.ariaActiveAttribute); | 3322 }, |
3982 } | 3323 _onFocus: function(event) { |
3983 this._changedButtonState(); | 3324 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { |
3984 }, | 3325 return; |
3985 | 3326 } |
3986 _controlStateChanged: function() { | 3327 var rootTarget = Polymer.dom(event).rootTarget; |
3987 if (this.disabled) { | 3328 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !th
is.isLightDescendant(rootTarget)) { |
3988 this._setPressed(false); | 3329 return; |
3989 } else { | 3330 } |
3990 this._changedButtonState(); | 3331 this._defaultFocusAsync = this.async(function() { |
3991 } | 3332 var selectedItem = this.multi ? this.selectedItems && this.selectedItems[0
] : this.selectedItem; |
3992 }, | 3333 this._setFocusedItem(null); |
3993 | 3334 if (selectedItem) { |
3994 // provide hook for follow-on behaviors to react to button-state | 3335 this._setFocusedItem(selectedItem); |
3995 | 3336 } else if (this.items[0]) { |
3996 _changedButtonState: function() { | 3337 this._focusNext(); |
3997 if (this._buttonStateChanged) { | 3338 } |
3998 this._buttonStateChanged(); // abstract | |
3999 } | |
4000 } | |
4001 | |
4002 }; | |
4003 | |
4004 /** @polymerBehavior */ | |
4005 Polymer.IronButtonState = [ | |
4006 Polymer.IronA11yKeysBehavior, | |
4007 Polymer.IronButtonStateImpl | |
4008 ]; | |
4009 (function() { | |
4010 var Utility = { | |
4011 distance: function(x1, y1, x2, y2) { | |
4012 var xDelta = (x1 - x2); | |
4013 var yDelta = (y1 - y2); | |
4014 | |
4015 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
4016 }, | |
4017 | |
4018 now: window.performance && window.performance.now ? | |
4019 window.performance.now.bind(window.performance) : Date.now | |
4020 }; | |
4021 | |
4022 /** | |
4023 * @param {HTMLElement} element | |
4024 * @constructor | |
4025 */ | |
4026 function ElementMetrics(element) { | |
4027 this.element = element; | |
4028 this.width = this.boundingRect.width; | |
4029 this.height = this.boundingRect.height; | |
4030 | |
4031 this.size = Math.max(this.width, this.height); | |
4032 } | |
4033 | |
4034 ElementMetrics.prototype = { | |
4035 get boundingRect () { | |
4036 return this.element.getBoundingClientRect(); | |
4037 }, | |
4038 | |
4039 furthestCornerDistanceFrom: function(x, y) { | |
4040 var topLeft = Utility.distance(x, y, 0, 0); | |
4041 var topRight = Utility.distance(x, y, this.width, 0); | |
4042 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
4043 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
4044 | |
4045 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
4046 } | |
4047 }; | |
4048 | |
4049 /** | |
4050 * @param {HTMLElement} element | |
4051 * @constructor | |
4052 */ | |
4053 function Ripple(element) { | |
4054 this.element = element; | |
4055 this.color = window.getComputedStyle(element).color; | |
4056 | |
4057 this.wave = document.createElement('div'); | |
4058 this.waveContainer = document.createElement('div'); | |
4059 this.wave.style.backgroundColor = this.color; | |
4060 this.wave.classList.add('wave'); | |
4061 this.waveContainer.classList.add('wave-container'); | |
4062 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
4063 | |
4064 this.resetInteractionState(); | |
4065 } | |
4066 | |
4067 Ripple.MAX_RADIUS = 300; | |
4068 | |
4069 Ripple.prototype = { | |
4070 get recenters() { | |
4071 return this.element.recenters; | |
4072 }, | |
4073 | |
4074 get center() { | |
4075 return this.element.center; | |
4076 }, | |
4077 | |
4078 get mouseDownElapsed() { | |
4079 var elapsed; | |
4080 | |
4081 if (!this.mouseDownStart) { | |
4082 return 0; | |
4083 } | |
4084 | |
4085 elapsed = Utility.now() - this.mouseDownStart; | |
4086 | |
4087 if (this.mouseUpStart) { | |
4088 elapsed -= this.mouseUpElapsed; | |
4089 } | |
4090 | |
4091 return elapsed; | |
4092 }, | |
4093 | |
4094 get mouseUpElapsed() { | |
4095 return this.mouseUpStart ? | |
4096 Utility.now () - this.mouseUpStart : 0; | |
4097 }, | |
4098 | |
4099 get mouseDownElapsedSeconds() { | |
4100 return this.mouseDownElapsed / 1000; | |
4101 }, | |
4102 | |
4103 get mouseUpElapsedSeconds() { | |
4104 return this.mouseUpElapsed / 1000; | |
4105 }, | |
4106 | |
4107 get mouseInteractionSeconds() { | |
4108 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
4109 }, | |
4110 | |
4111 get initialOpacity() { | |
4112 return this.element.initialOpacity; | |
4113 }, | |
4114 | |
4115 get opacityDecayVelocity() { | |
4116 return this.element.opacityDecayVelocity; | |
4117 }, | |
4118 | |
4119 get radius() { | |
4120 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
4121 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
4122 var waveRadius = Math.min( | |
4123 Math.sqrt(width2 + height2), | |
4124 Ripple.MAX_RADIUS | |
4125 ) * 1.1 + 5; | |
4126 | |
4127 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
4128 var timeNow = this.mouseInteractionSeconds / duration; | |
4129 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
4130 | |
4131 return Math.abs(size); | |
4132 }, | |
4133 | |
4134 get opacity() { | |
4135 if (!this.mouseUpStart) { | |
4136 return this.initialOpacity; | |
4137 } | |
4138 | |
4139 return Math.max( | |
4140 0, | |
4141 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
4142 ); | |
4143 }, | |
4144 | |
4145 get outerOpacity() { | |
4146 // Linear increase in background opacity, capped at the opacity | |
4147 // of the wavefront (waveOpacity). | |
4148 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
4149 var waveOpacity = this.opacity; | |
4150 | |
4151 return Math.max( | |
4152 0, | |
4153 Math.min(outerOpacity, waveOpacity) | |
4154 ); | |
4155 }, | |
4156 | |
4157 get isOpacityFullyDecayed() { | |
4158 return this.opacity < 0.01 && | |
4159 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
4160 }, | |
4161 | |
4162 get isRestingAtMaxRadius() { | |
4163 return this.opacity >= this.initialOpacity && | |
4164 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
4165 }, | |
4166 | |
4167 get isAnimationComplete() { | |
4168 return this.mouseUpStart ? | |
4169 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
4170 }, | |
4171 | |
4172 get translationFraction() { | |
4173 return Math.min( | |
4174 1, | |
4175 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
4176 ); | |
4177 }, | |
4178 | |
4179 get xNow() { | |
4180 if (this.xEnd) { | |
4181 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
4182 } | |
4183 | |
4184 return this.xStart; | |
4185 }, | |
4186 | |
4187 get yNow() { | |
4188 if (this.yEnd) { | |
4189 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
4190 } | |
4191 | |
4192 return this.yStart; | |
4193 }, | |
4194 | |
4195 get isMouseDown() { | |
4196 return this.mouseDownStart && !this.mouseUpStart; | |
4197 }, | |
4198 | |
4199 resetInteractionState: function() { | |
4200 this.maxRadius = 0; | |
4201 this.mouseDownStart = 0; | |
4202 this.mouseUpStart = 0; | |
4203 | |
4204 this.xStart = 0; | |
4205 this.yStart = 0; | |
4206 this.xEnd = 0; | |
4207 this.yEnd = 0; | |
4208 this.slideDistance = 0; | |
4209 | |
4210 this.containerMetrics = new ElementMetrics(this.element); | |
4211 }, | |
4212 | |
4213 draw: function() { | |
4214 var scale; | |
4215 var translateString; | |
4216 var dx; | |
4217 var dy; | |
4218 | |
4219 this.wave.style.opacity = this.opacity; | |
4220 | |
4221 scale = this.radius / (this.containerMetrics.size / 2); | |
4222 dx = this.xNow - (this.containerMetrics.width / 2); | |
4223 dy = this.yNow - (this.containerMetrics.height / 2); | |
4224 | |
4225 | |
4226 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
4227 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
4228 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
4229 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
4230 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
4231 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
4232 }, | |
4233 | |
4234 /** @param {Event=} event */ | |
4235 downAction: function(event) { | |
4236 var xCenter = this.containerMetrics.width / 2; | |
4237 var yCenter = this.containerMetrics.height / 2; | |
4238 | |
4239 this.resetInteractionState(); | |
4240 this.mouseDownStart = Utility.now(); | |
4241 | |
4242 if (this.center) { | |
4243 this.xStart = xCenter; | |
4244 this.yStart = yCenter; | |
4245 this.slideDistance = Utility.distance( | |
4246 this.xStart, this.yStart, this.xEnd, this.yEnd | |
4247 ); | |
4248 } else { | |
4249 this.xStart = event ? | |
4250 event.detail.x - this.containerMetrics.boundingRect.left : | |
4251 this.containerMetrics.width / 2; | |
4252 this.yStart = event ? | |
4253 event.detail.y - this.containerMetrics.boundingRect.top : | |
4254 this.containerMetrics.height / 2; | |
4255 } | |
4256 | |
4257 if (this.recenters) { | |
4258 this.xEnd = xCenter; | |
4259 this.yEnd = yCenter; | |
4260 this.slideDistance = Utility.distance( | |
4261 this.xStart, this.yStart, this.xEnd, this.yEnd | |
4262 ); | |
4263 } | |
4264 | |
4265 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
4266 this.xStart, | |
4267 this.yStart | |
4268 ); | |
4269 | |
4270 this.waveContainer.style.top = | |
4271 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
4272 this.waveContainer.style.left = | |
4273 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
4274 | |
4275 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
4276 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
4277 }, | |
4278 | |
4279 /** @param {Event=} event */ | |
4280 upAction: function(event) { | |
4281 if (!this.isMouseDown) { | |
4282 return; | |
4283 } | |
4284 | |
4285 this.mouseUpStart = Utility.now(); | |
4286 }, | |
4287 | |
4288 remove: function() { | |
4289 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
4290 this.waveContainer | |
4291 ); | |
4292 } | |
4293 }; | |
4294 | |
4295 Polymer({ | |
4296 is: 'paper-ripple', | |
4297 | |
4298 behaviors: [ | |
4299 Polymer.IronA11yKeysBehavior | |
4300 ], | |
4301 | |
4302 properties: { | |
4303 /** | |
4304 * The initial opacity set on the wave. | |
4305 * | |
4306 * @attribute initialOpacity | |
4307 * @type number | |
4308 * @default 0.25 | |
4309 */ | |
4310 initialOpacity: { | |
4311 type: Number, | |
4312 value: 0.25 | |
4313 }, | |
4314 | |
4315 /** | |
4316 * How fast (opacity per second) the wave fades out. | |
4317 * | |
4318 * @attribute opacityDecayVelocity | |
4319 * @type number | |
4320 * @default 0.8 | |
4321 */ | |
4322 opacityDecayVelocity: { | |
4323 type: Number, | |
4324 value: 0.8 | |
4325 }, | |
4326 | |
4327 /** | |
4328 * If true, ripples will exhibit a gravitational pull towards | |
4329 * the center of their container as they fade away. | |
4330 * | |
4331 * @attribute recenters | |
4332 * @type boolean | |
4333 * @default false | |
4334 */ | |
4335 recenters: { | |
4336 type: Boolean, | |
4337 value: false | |
4338 }, | |
4339 | |
4340 /** | |
4341 * If true, ripples will center inside its container | |
4342 * | |
4343 * @attribute recenters | |
4344 * @type boolean | |
4345 * @default false | |
4346 */ | |
4347 center: { | |
4348 type: Boolean, | |
4349 value: false | |
4350 }, | |
4351 | |
4352 /** | |
4353 * A list of the visual ripples. | |
4354 * | |
4355 * @attribute ripples | |
4356 * @type Array | |
4357 * @default [] | |
4358 */ | |
4359 ripples: { | |
4360 type: Array, | |
4361 value: function() { | |
4362 return []; | |
4363 } | |
4364 }, | |
4365 | |
4366 /** | |
4367 * True when there are visible ripples animating within the | |
4368 * element. | |
4369 */ | |
4370 animating: { | |
4371 type: Boolean, | |
4372 readOnly: true, | |
4373 reflectToAttribute: true, | |
4374 value: false | |
4375 }, | |
4376 | |
4377 /** | |
4378 * If true, the ripple will remain in the "down" state until `holdDown` | |
4379 * is set to false again. | |
4380 */ | |
4381 holdDown: { | |
4382 type: Boolean, | |
4383 value: false, | |
4384 observer: '_holdDownChanged' | |
4385 }, | |
4386 | |
4387 /** | |
4388 * If true, the ripple will not generate a ripple effect | |
4389 * via pointer interaction. | |
4390 * Calling ripple's imperative api like `simulatedRipple` will | |
4391 * still generate the ripple effect. | |
4392 */ | |
4393 noink: { | |
4394 type: Boolean, | |
4395 value: false | |
4396 }, | |
4397 | |
4398 _animating: { | |
4399 type: Boolean | |
4400 }, | |
4401 | |
4402 _boundAnimate: { | |
4403 type: Function, | |
4404 value: function() { | |
4405 return this.animate.bind(this); | |
4406 } | |
4407 } | |
4408 }, | |
4409 | |
4410 get target () { | |
4411 return this.keyEventTarget; | |
4412 }, | |
4413 | |
4414 keyBindings: { | |
4415 'enter:keydown': '_onEnterKeydown', | |
4416 'space:keydown': '_onSpaceKeydown', | |
4417 'space:keyup': '_onSpaceKeyup' | |
4418 }, | |
4419 | |
4420 attached: function() { | |
4421 // Set up a11yKeysBehavior to listen to key events on the target, | |
4422 // so that space and enter activate the ripple even if the target doesn'
t | |
4423 // handle key events. The key handlers deal with `noink` themselves. | |
4424 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | |
4425 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; | |
4426 } else { | |
4427 this.keyEventTarget = this.parentNode; | |
4428 } | |
4429 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); | |
4430 this.listen(keyEventTarget, 'up', 'uiUpAction'); | |
4431 this.listen(keyEventTarget, 'down', 'uiDownAction'); | |
4432 }, | |
4433 | |
4434 detached: function() { | |
4435 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); | |
4436 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); | |
4437 this.keyEventTarget = null; | |
4438 }, | |
4439 | |
4440 get shouldKeepAnimating () { | |
4441 for (var index = 0; index < this.ripples.length; ++index) { | |
4442 if (!this.ripples[index].isAnimationComplete) { | |
4443 return true; | |
4444 } | |
4445 } | |
4446 | |
4447 return false; | |
4448 }, | |
4449 | |
4450 simulatedRipple: function() { | |
4451 this.downAction(null); | |
4452 | |
4453 // Please see polymer/polymer#1305 | |
4454 this.async(function() { | |
4455 this.upAction(); | |
4456 }, 1); | |
4457 }, | |
4458 | |
4459 /** | |
4460 * Provokes a ripple down effect via a UI event, | |
4461 * respecting the `noink` property. | |
4462 * @param {Event=} event | |
4463 */ | |
4464 uiDownAction: function(event) { | |
4465 if (!this.noink) { | |
4466 this.downAction(event); | |
4467 } | |
4468 }, | |
4469 | |
4470 /** | |
4471 * Provokes a ripple down effect via a UI event, | |
4472 * *not* respecting the `noink` property. | |
4473 * @param {Event=} event | |
4474 */ | |
4475 downAction: function(event) { | |
4476 if (this.holdDown && this.ripples.length > 0) { | |
4477 return; | |
4478 } | |
4479 | |
4480 var ripple = this.addRipple(); | |
4481 | |
4482 ripple.downAction(event); | |
4483 | |
4484 if (!this._animating) { | |
4485 this._animating = true; | |
4486 this.animate(); | |
4487 } | |
4488 }, | |
4489 | |
4490 /** | |
4491 * Provokes a ripple up effect via a UI event, | |
4492 * respecting the `noink` property. | |
4493 * @param {Event=} event | |
4494 */ | |
4495 uiUpAction: function(event) { | |
4496 if (!this.noink) { | |
4497 this.upAction(event); | |
4498 } | |
4499 }, | |
4500 | |
4501 /** | |
4502 * Provokes a ripple up effect via a UI event, | |
4503 * *not* respecting the `noink` property. | |
4504 * @param {Event=} event | |
4505 */ | |
4506 upAction: function(event) { | |
4507 if (this.holdDown) { | |
4508 return; | |
4509 } | |
4510 | |
4511 this.ripples.forEach(function(ripple) { | |
4512 ripple.upAction(event); | |
4513 }); | |
4514 | |
4515 this._animating = true; | |
4516 this.animate(); | |
4517 }, | |
4518 | |
4519 onAnimationComplete: function() { | |
4520 this._animating = false; | |
4521 this.$.background.style.backgroundColor = null; | |
4522 this.fire('transitionend'); | |
4523 }, | |
4524 | |
4525 addRipple: function() { | |
4526 var ripple = new Ripple(this); | |
4527 | |
4528 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | |
4529 this.$.background.style.backgroundColor = ripple.color; | |
4530 this.ripples.push(ripple); | |
4531 | |
4532 this._setAnimating(true); | |
4533 | |
4534 return ripple; | |
4535 }, | |
4536 | |
4537 removeRipple: function(ripple) { | |
4538 var rippleIndex = this.ripples.indexOf(ripple); | |
4539 | |
4540 if (rippleIndex < 0) { | |
4541 return; | |
4542 } | |
4543 | |
4544 this.ripples.splice(rippleIndex, 1); | |
4545 | |
4546 ripple.remove(); | |
4547 | |
4548 if (!this.ripples.length) { | |
4549 this._setAnimating(false); | |
4550 } | |
4551 }, | |
4552 | |
4553 animate: function() { | |
4554 if (!this._animating) { | |
4555 return; | |
4556 } | |
4557 var index; | |
4558 var ripple; | |
4559 | |
4560 for (index = 0; index < this.ripples.length; ++index) { | |
4561 ripple = this.ripples[index]; | |
4562 | |
4563 ripple.draw(); | |
4564 | |
4565 this.$.background.style.opacity = ripple.outerOpacity; | |
4566 | |
4567 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | |
4568 this.removeRipple(ripple); | |
4569 } | |
4570 } | |
4571 | |
4572 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | |
4573 this.onAnimationComplete(); | |
4574 } else { | |
4575 window.requestAnimationFrame(this._boundAnimate); | |
4576 } | |
4577 }, | |
4578 | |
4579 _onEnterKeydown: function() { | |
4580 this.uiDownAction(); | |
4581 this.async(this.uiUpAction, 1); | |
4582 }, | |
4583 | |
4584 _onSpaceKeydown: function() { | |
4585 this.uiDownAction(); | |
4586 }, | |
4587 | |
4588 _onSpaceKeyup: function() { | |
4589 this.uiUpAction(); | |
4590 }, | |
4591 | |
4592 // note: holdDown does not respect noink since it can be a focus based | |
4593 // effect. | |
4594 _holdDownChanged: function(newVal, oldVal) { | |
4595 if (oldVal === undefined) { | |
4596 return; | |
4597 } | |
4598 if (newVal) { | |
4599 this.downAction(); | |
4600 } else { | |
4601 this.upAction(); | |
4602 } | |
4603 } | |
4604 | |
4605 /** | |
4606 Fired when the animation finishes. | |
4607 This is useful if you want to wait until | |
4608 the ripple animation finishes to perform some action. | |
4609 | |
4610 @event transitionend | |
4611 @param {{node: Object}} detail Contains the animated node. | |
4612 */ | |
4613 }); | 3339 }); |
4614 })(); | 3340 }, |
4615 /** | 3341 _onUpKey: function(event) { |
4616 * `Polymer.PaperRippleBehavior` dynamically implements a ripple | 3342 this._focusPrevious(); |
4617 * when the element has focus via pointer or keyboard. | 3343 event.detail.keyboardEvent.preventDefault(); |
4618 * | 3344 }, |
4619 * NOTE: This behavior is intended to be used in conjunction with and after | 3345 _onDownKey: function(event) { |
4620 * `Polymer.IronButtonState` and `Polymer.IronControlState`. | 3346 this._focusNext(); |
4621 * | 3347 event.detail.keyboardEvent.preventDefault(); |
4622 * @polymerBehavior Polymer.PaperRippleBehavior | 3348 }, |
4623 */ | 3349 _onEscKey: function(event) { |
4624 Polymer.PaperRippleBehavior = { | 3350 this.focusedItem.blur(); |
4625 properties: { | 3351 }, |
4626 /** | 3352 _onKeydown: function(event) { |
4627 * If true, the element will not produce a ripple effect when interacted | 3353 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { |
4628 * with via the pointer. | 3354 this._focusWithKeyboardEvent(event); |
4629 */ | 3355 } |
4630 noink: { | 3356 event.stopPropagation(); |
4631 type: Boolean, | 3357 }, |
4632 observer: '_noinkChanged' | 3358 _activateHandler: function(event) { |
4633 }, | 3359 Polymer.IronSelectableBehavior._activateHandler.call(this, event); |
4634 | 3360 event.stopPropagation(); |
4635 /** | 3361 } |
4636 * @type {Element|undefined} | 3362 }; |
4637 */ | 3363 |
4638 _rippleContainer: { | 3364 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; |
4639 type: Object, | 3365 |
4640 } | 3366 Polymer.IronMenuBehavior = [ Polymer.IronMultiSelectableBehavior, Polymer.IronA1
1yKeysBehavior, Polymer.IronMenuBehaviorImpl ]; |
4641 }, | 3367 |
4642 | 3368 Polymer.IronMenubarBehaviorImpl = { |
4643 /** | 3369 hostAttributes: { |
4644 * Ensures a `<paper-ripple>` element is available when the element is | 3370 role: 'menubar' |
4645 * focused. | 3371 }, |
4646 */ | 3372 keyBindings: { |
4647 _buttonStateChanged: function() { | 3373 left: '_onLeftKey', |
4648 if (this.focused) { | 3374 right: '_onRightKey' |
4649 this.ensureRipple(); | 3375 }, |
4650 } | 3376 _onUpKey: function(event) { |
4651 }, | 3377 this.focusedItem.click(); |
4652 | 3378 event.detail.keyboardEvent.preventDefault(); |
4653 /** | 3379 }, |
4654 * In addition to the functionality provided in `IronButtonState`, ensures | 3380 _onDownKey: function(event) { |
4655 * a ripple effect is created when the element is in a `pressed` state. | 3381 this.focusedItem.click(); |
4656 */ | 3382 event.detail.keyboardEvent.preventDefault(); |
4657 _downHandler: function(event) { | 3383 }, |
4658 Polymer.IronButtonStateImpl._downHandler.call(this, event); | 3384 get _isRTL() { |
4659 if (this.pressed) { | 3385 return window.getComputedStyle(this)['direction'] === 'rtl'; |
4660 this.ensureRipple(event); | 3386 }, |
4661 } | 3387 _onLeftKey: function(event) { |
4662 }, | 3388 if (this._isRTL) { |
4663 | 3389 this._focusNext(); |
4664 /** | 3390 } else { |
4665 * Ensures this element contains a ripple effect. For startup efficiency | 3391 this._focusPrevious(); |
4666 * the ripple effect is dynamically on demand when needed. | 3392 } |
4667 * @param {!Event=} optTriggeringEvent (optional) event that triggered the | 3393 event.detail.keyboardEvent.preventDefault(); |
4668 * ripple. | 3394 }, |
4669 */ | 3395 _onRightKey: function(event) { |
4670 ensureRipple: function(optTriggeringEvent) { | 3396 if (this._isRTL) { |
4671 if (!this.hasRipple()) { | 3397 this._focusPrevious(); |
4672 this._ripple = this._createRipple(); | 3398 } else { |
4673 this._ripple.noink = this.noink; | 3399 this._focusNext(); |
4674 var rippleContainer = this._rippleContainer || this.root; | 3400 } |
4675 if (rippleContainer) { | 3401 event.detail.keyboardEvent.preventDefault(); |
4676 Polymer.dom(rippleContainer).appendChild(this._ripple); | 3402 }, |
4677 } | 3403 _onKeydown: function(event) { |
4678 if (optTriggeringEvent) { | 3404 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) { |
4679 // Check if the event happened inside of the ripple container | 3405 return; |
4680 // Fall back to host instead of the root because distributed text | 3406 } |
4681 // nodes are not valid event targets | 3407 this._focusWithKeyboardEvent(event); |
4682 var domContainer = Polymer.dom(this._rippleContainer || this); | 3408 } |
4683 var target = Polymer.dom(optTriggeringEvent).rootTarget; | 3409 }; |
4684 if (domContainer.deepContains( /** @type {Node} */(target))) { | 3410 |
4685 this._ripple.uiDownAction(optTriggeringEvent); | 3411 Polymer.IronMenubarBehavior = [ Polymer.IronMenuBehavior, Polymer.IronMenubarBeh
aviorImpl ]; |
4686 } | 3412 |
4687 } | |
4688 } | |
4689 }, | |
4690 | |
4691 /** | |
4692 * Returns the `<paper-ripple>` element used by this element to create | |
4693 * ripple effects. The element's ripple is created on demand, when | |
4694 * necessary, and calling this method will force the | |
4695 * ripple to be created. | |
4696 */ | |
4697 getRipple: function() { | |
4698 this.ensureRipple(); | |
4699 return this._ripple; | |
4700 }, | |
4701 | |
4702 /** | |
4703 * Returns true if this element currently contains a ripple effect. | |
4704 * @return {boolean} | |
4705 */ | |
4706 hasRipple: function() { | |
4707 return Boolean(this._ripple); | |
4708 }, | |
4709 | |
4710 /** | |
4711 * Create the element's ripple effect via creating a `<paper-ripple>`. | |
4712 * Override this method to customize the ripple element. | |
4713 * @return {!PaperRippleElement} Returns a `<paper-ripple>` element. | |
4714 */ | |
4715 _createRipple: function() { | |
4716 return /** @type {!PaperRippleElement} */ ( | |
4717 document.createElement('paper-ripple')); | |
4718 }, | |
4719 | |
4720 _noinkChanged: function(noink) { | |
4721 if (this.hasRipple()) { | |
4722 this._ripple.noink = noink; | |
4723 } | |
4724 } | |
4725 }; | |
4726 /** @polymerBehavior Polymer.PaperButtonBehavior */ | |
4727 Polymer.PaperButtonBehaviorImpl = { | |
4728 properties: { | |
4729 /** | |
4730 * The z-depth of this element, from 0-5. Setting to 0 will remove the | |
4731 * shadow, and each increasing number greater than 0 will be "deeper" | |
4732 * than the last. | |
4733 * | |
4734 * @attribute elevation | |
4735 * @type number | |
4736 * @default 1 | |
4737 */ | |
4738 elevation: { | |
4739 type: Number, | |
4740 reflectToAttribute: true, | |
4741 readOnly: true | |
4742 } | |
4743 }, | |
4744 | |
4745 observers: [ | |
4746 '_calculateElevation(focused, disabled, active, pressed, receivedFocusFrom
Keyboard)', | |
4747 '_computeKeyboardClass(receivedFocusFromKeyboard)' | |
4748 ], | |
4749 | |
4750 hostAttributes: { | |
4751 role: 'button', | |
4752 tabindex: '0', | |
4753 animated: true | |
4754 }, | |
4755 | |
4756 _calculateElevation: function() { | |
4757 var e = 1; | |
4758 if (this.disabled) { | |
4759 e = 0; | |
4760 } else if (this.active || this.pressed) { | |
4761 e = 4; | |
4762 } else if (this.receivedFocusFromKeyboard) { | |
4763 e = 3; | |
4764 } | |
4765 this._setElevation(e); | |
4766 }, | |
4767 | |
4768 _computeKeyboardClass: function(receivedFocusFromKeyboard) { | |
4769 this.toggleClass('keyboard-focus', receivedFocusFromKeyboard); | |
4770 }, | |
4771 | |
4772 /** | |
4773 * In addition to `IronButtonState` behavior, when space key goes down, | |
4774 * create a ripple down effect. | |
4775 * | |
4776 * @param {!KeyboardEvent} event . | |
4777 */ | |
4778 _spaceKeyDownHandler: function(event) { | |
4779 Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event); | |
4780 // Ensure that there is at most one ripple when the space key is held down
. | |
4781 if (this.hasRipple() && this.getRipple().ripples.length < 1) { | |
4782 this._ripple.uiDownAction(); | |
4783 } | |
4784 }, | |
4785 | |
4786 /** | |
4787 * In addition to `IronButtonState` behavior, when space key goes up, | |
4788 * create a ripple up effect. | |
4789 * | |
4790 * @param {!KeyboardEvent} event . | |
4791 */ | |
4792 _spaceKeyUpHandler: function(event) { | |
4793 Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event); | |
4794 if (this.hasRipple()) { | |
4795 this._ripple.uiUpAction(); | |
4796 } | |
4797 } | |
4798 }; | |
4799 | |
4800 /** @polymerBehavior */ | |
4801 Polymer.PaperButtonBehavior = [ | |
4802 Polymer.IronButtonState, | |
4803 Polymer.IronControlState, | |
4804 Polymer.PaperRippleBehavior, | |
4805 Polymer.PaperButtonBehaviorImpl | |
4806 ]; | |
4807 Polymer({ | 3413 Polymer({ |
4808 is: 'paper-button', | 3414 is: 'iron-iconset-svg', |
4809 | 3415 properties: { |
4810 behaviors: [ | 3416 name: { |
4811 Polymer.PaperButtonBehavior | 3417 type: String, |
4812 ], | 3418 observer: '_nameChanged' |
4813 | 3419 }, |
4814 properties: { | 3420 size: { |
4815 /** | 3421 type: Number, |
4816 * If true, the button should be styled with a shadow. | 3422 value: 24 |
4817 */ | 3423 } |
4818 raised: { | 3424 }, |
4819 type: Boolean, | 3425 attached: function() { |
4820 reflectToAttribute: true, | 3426 this.style.display = 'none'; |
4821 value: false, | 3427 }, |
4822 observer: '_calculateElevation' | 3428 getIconNames: function() { |
4823 } | 3429 this._icons = this._createIconMap(); |
4824 }, | 3430 return Object.keys(this._icons).map(function(n) { |
4825 | 3431 return this.name + ':' + n; |
4826 _calculateElevation: function() { | 3432 }, this); |
4827 if (!this.raised) { | 3433 }, |
4828 this._setElevation(0); | 3434 applyIcon: function(element, iconName) { |
4829 } else { | 3435 element = element.root || element; |
4830 Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this); | 3436 this.removeIcon(element); |
4831 } | 3437 var svg = this._cloneIcon(iconName); |
4832 } | 3438 if (svg) { |
4833 | 3439 var pde = Polymer.dom(element); |
4834 /** | 3440 pde.insertBefore(svg, pde.childNodes[0]); |
4835 Fired when the animation finishes. | 3441 return element._svgIcon = svg; |
4836 This is useful if you want to wait until | 3442 } |
4837 the ripple animation finishes to perform some action. | 3443 return null; |
4838 | 3444 }, |
4839 @event transitionend | 3445 removeIcon: function(element) { |
4840 Event param: {{node: Object}} detail Contains the animated node. | 3446 if (element._svgIcon) { |
4841 */ | 3447 Polymer.dom(element).removeChild(element._svgIcon); |
| 3448 element._svgIcon = null; |
| 3449 } |
| 3450 }, |
| 3451 _nameChanged: function() { |
| 3452 new Polymer.IronMeta({ |
| 3453 type: 'iconset', |
| 3454 key: this.name, |
| 3455 value: this |
4842 }); | 3456 }); |
4843 (function() { | 3457 this.async(function() { |
4844 | 3458 this.fire('iron-iconset-added', this, { |
4845 // monostate data | 3459 node: window |
4846 var metaDatas = {}; | 3460 }); |
4847 var metaArrays = {}; | |
4848 var singleton = null; | |
4849 | |
4850 Polymer.IronMeta = Polymer({ | |
4851 | |
4852 is: 'iron-meta', | |
4853 | |
4854 properties: { | |
4855 | |
4856 /** | |
4857 * The type of meta-data. All meta-data of the same type is stored | |
4858 * together. | |
4859 */ | |
4860 type: { | |
4861 type: String, | |
4862 value: 'default', | |
4863 observer: '_typeChanged' | |
4864 }, | |
4865 | |
4866 /** | |
4867 * The key used to store `value` under the `type` namespace. | |
4868 */ | |
4869 key: { | |
4870 type: String, | |
4871 observer: '_keyChanged' | |
4872 }, | |
4873 | |
4874 /** | |
4875 * The meta-data to store or retrieve. | |
4876 */ | |
4877 value: { | |
4878 type: Object, | |
4879 notify: true, | |
4880 observer: '_valueChanged' | |
4881 }, | |
4882 | |
4883 /** | |
4884 * If true, `value` is set to the iron-meta instance itself. | |
4885 */ | |
4886 self: { | |
4887 type: Boolean, | |
4888 observer: '_selfChanged' | |
4889 }, | |
4890 | |
4891 /** | |
4892 * Array of all meta-data values for the given type. | |
4893 */ | |
4894 list: { | |
4895 type: Array, | |
4896 notify: true | |
4897 } | |
4898 | |
4899 }, | |
4900 | |
4901 hostAttributes: { | |
4902 hidden: true | |
4903 }, | |
4904 | |
4905 /** | |
4906 * Only runs if someone invokes the factory/constructor directly | |
4907 * e.g. `new Polymer.IronMeta()` | |
4908 * | |
4909 * @param {{type: (string|undefined), key: (string|undefined), value}=} co
nfig | |
4910 */ | |
4911 factoryImpl: function(config) { | |
4912 if (config) { | |
4913 for (var n in config) { | |
4914 switch(n) { | |
4915 case 'type': | |
4916 case 'key': | |
4917 case 'value': | |
4918 this[n] = config[n]; | |
4919 break; | |
4920 } | |
4921 } | |
4922 } | |
4923 }, | |
4924 | |
4925 created: function() { | |
4926 // TODO(sjmiles): good for debugging? | |
4927 this._metaDatas = metaDatas; | |
4928 this._metaArrays = metaArrays; | |
4929 }, | |
4930 | |
4931 _keyChanged: function(key, old) { | |
4932 this._resetRegistration(old); | |
4933 }, | |
4934 | |
4935 _valueChanged: function(value) { | |
4936 this._resetRegistration(this.key); | |
4937 }, | |
4938 | |
4939 _selfChanged: function(self) { | |
4940 if (self) { | |
4941 this.value = this; | |
4942 } | |
4943 }, | |
4944 | |
4945 _typeChanged: function(type) { | |
4946 this._unregisterKey(this.key); | |
4947 if (!metaDatas[type]) { | |
4948 metaDatas[type] = {}; | |
4949 } | |
4950 this._metaData = metaDatas[type]; | |
4951 if (!metaArrays[type]) { | |
4952 metaArrays[type] = []; | |
4953 } | |
4954 this.list = metaArrays[type]; | |
4955 this._registerKeyValue(this.key, this.value); | |
4956 }, | |
4957 | |
4958 /** | |
4959 * Retrieves meta data value by key. | |
4960 * | |
4961 * @method byKey | |
4962 * @param {string} key The key of the meta-data to be returned. | |
4963 * @return {*} | |
4964 */ | |
4965 byKey: function(key) { | |
4966 return this._metaData && this._metaData[key]; | |
4967 }, | |
4968 | |
4969 _resetRegistration: function(oldKey) { | |
4970 this._unregisterKey(oldKey); | |
4971 this._registerKeyValue(this.key, this.value); | |
4972 }, | |
4973 | |
4974 _unregisterKey: function(key) { | |
4975 this._unregister(key, this._metaData, this.list); | |
4976 }, | |
4977 | |
4978 _registerKeyValue: function(key, value) { | |
4979 this._register(key, value, this._metaData, this.list); | |
4980 }, | |
4981 | |
4982 _register: function(key, value, data, list) { | |
4983 if (key && data && value !== undefined) { | |
4984 data[key] = value; | |
4985 list.push(value); | |
4986 } | |
4987 }, | |
4988 | |
4989 _unregister: function(key, data, list) { | |
4990 if (key && data) { | |
4991 if (key in data) { | |
4992 var value = data[key]; | |
4993 delete data[key]; | |
4994 this.arrayDelete(list, value); | |
4995 } | |
4996 } | |
4997 } | |
4998 | |
4999 }); | 3461 }); |
5000 | 3462 }, |
5001 Polymer.IronMeta.getIronMeta = function getIronMeta() { | 3463 _createIconMap: function() { |
5002 if (singleton === null) { | 3464 var icons = Object.create(null); |
5003 singleton = new Polymer.IronMeta(); | 3465 Polymer.dom(this).querySelectorAll('[id]').forEach(function(icon) { |
5004 } | 3466 icons[icon.id] = icon; |
5005 return singleton; | |
5006 }; | |
5007 | |
5008 /** | |
5009 `iron-meta-query` can be used to access infomation stored in `iron-meta`. | |
5010 | |
5011 Examples: | |
5012 | |
5013 If I create an instance like this: | |
5014 | |
5015 <iron-meta key="info" value="foo/bar"></iron-meta> | |
5016 | |
5017 Note that value="foo/bar" is the metadata I've defined. I could define more | |
5018 attributes or use child nodes to define additional metadata. | |
5019 | |
5020 Now I can access that element (and it's metadata) from any `iron-meta-query`
instance: | |
5021 | |
5022 var value = new Polymer.IronMetaQuery({key: 'info'}).value; | |
5023 | |
5024 @group Polymer Iron Elements | |
5025 @element iron-meta-query | |
5026 */ | |
5027 Polymer.IronMetaQuery = Polymer({ | |
5028 | |
5029 is: 'iron-meta-query', | |
5030 | |
5031 properties: { | |
5032 | |
5033 /** | |
5034 * The type of meta-data. All meta-data of the same type is stored | |
5035 * together. | |
5036 */ | |
5037 type: { | |
5038 type: String, | |
5039 value: 'default', | |
5040 observer: '_typeChanged' | |
5041 }, | |
5042 | |
5043 /** | |
5044 * Specifies a key to use for retrieving `value` from the `type` | |
5045 * namespace. | |
5046 */ | |
5047 key: { | |
5048 type: String, | |
5049 observer: '_keyChanged' | |
5050 }, | |
5051 | |
5052 /** | |
5053 * The meta-data to store or retrieve. | |
5054 */ | |
5055 value: { | |
5056 type: Object, | |
5057 notify: true, | |
5058 readOnly: true | |
5059 }, | |
5060 | |
5061 /** | |
5062 * Array of all meta-data values for the given type. | |
5063 */ | |
5064 list: { | |
5065 type: Array, | |
5066 notify: true | |
5067 } | |
5068 | |
5069 }, | |
5070 | |
5071 /** | |
5072 * Actually a factory method, not a true constructor. Only runs if | |
5073 * someone invokes it directly (via `new Polymer.IronMeta()`); | |
5074 * | |
5075 * @param {{type: (string|undefined), key: (string|undefined)}=} config | |
5076 */ | |
5077 factoryImpl: function(config) { | |
5078 if (config) { | |
5079 for (var n in config) { | |
5080 switch(n) { | |
5081 case 'type': | |
5082 case 'key': | |
5083 this[n] = config[n]; | |
5084 break; | |
5085 } | |
5086 } | |
5087 } | |
5088 }, | |
5089 | |
5090 created: function() { | |
5091 // TODO(sjmiles): good for debugging? | |
5092 this._metaDatas = metaDatas; | |
5093 this._metaArrays = metaArrays; | |
5094 }, | |
5095 | |
5096 _keyChanged: function(key) { | |
5097 this._setValue(this._metaData && this._metaData[key]); | |
5098 }, | |
5099 | |
5100 _typeChanged: function(type) { | |
5101 this._metaData = metaDatas[type]; | |
5102 this.list = metaArrays[type]; | |
5103 if (this.key) { | |
5104 this._keyChanged(this.key); | |
5105 } | |
5106 }, | |
5107 | |
5108 /** | |
5109 * Retrieves meta data value by key. | |
5110 * @param {string} key The key of the meta-data to be returned. | |
5111 * @return {*} | |
5112 */ | |
5113 byKey: function(key) { | |
5114 return this._metaData && this._metaData[key]; | |
5115 } | |
5116 | |
5117 }); | 3467 }); |
5118 | 3468 return icons; |
5119 })(); | 3469 }, |
| 3470 _cloneIcon: function(id) { |
| 3471 this._icons = this._icons || this._createIconMap(); |
| 3472 return this._prepareSvgClone(this._icons[id], this.size); |
| 3473 }, |
| 3474 _prepareSvgClone: function(sourceSvg, size) { |
| 3475 if (sourceSvg) { |
| 3476 var content = sourceSvg.cloneNode(true), svg = document.createElementNS('h
ttp://www.w3.org/2000/svg', 'svg'), viewBox = content.getAttribute('viewBox') ||
'0 0 ' + size + ' ' + size; |
| 3477 svg.setAttribute('viewBox', viewBox); |
| 3478 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); |
| 3479 svg.style.cssText = 'pointer-events: none; display: block; width: 100%; he
ight: 100%;'; |
| 3480 svg.appendChild(content).removeAttribute('id'); |
| 3481 return svg; |
| 3482 } |
| 3483 return null; |
| 3484 } |
| 3485 }); |
| 3486 |
5120 Polymer({ | 3487 Polymer({ |
5121 | 3488 is: 'paper-tabs', |
5122 is: 'iron-icon', | 3489 behaviors: [ Polymer.IronResizableBehavior, Polymer.IronMenubarBehavior ], |
5123 | 3490 properties: { |
5124 properties: { | 3491 noink: { |
5125 | 3492 type: Boolean, |
5126 /** | 3493 value: false, |
5127 * The name of the icon to use. The name should be of the form: | 3494 observer: '_noinkChanged' |
5128 * `iconset_name:icon_name`. | 3495 }, |
5129 */ | 3496 noBar: { |
5130 icon: { | 3497 type: Boolean, |
5131 type: String, | 3498 value: false |
5132 observer: '_iconChanged' | 3499 }, |
5133 }, | 3500 noSlide: { |
5134 | 3501 type: Boolean, |
5135 /** | 3502 value: false |
5136 * The name of the theme to used, if one is specified by the | 3503 }, |
5137 * iconset. | 3504 scrollable: { |
5138 */ | 3505 type: Boolean, |
5139 theme: { | 3506 value: false |
5140 type: String, | 3507 }, |
5141 observer: '_updateIcon' | 3508 fitContainer: { |
5142 }, | 3509 type: Boolean, |
5143 | 3510 value: false |
5144 /** | 3511 }, |
5145 * If using iron-icon without an iconset, you can set the src to be | 3512 disableDrag: { |
5146 * the URL of an individual icon image file. Note that this will take | 3513 type: Boolean, |
5147 * precedence over a given icon attribute. | 3514 value: false |
5148 */ | 3515 }, |
5149 src: { | 3516 hideScrollButtons: { |
5150 type: String, | 3517 type: Boolean, |
5151 observer: '_srcChanged' | 3518 value: false |
5152 }, | 3519 }, |
5153 | 3520 alignBottom: { |
5154 /** | 3521 type: Boolean, |
5155 * @type {!Polymer.IronMeta} | 3522 value: false |
5156 */ | 3523 }, |
5157 _meta: { | 3524 selectable: { |
5158 value: Polymer.Base.create('iron-meta', {type: 'iconset'}), | 3525 type: String, |
5159 observer: '_updateIcon' | 3526 value: 'paper-tab' |
5160 } | 3527 }, |
5161 | 3528 autoselect: { |
5162 }, | 3529 type: Boolean, |
5163 | 3530 value: false |
5164 _DEFAULT_ICONSET: 'icons', | 3531 }, |
5165 | 3532 autoselectDelay: { |
5166 _iconChanged: function(icon) { | 3533 type: Number, |
5167 var parts = (icon || '').split(':'); | 3534 value: 0 |
5168 this._iconName = parts.pop(); | 3535 }, |
5169 this._iconsetName = parts.pop() || this._DEFAULT_ICONSET; | 3536 _step: { |
5170 this._updateIcon(); | 3537 type: Number, |
5171 }, | 3538 value: 10 |
5172 | 3539 }, |
5173 _srcChanged: function(src) { | 3540 _holdDelay: { |
5174 this._updateIcon(); | 3541 type: Number, |
5175 }, | 3542 value: 1 |
5176 | 3543 }, |
5177 _usesIconset: function() { | 3544 _leftHidden: { |
5178 return this.icon || !this.src; | 3545 type: Boolean, |
5179 }, | 3546 value: false |
5180 | 3547 }, |
5181 /** @suppress {visibility} */ | 3548 _rightHidden: { |
5182 _updateIcon: function() { | 3549 type: Boolean, |
5183 if (this._usesIconset()) { | 3550 value: false |
5184 if (this._img && this._img.parentNode) { | 3551 }, |
5185 Polymer.dom(this.root).removeChild(this._img); | 3552 _previousTab: { |
5186 } | 3553 type: Object |
5187 if (this._iconName === "") { | 3554 } |
5188 if (this._iconset) { | 3555 }, |
5189 this._iconset.removeIcon(this); | 3556 hostAttributes: { |
5190 } | 3557 role: 'tablist' |
5191 } else if (this._iconsetName && this._meta) { | 3558 }, |
5192 this._iconset = /** @type {?Polymer.Iconset} */ ( | 3559 listeners: { |
5193 this._meta.byKey(this._iconsetName)); | 3560 'iron-resize': '_onTabSizingChanged', |
5194 if (this._iconset) { | 3561 'iron-items-changed': '_onTabSizingChanged', |
5195 this._iconset.applyIcon(this, this._iconName, this.theme); | 3562 'iron-select': '_onIronSelect', |
5196 this.unlisten(window, 'iron-iconset-added', '_updateIcon'); | 3563 'iron-deselect': '_onIronDeselect' |
5197 } else { | 3564 }, |
5198 this.listen(window, 'iron-iconset-added', '_updateIcon'); | 3565 keyBindings: { |
5199 } | 3566 'left:keyup right:keyup': '_onArrowKeyup' |
5200 } | 3567 }, |
5201 } else { | 3568 created: function() { |
5202 if (this._iconset) { | 3569 this._holdJob = null; |
5203 this._iconset.removeIcon(this); | 3570 this._pendingActivationItem = undefined; |
5204 } | 3571 this._pendingActivationTimeout = undefined; |
5205 if (!this._img) { | 3572 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind(thi
s); |
5206 this._img = document.createElement('img'); | 3573 this.addEventListener('blur', this._onBlurCapture.bind(this), true); |
5207 this._img.style.width = '100%'; | 3574 }, |
5208 this._img.style.height = '100%'; | 3575 ready: function() { |
5209 this._img.draggable = false; | 3576 this.setScrollDirection('y', this.$.tabsContainer); |
5210 } | 3577 }, |
5211 this._img.src = this.src; | 3578 detached: function() { |
5212 Polymer.dom(this.root).appendChild(this._img); | 3579 this._cancelPendingActivation(); |
5213 } | 3580 }, |
5214 } | 3581 _noinkChanged: function(noink) { |
5215 | 3582 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab'); |
| 3583 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAttribu
te); |
| 3584 }, |
| 3585 _setNoinkAttribute: function(element) { |
| 3586 element.setAttribute('noink', ''); |
| 3587 }, |
| 3588 _removeNoinkAttribute: function(element) { |
| 3589 element.removeAttribute('noink'); |
| 3590 }, |
| 3591 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScrollButt
ons) { |
| 3592 if (!scrollable || hideScrollButtons) { |
| 3593 return 'hidden'; |
| 3594 } |
| 3595 if (hideThisButton) { |
| 3596 return 'not-visible'; |
| 3597 } |
| 3598 return ''; |
| 3599 }, |
| 3600 _computeTabsContentClass: function(scrollable, fitContainer) { |
| 3601 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : '') :
' fit-container'; |
| 3602 }, |
| 3603 _computeSelectionBarClass: function(noBar, alignBottom) { |
| 3604 if (noBar) { |
| 3605 return 'hidden'; |
| 3606 } else if (alignBottom) { |
| 3607 return 'align-bottom'; |
| 3608 } |
| 3609 return ''; |
| 3610 }, |
| 3611 _onTabSizingChanged: function() { |
| 3612 this.debounce('_onTabSizingChanged', function() { |
| 3613 this._scroll(); |
| 3614 this._tabChanged(this.selectedItem); |
| 3615 }, 10); |
| 3616 }, |
| 3617 _onIronSelect: function(event) { |
| 3618 this._tabChanged(event.detail.item, this._previousTab); |
| 3619 this._previousTab = event.detail.item; |
| 3620 this.cancelDebouncer('tab-changed'); |
| 3621 }, |
| 3622 _onIronDeselect: function(event) { |
| 3623 this.debounce('tab-changed', function() { |
| 3624 this._tabChanged(null, this._previousTab); |
| 3625 this._previousTab = null; |
| 3626 }, 1); |
| 3627 }, |
| 3628 _activateHandler: function() { |
| 3629 this._cancelPendingActivation(); |
| 3630 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments); |
| 3631 }, |
| 3632 _scheduleActivation: function(item, delay) { |
| 3633 this._pendingActivationItem = item; |
| 3634 this._pendingActivationTimeout = this.async(this._bindDelayedActivationHandl
er, delay); |
| 3635 }, |
| 3636 _delayedActivationHandler: function() { |
| 3637 var item = this._pendingActivationItem; |
| 3638 this._pendingActivationItem = undefined; |
| 3639 this._pendingActivationTimeout = undefined; |
| 3640 item.fire(this.activateEvent, null, { |
| 3641 bubbles: true, |
| 3642 cancelable: true |
5216 }); | 3643 }); |
5217 /** | 3644 }, |
5218 * `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has k
eyboard focus. | 3645 _cancelPendingActivation: function() { |
5219 * | 3646 if (this._pendingActivationTimeout !== undefined) { |
5220 * @polymerBehavior Polymer.PaperInkyFocusBehavior | 3647 this.cancelAsync(this._pendingActivationTimeout); |
5221 */ | 3648 this._pendingActivationItem = undefined; |
5222 Polymer.PaperInkyFocusBehaviorImpl = { | 3649 this._pendingActivationTimeout = undefined; |
5223 observers: [ | 3650 } |
5224 '_focusedChanged(receivedFocusFromKeyboard)' | 3651 }, |
5225 ], | 3652 _onArrowKeyup: function(event) { |
5226 | 3653 if (this.autoselect) { |
5227 _focusedChanged: function(receivedFocusFromKeyboard) { | 3654 this._scheduleActivation(this.focusedItem, this.autoselectDelay); |
5228 if (receivedFocusFromKeyboard) { | 3655 } |
5229 this.ensureRipple(); | 3656 }, |
5230 } | 3657 _onBlurCapture: function(event) { |
5231 if (this.hasRipple()) { | 3658 if (event.target === this._pendingActivationItem) { |
5232 this._ripple.holdDown = receivedFocusFromKeyboard; | 3659 this._cancelPendingActivation(); |
5233 } | 3660 } |
5234 }, | 3661 }, |
5235 | 3662 get _tabContainerScrollSize() { |
5236 _createRipple: function() { | 3663 return Math.max(0, this.$.tabsContainer.scrollWidth - this.$.tabsContainer.o
ffsetWidth); |
5237 var ripple = Polymer.PaperRippleBehavior._createRipple(); | 3664 }, |
5238 ripple.id = 'ink'; | 3665 _scroll: function(e, detail) { |
5239 ripple.setAttribute('center', ''); | 3666 if (!this.scrollable) { |
5240 ripple.classList.add('circle'); | 3667 return; |
5241 return ripple; | 3668 } |
5242 } | 3669 var ddx = detail && -detail.ddx || 0; |
5243 }; | 3670 this._affectScroll(ddx); |
5244 | 3671 }, |
5245 /** @polymerBehavior Polymer.PaperInkyFocusBehavior */ | 3672 _down: function(e) { |
5246 Polymer.PaperInkyFocusBehavior = [ | 3673 this.async(function() { |
5247 Polymer.IronButtonState, | |
5248 Polymer.IronControlState, | |
5249 Polymer.PaperRippleBehavior, | |
5250 Polymer.PaperInkyFocusBehaviorImpl | |
5251 ]; | |
5252 Polymer({ | |
5253 is: 'paper-icon-button', | |
5254 | |
5255 hostAttributes: { | |
5256 role: 'button', | |
5257 tabindex: '0' | |
5258 }, | |
5259 | |
5260 behaviors: [ | |
5261 Polymer.PaperInkyFocusBehavior | |
5262 ], | |
5263 | |
5264 properties: { | |
5265 /** | |
5266 * The URL of an image for the icon. If the src property is specified, | |
5267 * the icon property should not be. | |
5268 */ | |
5269 src: { | |
5270 type: String | |
5271 }, | |
5272 | |
5273 /** | |
5274 * Specifies the icon name or index in the set of icons available in | |
5275 * the icon's icon set. If the icon property is specified, | |
5276 * the src property should not be. | |
5277 */ | |
5278 icon: { | |
5279 type: String | |
5280 }, | |
5281 | |
5282 /** | |
5283 * Specifies the alternate text for the button, for accessibility. | |
5284 */ | |
5285 alt: { | |
5286 type: String, | |
5287 observer: "_altChanged" | |
5288 } | |
5289 }, | |
5290 | |
5291 _altChanged: function(newValue, oldValue) { | |
5292 var label = this.getAttribute('aria-label'); | |
5293 | |
5294 // Don't stomp over a user-set aria-label. | |
5295 if (!label || oldValue == label) { | |
5296 this.setAttribute('aria-label', newValue); | |
5297 } | |
5298 } | |
5299 }); | |
5300 Polymer({ | |
5301 is: 'paper-tab', | |
5302 | |
5303 behaviors: [ | |
5304 Polymer.IronControlState, | |
5305 Polymer.IronButtonState, | |
5306 Polymer.PaperRippleBehavior | |
5307 ], | |
5308 | |
5309 properties: { | |
5310 | |
5311 /** | |
5312 * If true, the tab will forward keyboard clicks (enter/space) to | |
5313 * the first anchor element found in its descendants | |
5314 */ | |
5315 link: { | |
5316 type: Boolean, | |
5317 value: false, | |
5318 reflectToAttribute: true | |
5319 } | |
5320 | |
5321 }, | |
5322 | |
5323 hostAttributes: { | |
5324 role: 'tab' | |
5325 }, | |
5326 | |
5327 listeners: { | |
5328 down: '_updateNoink', | |
5329 tap: '_onTap' | |
5330 }, | |
5331 | |
5332 attached: function() { | |
5333 this._updateNoink(); | |
5334 }, | |
5335 | |
5336 get _parentNoink () { | |
5337 var parent = Polymer.dom(this).parentNode; | |
5338 return !!parent && !!parent.noink; | |
5339 }, | |
5340 | |
5341 _updateNoink: function() { | |
5342 this.noink = !!this.noink || !!this._parentNoink; | |
5343 }, | |
5344 | |
5345 _onTap: function(event) { | |
5346 if (this.link) { | |
5347 var anchor = this.queryEffectiveChildren('a'); | |
5348 | |
5349 if (!anchor) { | |
5350 return; | |
5351 } | |
5352 | |
5353 // Don't get stuck in a loop delegating | |
5354 // the listener from the child anchor | |
5355 if (event.target === anchor) { | |
5356 return; | |
5357 } | |
5358 | |
5359 anchor.click(); | |
5360 } | |
5361 } | |
5362 | |
5363 }); | |
5364 /** @polymerBehavior Polymer.IronMultiSelectableBehavior */ | |
5365 Polymer.IronMultiSelectableBehaviorImpl = { | |
5366 properties: { | |
5367 | |
5368 /** | |
5369 * If true, multiple selections are allowed. | |
5370 */ | |
5371 multi: { | |
5372 type: Boolean, | |
5373 value: false, | |
5374 observer: 'multiChanged' | |
5375 }, | |
5376 | |
5377 /** | |
5378 * Gets or sets the selected elements. This is used instead of `selected`
when `multi` | |
5379 * is true. | |
5380 */ | |
5381 selectedValues: { | |
5382 type: Array, | |
5383 notify: true | |
5384 }, | |
5385 | |
5386 /** | |
5387 * Returns an array of currently selected items. | |
5388 */ | |
5389 selectedItems: { | |
5390 type: Array, | |
5391 readOnly: true, | |
5392 notify: true | |
5393 }, | |
5394 | |
5395 }, | |
5396 | |
5397 observers: [ | |
5398 '_updateSelected(selectedValues.splices)' | |
5399 ], | |
5400 | |
5401 /** | |
5402 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
5403 * `value` will be toggled; otherwise the `value` will be selected. | |
5404 * | |
5405 * @method select | |
5406 * @param {string|number} value the value to select. | |
5407 */ | |
5408 select: function(value) { | |
5409 if (this.multi) { | |
5410 if (this.selectedValues) { | |
5411 this._toggleSelected(value); | |
5412 } else { | |
5413 this.selectedValues = [value]; | |
5414 } | |
5415 } else { | |
5416 this.selected = value; | |
5417 } | |
5418 }, | |
5419 | |
5420 multiChanged: function(multi) { | |
5421 this._selection.multi = multi; | |
5422 }, | |
5423 | |
5424 get _shouldUpdateSelection() { | |
5425 return this.selected != null || | |
5426 (this.selectedValues != null && this.selectedValues.length); | |
5427 }, | |
5428 | |
5429 _updateAttrForSelected: function() { | |
5430 if (!this.multi) { | |
5431 Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this); | |
5432 } else if (this._shouldUpdateSelection) { | |
5433 this.selectedValues = this.selectedItems.map(function(selectedItem) { | |
5434 return this._indexToValue(this.indexOf(selectedItem)); | |
5435 }, this).filter(function(unfilteredValue) { | |
5436 return unfilteredValue != null; | |
5437 }, this); | |
5438 } | |
5439 }, | |
5440 | |
5441 _updateSelected: function() { | |
5442 if (this.multi) { | |
5443 this._selectMulti(this.selectedValues); | |
5444 } else { | |
5445 this._selectSelected(this.selected); | |
5446 } | |
5447 }, | |
5448 | |
5449 _selectMulti: function(values) { | |
5450 if (values) { | |
5451 var selectedItems = this._valuesToItems(values); | |
5452 // clear all but the current selected items | |
5453 this._selection.clear(selectedItems); | |
5454 // select only those not selected yet | |
5455 for (var i = 0; i < selectedItems.length; i++) { | |
5456 this._selection.setItemSelected(selectedItems[i], true); | |
5457 } | |
5458 // Check for items, since this array is populated only when attached | |
5459 if (this.fallbackSelection && this.items.length && !this._selection.get(
).length) { | |
5460 var fallback = this._valueToItem(this.fallbackSelection); | |
5461 if (fallback) { | |
5462 this.selectedValues = [this.fallbackSelection]; | |
5463 } | |
5464 } | |
5465 } else { | |
5466 this._selection.clear(); | |
5467 } | |
5468 }, | |
5469 | |
5470 _selectionChange: function() { | |
5471 var s = this._selection.get(); | |
5472 if (this.multi) { | |
5473 this._setSelectedItems(s); | |
5474 } else { | |
5475 this._setSelectedItems([s]); | |
5476 this._setSelectedItem(s); | |
5477 } | |
5478 }, | |
5479 | |
5480 _toggleSelected: function(value) { | |
5481 var i = this.selectedValues.indexOf(value); | |
5482 var unselected = i < 0; | |
5483 if (unselected) { | |
5484 this.push('selectedValues',value); | |
5485 } else { | |
5486 this.splice('selectedValues',i,1); | |
5487 } | |
5488 }, | |
5489 | |
5490 _valuesToItems: function(values) { | |
5491 return (values == null) ? null : values.map(function(value) { | |
5492 return this._valueToItem(value); | |
5493 }, this); | |
5494 } | |
5495 }; | |
5496 | |
5497 /** @polymerBehavior */ | |
5498 Polymer.IronMultiSelectableBehavior = [ | |
5499 Polymer.IronSelectableBehavior, | |
5500 Polymer.IronMultiSelectableBehaviorImpl | |
5501 ]; | |
5502 /** | |
5503 * `Polymer.IronMenuBehavior` implements accessible menu behavior. | |
5504 * | |
5505 * @demo demo/index.html | |
5506 * @polymerBehavior Polymer.IronMenuBehavior | |
5507 */ | |
5508 Polymer.IronMenuBehaviorImpl = { | |
5509 | |
5510 properties: { | |
5511 | |
5512 /** | |
5513 * Returns the currently focused item. | |
5514 * @type {?Object} | |
5515 */ | |
5516 focusedItem: { | |
5517 observer: '_focusedItemChanged', | |
5518 readOnly: true, | |
5519 type: Object | |
5520 }, | |
5521 | |
5522 /** | |
5523 * The attribute to use on menu items to look up the item title. Typing th
e first | |
5524 * letter of an item when the menu is open focuses that item. If unset, `t
extContent` | |
5525 * will be used. | |
5526 */ | |
5527 attrForItemTitle: { | |
5528 type: String | |
5529 } | |
5530 }, | |
5531 | |
5532 hostAttributes: { | |
5533 'role': 'menu', | |
5534 'tabindex': '0' | |
5535 }, | |
5536 | |
5537 observers: [ | |
5538 '_updateMultiselectable(multi)' | |
5539 ], | |
5540 | |
5541 listeners: { | |
5542 'focus': '_onFocus', | |
5543 'keydown': '_onKeydown', | |
5544 'iron-items-changed': '_onIronItemsChanged' | |
5545 }, | |
5546 | |
5547 keyBindings: { | |
5548 'up': '_onUpKey', | |
5549 'down': '_onDownKey', | |
5550 'esc': '_onEscKey', | |
5551 'shift+tab:keydown': '_onShiftTabDown' | |
5552 }, | |
5553 | |
5554 attached: function() { | |
5555 this._resetTabindices(); | |
5556 }, | |
5557 | |
5558 /** | |
5559 * Selects the given value. If the `multi` property is true, then the select
ed state of the | |
5560 * `value` will be toggled; otherwise the `value` will be selected. | |
5561 * | |
5562 * @param {string|number} value the value to select. | |
5563 */ | |
5564 select: function(value) { | |
5565 // Cancel automatically focusing a default item if the menu received focus | |
5566 // through a user action selecting a particular item. | |
5567 if (this._defaultFocusAsync) { | 3674 if (this._defaultFocusAsync) { |
5568 this.cancelAsync(this._defaultFocusAsync); | 3675 this.cancelAsync(this._defaultFocusAsync); |
5569 this._defaultFocusAsync = null; | 3676 this._defaultFocusAsync = null; |
5570 } | 3677 } |
5571 var item = this._valueToItem(value); | 3678 }, 1); |
5572 if (item && item.hasAttribute('disabled')) return; | 3679 }, |
5573 this._setFocusedItem(item); | 3680 _affectScroll: function(dx) { |
5574 Polymer.IronMultiSelectableBehaviorImpl.select.apply(this, arguments); | 3681 this.$.tabsContainer.scrollLeft += dx; |
5575 }, | 3682 var scrollLeft = this.$.tabsContainer.scrollLeft; |
5576 | 3683 this._leftHidden = scrollLeft === 0; |
5577 /** | 3684 this._rightHidden = scrollLeft === this._tabContainerScrollSize; |
5578 * Resets all tabindex attributes to the appropriate value based on the | 3685 }, |
5579 * current selection state. The appropriate value is `0` (focusable) for | 3686 _onLeftScrollButtonDown: function() { |
5580 * the default selected item, and `-1` (not keyboard focusable) for all | 3687 this._scrollToLeft(); |
5581 * other items. | 3688 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDelay); |
5582 */ | 3689 }, |
5583 _resetTabindices: function() { | 3690 _onRightScrollButtonDown: function() { |
5584 var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[
0]) : this.selectedItem; | 3691 this._scrollToRight(); |
5585 | 3692 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDelay)
; |
5586 this.items.forEach(function(item) { | 3693 }, |
5587 item.setAttribute('tabindex', item === selectedItem ? '0' : '-1'); | 3694 _onScrollButtonUp: function() { |
5588 }, this); | 3695 clearInterval(this._holdJob); |
5589 }, | 3696 this._holdJob = null; |
5590 | 3697 }, |
5591 /** | 3698 _scrollToLeft: function() { |
5592 * Sets appropriate ARIA based on whether or not the menu is meant to be | 3699 this._affectScroll(-this._step); |
5593 * multi-selectable. | 3700 }, |
5594 * | 3701 _scrollToRight: function() { |
5595 * @param {boolean} multi True if the menu should be multi-selectable. | 3702 this._affectScroll(this._step); |
5596 */ | 3703 }, |
5597 _updateMultiselectable: function(multi) { | 3704 _tabChanged: function(tab, old) { |
5598 if (multi) { | 3705 if (!tab) { |
5599 this.setAttribute('aria-multiselectable', 'true'); | 3706 this.$.selectionBar.classList.remove('expand'); |
5600 } else { | 3707 this.$.selectionBar.classList.remove('contract'); |
5601 this.removeAttribute('aria-multiselectable'); | 3708 this._positionBar(0, 0); |
5602 } | 3709 return; |
5603 }, | 3710 } |
5604 | 3711 var r = this.$.tabsContent.getBoundingClientRect(); |
5605 /** | 3712 var w = r.width; |
5606 * Given a KeyboardEvent, this method will focus the appropriate item in the | 3713 var tabRect = tab.getBoundingClientRect(); |
5607 * menu (if there is a relevant item, and it is possible to focus it). | 3714 var tabOffsetLeft = tabRect.left - r.left; |
5608 * | 3715 this._pos = { |
5609 * @param {KeyboardEvent} event A KeyboardEvent. | 3716 width: this._calcPercent(tabRect.width, w), |
5610 */ | 3717 left: this._calcPercent(tabOffsetLeft, w) |
5611 _focusWithKeyboardEvent: function(event) { | 3718 }; |
5612 for (var i = 0, item; item = this.items[i]; i++) { | 3719 if (this.noSlide || old == null) { |
5613 var attr = this.attrForItemTitle || 'textContent'; | 3720 this.$.selectionBar.classList.remove('expand'); |
5614 var title = item[attr] || item.getAttribute(attr); | 3721 this.$.selectionBar.classList.remove('contract'); |
5615 | 3722 this._positionBar(this._pos.width, this._pos.left); |
5616 if (!item.hasAttribute('disabled') && title && | 3723 return; |
5617 title.trim().charAt(0).toLowerCase() === String.fromCharCode(event.k
eyCode).toLowerCase()) { | 3724 } |
5618 this._setFocusedItem(item); | 3725 var oldRect = old.getBoundingClientRect(); |
5619 break; | 3726 var oldIndex = this.items.indexOf(old); |
5620 } | 3727 var index = this.items.indexOf(tab); |
5621 } | 3728 var m = 5; |
5622 }, | 3729 this.$.selectionBar.classList.add('expand'); |
5623 | 3730 var moveRight = oldIndex < index; |
5624 /** | 3731 var isRTL = this._isRTL; |
5625 * Focuses the previous item (relative to the currently focused item) in the | 3732 if (isRTL) { |
5626 * menu, disabled items will be skipped. | 3733 moveRight = !moveRight; |
5627 * Loop until length + 1 to handle case of single item in menu. | 3734 } |
5628 */ | 3735 if (moveRight) { |
5629 _focusPrevious: function() { | 3736 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - oldRect
.left, w) - m, this._left); |
5630 var length = this.items.length; | 3737 } else { |
5631 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | 3738 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tabRect
.left, w) - m, this._calcPercent(tabOffsetLeft, w) + m); |
5632 for (var i = 1; i < length + 1; i++) { | 3739 } |
5633 var item = this.items[(curFocusIndex - i + length) % length]; | 3740 if (this.scrollable) { |
5634 if (!item.hasAttribute('disabled')) { | 3741 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft); |
5635 this._setFocusedItem(item); | 3742 } |
5636 return; | 3743 }, |
5637 } | 3744 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) { |
5638 } | 3745 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft; |
5639 }, | 3746 if (l < 0) { |
5640 | 3747 this.$.tabsContainer.scrollLeft += l; |
5641 /** | 3748 } else { |
5642 * Focuses the next item (relative to the currently focused item) in the | 3749 l += tabWidth - this.$.tabsContainer.offsetWidth; |
5643 * menu, disabled items will be skipped. | 3750 if (l > 0) { |
5644 * Loop until length + 1 to handle case of single item in menu. | 3751 this.$.tabsContainer.scrollLeft += l; |
5645 */ | 3752 } |
5646 _focusNext: function() { | 3753 } |
5647 var length = this.items.length; | 3754 }, |
5648 var curFocusIndex = Number(this.indexOf(this.focusedItem)); | 3755 _calcPercent: function(w, w0) { |
5649 for (var i = 1; i < length + 1; i++) { | 3756 return 100 * w / w0; |
5650 var item = this.items[(curFocusIndex + i) % length]; | 3757 }, |
5651 if (!item.hasAttribute('disabled')) { | 3758 _positionBar: function(width, left) { |
5652 this._setFocusedItem(item); | 3759 width = width || 0; |
5653 return; | 3760 left = left || 0; |
5654 } | 3761 this._width = width; |
5655 } | 3762 this._left = left; |
5656 }, | 3763 this.transform('translateX(' + left + '%) scaleX(' + width / 100 + ')', this
.$.selectionBar); |
5657 | 3764 }, |
5658 /** | 3765 _onBarTransitionEnd: function(e) { |
5659 * Mutates items in the menu based on provided selection details, so that | 3766 var cl = this.$.selectionBar.classList; |
5660 * all items correctly reflect selection state. | 3767 if (cl.contains('expand')) { |
5661 * | 3768 cl.remove('expand'); |
5662 * @param {Element} item An item in the menu. | 3769 cl.add('contract'); |
5663 * @param {boolean} isSelected True if the item should be shown in a | 3770 this._positionBar(this._pos.width, this._pos.left); |
5664 * selected state, otherwise false. | 3771 } else if (cl.contains('contract')) { |
5665 */ | 3772 cl.remove('contract'); |
5666 _applySelection: function(item, isSelected) { | 3773 } |
5667 if (isSelected) { | 3774 } |
5668 item.setAttribute('aria-selected', 'true'); | 3775 }); |
5669 } else { | 3776 |
5670 item.removeAttribute('aria-selected'); | 3777 (function() { |
5671 } | 3778 'use strict'; |
5672 Polymer.IronSelectableBehavior._applySelection.apply(this, arguments); | 3779 Polymer.IronA11yAnnouncer = Polymer({ |
5673 }, | 3780 is: 'iron-a11y-announcer', |
5674 | |
5675 /** | |
5676 * Discretely updates tabindex values among menu items as the focused item | |
5677 * changes. | |
5678 * | |
5679 * @param {Element} focusedItem The element that is currently focused. | |
5680 * @param {?Element} old The last element that was considered focused, if | |
5681 * applicable. | |
5682 */ | |
5683 _focusedItemChanged: function(focusedItem, old) { | |
5684 old && old.setAttribute('tabindex', '-1'); | |
5685 if (focusedItem) { | |
5686 focusedItem.setAttribute('tabindex', '0'); | |
5687 focusedItem.focus(); | |
5688 } | |
5689 }, | |
5690 | |
5691 /** | |
5692 * A handler that responds to mutation changes related to the list of items | |
5693 * in the menu. | |
5694 * | |
5695 * @param {CustomEvent} event An event containing mutation records as its | |
5696 * detail. | |
5697 */ | |
5698 _onIronItemsChanged: function(event) { | |
5699 if (event.detail.addedNodes.length) { | |
5700 this._resetTabindices(); | |
5701 } | |
5702 }, | |
5703 | |
5704 /** | |
5705 * Handler that is called when a shift+tab keypress is detected by the menu. | |
5706 * | |
5707 * @param {CustomEvent} event A key combination event. | |
5708 */ | |
5709 _onShiftTabDown: function(event) { | |
5710 var oldTabIndex = this.getAttribute('tabindex'); | |
5711 | |
5712 Polymer.IronMenuBehaviorImpl._shiftTabPressed = true; | |
5713 | |
5714 this._setFocusedItem(null); | |
5715 | |
5716 this.setAttribute('tabindex', '-1'); | |
5717 | |
5718 this.async(function() { | |
5719 this.setAttribute('tabindex', oldTabIndex); | |
5720 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
5721 // NOTE(cdata): polymer/polymer#1305 | |
5722 }, 1); | |
5723 }, | |
5724 | |
5725 /** | |
5726 * Handler that is called when the menu receives focus. | |
5727 * | |
5728 * @param {FocusEvent} event A focus event. | |
5729 */ | |
5730 _onFocus: function(event) { | |
5731 if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) { | |
5732 // do not focus the menu itself | |
5733 return; | |
5734 } | |
5735 | |
5736 // Do not focus the selected tab if the deepest target is part of the | |
5737 // menu element's local DOM and is focusable. | |
5738 var rootTarget = /** @type {?HTMLElement} */( | |
5739 Polymer.dom(event).rootTarget); | |
5740 if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !
this.isLightDescendant(rootTarget)) { | |
5741 return; | |
5742 } | |
5743 | |
5744 // clear the cached focus item | |
5745 this._defaultFocusAsync = this.async(function() { | |
5746 // focus the selected item when the menu receives focus, or the first it
em | |
5747 // if no item is selected | |
5748 var selectedItem = this.multi ? (this.selectedItems && this.selectedItem
s[0]) : this.selectedItem; | |
5749 | |
5750 this._setFocusedItem(null); | |
5751 | |
5752 if (selectedItem) { | |
5753 this._setFocusedItem(selectedItem); | |
5754 } else if (this.items[0]) { | |
5755 // We find the first none-disabled item (if one exists) | |
5756 this._focusNext(); | |
5757 } | |
5758 }); | |
5759 }, | |
5760 | |
5761 /** | |
5762 * Handler that is called when the up key is pressed. | |
5763 * | |
5764 * @param {CustomEvent} event A key combination event. | |
5765 */ | |
5766 _onUpKey: function(event) { | |
5767 // up and down arrows moves the focus | |
5768 this._focusPrevious(); | |
5769 event.detail.keyboardEvent.preventDefault(); | |
5770 }, | |
5771 | |
5772 /** | |
5773 * Handler that is called when the down key is pressed. | |
5774 * | |
5775 * @param {CustomEvent} event A key combination event. | |
5776 */ | |
5777 _onDownKey: function(event) { | |
5778 this._focusNext(); | |
5779 event.detail.keyboardEvent.preventDefault(); | |
5780 }, | |
5781 | |
5782 /** | |
5783 * Handler that is called when the esc key is pressed. | |
5784 * | |
5785 * @param {CustomEvent} event A key combination event. | |
5786 */ | |
5787 _onEscKey: function(event) { | |
5788 // esc blurs the control | |
5789 this.focusedItem.blur(); | |
5790 }, | |
5791 | |
5792 /** | |
5793 * Handler that is called when a keydown event is detected. | |
5794 * | |
5795 * @param {KeyboardEvent} event A keyboard event. | |
5796 */ | |
5797 _onKeydown: function(event) { | |
5798 if (!this.keyboardEventMatchesKeys(event, 'up down esc')) { | |
5799 // all other keys focus the menu item starting with that character | |
5800 this._focusWithKeyboardEvent(event); | |
5801 } | |
5802 event.stopPropagation(); | |
5803 }, | |
5804 | |
5805 // override _activateHandler | |
5806 _activateHandler: function(event) { | |
5807 Polymer.IronSelectableBehavior._activateHandler.call(this, event); | |
5808 event.stopPropagation(); | |
5809 } | |
5810 }; | |
5811 | |
5812 Polymer.IronMenuBehaviorImpl._shiftTabPressed = false; | |
5813 | |
5814 /** @polymerBehavior Polymer.IronMenuBehavior */ | |
5815 Polymer.IronMenuBehavior = [ | |
5816 Polymer.IronMultiSelectableBehavior, | |
5817 Polymer.IronA11yKeysBehavior, | |
5818 Polymer.IronMenuBehaviorImpl | |
5819 ]; | |
5820 /** | |
5821 * `Polymer.IronMenubarBehavior` implements accessible menubar behavior. | |
5822 * | |
5823 * @polymerBehavior Polymer.IronMenubarBehavior | |
5824 */ | |
5825 Polymer.IronMenubarBehaviorImpl = { | |
5826 | |
5827 hostAttributes: { | |
5828 'role': 'menubar' | |
5829 }, | |
5830 | |
5831 keyBindings: { | |
5832 'left': '_onLeftKey', | |
5833 'right': '_onRightKey' | |
5834 }, | |
5835 | |
5836 _onUpKey: function(event) { | |
5837 this.focusedItem.click(); | |
5838 event.detail.keyboardEvent.preventDefault(); | |
5839 }, | |
5840 | |
5841 _onDownKey: function(event) { | |
5842 this.focusedItem.click(); | |
5843 event.detail.keyboardEvent.preventDefault(); | |
5844 }, | |
5845 | |
5846 get _isRTL() { | |
5847 return window.getComputedStyle(this)['direction'] === 'rtl'; | |
5848 }, | |
5849 | |
5850 _onLeftKey: function(event) { | |
5851 if (this._isRTL) { | |
5852 this._focusNext(); | |
5853 } else { | |
5854 this._focusPrevious(); | |
5855 } | |
5856 event.detail.keyboardEvent.preventDefault(); | |
5857 }, | |
5858 | |
5859 _onRightKey: function(event) { | |
5860 if (this._isRTL) { | |
5861 this._focusPrevious(); | |
5862 } else { | |
5863 this._focusNext(); | |
5864 } | |
5865 event.detail.keyboardEvent.preventDefault(); | |
5866 }, | |
5867 | |
5868 _onKeydown: function(event) { | |
5869 if (this.keyboardEventMatchesKeys(event, 'up down left right esc')) { | |
5870 return; | |
5871 } | |
5872 | |
5873 // all other keys focus the menu item starting with that character | |
5874 this._focusWithKeyboardEvent(event); | |
5875 } | |
5876 | |
5877 }; | |
5878 | |
5879 /** @polymerBehavior Polymer.IronMenubarBehavior */ | |
5880 Polymer.IronMenubarBehavior = [ | |
5881 Polymer.IronMenuBehavior, | |
5882 Polymer.IronMenubarBehaviorImpl | |
5883 ]; | |
5884 /** | |
5885 * The `iron-iconset-svg` element allows users to define their own icon sets | |
5886 * that contain svg icons. The svg icon elements should be children of the | |
5887 * `iron-iconset-svg` element. Multiple icons should be given distinct id's. | |
5888 * | |
5889 * Using svg elements to create icons has a few advantages over traditional | |
5890 * bitmap graphics like jpg or png. Icons that use svg are vector based so | |
5891 * they are resolution independent and should look good on any device. They | |
5892 * are stylable via css. Icons can be themed, colorized, and even animated. | |
5893 * | |
5894 * Example: | |
5895 * | |
5896 * <iron-iconset-svg name="my-svg-icons" size="24"> | |
5897 * <svg> | |
5898 * <defs> | |
5899 * <g id="shape"> | |
5900 * <rect x="12" y="0" width="12" height="24" /> | |
5901 * <circle cx="12" cy="12" r="12" /> | |
5902 * </g> | |
5903 * </defs> | |
5904 * </svg> | |
5905 * </iron-iconset-svg> | |
5906 * | |
5907 * This will automatically register the icon set "my-svg-icons" to the iconset | |
5908 * database. To use these icons from within another element, make a | |
5909 * `iron-iconset` element and call the `byId` method | |
5910 * to retrieve a given iconset. To apply a particular icon inside an | |
5911 * element use the `applyIcon` method. For example: | |
5912 * | |
5913 * iconset.applyIcon(iconNode, 'car'); | |
5914 * | |
5915 * @element iron-iconset-svg | |
5916 * @demo demo/index.html | |
5917 * @implements {Polymer.Iconset} | |
5918 */ | |
5919 Polymer({ | |
5920 is: 'iron-iconset-svg', | |
5921 | |
5922 properties: { | 3781 properties: { |
5923 | 3782 mode: { |
5924 /** | |
5925 * The name of the iconset. | |
5926 */ | |
5927 name: { | |
5928 type: String, | 3783 type: String, |
5929 observer: '_nameChanged' | 3784 value: 'polite' |
5930 }, | 3785 }, |
5931 | 3786 _text: { |
5932 /** | |
5933 * The size of an individual icon. Note that icons must be square. | |
5934 */ | |
5935 size: { | |
5936 type: Number, | |
5937 value: 24 | |
5938 } | |
5939 | |
5940 }, | |
5941 | |
5942 attached: function() { | |
5943 this.style.display = 'none'; | |
5944 }, | |
5945 | |
5946 /** | |
5947 * Construct an array of all icon names in this iconset. | |
5948 * | |
5949 * @return {!Array} Array of icon names. | |
5950 */ | |
5951 getIconNames: function() { | |
5952 this._icons = this._createIconMap(); | |
5953 return Object.keys(this._icons).map(function(n) { | |
5954 return this.name + ':' + n; | |
5955 }, this); | |
5956 }, | |
5957 | |
5958 /** | |
5959 * Applies an icon to the given element. | |
5960 * | |
5961 * An svg icon is prepended to the element's shadowRoot if it exists, | |
5962 * otherwise to the element itself. | |
5963 * | |
5964 * @method applyIcon | |
5965 * @param {Element} element Element to which the icon is applied. | |
5966 * @param {string} iconName Name of the icon to apply. | |
5967 * @return {?Element} The svg element which renders the icon. | |
5968 */ | |
5969 applyIcon: function(element, iconName) { | |
5970 // insert svg element into shadow root, if it exists | |
5971 element = element.root || element; | |
5972 // Remove old svg element | |
5973 this.removeIcon(element); | |
5974 // install new svg element | |
5975 var svg = this._cloneIcon(iconName); | |
5976 if (svg) { | |
5977 var pde = Polymer.dom(element); | |
5978 pde.insertBefore(svg, pde.childNodes[0]); | |
5979 return element._svgIcon = svg; | |
5980 } | |
5981 return null; | |
5982 }, | |
5983 | |
5984 /** | |
5985 * Remove an icon from the given element by undoing the changes effected | |
5986 * by `applyIcon`. | |
5987 * | |
5988 * @param {Element} element The element from which the icon is removed. | |
5989 */ | |
5990 removeIcon: function(element) { | |
5991 // Remove old svg element | |
5992 if (element._svgIcon) { | |
5993 Polymer.dom(element).removeChild(element._svgIcon); | |
5994 element._svgIcon = null; | |
5995 } | |
5996 }, | |
5997 | |
5998 /** | |
5999 * | |
6000 * When name is changed, register iconset metadata | |
6001 * | |
6002 */ | |
6003 _nameChanged: function() { | |
6004 new Polymer.IronMeta({type: 'iconset', key: this.name, value: this}); | |
6005 this.async(function() { | |
6006 this.fire('iron-iconset-added', this, {node: window}); | |
6007 }); | |
6008 }, | |
6009 | |
6010 /** | |
6011 * Create a map of child SVG elements by id. | |
6012 * | |
6013 * @return {!Object} Map of id's to SVG elements. | |
6014 */ | |
6015 _createIconMap: function() { | |
6016 // Objects chained to Object.prototype (`{}`) have members. Specifically, | |
6017 // on FF there is a `watch` method that confuses the icon map, so we | |
6018 // need to use a null-based object here. | |
6019 var icons = Object.create(null); | |
6020 Polymer.dom(this).querySelectorAll('[id]') | |
6021 .forEach(function(icon) { | |
6022 icons[icon.id] = icon; | |
6023 }); | |
6024 return icons; | |
6025 }, | |
6026 | |
6027 /** | |
6028 * Produce installable clone of the SVG element matching `id` in this | |
6029 * iconset, or `undefined` if there is no matching element. | |
6030 * | |
6031 * @return {Element} Returns an installable clone of the SVG element | |
6032 * matching `id`. | |
6033 */ | |
6034 _cloneIcon: function(id) { | |
6035 // create the icon map on-demand, since the iconset itself has no discrete | |
6036 // signal to know when it's children are fully parsed | |
6037 this._icons = this._icons || this._createIconMap(); | |
6038 return this._prepareSvgClone(this._icons[id], this.size); | |
6039 }, | |
6040 | |
6041 /** | |
6042 * @param {Element} sourceSvg | |
6043 * @param {number} size | |
6044 * @return {Element} | |
6045 */ | |
6046 _prepareSvgClone: function(sourceSvg, size) { | |
6047 if (sourceSvg) { | |
6048 var content = sourceSvg.cloneNode(true), | |
6049 svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), | |
6050 viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + s
ize; | |
6051 svg.setAttribute('viewBox', viewBox); | |
6052 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); | |
6053 // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/
370136 | |
6054 // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
shadow-root | |
6055 svg.style.cssText = 'pointer-events: none; display: block; width: 100%;
height: 100%;'; | |
6056 svg.appendChild(content).removeAttribute('id'); | |
6057 return svg; | |
6058 } | |
6059 return null; | |
6060 } | |
6061 | |
6062 }); | |
6063 Polymer({ | |
6064 is: 'paper-tabs', | |
6065 | |
6066 behaviors: [ | |
6067 Polymer.IronResizableBehavior, | |
6068 Polymer.IronMenubarBehavior | |
6069 ], | |
6070 | |
6071 properties: { | |
6072 /** | |
6073 * If true, ink ripple effect is disabled. When this property is changed
, | |
6074 * all descendant `<paper-tab>` elements have their `noink` property | |
6075 * changed to the new value as well. | |
6076 */ | |
6077 noink: { | |
6078 type: Boolean, | |
6079 value: false, | |
6080 observer: '_noinkChanged' | |
6081 }, | |
6082 | |
6083 /** | |
6084 * If true, the bottom bar to indicate the selected tab will not be show
n. | |
6085 */ | |
6086 noBar: { | |
6087 type: Boolean, | |
6088 value: false | |
6089 }, | |
6090 | |
6091 /** | |
6092 * If true, the slide effect for the bottom bar is disabled. | |
6093 */ | |
6094 noSlide: { | |
6095 type: Boolean, | |
6096 value: false | |
6097 }, | |
6098 | |
6099 /** | |
6100 * If true, tabs are scrollable and the tab width is based on the label
width. | |
6101 */ | |
6102 scrollable: { | |
6103 type: Boolean, | |
6104 value: false | |
6105 }, | |
6106 | |
6107 /** | |
6108 * If true, tabs expand to fit their container. This currently only appl
ies when | |
6109 * scrollable is true. | |
6110 */ | |
6111 fitContainer: { | |
6112 type: Boolean, | |
6113 value: false | |
6114 }, | |
6115 | |
6116 /** | |
6117 * If true, dragging on the tabs to scroll is disabled. | |
6118 */ | |
6119 disableDrag: { | |
6120 type: Boolean, | |
6121 value: false | |
6122 }, | |
6123 | |
6124 /** | |
6125 * If true, scroll buttons (left/right arrow) will be hidden for scrolla
ble tabs. | |
6126 */ | |
6127 hideScrollButtons: { | |
6128 type: Boolean, | |
6129 value: false | |
6130 }, | |
6131 | |
6132 /** | |
6133 * If true, the tabs are aligned to bottom (the selection bar appears at
the top). | |
6134 */ | |
6135 alignBottom: { | |
6136 type: Boolean, | |
6137 value: false | |
6138 }, | |
6139 | |
6140 selectable: { | |
6141 type: String, | |
6142 value: 'paper-tab' | |
6143 }, | |
6144 | |
6145 /** | |
6146 * If true, tabs are automatically selected when focused using the | |
6147 * keyboard. | |
6148 */ | |
6149 autoselect: { | |
6150 type: Boolean, | |
6151 value: false | |
6152 }, | |
6153 | |
6154 /** | |
6155 * The delay (in milliseconds) between when the user stops interacting | |
6156 * with the tabs through the keyboard and when the focused item is | |
6157 * automatically selected (if `autoselect` is true). | |
6158 */ | |
6159 autoselectDelay: { | |
6160 type: Number, | |
6161 value: 0 | |
6162 }, | |
6163 | |
6164 _step: { | |
6165 type: Number, | |
6166 value: 10 | |
6167 }, | |
6168 | |
6169 _holdDelay: { | |
6170 type: Number, | |
6171 value: 1 | |
6172 }, | |
6173 | |
6174 _leftHidden: { | |
6175 type: Boolean, | |
6176 value: false | |
6177 }, | |
6178 | |
6179 _rightHidden: { | |
6180 type: Boolean, | |
6181 value: false | |
6182 }, | |
6183 | |
6184 _previousTab: { | |
6185 type: Object | |
6186 } | |
6187 }, | |
6188 | |
6189 hostAttributes: { | |
6190 role: 'tablist' | |
6191 }, | |
6192 | |
6193 listeners: { | |
6194 'iron-resize': '_onTabSizingChanged', | |
6195 'iron-items-changed': '_onTabSizingChanged', | |
6196 'iron-select': '_onIronSelect', | |
6197 'iron-deselect': '_onIronDeselect' | |
6198 }, | |
6199 | |
6200 keyBindings: { | |
6201 'left:keyup right:keyup': '_onArrowKeyup' | |
6202 }, | |
6203 | |
6204 created: function() { | |
6205 this._holdJob = null; | |
6206 this._pendingActivationItem = undefined; | |
6207 this._pendingActivationTimeout = undefined; | |
6208 this._bindDelayedActivationHandler = this._delayedActivationHandler.bind
(this); | |
6209 this.addEventListener('blur', this._onBlurCapture.bind(this), true); | |
6210 }, | |
6211 | |
6212 ready: function() { | |
6213 this.setScrollDirection('y', this.$.tabsContainer); | |
6214 }, | |
6215 | |
6216 detached: function() { | |
6217 this._cancelPendingActivation(); | |
6218 }, | |
6219 | |
6220 _noinkChanged: function(noink) { | |
6221 var childTabs = Polymer.dom(this).querySelectorAll('paper-tab'); | |
6222 childTabs.forEach(noink ? this._setNoinkAttribute : this._removeNoinkAtt
ribute); | |
6223 }, | |
6224 | |
6225 _setNoinkAttribute: function(element) { | |
6226 element.setAttribute('noink', ''); | |
6227 }, | |
6228 | |
6229 _removeNoinkAttribute: function(element) { | |
6230 element.removeAttribute('noink'); | |
6231 }, | |
6232 | |
6233 _computeScrollButtonClass: function(hideThisButton, scrollable, hideScroll
Buttons) { | |
6234 if (!scrollable || hideScrollButtons) { | |
6235 return 'hidden'; | |
6236 } | |
6237 | |
6238 if (hideThisButton) { | |
6239 return 'not-visible'; | |
6240 } | |
6241 | |
6242 return ''; | |
6243 }, | |
6244 | |
6245 _computeTabsContentClass: function(scrollable, fitContainer) { | |
6246 return scrollable ? 'scrollable' + (fitContainer ? ' fit-container' : ''
) : ' fit-container'; | |
6247 }, | |
6248 | |
6249 _computeSelectionBarClass: function(noBar, alignBottom) { | |
6250 if (noBar) { | |
6251 return 'hidden'; | |
6252 } else if (alignBottom) { | |
6253 return 'align-bottom'; | |
6254 } | |
6255 | |
6256 return ''; | |
6257 }, | |
6258 | |
6259 // TODO(cdata): Add `track` response back in when gesture lands. | |
6260 | |
6261 _onTabSizingChanged: function() { | |
6262 this.debounce('_onTabSizingChanged', function() { | |
6263 this._scroll(); | |
6264 this._tabChanged(this.selectedItem); | |
6265 }, 10); | |
6266 }, | |
6267 | |
6268 _onIronSelect: function(event) { | |
6269 this._tabChanged(event.detail.item, this._previousTab); | |
6270 this._previousTab = event.detail.item; | |
6271 this.cancelDebouncer('tab-changed'); | |
6272 }, | |
6273 | |
6274 _onIronDeselect: function(event) { | |
6275 this.debounce('tab-changed', function() { | |
6276 this._tabChanged(null, this._previousTab); | |
6277 this._previousTab = null; | |
6278 // See polymer/polymer#1305 | |
6279 }, 1); | |
6280 }, | |
6281 | |
6282 _activateHandler: function() { | |
6283 // Cancel item activations scheduled by keyboard events when any other | |
6284 // action causes an item to be activated (e.g. clicks). | |
6285 this._cancelPendingActivation(); | |
6286 | |
6287 Polymer.IronMenuBehaviorImpl._activateHandler.apply(this, arguments); | |
6288 }, | |
6289 | |
6290 /** | |
6291 * Activates an item after a delay (in milliseconds). | |
6292 */ | |
6293 _scheduleActivation: function(item, delay) { | |
6294 this._pendingActivationItem = item; | |
6295 this._pendingActivationTimeout = this.async( | |
6296 this._bindDelayedActivationHandler, delay); | |
6297 }, | |
6298 | |
6299 /** | |
6300 * Activates the last item given to `_scheduleActivation`. | |
6301 */ | |
6302 _delayedActivationHandler: function() { | |
6303 var item = this._pendingActivationItem; | |
6304 this._pendingActivationItem = undefined; | |
6305 this._pendingActivationTimeout = undefined; | |
6306 item.fire(this.activateEvent, null, { | |
6307 bubbles: true, | |
6308 cancelable: true | |
6309 }); | |
6310 }, | |
6311 | |
6312 /** | |
6313 * Cancels a previously scheduled item activation made with | |
6314 * `_scheduleActivation`. | |
6315 */ | |
6316 _cancelPendingActivation: function() { | |
6317 if (this._pendingActivationTimeout !== undefined) { | |
6318 this.cancelAsync(this._pendingActivationTimeout); | |
6319 this._pendingActivationItem = undefined; | |
6320 this._pendingActivationTimeout = undefined; | |
6321 } | |
6322 }, | |
6323 | |
6324 _onArrowKeyup: function(event) { | |
6325 if (this.autoselect) { | |
6326 this._scheduleActivation(this.focusedItem, this.autoselectDelay); | |
6327 } | |
6328 }, | |
6329 | |
6330 _onBlurCapture: function(event) { | |
6331 // Cancel a scheduled item activation (if any) when that item is | |
6332 // blurred. | |
6333 if (event.target === this._pendingActivationItem) { | |
6334 this._cancelPendingActivation(); | |
6335 } | |
6336 }, | |
6337 | |
6338 get _tabContainerScrollSize () { | |
6339 return Math.max( | |
6340 0, | |
6341 this.$.tabsContainer.scrollWidth - | |
6342 this.$.tabsContainer.offsetWidth | |
6343 ); | |
6344 }, | |
6345 | |
6346 _scroll: function(e, detail) { | |
6347 if (!this.scrollable) { | |
6348 return; | |
6349 } | |
6350 | |
6351 var ddx = (detail && -detail.ddx) || 0; | |
6352 this._affectScroll(ddx); | |
6353 }, | |
6354 | |
6355 _down: function(e) { | |
6356 // go one beat async to defeat IronMenuBehavior | |
6357 // autorefocus-on-no-selection timeout | |
6358 this.async(function() { | |
6359 if (this._defaultFocusAsync) { | |
6360 this.cancelAsync(this._defaultFocusAsync); | |
6361 this._defaultFocusAsync = null; | |
6362 } | |
6363 }, 1); | |
6364 }, | |
6365 | |
6366 _affectScroll: function(dx) { | |
6367 this.$.tabsContainer.scrollLeft += dx; | |
6368 | |
6369 var scrollLeft = this.$.tabsContainer.scrollLeft; | |
6370 | |
6371 this._leftHidden = scrollLeft === 0; | |
6372 this._rightHidden = scrollLeft === this._tabContainerScrollSize; | |
6373 }, | |
6374 | |
6375 _onLeftScrollButtonDown: function() { | |
6376 this._scrollToLeft(); | |
6377 this._holdJob = setInterval(this._scrollToLeft.bind(this), this._holdDel
ay); | |
6378 }, | |
6379 | |
6380 _onRightScrollButtonDown: function() { | |
6381 this._scrollToRight(); | |
6382 this._holdJob = setInterval(this._scrollToRight.bind(this), this._holdDe
lay); | |
6383 }, | |
6384 | |
6385 _onScrollButtonUp: function() { | |
6386 clearInterval(this._holdJob); | |
6387 this._holdJob = null; | |
6388 }, | |
6389 | |
6390 _scrollToLeft: function() { | |
6391 this._affectScroll(-this._step); | |
6392 }, | |
6393 | |
6394 _scrollToRight: function() { | |
6395 this._affectScroll(this._step); | |
6396 }, | |
6397 | |
6398 _tabChanged: function(tab, old) { | |
6399 if (!tab) { | |
6400 // Remove the bar without animation. | |
6401 this.$.selectionBar.classList.remove('expand'); | |
6402 this.$.selectionBar.classList.remove('contract'); | |
6403 this._positionBar(0, 0); | |
6404 return; | |
6405 } | |
6406 | |
6407 var r = this.$.tabsContent.getBoundingClientRect(); | |
6408 var w = r.width; | |
6409 var tabRect = tab.getBoundingClientRect(); | |
6410 var tabOffsetLeft = tabRect.left - r.left; | |
6411 | |
6412 this._pos = { | |
6413 width: this._calcPercent(tabRect.width, w), | |
6414 left: this._calcPercent(tabOffsetLeft, w) | |
6415 }; | |
6416 | |
6417 if (this.noSlide || old == null) { | |
6418 // Position the bar without animation. | |
6419 this.$.selectionBar.classList.remove('expand'); | |
6420 this.$.selectionBar.classList.remove('contract'); | |
6421 this._positionBar(this._pos.width, this._pos.left); | |
6422 return; | |
6423 } | |
6424 | |
6425 var oldRect = old.getBoundingClientRect(); | |
6426 var oldIndex = this.items.indexOf(old); | |
6427 var index = this.items.indexOf(tab); | |
6428 var m = 5; | |
6429 | |
6430 // bar animation: expand | |
6431 this.$.selectionBar.classList.add('expand'); | |
6432 | |
6433 var moveRight = oldIndex < index; | |
6434 var isRTL = this._isRTL; | |
6435 if (isRTL) { | |
6436 moveRight = !moveRight; | |
6437 } | |
6438 | |
6439 if (moveRight) { | |
6440 this._positionBar(this._calcPercent(tabRect.left + tabRect.width - old
Rect.left, w) - m, | |
6441 this._left); | |
6442 } else { | |
6443 this._positionBar(this._calcPercent(oldRect.left + oldRect.width - tab
Rect.left, w) - m, | |
6444 this._calcPercent(tabOffsetLeft, w) + m); | |
6445 } | |
6446 | |
6447 if (this.scrollable) { | |
6448 this._scrollToSelectedIfNeeded(tabRect.width, tabOffsetLeft); | |
6449 } | |
6450 }, | |
6451 | |
6452 _scrollToSelectedIfNeeded: function(tabWidth, tabOffsetLeft) { | |
6453 var l = tabOffsetLeft - this.$.tabsContainer.scrollLeft; | |
6454 if (l < 0) { | |
6455 this.$.tabsContainer.scrollLeft += l; | |
6456 } else { | |
6457 l += (tabWidth - this.$.tabsContainer.offsetWidth); | |
6458 if (l > 0) { | |
6459 this.$.tabsContainer.scrollLeft += l; | |
6460 } | |
6461 } | |
6462 }, | |
6463 | |
6464 _calcPercent: function(w, w0) { | |
6465 return 100 * w / w0; | |
6466 }, | |
6467 | |
6468 _positionBar: function(width, left) { | |
6469 width = width || 0; | |
6470 left = left || 0; | |
6471 | |
6472 this._width = width; | |
6473 this._left = left; | |
6474 this.transform( | |
6475 'translateX(' + left + '%) scaleX(' + (width / 100) + ')', | |
6476 this.$.selectionBar); | |
6477 }, | |
6478 | |
6479 _onBarTransitionEnd: function(e) { | |
6480 var cl = this.$.selectionBar.classList; | |
6481 // bar animation: expand -> contract | |
6482 if (cl.contains('expand')) { | |
6483 cl.remove('expand'); | |
6484 cl.add('contract'); | |
6485 this._positionBar(this._pos.width, this._pos.left); | |
6486 // bar animation done | |
6487 } else if (cl.contains('contract')) { | |
6488 cl.remove('contract'); | |
6489 } | |
6490 } | |
6491 }); | |
6492 (function() { | |
6493 'use strict'; | |
6494 | |
6495 Polymer.IronA11yAnnouncer = Polymer({ | |
6496 is: 'iron-a11y-announcer', | |
6497 | |
6498 properties: { | |
6499 | |
6500 /** | |
6501 * The value of mode is used to set the `aria-live` attribute | |
6502 * for the element that will be announced. Valid values are: `off`, | |
6503 * `polite` and `assertive`. | |
6504 */ | |
6505 mode: { | |
6506 type: String, | |
6507 value: 'polite' | |
6508 }, | |
6509 | |
6510 _text: { | |
6511 type: String, | |
6512 value: '' | |
6513 } | |
6514 }, | |
6515 | |
6516 created: function() { | |
6517 if (!Polymer.IronA11yAnnouncer.instance) { | |
6518 Polymer.IronA11yAnnouncer.instance = this; | |
6519 } | |
6520 | |
6521 document.body.addEventListener('iron-announce', this._onIronAnnounce.b
ind(this)); | |
6522 }, | |
6523 | |
6524 /** | |
6525 * Cause a text string to be announced by screen readers. | |
6526 * | |
6527 * @param {string} text The text that should be announced. | |
6528 */ | |
6529 announce: function(text) { | |
6530 this._text = ''; | |
6531 this.async(function() { | |
6532 this._text = text; | |
6533 }, 100); | |
6534 }, | |
6535 | |
6536 _onIronAnnounce: function(event) { | |
6537 if (event.detail && event.detail.text) { | |
6538 this.announce(event.detail.text); | |
6539 } | |
6540 } | |
6541 }); | |
6542 | |
6543 Polymer.IronA11yAnnouncer.instance = null; | |
6544 | |
6545 Polymer.IronA11yAnnouncer.requestAvailability = function() { | |
6546 if (!Polymer.IronA11yAnnouncer.instance) { | |
6547 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y
-announcer'); | |
6548 } | |
6549 | |
6550 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); | |
6551 }; | |
6552 })(); | |
6553 /** | |
6554 * Singleton IronMeta instance. | |
6555 */ | |
6556 Polymer.IronValidatableBehaviorMeta = null; | |
6557 | |
6558 /** | |
6559 * `Use Polymer.IronValidatableBehavior` to implement an element that validate
s user input. | |
6560 * Use the related `Polymer.IronValidatorBehavior` to add custom validation lo
gic to an iron-input. | |
6561 * | |
6562 * By default, an `<iron-form>` element validates its fields when the user pre
sses the submit button. | |
6563 * To validate a form imperatively, call the form's `validate()` method, which
in turn will | |
6564 * call `validate()` on all its children. By using `Polymer.IronValidatableBeh
avior`, your | |
6565 * custom element will get a public `validate()`, which | |
6566 * will return the validity of the element, and a corresponding `invalid` attr
ibute, | |
6567 * which can be used for styling. | |
6568 * | |
6569 * To implement the custom validation logic of your element, you must override | |
6570 * the protected `_getValidity()` method of this behaviour, rather than `valid
ate()`. | |
6571 * See [this](https://github.com/PolymerElements/iron-form/blob/master/demo/si
mple-element.html) | |
6572 * for an example. | |
6573 * | |
6574 * ### Accessibility | |
6575 * | |
6576 * Changing the `invalid` property, either manually or by calling `validate()`
will update the | |
6577 * `aria-invalid` attribute. | |
6578 * | |
6579 * @demo demo/index.html | |
6580 * @polymerBehavior | |
6581 */ | |
6582 Polymer.IronValidatableBehavior = { | |
6583 | |
6584 properties: { | |
6585 | |
6586 /** | |
6587 * Name of the validator to use. | |
6588 */ | |
6589 validator: { | |
6590 type: String | |
6591 }, | |
6592 | |
6593 /** | |
6594 * True if the last call to `validate` is invalid. | |
6595 */ | |
6596 invalid: { | |
6597 notify: true, | |
6598 reflectToAttribute: true, | |
6599 type: Boolean, | |
6600 value: false | |
6601 }, | |
6602 | |
6603 /** | |
6604 * This property is deprecated and should not be used. Use the global | |
6605 * validator meta singleton, `Polymer.IronValidatableBehaviorMeta` instead
. | |
6606 */ | |
6607 _validatorMeta: { | |
6608 type: Object | |
6609 }, | |
6610 | |
6611 /** | |
6612 * Namespace for this validator. This property is deprecated and should | |
6613 * not be used. For all intents and purposes, please consider it a | |
6614 * read-only, config-time property. | |
6615 */ | |
6616 validatorType: { | |
6617 type: String, | |
6618 value: 'validator' | |
6619 }, | |
6620 | |
6621 _validator: { | |
6622 type: Object, | |
6623 computed: '__computeValidator(validator)' | |
6624 } | |
6625 }, | |
6626 | |
6627 observers: [ | |
6628 '_invalidChanged(invalid)' | |
6629 ], | |
6630 | |
6631 registered: function() { | |
6632 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({type: 'validat
or'}); | |
6633 }, | |
6634 | |
6635 _invalidChanged: function() { | |
6636 if (this.invalid) { | |
6637 this.setAttribute('aria-invalid', 'true'); | |
6638 } else { | |
6639 this.removeAttribute('aria-invalid'); | |
6640 } | |
6641 }, | |
6642 | |
6643 /** | |
6644 * @return {boolean} True if the validator `validator` exists. | |
6645 */ | |
6646 hasValidator: function() { | |
6647 return this._validator != null; | |
6648 }, | |
6649 | |
6650 /** | |
6651 * Returns true if the `value` is valid, and updates `invalid`. If you want | |
6652 * your element to have custom validation logic, do not override this method
; | |
6653 * override `_getValidity(value)` instead. | |
6654 | |
6655 * @param {Object} value The value to be validated. By default, it is passed | |
6656 * to the validator's `validate()` function, if a validator is set. | |
6657 * @return {boolean} True if `value` is valid. | |
6658 */ | |
6659 validate: function(value) { | |
6660 this.invalid = !this._getValidity(value); | |
6661 return !this.invalid; | |
6662 }, | |
6663 | |
6664 /** | |
6665 * Returns true if `value` is valid. By default, it is passed | |
6666 * to the validator's `validate()` function, if a validator is set. You | |
6667 * should override this method if you want to implement custom validity | |
6668 * logic for your element. | |
6669 * | |
6670 * @param {Object} value The value to be validated. | |
6671 * @return {boolean} True if `value` is valid. | |
6672 */ | |
6673 | |
6674 _getValidity: function(value) { | |
6675 if (this.hasValidator()) { | |
6676 return this._validator.validate(value); | |
6677 } | |
6678 return true; | |
6679 }, | |
6680 | |
6681 __computeValidator: function() { | |
6682 return Polymer.IronValidatableBehaviorMeta && | |
6683 Polymer.IronValidatableBehaviorMeta.byKey(this.validator); | |
6684 } | |
6685 }; | |
6686 /* | |
6687 `<iron-input>` adds two-way binding and custom validators using `Polymer.IronVal
idatorBehavior` | |
6688 to `<input>`. | |
6689 | |
6690 ### Two-way binding | |
6691 | |
6692 By default you can only get notified of changes to an `input`'s `value` due to u
ser input: | |
6693 | |
6694 <input value="{{myValue::input}}"> | |
6695 | |
6696 `iron-input` adds the `bind-value` property that mirrors the `value` property, a
nd can be used | |
6697 for two-way data binding. `bind-value` will notify if it is changed either by us
er input or by script. | |
6698 | |
6699 <input is="iron-input" bind-value="{{myValue}}"> | |
6700 | |
6701 ### Custom validators | |
6702 | |
6703 You can use custom validators that implement `Polymer.IronValidatorBehavior` wit
h `<iron-input>`. | |
6704 | |
6705 <input is="iron-input" validator="my-custom-validator"> | |
6706 | |
6707 ### Stopping invalid input | |
6708 | |
6709 It may be desirable to only allow users to enter certain characters. You can use
the | |
6710 `prevent-invalid-input` and `allowed-pattern` attributes together to accomplish
this. This feature | |
6711 is separate from validation, and `allowed-pattern` does not affect how the input
is validated. | |
6712 | |
6713 \x3c!-- only allow characters that match [0-9] --\x3e | |
6714 <input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]"> | |
6715 | |
6716 @hero hero.svg | |
6717 @demo demo/index.html | |
6718 */ | |
6719 | |
6720 Polymer({ | |
6721 | |
6722 is: 'iron-input', | |
6723 | |
6724 extends: 'input', | |
6725 | |
6726 behaviors: [ | |
6727 Polymer.IronValidatableBehavior | |
6728 ], | |
6729 | |
6730 properties: { | |
6731 | |
6732 /** | |
6733 * Use this property instead of `value` for two-way data binding. | |
6734 */ | |
6735 bindValue: { | |
6736 observer: '_bindValueChanged', | |
6737 type: String | |
6738 }, | |
6739 | |
6740 /** | |
6741 * Set to true to prevent the user from entering invalid input. If `allowe
dPattern` is set, | |
6742 * any character typed by the user will be matched against that pattern, a
nd rejected if it's not a match. | |
6743 * Pasted input will have each character checked individually; if any char
acter | |
6744 * doesn't match `allowedPattern`, the entire pasted string will be reject
ed. | |
6745 * If `allowedPattern` is not set, it will use the `type` attribute (only
supported for `type=number`). | |
6746 */ | |
6747 preventInvalidInput: { | |
6748 type: Boolean | |
6749 }, | |
6750 | |
6751 /** | |
6752 * Regular expression that list the characters allowed as input. | |
6753 * This pattern represents the allowed characters for the field; as the us
er inputs text, | |
6754 * each individual character will be checked against the pattern (rather t
han checking | |
6755 * the entire value as a whole). The recommended format should be a list o
f allowed characters; | |
6756 * for example, `[a-zA-Z0-9.+-!;:]` | |
6757 */ | |
6758 allowedPattern: { | |
6759 type: String, | |
6760 observer: "_allowedPatternChanged" | |
6761 }, | |
6762 | |
6763 _previousValidInput: { | |
6764 type: String, | 3787 type: String, |
6765 value: '' | 3788 value: '' |
6766 }, | 3789 } |
6767 | 3790 }, |
6768 _patternAlreadyChecked: { | |
6769 type: Boolean, | |
6770 value: false | |
6771 } | |
6772 | |
6773 }, | |
6774 | |
6775 listeners: { | |
6776 'input': '_onInput', | |
6777 'keypress': '_onKeypress' | |
6778 }, | |
6779 | |
6780 /** @suppress {checkTypes} */ | |
6781 registered: function() { | |
6782 // Feature detect whether we need to patch dispatchEvent (i.e. on FF and I
E). | |
6783 if (!this._canDispatchEventOnDisabled()) { | |
6784 this._origDispatchEvent = this.dispatchEvent; | |
6785 this.dispatchEvent = this._dispatchEventFirefoxIE; | |
6786 } | |
6787 }, | |
6788 | |
6789 created: function() { | 3791 created: function() { |
6790 Polymer.IronA11yAnnouncer.requestAvailability(); | 3792 if (!Polymer.IronA11yAnnouncer.instance) { |
6791 }, | 3793 Polymer.IronA11yAnnouncer.instance = this; |
6792 | 3794 } |
6793 _canDispatchEventOnDisabled: function() { | 3795 document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(
this)); |
6794 var input = document.createElement('input'); | 3796 }, |
6795 var canDispatch = false; | 3797 announce: function(text) { |
6796 input.disabled = true; | 3798 this._text = ''; |
6797 | 3799 this.async(function() { |
6798 input.addEventListener('feature-check-dispatch-event', function() { | 3800 this._text = text; |
6799 canDispatch = true; | 3801 }, 100); |
6800 }); | 3802 }, |
6801 | 3803 _onIronAnnounce: function(event) { |
6802 try { | 3804 if (event.detail && event.detail.text) { |
6803 input.dispatchEvent(new Event('feature-check-dispatch-event')); | 3805 this.announce(event.detail.text); |
6804 } catch(e) {} | 3806 } |
6805 | 3807 } |
6806 return canDispatch; | 3808 }); |
6807 }, | 3809 Polymer.IronA11yAnnouncer.instance = null; |
6808 | 3810 Polymer.IronA11yAnnouncer.requestAvailability = function() { |
6809 _dispatchEventFirefoxIE: function() { | 3811 if (!Polymer.IronA11yAnnouncer.instance) { |
6810 // Due to Firefox bug, events fired on disabled form controls can throw | 3812 Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-ann
ouncer'); |
6811 // errors; furthermore, neither IE nor Firefox will actually dispatch | 3813 } |
6812 // events from disabled form controls; as such, we toggle disable around | 3814 document.body.appendChild(Polymer.IronA11yAnnouncer.instance); |
6813 // the dispatch to allow notifying properties to notify | 3815 }; |
6814 // See issue #47 for details | 3816 })(); |
6815 var disabled = this.disabled; | 3817 |
6816 this.disabled = false; | 3818 Polymer.IronValidatableBehaviorMeta = null; |
6817 this._origDispatchEvent.apply(this, arguments); | 3819 |
6818 this.disabled = disabled; | 3820 Polymer.IronValidatableBehavior = { |
6819 }, | 3821 properties: { |
6820 | 3822 validator: { |
6821 get _patternRegExp() { | 3823 type: String |
6822 var pattern; | 3824 }, |
6823 if (this.allowedPattern) { | 3825 invalid: { |
6824 pattern = new RegExp(this.allowedPattern); | 3826 notify: true, |
| 3827 reflectToAttribute: true, |
| 3828 type: Boolean, |
| 3829 value: false |
| 3830 }, |
| 3831 _validatorMeta: { |
| 3832 type: Object |
| 3833 }, |
| 3834 validatorType: { |
| 3835 type: String, |
| 3836 value: 'validator' |
| 3837 }, |
| 3838 _validator: { |
| 3839 type: Object, |
| 3840 computed: '__computeValidator(validator)' |
| 3841 } |
| 3842 }, |
| 3843 observers: [ '_invalidChanged(invalid)' ], |
| 3844 registered: function() { |
| 3845 Polymer.IronValidatableBehaviorMeta = new Polymer.IronMeta({ |
| 3846 type: 'validator' |
| 3847 }); |
| 3848 }, |
| 3849 _invalidChanged: function() { |
| 3850 if (this.invalid) { |
| 3851 this.setAttribute('aria-invalid', 'true'); |
| 3852 } else { |
| 3853 this.removeAttribute('aria-invalid'); |
| 3854 } |
| 3855 }, |
| 3856 hasValidator: function() { |
| 3857 return this._validator != null; |
| 3858 }, |
| 3859 validate: function(value) { |
| 3860 this.invalid = !this._getValidity(value); |
| 3861 return !this.invalid; |
| 3862 }, |
| 3863 _getValidity: function(value) { |
| 3864 if (this.hasValidator()) { |
| 3865 return this._validator.validate(value); |
| 3866 } |
| 3867 return true; |
| 3868 }, |
| 3869 __computeValidator: function() { |
| 3870 return Polymer.IronValidatableBehaviorMeta && Polymer.IronValidatableBehavio
rMeta.byKey(this.validator); |
| 3871 } |
| 3872 }; |
| 3873 |
| 3874 Polymer({ |
| 3875 is: 'iron-input', |
| 3876 "extends": 'input', |
| 3877 behaviors: [ Polymer.IronValidatableBehavior ], |
| 3878 properties: { |
| 3879 bindValue: { |
| 3880 observer: '_bindValueChanged', |
| 3881 type: String |
| 3882 }, |
| 3883 preventInvalidInput: { |
| 3884 type: Boolean |
| 3885 }, |
| 3886 allowedPattern: { |
| 3887 type: String, |
| 3888 observer: "_allowedPatternChanged" |
| 3889 }, |
| 3890 _previousValidInput: { |
| 3891 type: String, |
| 3892 value: '' |
| 3893 }, |
| 3894 _patternAlreadyChecked: { |
| 3895 type: Boolean, |
| 3896 value: false |
| 3897 } |
| 3898 }, |
| 3899 listeners: { |
| 3900 input: '_onInput', |
| 3901 keypress: '_onKeypress' |
| 3902 }, |
| 3903 registered: function() { |
| 3904 if (!this._canDispatchEventOnDisabled()) { |
| 3905 this._origDispatchEvent = this.dispatchEvent; |
| 3906 this.dispatchEvent = this._dispatchEventFirefoxIE; |
| 3907 } |
| 3908 }, |
| 3909 created: function() { |
| 3910 Polymer.IronA11yAnnouncer.requestAvailability(); |
| 3911 }, |
| 3912 _canDispatchEventOnDisabled: function() { |
| 3913 var input = document.createElement('input'); |
| 3914 var canDispatch = false; |
| 3915 input.disabled = true; |
| 3916 input.addEventListener('feature-check-dispatch-event', function() { |
| 3917 canDispatch = true; |
| 3918 }); |
| 3919 try { |
| 3920 input.dispatchEvent(new Event('feature-check-dispatch-event')); |
| 3921 } catch (e) {} |
| 3922 return canDispatch; |
| 3923 }, |
| 3924 _dispatchEventFirefoxIE: function() { |
| 3925 var disabled = this.disabled; |
| 3926 this.disabled = false; |
| 3927 this._origDispatchEvent.apply(this, arguments); |
| 3928 this.disabled = disabled; |
| 3929 }, |
| 3930 get _patternRegExp() { |
| 3931 var pattern; |
| 3932 if (this.allowedPattern) { |
| 3933 pattern = new RegExp(this.allowedPattern); |
| 3934 } else { |
| 3935 switch (this.type) { |
| 3936 case 'number': |
| 3937 pattern = /[0-9.,e-]/; |
| 3938 break; |
| 3939 } |
| 3940 } |
| 3941 return pattern; |
| 3942 }, |
| 3943 ready: function() { |
| 3944 this.bindValue = this.value; |
| 3945 }, |
| 3946 _bindValueChanged: function() { |
| 3947 if (this.value !== this.bindValue) { |
| 3948 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue ==
= false) ? '' : this.bindValue; |
| 3949 } |
| 3950 this.fire('bind-value-changed', { |
| 3951 value: this.bindValue |
| 3952 }); |
| 3953 }, |
| 3954 _allowedPatternChanged: function() { |
| 3955 this.preventInvalidInput = this.allowedPattern ? true : false; |
| 3956 }, |
| 3957 _onInput: function() { |
| 3958 if (this.preventInvalidInput && !this._patternAlreadyChecked) { |
| 3959 var valid = this._checkPatternValidity(); |
| 3960 if (!valid) { |
| 3961 this._announceInvalidCharacter('Invalid string of characters not entered
.'); |
| 3962 this.value = this._previousValidInput; |
| 3963 } |
| 3964 } |
| 3965 this.bindValue = this.value; |
| 3966 this._previousValidInput = this.value; |
| 3967 this._patternAlreadyChecked = false; |
| 3968 }, |
| 3969 _isPrintable: function(event) { |
| 3970 var anyNonPrintable = event.keyCode == 8 || event.keyCode == 9 || event.keyC
ode == 13 || event.keyCode == 27; |
| 3971 var mozNonPrintable = event.keyCode == 19 || event.keyCode == 20 || event.ke
yCode == 45 || event.keyCode == 46 || event.keyCode == 144 || event.keyCode == 1
45 || event.keyCode > 32 && event.keyCode < 41 || event.keyCode > 111 && event.k
eyCode < 124; |
| 3972 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); |
| 3973 }, |
| 3974 _onKeypress: function(event) { |
| 3975 if (!this.preventInvalidInput && this.type !== 'number') { |
| 3976 return; |
| 3977 } |
| 3978 var regexp = this._patternRegExp; |
| 3979 if (!regexp) { |
| 3980 return; |
| 3981 } |
| 3982 if (event.metaKey || event.ctrlKey || event.altKey) return; |
| 3983 this._patternAlreadyChecked = true; |
| 3984 var thisChar = String.fromCharCode(event.charCode); |
| 3985 if (this._isPrintable(event) && !regexp.test(thisChar)) { |
| 3986 event.preventDefault(); |
| 3987 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not ent
ered.'); |
| 3988 } |
| 3989 }, |
| 3990 _checkPatternValidity: function() { |
| 3991 var regexp = this._patternRegExp; |
| 3992 if (!regexp) { |
| 3993 return true; |
| 3994 } |
| 3995 for (var i = 0; i < this.value.length; i++) { |
| 3996 if (!regexp.test(this.value[i])) { |
| 3997 return false; |
| 3998 } |
| 3999 } |
| 4000 return true; |
| 4001 }, |
| 4002 validate: function() { |
| 4003 var valid = this.checkValidity(); |
| 4004 if (valid) { |
| 4005 if (this.required && this.value === '') { |
| 4006 valid = false; |
| 4007 } else if (this.hasValidator()) { |
| 4008 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value); |
| 4009 } |
| 4010 } |
| 4011 this.invalid = !valid; |
| 4012 this.fire('iron-input-validate'); |
| 4013 return valid; |
| 4014 }, |
| 4015 _announceInvalidCharacter: function(message) { |
| 4016 this.fire('iron-announce', { |
| 4017 text: message |
| 4018 }); |
| 4019 } |
| 4020 }); |
| 4021 |
| 4022 Polymer({ |
| 4023 is: 'paper-input-container', |
| 4024 properties: { |
| 4025 noLabelFloat: { |
| 4026 type: Boolean, |
| 4027 value: false |
| 4028 }, |
| 4029 alwaysFloatLabel: { |
| 4030 type: Boolean, |
| 4031 value: false |
| 4032 }, |
| 4033 attrForValue: { |
| 4034 type: String, |
| 4035 value: 'bind-value' |
| 4036 }, |
| 4037 autoValidate: { |
| 4038 type: Boolean, |
| 4039 value: false |
| 4040 }, |
| 4041 invalid: { |
| 4042 observer: '_invalidChanged', |
| 4043 type: Boolean, |
| 4044 value: false |
| 4045 }, |
| 4046 focused: { |
| 4047 readOnly: true, |
| 4048 type: Boolean, |
| 4049 value: false, |
| 4050 notify: true |
| 4051 }, |
| 4052 _addons: { |
| 4053 type: Array |
| 4054 }, |
| 4055 _inputHasContent: { |
| 4056 type: Boolean, |
| 4057 value: false |
| 4058 }, |
| 4059 _inputSelector: { |
| 4060 type: String, |
| 4061 value: 'input,textarea,.paper-input-input' |
| 4062 }, |
| 4063 _boundOnFocus: { |
| 4064 type: Function, |
| 4065 value: function() { |
| 4066 return this._onFocus.bind(this); |
| 4067 } |
| 4068 }, |
| 4069 _boundOnBlur: { |
| 4070 type: Function, |
| 4071 value: function() { |
| 4072 return this._onBlur.bind(this); |
| 4073 } |
| 4074 }, |
| 4075 _boundOnInput: { |
| 4076 type: Function, |
| 4077 value: function() { |
| 4078 return this._onInput.bind(this); |
| 4079 } |
| 4080 }, |
| 4081 _boundValueChanged: { |
| 4082 type: Function, |
| 4083 value: function() { |
| 4084 return this._onValueChanged.bind(this); |
| 4085 } |
| 4086 } |
| 4087 }, |
| 4088 listeners: { |
| 4089 'addon-attached': '_onAddonAttached', |
| 4090 'iron-input-validate': '_onIronInputValidate' |
| 4091 }, |
| 4092 get _valueChangedEvent() { |
| 4093 return this.attrForValue + '-changed'; |
| 4094 }, |
| 4095 get _propertyForValue() { |
| 4096 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); |
| 4097 }, |
| 4098 get _inputElement() { |
| 4099 return Polymer.dom(this).querySelector(this._inputSelector); |
| 4100 }, |
| 4101 get _inputElementValue() { |
| 4102 return this._inputElement[this._propertyForValue] || this._inputElement.valu
e; |
| 4103 }, |
| 4104 ready: function() { |
| 4105 if (!this._addons) { |
| 4106 this._addons = []; |
| 4107 } |
| 4108 this.addEventListener('focus', this._boundOnFocus, true); |
| 4109 this.addEventListener('blur', this._boundOnBlur, true); |
| 4110 }, |
| 4111 attached: function() { |
| 4112 if (this.attrForValue) { |
| 4113 this._inputElement.addEventListener(this._valueChangedEvent, this._boundVa
lueChanged); |
| 4114 } else { |
| 4115 this.addEventListener('input', this._onInput); |
| 4116 } |
| 4117 if (this._inputElementValue != '') { |
| 4118 this._handleValueAndAutoValidate(this._inputElement); |
| 4119 } else { |
| 4120 this._handleValue(this._inputElement); |
| 4121 } |
| 4122 }, |
| 4123 _onAddonAttached: function(event) { |
| 4124 if (!this._addons) { |
| 4125 this._addons = []; |
| 4126 } |
| 4127 var target = event.target; |
| 4128 if (this._addons.indexOf(target) === -1) { |
| 4129 this._addons.push(target); |
| 4130 if (this.isAttached) { |
| 4131 this._handleValue(this._inputElement); |
| 4132 } |
| 4133 } |
| 4134 }, |
| 4135 _onFocus: function() { |
| 4136 this._setFocused(true); |
| 4137 }, |
| 4138 _onBlur: function() { |
| 4139 this._setFocused(false); |
| 4140 this._handleValueAndAutoValidate(this._inputElement); |
| 4141 }, |
| 4142 _onInput: function(event) { |
| 4143 this._handleValueAndAutoValidate(event.target); |
| 4144 }, |
| 4145 _onValueChanged: function(event) { |
| 4146 this._handleValueAndAutoValidate(event.target); |
| 4147 }, |
| 4148 _handleValue: function(inputElement) { |
| 4149 var value = this._inputElementValue; |
| 4150 if (value || value === 0 || inputElement.type === 'number' && !inputElement.
checkValidity()) { |
| 4151 this._inputHasContent = true; |
| 4152 } else { |
| 4153 this._inputHasContent = false; |
| 4154 } |
| 4155 this.updateAddons({ |
| 4156 inputElement: inputElement, |
| 4157 value: value, |
| 4158 invalid: this.invalid |
| 4159 }); |
| 4160 }, |
| 4161 _handleValueAndAutoValidate: function(inputElement) { |
| 4162 if (this.autoValidate) { |
| 4163 var valid; |
| 4164 if (inputElement.validate) { |
| 4165 valid = inputElement.validate(this._inputElementValue); |
6825 } else { | 4166 } else { |
6826 switch (this.type) { | 4167 valid = inputElement.checkValidity(); |
6827 case 'number': | 4168 } |
6828 pattern = /[0-9.,e-]/; | |
6829 break; | |
6830 } | |
6831 } | |
6832 return pattern; | |
6833 }, | |
6834 | |
6835 ready: function() { | |
6836 this.bindValue = this.value; | |
6837 }, | |
6838 | |
6839 /** | |
6840 * @suppress {checkTypes} | |
6841 */ | |
6842 _bindValueChanged: function() { | |
6843 if (this.value !== this.bindValue) { | |
6844 this.value = !(this.bindValue || this.bindValue === 0 || this.bindValue
=== false) ? '' : this.bindValue; | |
6845 } | |
6846 // manually notify because we don't want to notify until after setting val
ue | |
6847 this.fire('bind-value-changed', {value: this.bindValue}); | |
6848 }, | |
6849 | |
6850 _allowedPatternChanged: function() { | |
6851 // Force to prevent invalid input when an `allowed-pattern` is set | |
6852 this.preventInvalidInput = this.allowedPattern ? true : false; | |
6853 }, | |
6854 | |
6855 _onInput: function() { | |
6856 // Need to validate each of the characters pasted if they haven't | |
6857 // been validated inside `_onKeypress` already. | |
6858 if (this.preventInvalidInput && !this._patternAlreadyChecked) { | |
6859 var valid = this._checkPatternValidity(); | |
6860 if (!valid) { | |
6861 this._announceInvalidCharacter('Invalid string of characters not enter
ed.'); | |
6862 this.value = this._previousValidInput; | |
6863 } | |
6864 } | |
6865 | |
6866 this.bindValue = this.value; | |
6867 this._previousValidInput = this.value; | |
6868 this._patternAlreadyChecked = false; | |
6869 }, | |
6870 | |
6871 _isPrintable: function(event) { | |
6872 // What a control/printable character is varies wildly based on the browse
r. | |
6873 // - most control characters (arrows, backspace) do not send a `keypress`
event | |
6874 // in Chrome, but the *do* on Firefox | |
6875 // - in Firefox, when they do send a `keypress` event, control chars have | |
6876 // a charCode = 0, keyCode = xx (for ex. 40 for down arrow) | |
6877 // - printable characters always send a keypress event. | |
6878 // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
keyCode | |
6879 // always matches the charCode. | |
6880 // None of this makes any sense. | |
6881 | |
6882 // For these keys, ASCII code == browser keycode. | |
6883 var anyNonPrintable = | |
6884 (event.keyCode == 8) || // backspace | |
6885 (event.keyCode == 9) || // tab | |
6886 (event.keyCode == 13) || // enter | |
6887 (event.keyCode == 27); // escape | |
6888 | |
6889 // For these keys, make sure it's a browser keycode and not an ASCII code. | |
6890 var mozNonPrintable = | |
6891 (event.keyCode == 19) || // pause | |
6892 (event.keyCode == 20) || // caps lock | |
6893 (event.keyCode == 45) || // insert | |
6894 (event.keyCode == 46) || // delete | |
6895 (event.keyCode == 144) || // num lock | |
6896 (event.keyCode == 145) || // scroll lock | |
6897 (event.keyCode > 32 && event.keyCode < 41) || // page up/down, end, ho
me, arrows | |
6898 (event.keyCode > 111 && event.keyCode < 124); // fn keys | |
6899 | |
6900 return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable); | |
6901 }, | |
6902 | |
6903 _onKeypress: function(event) { | |
6904 if (!this.preventInvalidInput && this.type !== 'number') { | |
6905 return; | |
6906 } | |
6907 var regexp = this._patternRegExp; | |
6908 if (!regexp) { | |
6909 return; | |
6910 } | |
6911 | |
6912 // Handle special keys and backspace | |
6913 if (event.metaKey || event.ctrlKey || event.altKey) | |
6914 return; | |
6915 | |
6916 // Check the pattern either here or in `_onInput`, but not in both. | |
6917 this._patternAlreadyChecked = true; | |
6918 | |
6919 var thisChar = String.fromCharCode(event.charCode); | |
6920 if (this._isPrintable(event) && !regexp.test(thisChar)) { | |
6921 event.preventDefault(); | |
6922 this._announceInvalidCharacter('Invalid character ' + thisChar + ' not e
ntered.'); | |
6923 } | |
6924 }, | |
6925 | |
6926 _checkPatternValidity: function() { | |
6927 var regexp = this._patternRegExp; | |
6928 if (!regexp) { | |
6929 return true; | |
6930 } | |
6931 for (var i = 0; i < this.value.length; i++) { | |
6932 if (!regexp.test(this.value[i])) { | |
6933 return false; | |
6934 } | |
6935 } | |
6936 return true; | |
6937 }, | |
6938 | |
6939 /** | |
6940 * Returns true if `value` is valid. The validator provided in `validator` w
ill be used first, | |
6941 * then any constraints. | |
6942 * @return {boolean} True if the value is valid. | |
6943 */ | |
6944 validate: function() { | |
6945 // First, check what the browser thinks. Some inputs (like type=number) | |
6946 // behave weirdly and will set the value to "" if something invalid is | |
6947 // entered, but will set the validity correctly. | |
6948 var valid = this.checkValidity(); | |
6949 | |
6950 // Only do extra checking if the browser thought this was valid. | |
6951 if (valid) { | |
6952 // Empty, required input is invalid | |
6953 if (this.required && this.value === '') { | |
6954 valid = false; | |
6955 } else if (this.hasValidator()) { | |
6956 valid = Polymer.IronValidatableBehavior.validate.call(this, this.value
); | |
6957 } | |
6958 } | |
6959 | |
6960 this.invalid = !valid; | 4169 this.invalid = !valid; |
6961 this.fire('iron-input-validate'); | 4170 } |
6962 return valid; | 4171 this._handleValue(inputElement); |
6963 }, | 4172 }, |
6964 | 4173 _onIronInputValidate: function(event) { |
6965 _announceInvalidCharacter: function(message) { | 4174 this.invalid = this._inputElement.invalid; |
6966 this.fire('iron-announce', { text: message }); | 4175 }, |
6967 } | 4176 _invalidChanged: function() { |
6968 }); | 4177 if (this._addons) { |
6969 | |
6970 /* | |
6971 The `iron-input-validate` event is fired whenever `validate()` is called. | |
6972 @event iron-input-validate | |
6973 */ | |
6974 Polymer({ | |
6975 is: 'paper-input-container', | |
6976 | |
6977 properties: { | |
6978 /** | |
6979 * Set to true to disable the floating label. The label disappears when th
e input value is | |
6980 * not null. | |
6981 */ | |
6982 noLabelFloat: { | |
6983 type: Boolean, | |
6984 value: false | |
6985 }, | |
6986 | |
6987 /** | |
6988 * Set to true to always float the floating label. | |
6989 */ | |
6990 alwaysFloatLabel: { | |
6991 type: Boolean, | |
6992 value: false | |
6993 }, | |
6994 | |
6995 /** | |
6996 * The attribute to listen for value changes on. | |
6997 */ | |
6998 attrForValue: { | |
6999 type: String, | |
7000 value: 'bind-value' | |
7001 }, | |
7002 | |
7003 /** | |
7004 * Set to true to auto-validate the input value when it changes. | |
7005 */ | |
7006 autoValidate: { | |
7007 type: Boolean, | |
7008 value: false | |
7009 }, | |
7010 | |
7011 /** | |
7012 * True if the input is invalid. This property is set automatically when t
he input value | |
7013 * changes if auto-validating, or when the `iron-input-validate` event is
heard from a child. | |
7014 */ | |
7015 invalid: { | |
7016 observer: '_invalidChanged', | |
7017 type: Boolean, | |
7018 value: false | |
7019 }, | |
7020 | |
7021 /** | |
7022 * True if the input has focus. | |
7023 */ | |
7024 focused: { | |
7025 readOnly: true, | |
7026 type: Boolean, | |
7027 value: false, | |
7028 notify: true | |
7029 }, | |
7030 | |
7031 _addons: { | |
7032 type: Array | |
7033 // do not set a default value here intentionally - it will be initialize
d lazily when a | |
7034 // distributed child is attached, which may occur before configuration f
or this element | |
7035 // in polyfill. | |
7036 }, | |
7037 | |
7038 _inputHasContent: { | |
7039 type: Boolean, | |
7040 value: false | |
7041 }, | |
7042 | |
7043 _inputSelector: { | |
7044 type: String, | |
7045 value: 'input,textarea,.paper-input-input' | |
7046 }, | |
7047 | |
7048 _boundOnFocus: { | |
7049 type: Function, | |
7050 value: function() { | |
7051 return this._onFocus.bind(this); | |
7052 } | |
7053 }, | |
7054 | |
7055 _boundOnBlur: { | |
7056 type: Function, | |
7057 value: function() { | |
7058 return this._onBlur.bind(this); | |
7059 } | |
7060 }, | |
7061 | |
7062 _boundOnInput: { | |
7063 type: Function, | |
7064 value: function() { | |
7065 return this._onInput.bind(this); | |
7066 } | |
7067 }, | |
7068 | |
7069 _boundValueChanged: { | |
7070 type: Function, | |
7071 value: function() { | |
7072 return this._onValueChanged.bind(this); | |
7073 } | |
7074 } | |
7075 }, | |
7076 | |
7077 listeners: { | |
7078 'addon-attached': '_onAddonAttached', | |
7079 'iron-input-validate': '_onIronInputValidate' | |
7080 }, | |
7081 | |
7082 get _valueChangedEvent() { | |
7083 return this.attrForValue + '-changed'; | |
7084 }, | |
7085 | |
7086 get _propertyForValue() { | |
7087 return Polymer.CaseMap.dashToCamelCase(this.attrForValue); | |
7088 }, | |
7089 | |
7090 get _inputElement() { | |
7091 return Polymer.dom(this).querySelector(this._inputSelector); | |
7092 }, | |
7093 | |
7094 get _inputElementValue() { | |
7095 return this._inputElement[this._propertyForValue] || this._inputElement.va
lue; | |
7096 }, | |
7097 | |
7098 ready: function() { | |
7099 if (!this._addons) { | |
7100 this._addons = []; | |
7101 } | |
7102 this.addEventListener('focus', this._boundOnFocus, true); | |
7103 this.addEventListener('blur', this._boundOnBlur, true); | |
7104 }, | |
7105 | |
7106 attached: function() { | |
7107 if (this.attrForValue) { | |
7108 this._inputElement.addEventListener(this._valueChangedEvent, this._bound
ValueChanged); | |
7109 } else { | |
7110 this.addEventListener('input', this._onInput); | |
7111 } | |
7112 | |
7113 // Only validate when attached if the input already has a value. | |
7114 if (this._inputElementValue != '') { | |
7115 this._handleValueAndAutoValidate(this._inputElement); | |
7116 } else { | |
7117 this._handleValue(this._inputElement); | |
7118 } | |
7119 }, | |
7120 | |
7121 _onAddonAttached: function(event) { | |
7122 if (!this._addons) { | |
7123 this._addons = []; | |
7124 } | |
7125 var target = event.target; | |
7126 if (this._addons.indexOf(target) === -1) { | |
7127 this._addons.push(target); | |
7128 if (this.isAttached) { | |
7129 this._handleValue(this._inputElement); | |
7130 } | |
7131 } | |
7132 }, | |
7133 | |
7134 _onFocus: function() { | |
7135 this._setFocused(true); | |
7136 }, | |
7137 | |
7138 _onBlur: function() { | |
7139 this._setFocused(false); | |
7140 this._handleValueAndAutoValidate(this._inputElement); | |
7141 }, | |
7142 | |
7143 _onInput: function(event) { | |
7144 this._handleValueAndAutoValidate(event.target); | |
7145 }, | |
7146 | |
7147 _onValueChanged: function(event) { | |
7148 this._handleValueAndAutoValidate(event.target); | |
7149 }, | |
7150 | |
7151 _handleValue: function(inputElement) { | |
7152 var value = this._inputElementValue; | |
7153 | |
7154 // type="number" hack needed because this.value is empty until it's valid | |
7155 if (value || value === 0 || (inputElement.type === 'number' && !inputEleme
nt.checkValidity())) { | |
7156 this._inputHasContent = true; | |
7157 } else { | |
7158 this._inputHasContent = false; | |
7159 } | |
7160 | |
7161 this.updateAddons({ | 4178 this.updateAddons({ |
7162 inputElement: inputElement, | |
7163 value: value, | |
7164 invalid: this.invalid | 4179 invalid: this.invalid |
7165 }); | 4180 }); |
7166 }, | 4181 } |
7167 | 4182 }, |
7168 _handleValueAndAutoValidate: function(inputElement) { | 4183 updateAddons: function(state) { |
7169 if (this.autoValidate) { | 4184 for (var addon, index = 0; addon = this._addons[index]; index++) { |
7170 var valid; | 4185 addon.update(state); |
7171 if (inputElement.validate) { | 4186 } |
7172 valid = inputElement.validate(this._inputElementValue); | 4187 }, |
7173 } else { | 4188 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, i
nvalid, _inputHasContent) { |
7174 valid = inputElement.checkValidity(); | 4189 var cls = 'input-content'; |
7175 } | 4190 if (!noLabelFloat) { |
7176 this.invalid = !valid; | 4191 var label = this.querySelector('label'); |
7177 } | 4192 if (alwaysFloatLabel || _inputHasContent) { |
7178 | 4193 cls += ' label-is-floating'; |
7179 // Call this last to notify the add-ons. | 4194 this.$.labelAndInputContainer.style.position = 'static'; |
7180 this._handleValue(inputElement); | 4195 if (invalid) { |
7181 }, | 4196 cls += ' is-invalid'; |
7182 | 4197 } else if (focused) { |
7183 _onIronInputValidate: function(event) { | 4198 cls += " label-is-highlighted"; |
7184 this.invalid = this._inputElement.invalid; | |
7185 }, | |
7186 | |
7187 _invalidChanged: function() { | |
7188 if (this._addons) { | |
7189 this.updateAddons({invalid: this.invalid}); | |
7190 } | |
7191 }, | |
7192 | |
7193 /** | |
7194 * Call this to update the state of add-ons. | |
7195 * @param {Object} state Add-on state. | |
7196 */ | |
7197 updateAddons: function(state) { | |
7198 for (var addon, index = 0; addon = this._addons[index]; index++) { | |
7199 addon.update(state); | |
7200 } | |
7201 }, | |
7202 | |
7203 _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused,
invalid, _inputHasContent) { | |
7204 var cls = 'input-content'; | |
7205 if (!noLabelFloat) { | |
7206 var label = this.querySelector('label'); | |
7207 | |
7208 if (alwaysFloatLabel || _inputHasContent) { | |
7209 cls += ' label-is-floating'; | |
7210 // If the label is floating, ignore any offsets that may have been | |
7211 // applied from a prefix element. | |
7212 this.$.labelAndInputContainer.style.position = 'static'; | |
7213 | |
7214 if (invalid) { | |
7215 cls += ' is-invalid'; | |
7216 } else if (focused) { | |
7217 cls += " label-is-highlighted"; | |
7218 } | |
7219 } else { | |
7220 // When the label is not floating, it should overlap the input element
. | |
7221 if (label) { | |
7222 this.$.labelAndInputContainer.style.position = 'relative'; | |
7223 } | |
7224 } | 4199 } |
7225 } else { | 4200 } else { |
7226 if (_inputHasContent) { | 4201 if (label) { |
7227 cls += ' label-is-hidden'; | 4202 this.$.labelAndInputContainer.style.position = 'relative'; |
7228 } | 4203 } |
7229 } | 4204 } |
7230 return cls; | 4205 } else { |
7231 }, | 4206 if (_inputHasContent) { |
7232 | 4207 cls += ' label-is-hidden'; |
7233 _computeUnderlineClass: function(focused, invalid) { | 4208 } |
7234 var cls = 'underline'; | 4209 } |
7235 if (invalid) { | 4210 return cls; |
7236 cls += ' is-invalid'; | 4211 }, |
7237 } else if (focused) { | 4212 _computeUnderlineClass: function(focused, invalid) { |
7238 cls += ' is-highlighted' | 4213 var cls = 'underline'; |
7239 } | 4214 if (invalid) { |
7240 return cls; | 4215 cls += ' is-invalid'; |
7241 }, | 4216 } else if (focused) { |
7242 | 4217 cls += ' is-highlighted'; |
7243 _computeAddOnContentClass: function(focused, invalid) { | 4218 } |
7244 var cls = 'add-on-content'; | 4219 return cls; |
7245 if (invalid) { | 4220 }, |
7246 cls += ' is-invalid'; | 4221 _computeAddOnContentClass: function(focused, invalid) { |
7247 } else if (focused) { | 4222 var cls = 'add-on-content'; |
7248 cls += ' is-highlighted' | 4223 if (invalid) { |
7249 } | 4224 cls += ' is-invalid'; |
7250 return cls; | 4225 } else if (focused) { |
7251 } | 4226 cls += ' is-highlighted'; |
7252 }); | 4227 } |
7253 /** @polymerBehavior */ | 4228 return cls; |
7254 Polymer.PaperSpinnerBehavior = { | 4229 } |
7255 | 4230 }); |
7256 listeners: { | 4231 |
7257 'animationend': '__reset', | 4232 Polymer.PaperSpinnerBehavior = { |
7258 'webkitAnimationEnd': '__reset' | 4233 listeners: { |
7259 }, | 4234 animationend: '__reset', |
7260 | 4235 webkitAnimationEnd: '__reset' |
7261 properties: { | 4236 }, |
7262 /** | 4237 properties: { |
7263 * Displays the spinner. | 4238 active: { |
7264 */ | 4239 type: Boolean, |
7265 active: { | 4240 value: false, |
7266 type: Boolean, | 4241 reflectToAttribute: true, |
7267 value: false, | 4242 observer: '__activeChanged' |
7268 reflectToAttribute: true, | 4243 }, |
7269 observer: '__activeChanged' | 4244 alt: { |
7270 }, | 4245 type: String, |
7271 | 4246 value: 'loading', |
7272 /** | 4247 observer: '__altChanged' |
7273 * Alternative text content for accessibility support. | 4248 }, |
7274 * If alt is present, it will add an aria-label whose content matches alt
when active. | 4249 __coolingDown: { |
7275 * If alt is not present, it will default to 'loading' as the alt value. | 4250 type: Boolean, |
7276 */ | 4251 value: false |
7277 alt: { | 4252 } |
7278 type: String, | 4253 }, |
7279 value: 'loading', | 4254 __computeContainerClasses: function(active, coolingDown) { |
7280 observer: '__altChanged' | 4255 return [ active || coolingDown ? 'active' : '', coolingDown ? 'cooldown' : '
' ].join(' '); |
7281 }, | 4256 }, |
7282 | 4257 __activeChanged: function(active, old) { |
7283 __coolingDown: { | 4258 this.__setAriaHidden(!active); |
7284 type: Boolean, | 4259 this.__coolingDown = !active && old; |
7285 value: false | 4260 }, |
7286 } | 4261 __altChanged: function(alt) { |
7287 }, | 4262 if (alt === this.getPropertyInfo('alt').value) { |
7288 | 4263 this.alt = this.getAttribute('aria-label') || alt; |
7289 __computeContainerClasses: function(active, coolingDown) { | 4264 } else { |
7290 return [ | 4265 this.__setAriaHidden(alt === ''); |
7291 active || coolingDown ? 'active' : '', | 4266 this.setAttribute('aria-label', alt); |
7292 coolingDown ? 'cooldown' : '' | 4267 } |
7293 ].join(' '); | 4268 }, |
7294 }, | 4269 __setAriaHidden: function(hidden) { |
7295 | 4270 var attr = 'aria-hidden'; |
7296 __activeChanged: function(active, old) { | 4271 if (hidden) { |
7297 this.__setAriaHidden(!active); | 4272 this.setAttribute(attr, 'true'); |
7298 this.__coolingDown = !active && old; | 4273 } else { |
7299 }, | 4274 this.removeAttribute(attr); |
7300 | 4275 } |
7301 __altChanged: function(alt) { | 4276 }, |
7302 // user-provided `aria-label` takes precedence over prototype default | 4277 __reset: function() { |
7303 if (alt === this.getPropertyInfo('alt').value) { | 4278 this.active = false; |
7304 this.alt = this.getAttribute('aria-label') || alt; | 4279 this.__coolingDown = false; |
7305 } else { | 4280 } |
7306 this.__setAriaHidden(alt===''); | 4281 }; |
7307 this.setAttribute('aria-label', alt); | 4282 |
7308 } | |
7309 }, | |
7310 | |
7311 __setAriaHidden: function(hidden) { | |
7312 var attr = 'aria-hidden'; | |
7313 if (hidden) { | |
7314 this.setAttribute(attr, 'true'); | |
7315 } else { | |
7316 this.removeAttribute(attr); | |
7317 } | |
7318 }, | |
7319 | |
7320 __reset: function() { | |
7321 this.active = false; | |
7322 this.__coolingDown = false; | |
7323 } | |
7324 }; | |
7325 Polymer({ | 4283 Polymer({ |
7326 is: 'paper-spinner-lite', | 4284 is: 'paper-spinner-lite', |
7327 | 4285 behaviors: [ Polymer.PaperSpinnerBehavior ] |
7328 behaviors: [ | 4286 }); |
7329 Polymer.PaperSpinnerBehavior | 4287 |
7330 ] | |
7331 }); | |
7332 // Copyright 2016 The Chromium Authors. All rights reserved. | 4288 // Copyright 2016 The Chromium Authors. All rights reserved. |
7333 // Use of this source code is governed by a BSD-style license that can be | 4289 // Use of this source code is governed by a BSD-style license that can be |
7334 // found in the LICENSE file. | 4290 // found in the LICENSE file. |
7335 | |
7336 /** | |
7337 * Implements an incremental search field which can be shown and hidden. | |
7338 * Canonical implementation is <cr-search-field>. | |
7339 * @polymerBehavior | |
7340 */ | |
7341 var CrSearchFieldBehavior = { | 4291 var CrSearchFieldBehavior = { |
7342 properties: { | 4292 properties: { |
7343 label: { | 4293 label: { |
7344 type: String, | 4294 type: String, |
7345 value: '', | 4295 value: '' |
7346 }, | 4296 }, |
7347 | |
7348 clearLabel: { | 4297 clearLabel: { |
7349 type: String, | 4298 type: String, |
7350 value: '', | 4299 value: '' |
7351 }, | 4300 }, |
7352 | |
7353 showingSearch: { | 4301 showingSearch: { |
7354 type: Boolean, | 4302 type: Boolean, |
7355 value: false, | 4303 value: false, |
7356 notify: true, | 4304 notify: true, |
7357 observer: 'showingSearchChanged_', | 4305 observer: 'showingSearchChanged_', |
7358 reflectToAttribute: true | 4306 reflectToAttribute: true |
7359 }, | 4307 }, |
7360 | |
7361 /** @private */ | |
7362 lastValue_: { | 4308 lastValue_: { |
7363 type: String, | 4309 type: String, |
7364 value: '', | 4310 value: '' |
7365 }, | 4311 } |
7366 }, | 4312 }, |
7367 | |
7368 /** | |
7369 * @abstract | |
7370 * @return {!HTMLInputElement} The input field element the behavior should | |
7371 * use. | |
7372 */ | |
7373 getSearchInput: function() {}, | 4313 getSearchInput: function() {}, |
7374 | |
7375 /** | |
7376 * @return {string} The value of the search field. | |
7377 */ | |
7378 getValue: function() { | 4314 getValue: function() { |
7379 return this.getSearchInput().value; | 4315 return this.getSearchInput().value; |
7380 }, | 4316 }, |
7381 | |
7382 /** | |
7383 * Sets the value of the search field. | |
7384 * @param {string} value | |
7385 */ | |
7386 setValue: function(value) { | 4317 setValue: function(value) { |
7387 // Use bindValue when setting the input value so that changes propagate | |
7388 // correctly. | |
7389 this.getSearchInput().bindValue = value; | 4318 this.getSearchInput().bindValue = value; |
7390 this.onValueChanged_(value); | 4319 this.onValueChanged_(value); |
7391 }, | 4320 }, |
7392 | |
7393 showAndFocus: function() { | 4321 showAndFocus: function() { |
7394 this.showingSearch = true; | 4322 this.showingSearch = true; |
7395 this.focus_(); | 4323 this.focus_(); |
7396 }, | 4324 }, |
7397 | |
7398 /** @private */ | |
7399 focus_: function() { | 4325 focus_: function() { |
7400 this.getSearchInput().focus(); | 4326 this.getSearchInput().focus(); |
7401 }, | 4327 }, |
7402 | |
7403 onSearchTermSearch: function() { | 4328 onSearchTermSearch: function() { |
7404 this.onValueChanged_(this.getValue()); | 4329 this.onValueChanged_(this.getValue()); |
7405 }, | 4330 }, |
7406 | |
7407 /** | |
7408 * Updates the internal state of the search field based on a change that has | |
7409 * already happened. | |
7410 * @param {string} newValue | |
7411 * @private | |
7412 */ | |
7413 onValueChanged_: function(newValue) { | 4331 onValueChanged_: function(newValue) { |
7414 if (newValue == this.lastValue_) | 4332 if (newValue == this.lastValue_) return; |
7415 return; | |
7416 | |
7417 this.fire('search-changed', newValue); | 4333 this.fire('search-changed', newValue); |
7418 this.lastValue_ = newValue; | 4334 this.lastValue_ = newValue; |
7419 }, | 4335 }, |
7420 | |
7421 onSearchTermKeydown: function(e) { | 4336 onSearchTermKeydown: function(e) { |
7422 if (e.key == 'Escape') | 4337 if (e.key == 'Escape') this.showingSearch = false; |
7423 this.showingSearch = false; | 4338 }, |
7424 }, | |
7425 | |
7426 /** @private */ | |
7427 showingSearchChanged_: function() { | 4339 showingSearchChanged_: function() { |
7428 if (this.showingSearch) { | 4340 if (this.showingSearch) { |
7429 this.focus_(); | 4341 this.focus_(); |
7430 return; | 4342 return; |
7431 } | 4343 } |
7432 | |
7433 this.setValue(''); | 4344 this.setValue(''); |
7434 this.getSearchInput().blur(); | 4345 this.getSearchInput().blur(); |
7435 } | 4346 } |
7436 }; | 4347 }; |
| 4348 |
7437 // Copyright 2016 The Chromium Authors. All rights reserved. | 4349 // Copyright 2016 The Chromium Authors. All rights reserved. |
7438 // Use of this source code is governed by a BSD-style license that can be | 4350 // Use of this source code is governed by a BSD-style license that can be |
7439 // found in the LICENSE file. | 4351 // found in the LICENSE file. |
7440 | |
7441 // TODO(tsergeant): Add tests for cr-toolbar-search-field. | |
7442 Polymer({ | 4352 Polymer({ |
7443 is: 'cr-toolbar-search-field', | 4353 is: 'cr-toolbar-search-field', |
7444 | 4354 behaviors: [ CrSearchFieldBehavior ], |
7445 behaviors: [CrSearchFieldBehavior], | |
7446 | |
7447 properties: { | 4355 properties: { |
7448 narrow: { | 4356 narrow: { |
7449 type: Boolean, | 4357 type: Boolean, |
7450 reflectToAttribute: true, | 4358 reflectToAttribute: true |
7451 }, | 4359 }, |
7452 | |
7453 // Prompt text to display in the search field. | |
7454 label: String, | 4360 label: String, |
7455 | |
7456 // Tooltip to display on the clear search button. | |
7457 clearLabel: String, | 4361 clearLabel: String, |
7458 | |
7459 // When true, show a loading spinner to indicate that the backend is | |
7460 // processing the search. Will only show if the search field is open. | |
7461 spinnerActive: { | 4362 spinnerActive: { |
7462 type: Boolean, | 4363 type: Boolean, |
7463 reflectToAttribute: true | 4364 reflectToAttribute: true |
7464 }, | 4365 }, |
7465 | 4366 hasSearchText_: Boolean |
7466 /** @private */ | 4367 }, |
7467 hasSearchText_: Boolean, | |
7468 }, | |
7469 | |
7470 listeners: { | 4368 listeners: { |
7471 'tap': 'showSearch_', | 4369 tap: 'showSearch_', |
7472 'searchInput.bind-value-changed': 'onBindValueChanged_', | 4370 'searchInput.bind-value-changed': 'onBindValueChanged_' |
7473 }, | 4371 }, |
7474 | |
7475 /** @return {!HTMLInputElement} */ | |
7476 getSearchInput: function() { | 4372 getSearchInput: function() { |
7477 return this.$.searchInput; | 4373 return this.$.searchInput; |
7478 }, | 4374 }, |
7479 | |
7480 /** @return {boolean} */ | |
7481 isSearchFocused: function() { | 4375 isSearchFocused: function() { |
7482 return this.$.searchTerm.focused; | 4376 return this.$.searchTerm.focused; |
7483 }, | 4377 }, |
7484 | |
7485 /** | |
7486 * @param {boolean} narrow | |
7487 * @return {number} | |
7488 * @private | |
7489 */ | |
7490 computeIconTabIndex_: function(narrow) { | 4378 computeIconTabIndex_: function(narrow) { |
7491 return narrow ? 0 : -1; | 4379 return narrow ? 0 : -1; |
7492 }, | 4380 }, |
7493 | |
7494 /** | |
7495 * @param {boolean} spinnerActive | |
7496 * @param {boolean} showingSearch | |
7497 * @return {boolean} | |
7498 * @private | |
7499 */ | |
7500 isSpinnerShown_: function(spinnerActive, showingSearch) { | 4381 isSpinnerShown_: function(spinnerActive, showingSearch) { |
7501 return spinnerActive && showingSearch; | 4382 return spinnerActive && showingSearch; |
7502 }, | 4383 }, |
7503 | |
7504 /** @private */ | |
7505 onInputBlur_: function() { | 4384 onInputBlur_: function() { |
7506 if (!this.hasSearchText_) | 4385 if (!this.hasSearchText_) this.showingSearch = false; |
7507 this.showingSearch = false; | 4386 }, |
7508 }, | |
7509 | |
7510 /** | |
7511 * Update the state of the search field whenever the underlying input value | |
7512 * changes. Unlike onsearch or onkeypress, this is reliably called immediately | |
7513 * after any change, whether the result of user input or JS modification. | |
7514 * @private | |
7515 */ | |
7516 onBindValueChanged_: function() { | 4387 onBindValueChanged_: function() { |
7517 var newValue = this.$.searchInput.bindValue; | 4388 var newValue = this.$.searchInput.bindValue; |
7518 this.hasSearchText_ = newValue != ''; | 4389 this.hasSearchText_ = newValue != ''; |
7519 if (newValue != '') | 4390 if (newValue != '') this.showingSearch = true; |
7520 this.showingSearch = true; | 4391 }, |
7521 }, | |
7522 | |
7523 /** | |
7524 * @param {Event} e | |
7525 * @private | |
7526 */ | |
7527 showSearch_: function(e) { | 4392 showSearch_: function(e) { |
7528 if (e.target != this.$.clearSearch) | 4393 if (e.target != this.$.clearSearch) this.showingSearch = true; |
7529 this.showingSearch = true; | 4394 }, |
7530 }, | |
7531 | |
7532 /** | |
7533 * @param {Event} e | |
7534 * @private | |
7535 */ | |
7536 hideSearch_: function(e) { | 4395 hideSearch_: function(e) { |
7537 this.showingSearch = false; | 4396 this.showingSearch = false; |
7538 e.stopPropagation(); | 4397 e.stopPropagation(); |
7539 } | 4398 } |
7540 }); | 4399 }); |
| 4400 |
7541 // Copyright 2016 The Chromium Authors. All rights reserved. | 4401 // Copyright 2016 The Chromium Authors. All rights reserved. |
7542 // Use of this source code is governed by a BSD-style license that can be | 4402 // Use of this source code is governed by a BSD-style license that can be |
7543 // found in the LICENSE file. | 4403 // found in the LICENSE file. |
7544 | |
7545 Polymer({ | 4404 Polymer({ |
7546 is: 'cr-toolbar', | 4405 is: 'cr-toolbar', |
7547 | |
7548 properties: { | 4406 properties: { |
7549 // Name to display in the toolbar, in titlecase. | |
7550 pageName: String, | 4407 pageName: String, |
7551 | |
7552 // Prompt text to display in the search field. | |
7553 searchPrompt: String, | 4408 searchPrompt: String, |
7554 | |
7555 // Tooltip to display on the clear search button. | |
7556 clearLabel: String, | 4409 clearLabel: String, |
7557 | |
7558 // Tooltip to display on the menu button. | |
7559 menuLabel: String, | 4410 menuLabel: String, |
7560 | |
7561 // Value is proxied through to cr-toolbar-search-field. When true, | |
7562 // the search field will show a processing spinner. | |
7563 spinnerActive: Boolean, | 4411 spinnerActive: Boolean, |
7564 | |
7565 // Controls whether the menu button is shown at the start of the menu. | |
7566 showMenu: { | 4412 showMenu: { |
7567 type: Boolean, | 4413 type: Boolean, |
7568 reflectToAttribute: true, | 4414 reflectToAttribute: true, |
7569 value: true | 4415 value: true |
7570 }, | 4416 }, |
7571 | |
7572 /** @private */ | |
7573 narrow_: { | 4417 narrow_: { |
7574 type: Boolean, | 4418 type: Boolean, |
7575 reflectToAttribute: true | 4419 reflectToAttribute: true |
7576 }, | 4420 }, |
7577 | |
7578 /** @private */ | |
7579 showingSearch_: { | 4421 showingSearch_: { |
7580 type: Boolean, | 4422 type: Boolean, |
7581 reflectToAttribute: true, | 4423 reflectToAttribute: true |
7582 }, | 4424 } |
7583 }, | 4425 }, |
7584 | |
7585 /** @return {!CrToolbarSearchFieldElement} */ | |
7586 getSearchField: function() { | 4426 getSearchField: function() { |
7587 return this.$.search; | 4427 return this.$.search; |
7588 }, | 4428 }, |
7589 | |
7590 /** @private */ | |
7591 onMenuTap_: function(e) { | 4429 onMenuTap_: function(e) { |
7592 this.fire('cr-menu-tap'); | 4430 this.fire('cr-menu-tap'); |
7593 } | 4431 } |
7594 }); | 4432 }); |
| 4433 |
7595 // Copyright 2015 The Chromium Authors. All rights reserved. | 4434 // Copyright 2015 The Chromium Authors. All rights reserved. |
7596 // Use of this source code is governed by a BSD-style license that can be | 4435 // Use of this source code is governed by a BSD-style license that can be |
7597 // found in the LICENSE file. | 4436 // found in the LICENSE file. |
7598 | |
7599 Polymer({ | 4437 Polymer({ |
7600 is: 'history-toolbar', | 4438 is: 'history-toolbar', |
7601 properties: { | 4439 properties: { |
7602 // Number of history items currently selected. | |
7603 // TODO(calamity): bind this to | |
7604 // listContainer.selectedItem.selectedPaths.length. | |
7605 count: { | 4440 count: { |
7606 type: Number, | 4441 type: Number, |
7607 value: 0, | 4442 value: 0, |
7608 observer: 'changeToolbarView_' | 4443 observer: 'changeToolbarView_' |
7609 }, | 4444 }, |
7610 | |
7611 // True if 1 or more history items are selected. When this value changes | |
7612 // the background colour changes. | |
7613 itemsSelected_: { | 4445 itemsSelected_: { |
7614 type: Boolean, | 4446 type: Boolean, |
7615 value: false, | 4447 value: false, |
7616 reflectToAttribute: true | 4448 reflectToAttribute: true |
7617 }, | 4449 }, |
7618 | |
7619 // The most recent term entered in the search field. Updated incrementally | |
7620 // as the user types. | |
7621 searchTerm: { | 4450 searchTerm: { |
7622 type: String, | 4451 type: String, |
7623 notify: true, | 4452 notify: true |
7624 }, | 4453 }, |
7625 | |
7626 // True if the backend is processing and a spinner should be shown in the | |
7627 // toolbar. | |
7628 spinnerActive: { | 4454 spinnerActive: { |
7629 type: Boolean, | 4455 type: Boolean, |
7630 value: false | 4456 value: false |
7631 }, | 4457 }, |
7632 | |
7633 hasDrawer: { | 4458 hasDrawer: { |
7634 type: Boolean, | 4459 type: Boolean, |
7635 observer: 'hasDrawerChanged_', | 4460 observer: 'hasDrawerChanged_', |
7636 reflectToAttribute: true, | 4461 reflectToAttribute: true |
7637 }, | 4462 }, |
7638 | |
7639 // Whether domain-grouped history is enabled. | |
7640 isGroupedMode: { | 4463 isGroupedMode: { |
7641 type: Boolean, | 4464 type: Boolean, |
7642 reflectToAttribute: true, | 4465 reflectToAttribute: true |
7643 }, | 4466 }, |
7644 | |
7645 // The period to search over. Matches BrowsingHistoryHandler::Range. | |
7646 groupedRange: { | 4467 groupedRange: { |
7647 type: Number, | 4468 type: Number, |
7648 value: 0, | 4469 value: 0, |
7649 reflectToAttribute: true, | 4470 reflectToAttribute: true, |
7650 notify: true | 4471 notify: true |
7651 }, | 4472 }, |
7652 | |
7653 // The start time of the query range. | |
7654 queryStartTime: String, | 4473 queryStartTime: String, |
7655 | 4474 queryEndTime: String |
7656 // The end time of the query range. | 4475 }, |
7657 queryEndTime: String, | |
7658 }, | |
7659 | |
7660 /** | |
7661 * Changes the toolbar background color depending on whether any history items | |
7662 * are currently selected. | |
7663 * @private | |
7664 */ | |
7665 changeToolbarView_: function() { | 4476 changeToolbarView_: function() { |
7666 this.itemsSelected_ = this.count > 0; | 4477 this.itemsSelected_ = this.count > 0; |
7667 }, | 4478 }, |
7668 | |
7669 /** | |
7670 * When changing the search term externally, update the search field to | |
7671 * reflect the new search term. | |
7672 * @param {string} search | |
7673 */ | |
7674 setSearchTerm: function(search) { | 4479 setSearchTerm: function(search) { |
7675 if (this.searchTerm == search) | 4480 if (this.searchTerm == search) return; |
7676 return; | |
7677 | |
7678 this.searchTerm = search; | 4481 this.searchTerm = search; |
7679 var searchField = /** @type {!CrToolbarElement} */(this.$['main-toolbar']) | 4482 var searchField = this.$['main-toolbar'].getSearchField(); |
7680 .getSearchField(); | |
7681 searchField.showAndFocus(); | 4483 searchField.showAndFocus(); |
7682 searchField.setValue(search); | 4484 searchField.setValue(search); |
7683 }, | 4485 }, |
7684 | |
7685 /** | |
7686 * @param {!CustomEvent} event | |
7687 * @private | |
7688 */ | |
7689 onSearchChanged_: function(event) { | 4486 onSearchChanged_: function(event) { |
7690 this.searchTerm = /** @type {string} */ (event.detail); | 4487 this.searchTerm = event.detail; |
7691 }, | 4488 }, |
7692 | |
7693 onClearSelectionTap_: function() { | 4489 onClearSelectionTap_: function() { |
7694 this.fire('unselect-all'); | 4490 this.fire('unselect-all'); |
7695 }, | 4491 }, |
7696 | |
7697 onDeleteTap_: function() { | 4492 onDeleteTap_: function() { |
7698 this.fire('delete-selected'); | 4493 this.fire('delete-selected'); |
7699 }, | 4494 }, |
7700 | |
7701 get searchBar() { | 4495 get searchBar() { |
7702 return this.$['main-toolbar'].getSearchField(); | 4496 return this.$['main-toolbar'].getSearchField(); |
7703 }, | 4497 }, |
7704 | |
7705 showSearchField: function() { | 4498 showSearchField: function() { |
7706 /** @type {!CrToolbarElement} */(this.$['main-toolbar']) | 4499 this.$['main-toolbar'].getSearchField().showAndFocus(); |
7707 .getSearchField() | 4500 }, |
7708 .showAndFocus(); | |
7709 }, | |
7710 | |
7711 /** | |
7712 * If the user is a supervised user the delete button is not shown. | |
7713 * @private | |
7714 */ | |
7715 deletingAllowed_: function() { | 4501 deletingAllowed_: function() { |
7716 return loadTimeData.getBoolean('allowDeletingHistory'); | 4502 return loadTimeData.getBoolean('allowDeletingHistory'); |
7717 }, | 4503 }, |
7718 | |
7719 numberOfItemsSelected_: function(count) { | 4504 numberOfItemsSelected_: function(count) { |
7720 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : ''; | 4505 return count > 0 ? loadTimeData.getStringF('itemsSelected', count) : ''; |
7721 }, | 4506 }, |
7722 | |
7723 getHistoryInterval_: function(queryStartTime, queryEndTime) { | 4507 getHistoryInterval_: function(queryStartTime, queryEndTime) { |
7724 // TODO(calamity): Fix the format of these dates. | 4508 return loadTimeData.getStringF('historyInterval', queryStartTime, queryEndTi
me); |
7725 return loadTimeData.getStringF( | 4509 }, |
7726 'historyInterval', queryStartTime, queryEndTime); | |
7727 }, | |
7728 | |
7729 /** @private */ | |
7730 hasDrawerChanged_: function() { | 4510 hasDrawerChanged_: function() { |
7731 this.updateStyles(); | 4511 this.updateStyles(); |
7732 }, | 4512 } |
7733 }); | 4513 }); |
| 4514 |
7734 // Copyright 2016 The Chromium Authors. All rights reserved. | 4515 // Copyright 2016 The Chromium Authors. All rights reserved. |
7735 // Use of this source code is governed by a BSD-style license that can be | 4516 // Use of this source code is governed by a BSD-style license that can be |
7736 // found in the LICENSE file. | 4517 // found in the LICENSE file. |
7737 | |
7738 /** | |
7739 * @fileoverview 'cr-dialog' is a component for showing a modal dialog. If the | |
7740 * dialog is closed via close(), a 'close' event is fired. If the dialog is | |
7741 * canceled via cancel(), a 'cancel' event is fired followed by a 'close' event. | |
7742 * Additionally clients can inspect the dialog's |returnValue| property inside | |
7743 * the 'close' event listener to determine whether it was canceled or just | |
7744 * closed, where a truthy value means success, and a falsy value means it was | |
7745 * canceled. | |
7746 */ | |
7747 Polymer({ | 4518 Polymer({ |
7748 is: 'cr-dialog', | 4519 is: 'cr-dialog', |
7749 extends: 'dialog', | 4520 "extends": 'dialog', |
7750 | |
7751 /** @override */ | |
7752 created: function() { | 4521 created: function() { |
7753 // If the active history entry changes (i.e. user clicks back button), | |
7754 // all open dialogs should be cancelled. | |
7755 window.addEventListener('popstate', function() { | 4522 window.addEventListener('popstate', function() { |
7756 if (this.open) | 4523 if (this.open) this.cancel(); |
7757 this.cancel(); | |
7758 }.bind(this)); | 4524 }.bind(this)); |
7759 }, | 4525 }, |
7760 | |
7761 cancel: function() { | 4526 cancel: function() { |
7762 this.fire('cancel'); | 4527 this.fire('cancel'); |
7763 HTMLDialogElement.prototype.close.call(this, ''); | 4528 HTMLDialogElement.prototype.close.call(this, ''); |
7764 }, | 4529 }, |
7765 | |
7766 /** | |
7767 * @param {string=} opt_returnValue | |
7768 * @override | |
7769 */ | |
7770 close: function(opt_returnValue) { | 4530 close: function(opt_returnValue) { |
7771 HTMLDialogElement.prototype.close.call(this, 'success'); | 4531 HTMLDialogElement.prototype.close.call(this, 'success'); |
7772 }, | 4532 }, |
7773 | |
7774 /** @return {!PaperIconButtonElement} */ | |
7775 getCloseButton: function() { | 4533 getCloseButton: function() { |
7776 return this.$.close; | 4534 return this.$.close; |
7777 }, | 4535 } |
7778 }); | 4536 }); |
7779 /** | |
7780 `Polymer.IronFitBehavior` fits an element in another element using `max-height`
and `max-width`, and | |
7781 optionally centers it in the window or another element. | |
7782 | 4537 |
7783 The element will only be sized and/or positioned if it has not already been size
d and/or positioned | 4538 Polymer.IronFitBehavior = { |
7784 by CSS. | 4539 properties: { |
| 4540 sizingTarget: { |
| 4541 type: Object, |
| 4542 value: function() { |
| 4543 return this; |
| 4544 } |
| 4545 }, |
| 4546 fitInto: { |
| 4547 type: Object, |
| 4548 value: window |
| 4549 }, |
| 4550 noOverlap: { |
| 4551 type: Boolean |
| 4552 }, |
| 4553 positionTarget: { |
| 4554 type: Element |
| 4555 }, |
| 4556 horizontalAlign: { |
| 4557 type: String |
| 4558 }, |
| 4559 verticalAlign: { |
| 4560 type: String |
| 4561 }, |
| 4562 dynamicAlign: { |
| 4563 type: Boolean |
| 4564 }, |
| 4565 horizontalOffset: { |
| 4566 type: Number, |
| 4567 value: 0, |
| 4568 notify: true |
| 4569 }, |
| 4570 verticalOffset: { |
| 4571 type: Number, |
| 4572 value: 0, |
| 4573 notify: true |
| 4574 }, |
| 4575 autoFitOnAttach: { |
| 4576 type: Boolean, |
| 4577 value: false |
| 4578 }, |
| 4579 _fitInfo: { |
| 4580 type: Object |
| 4581 } |
| 4582 }, |
| 4583 get _fitWidth() { |
| 4584 var fitWidth; |
| 4585 if (this.fitInto === window) { |
| 4586 fitWidth = this.fitInto.innerWidth; |
| 4587 } else { |
| 4588 fitWidth = this.fitInto.getBoundingClientRect().width; |
| 4589 } |
| 4590 return fitWidth; |
| 4591 }, |
| 4592 get _fitHeight() { |
| 4593 var fitHeight; |
| 4594 if (this.fitInto === window) { |
| 4595 fitHeight = this.fitInto.innerHeight; |
| 4596 } else { |
| 4597 fitHeight = this.fitInto.getBoundingClientRect().height; |
| 4598 } |
| 4599 return fitHeight; |
| 4600 }, |
| 4601 get _fitLeft() { |
| 4602 var fitLeft; |
| 4603 if (this.fitInto === window) { |
| 4604 fitLeft = 0; |
| 4605 } else { |
| 4606 fitLeft = this.fitInto.getBoundingClientRect().left; |
| 4607 } |
| 4608 return fitLeft; |
| 4609 }, |
| 4610 get _fitTop() { |
| 4611 var fitTop; |
| 4612 if (this.fitInto === window) { |
| 4613 fitTop = 0; |
| 4614 } else { |
| 4615 fitTop = this.fitInto.getBoundingClientRect().top; |
| 4616 } |
| 4617 return fitTop; |
| 4618 }, |
| 4619 get _defaultPositionTarget() { |
| 4620 var parent = Polymer.dom(this).parentNode; |
| 4621 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
| 4622 parent = parent.host; |
| 4623 } |
| 4624 return parent; |
| 4625 }, |
| 4626 get _localeHorizontalAlign() { |
| 4627 if (this._isRTL) { |
| 4628 if (this.horizontalAlign === 'right') { |
| 4629 return 'left'; |
| 4630 } |
| 4631 if (this.horizontalAlign === 'left') { |
| 4632 return 'right'; |
| 4633 } |
| 4634 } |
| 4635 return this.horizontalAlign; |
| 4636 }, |
| 4637 attached: function() { |
| 4638 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; |
| 4639 this.positionTarget = this.positionTarget || this._defaultPositionTarget; |
| 4640 if (this.autoFitOnAttach) { |
| 4641 if (window.getComputedStyle(this).display === 'none') { |
| 4642 setTimeout(function() { |
| 4643 this.fit(); |
| 4644 }.bind(this)); |
| 4645 } else { |
| 4646 this.fit(); |
| 4647 } |
| 4648 } |
| 4649 }, |
| 4650 fit: function() { |
| 4651 this.position(); |
| 4652 this.constrain(); |
| 4653 this.center(); |
| 4654 }, |
| 4655 _discoverInfo: function() { |
| 4656 if (this._fitInfo) { |
| 4657 return; |
| 4658 } |
| 4659 var target = window.getComputedStyle(this); |
| 4660 var sizer = window.getComputedStyle(this.sizingTarget); |
| 4661 this._fitInfo = { |
| 4662 inlineStyle: { |
| 4663 top: this.style.top || '', |
| 4664 left: this.style.left || '', |
| 4665 position: this.style.position || '' |
| 4666 }, |
| 4667 sizerInlineStyle: { |
| 4668 maxWidth: this.sizingTarget.style.maxWidth || '', |
| 4669 maxHeight: this.sizingTarget.style.maxHeight || '', |
| 4670 boxSizing: this.sizingTarget.style.boxSizing || '' |
| 4671 }, |
| 4672 positionedBy: { |
| 4673 vertically: target.top !== 'auto' ? 'top' : target.bottom !== 'auto' ? '
bottom' : null, |
| 4674 horizontally: target.left !== 'auto' ? 'left' : target.right !== 'auto'
? 'right' : null |
| 4675 }, |
| 4676 sizedBy: { |
| 4677 height: sizer.maxHeight !== 'none', |
| 4678 width: sizer.maxWidth !== 'none', |
| 4679 minWidth: parseInt(sizer.minWidth, 10) || 0, |
| 4680 minHeight: parseInt(sizer.minHeight, 10) || 0 |
| 4681 }, |
| 4682 margin: { |
| 4683 top: parseInt(target.marginTop, 10) || 0, |
| 4684 right: parseInt(target.marginRight, 10) || 0, |
| 4685 bottom: parseInt(target.marginBottom, 10) || 0, |
| 4686 left: parseInt(target.marginLeft, 10) || 0 |
| 4687 } |
| 4688 }; |
| 4689 if (this.verticalOffset) { |
| 4690 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffs
et; |
| 4691 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; |
| 4692 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; |
| 4693 this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px
'; |
| 4694 } |
| 4695 if (this.horizontalOffset) { |
| 4696 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOf
fset; |
| 4697 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; |
| 4698 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; |
| 4699 this.style.marginLeft = this.style.marginRight = this.horizontalOffset + '
px'; |
| 4700 } |
| 4701 }, |
| 4702 resetFit: function() { |
| 4703 var info = this._fitInfo || {}; |
| 4704 for (var property in info.sizerInlineStyle) { |
| 4705 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; |
| 4706 } |
| 4707 for (var property in info.inlineStyle) { |
| 4708 this.style[property] = info.inlineStyle[property]; |
| 4709 } |
| 4710 this._fitInfo = null; |
| 4711 }, |
| 4712 refit: function() { |
| 4713 var scrollLeft = this.sizingTarget.scrollLeft; |
| 4714 var scrollTop = this.sizingTarget.scrollTop; |
| 4715 this.resetFit(); |
| 4716 this.fit(); |
| 4717 this.sizingTarget.scrollLeft = scrollLeft; |
| 4718 this.sizingTarget.scrollTop = scrollTop; |
| 4719 }, |
| 4720 position: function() { |
| 4721 if (!this.horizontalAlign && !this.verticalAlign) { |
| 4722 return; |
| 4723 } |
| 4724 this._discoverInfo(); |
| 4725 this.style.position = 'fixed'; |
| 4726 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4727 this.style.left = '0px'; |
| 4728 this.style.top = '0px'; |
| 4729 var rect = this.getBoundingClientRect(); |
| 4730 var positionRect = this.__getNormalizedRect(this.positionTarget); |
| 4731 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4732 var margin = this._fitInfo.margin; |
| 4733 var size = { |
| 4734 width: rect.width + margin.left + margin.right, |
| 4735 height: rect.height + margin.top + margin.bottom |
| 4736 }; |
| 4737 var position = this.__getPosition(this._localeHorizontalAlign, this.vertical
Align, size, positionRect, fitRect); |
| 4738 var left = position.left + margin.left; |
| 4739 var top = position.top + margin.top; |
| 4740 var right = Math.min(fitRect.right - margin.right, left + rect.width); |
| 4741 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); |
| 4742 var minWidth = this._fitInfo.sizedBy.minWidth; |
| 4743 var minHeight = this._fitInfo.sizedBy.minHeight; |
| 4744 if (left < margin.left) { |
| 4745 left = margin.left; |
| 4746 if (right - left < minWidth) { |
| 4747 left = right - minWidth; |
| 4748 } |
| 4749 } |
| 4750 if (top < margin.top) { |
| 4751 top = margin.top; |
| 4752 if (bottom - top < minHeight) { |
| 4753 top = bottom - minHeight; |
| 4754 } |
| 4755 } |
| 4756 this.sizingTarget.style.maxWidth = right - left + 'px'; |
| 4757 this.sizingTarget.style.maxHeight = bottom - top + 'px'; |
| 4758 this.style.left = left - rect.left + 'px'; |
| 4759 this.style.top = top - rect.top + 'px'; |
| 4760 }, |
| 4761 constrain: function() { |
| 4762 if (this.horizontalAlign || this.verticalAlign) { |
| 4763 return; |
| 4764 } |
| 4765 this._discoverInfo(); |
| 4766 var info = this._fitInfo; |
| 4767 if (!info.positionedBy.vertically) { |
| 4768 this.style.position = 'fixed'; |
| 4769 this.style.top = '0px'; |
| 4770 } |
| 4771 if (!info.positionedBy.horizontally) { |
| 4772 this.style.position = 'fixed'; |
| 4773 this.style.left = '0px'; |
| 4774 } |
| 4775 this.sizingTarget.style.boxSizing = 'border-box'; |
| 4776 var rect = this.getBoundingClientRect(); |
| 4777 if (!info.sizedBy.height) { |
| 4778 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom',
'Height'); |
| 4779 } |
| 4780 if (!info.sizedBy.width) { |
| 4781 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right'
, 'Width'); |
| 4782 } |
| 4783 }, |
| 4784 _sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4785 this.__sizeDimension(rect, positionedBy, start, end, extent); |
| 4786 }, |
| 4787 __sizeDimension: function(rect, positionedBy, start, end, extent) { |
| 4788 var info = this._fitInfo; |
| 4789 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4790 var max = extent === 'Width' ? fitRect.width : fitRect.height; |
| 4791 var flip = positionedBy === end; |
| 4792 var offset = flip ? max - rect[end] : rect[start]; |
| 4793 var margin = info.margin[flip ? start : end]; |
| 4794 var offsetExtent = 'offset' + extent; |
| 4795 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; |
| 4796 this.sizingTarget.style['max' + extent] = max - margin - offset - sizingOffs
et + 'px'; |
| 4797 }, |
| 4798 center: function() { |
| 4799 if (this.horizontalAlign || this.verticalAlign) { |
| 4800 return; |
| 4801 } |
| 4802 this._discoverInfo(); |
| 4803 var positionedBy = this._fitInfo.positionedBy; |
| 4804 if (positionedBy.vertically && positionedBy.horizontally) { |
| 4805 return; |
| 4806 } |
| 4807 this.style.position = 'fixed'; |
| 4808 if (!positionedBy.vertically) { |
| 4809 this.style.top = '0px'; |
| 4810 } |
| 4811 if (!positionedBy.horizontally) { |
| 4812 this.style.left = '0px'; |
| 4813 } |
| 4814 var rect = this.getBoundingClientRect(); |
| 4815 var fitRect = this.__getNormalizedRect(this.fitInto); |
| 4816 if (!positionedBy.vertically) { |
| 4817 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; |
| 4818 this.style.top = top + 'px'; |
| 4819 } |
| 4820 if (!positionedBy.horizontally) { |
| 4821 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; |
| 4822 this.style.left = left + 'px'; |
| 4823 } |
| 4824 }, |
| 4825 __getNormalizedRect: function(target) { |
| 4826 if (target === document.documentElement || target === window) { |
| 4827 return { |
| 4828 top: 0, |
| 4829 left: 0, |
| 4830 width: window.innerWidth, |
| 4831 height: window.innerHeight, |
| 4832 right: window.innerWidth, |
| 4833 bottom: window.innerHeight |
| 4834 }; |
| 4835 } |
| 4836 return target.getBoundingClientRect(); |
| 4837 }, |
| 4838 __getCroppedArea: function(position, size, fitRect) { |
| 4839 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom -
(position.top + size.height)); |
| 4840 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right
- (position.left + size.width)); |
| 4841 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size
.height; |
| 4842 }, |
| 4843 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { |
| 4844 var positions = [ { |
| 4845 verticalAlign: 'top', |
| 4846 horizontalAlign: 'left', |
| 4847 top: positionRect.top, |
| 4848 left: positionRect.left |
| 4849 }, { |
| 4850 verticalAlign: 'top', |
| 4851 horizontalAlign: 'right', |
| 4852 top: positionRect.top, |
| 4853 left: positionRect.right - size.width |
| 4854 }, { |
| 4855 verticalAlign: 'bottom', |
| 4856 horizontalAlign: 'left', |
| 4857 top: positionRect.bottom - size.height, |
| 4858 left: positionRect.left |
| 4859 }, { |
| 4860 verticalAlign: 'bottom', |
| 4861 horizontalAlign: 'right', |
| 4862 top: positionRect.bottom - size.height, |
| 4863 left: positionRect.right - size.width |
| 4864 } ]; |
| 4865 if (this.noOverlap) { |
| 4866 for (var i = 0, l = positions.length; i < l; i++) { |
| 4867 var copy = {}; |
| 4868 for (var key in positions[i]) { |
| 4869 copy[key] = positions[i][key]; |
| 4870 } |
| 4871 positions.push(copy); |
| 4872 } |
| 4873 positions[0].top = positions[1].top += positionRect.height; |
| 4874 positions[2].top = positions[3].top -= positionRect.height; |
| 4875 positions[4].left = positions[6].left += positionRect.width; |
| 4876 positions[5].left = positions[7].left -= positionRect.width; |
| 4877 } |
| 4878 vAlign = vAlign === 'auto' ? null : vAlign; |
| 4879 hAlign = hAlign === 'auto' ? null : hAlign; |
| 4880 var position; |
| 4881 for (var i = 0; i < positions.length; i++) { |
| 4882 var pos = positions[i]; |
| 4883 if (!this.dynamicAlign && !this.noOverlap && pos.verticalAlign === vAlign
&& pos.horizontalAlign === hAlign) { |
| 4884 position = pos; |
| 4885 break; |
| 4886 } |
| 4887 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && (!hAlign || pos
.horizontalAlign === hAlign); |
| 4888 if (!this.dynamicAlign && !alignOk) { |
| 4889 continue; |
| 4890 } |
| 4891 position = position || pos; |
| 4892 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); |
| 4893 var diff = pos.croppedArea - position.croppedArea; |
| 4894 if (diff < 0 || diff === 0 && alignOk) { |
| 4895 position = pos; |
| 4896 } |
| 4897 if (position.croppedArea === 0 && alignOk) { |
| 4898 break; |
| 4899 } |
| 4900 } |
| 4901 return position; |
| 4902 } |
| 4903 }; |
7785 | 4904 |
7786 CSS properties | Action | 4905 (function() { |
7787 -----------------------------|------------------------------------------- | 4906 'use strict'; |
7788 `position` set | Element is not centered horizontally or verticall
y | 4907 Polymer({ |
7789 `top` or `bottom` set | Element is not vertically centered | 4908 is: 'iron-overlay-backdrop', |
7790 `left` or `right` set | Element is not horizontally centered | |
7791 `max-height` set | Element respects `max-height` | |
7792 `max-width` set | Element respects `max-width` | |
7793 | |
7794 `Polymer.IronFitBehavior` can position an element into another element using | |
7795 `verticalAlign` and `horizontalAlign`. This will override the element's css posi
tion. | |
7796 | |
7797 <div class="container"> | |
7798 <iron-fit-impl vertical-align="top" horizontal-align="auto"> | |
7799 Positioned into the container | |
7800 </iron-fit-impl> | |
7801 </div> | |
7802 | |
7803 Use `noOverlap` to position the element around another element without overlappi
ng it. | |
7804 | |
7805 <div class="container"> | |
7806 <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto"> | |
7807 Positioned around the container | |
7808 </iron-fit-impl> | |
7809 </div> | |
7810 | |
7811 @demo demo/index.html | |
7812 @polymerBehavior | |
7813 */ | |
7814 | |
7815 Polymer.IronFitBehavior = { | |
7816 | |
7817 properties: { | 4909 properties: { |
7818 | |
7819 /** | |
7820 * The element that will receive a `max-height`/`width`. By default it is
the same as `this`, | |
7821 * but it can be set to a child element. This is useful, for example, for
implementing a | |
7822 * scrolling region inside the element. | |
7823 * @type {!Element} | |
7824 */ | |
7825 sizingTarget: { | |
7826 type: Object, | |
7827 value: function() { | |
7828 return this; | |
7829 } | |
7830 }, | |
7831 | |
7832 /** | |
7833 * The element to fit `this` into. | |
7834 */ | |
7835 fitInto: { | |
7836 type: Object, | |
7837 value: window | |
7838 }, | |
7839 | |
7840 /** | |
7841 * Will position the element around the positionTarget without overlapping
it. | |
7842 */ | |
7843 noOverlap: { | |
7844 type: Boolean | |
7845 }, | |
7846 | |
7847 /** | |
7848 * The element that should be used to position the element. If not set, it
will | |
7849 * default to the parent node. | |
7850 * @type {!Element} | |
7851 */ | |
7852 positionTarget: { | |
7853 type: Element | |
7854 }, | |
7855 | |
7856 /** | |
7857 * The orientation against which to align the element horizontally | |
7858 * relative to the `positionTarget`. Possible values are "left", "right",
"auto". | |
7859 */ | |
7860 horizontalAlign: { | |
7861 type: String | |
7862 }, | |
7863 | |
7864 /** | |
7865 * The orientation against which to align the element vertically | |
7866 * relative to the `positionTarget`. Possible values are "top", "bottom",
"auto". | |
7867 */ | |
7868 verticalAlign: { | |
7869 type: String | |
7870 }, | |
7871 | |
7872 /** | |
7873 * If true, it will use `horizontalAlign` and `verticalAlign` values as pr
eferred alignment | |
7874 * and if there's not enough space, it will pick the values which minimize
the cropping. | |
7875 */ | |
7876 dynamicAlign: { | |
7877 type: Boolean | |
7878 }, | |
7879 | |
7880 /** | |
7881 * The same as setting margin-left and margin-right css properties. | |
7882 * @deprecated | |
7883 */ | |
7884 horizontalOffset: { | |
7885 type: Number, | |
7886 value: 0, | |
7887 notify: true | |
7888 }, | |
7889 | |
7890 /** | |
7891 * The same as setting margin-top and margin-bottom css properties. | |
7892 * @deprecated | |
7893 */ | |
7894 verticalOffset: { | |
7895 type: Number, | |
7896 value: 0, | |
7897 notify: true | |
7898 }, | |
7899 | |
7900 /** | |
7901 * Set to true to auto-fit on attach. | |
7902 */ | |
7903 autoFitOnAttach: { | |
7904 type: Boolean, | |
7905 value: false | |
7906 }, | |
7907 | |
7908 /** @type {?Object} */ | |
7909 _fitInfo: { | |
7910 type: Object | |
7911 } | |
7912 }, | |
7913 | |
7914 get _fitWidth() { | |
7915 var fitWidth; | |
7916 if (this.fitInto === window) { | |
7917 fitWidth = this.fitInto.innerWidth; | |
7918 } else { | |
7919 fitWidth = this.fitInto.getBoundingClientRect().width; | |
7920 } | |
7921 return fitWidth; | |
7922 }, | |
7923 | |
7924 get _fitHeight() { | |
7925 var fitHeight; | |
7926 if (this.fitInto === window) { | |
7927 fitHeight = this.fitInto.innerHeight; | |
7928 } else { | |
7929 fitHeight = this.fitInto.getBoundingClientRect().height; | |
7930 } | |
7931 return fitHeight; | |
7932 }, | |
7933 | |
7934 get _fitLeft() { | |
7935 var fitLeft; | |
7936 if (this.fitInto === window) { | |
7937 fitLeft = 0; | |
7938 } else { | |
7939 fitLeft = this.fitInto.getBoundingClientRect().left; | |
7940 } | |
7941 return fitLeft; | |
7942 }, | |
7943 | |
7944 get _fitTop() { | |
7945 var fitTop; | |
7946 if (this.fitInto === window) { | |
7947 fitTop = 0; | |
7948 } else { | |
7949 fitTop = this.fitInto.getBoundingClientRect().top; | |
7950 } | |
7951 return fitTop; | |
7952 }, | |
7953 | |
7954 /** | |
7955 * The element that should be used to position the element, | |
7956 * if no position target is configured. | |
7957 */ | |
7958 get _defaultPositionTarget() { | |
7959 var parent = Polymer.dom(this).parentNode; | |
7960 | |
7961 if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { | |
7962 parent = parent.host; | |
7963 } | |
7964 | |
7965 return parent; | |
7966 }, | |
7967 | |
7968 /** | |
7969 * The horizontal align value, accounting for the RTL/LTR text direction. | |
7970 */ | |
7971 get _localeHorizontalAlign() { | |
7972 if (this._isRTL) { | |
7973 // In RTL, "left" becomes "right". | |
7974 if (this.horizontalAlign === 'right') { | |
7975 return 'left'; | |
7976 } | |
7977 if (this.horizontalAlign === 'left') { | |
7978 return 'right'; | |
7979 } | |
7980 } | |
7981 return this.horizontalAlign; | |
7982 }, | |
7983 | |
7984 attached: function() { | |
7985 // Memoize this to avoid expensive calculations & relayouts. | |
7986 this._isRTL = window.getComputedStyle(this).direction == 'rtl'; | |
7987 this.positionTarget = this.positionTarget || this._defaultPositionTarget; | |
7988 if (this.autoFitOnAttach) { | |
7989 if (window.getComputedStyle(this).display === 'none') { | |
7990 setTimeout(function() { | |
7991 this.fit(); | |
7992 }.bind(this)); | |
7993 } else { | |
7994 this.fit(); | |
7995 } | |
7996 } | |
7997 }, | |
7998 | |
7999 /** | |
8000 * Positions and fits the element into the `fitInto` element. | |
8001 */ | |
8002 fit: function() { | |
8003 this.position(); | |
8004 this.constrain(); | |
8005 this.center(); | |
8006 }, | |
8007 | |
8008 /** | |
8009 * Memoize information needed to position and size the target element. | |
8010 * @suppress {deprecated} | |
8011 */ | |
8012 _discoverInfo: function() { | |
8013 if (this._fitInfo) { | |
8014 return; | |
8015 } | |
8016 var target = window.getComputedStyle(this); | |
8017 var sizer = window.getComputedStyle(this.sizingTarget); | |
8018 | |
8019 this._fitInfo = { | |
8020 inlineStyle: { | |
8021 top: this.style.top || '', | |
8022 left: this.style.left || '', | |
8023 position: this.style.position || '' | |
8024 }, | |
8025 sizerInlineStyle: { | |
8026 maxWidth: this.sizingTarget.style.maxWidth || '', | |
8027 maxHeight: this.sizingTarget.style.maxHeight || '', | |
8028 boxSizing: this.sizingTarget.style.boxSizing || '' | |
8029 }, | |
8030 positionedBy: { | |
8031 vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto'
? | |
8032 'bottom' : null), | |
8033 horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'aut
o' ? | |
8034 'right' : null) | |
8035 }, | |
8036 sizedBy: { | |
8037 height: sizer.maxHeight !== 'none', | |
8038 width: sizer.maxWidth !== 'none', | |
8039 minWidth: parseInt(sizer.minWidth, 10) || 0, | |
8040 minHeight: parseInt(sizer.minHeight, 10) || 0 | |
8041 }, | |
8042 margin: { | |
8043 top: parseInt(target.marginTop, 10) || 0, | |
8044 right: parseInt(target.marginRight, 10) || 0, | |
8045 bottom: parseInt(target.marginBottom, 10) || 0, | |
8046 left: parseInt(target.marginLeft, 10) || 0 | |
8047 } | |
8048 }; | |
8049 | |
8050 // Support these properties until they are removed. | |
8051 if (this.verticalOffset) { | |
8052 this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOf
fset; | |
8053 this._fitInfo.inlineStyle.marginTop = this.style.marginTop || ''; | |
8054 this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || ''; | |
8055 this.style.marginTop = this.style.marginBottom = this.verticalOffset + '
px'; | |
8056 } | |
8057 if (this.horizontalOffset) { | |
8058 this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontal
Offset; | |
8059 this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || ''; | |
8060 this._fitInfo.inlineStyle.marginRight = this.style.marginRight || ''; | |
8061 this.style.marginLeft = this.style.marginRight = this.horizontalOffset +
'px'; | |
8062 } | |
8063 }, | |
8064 | |
8065 /** | |
8066 * Resets the target element's position and size constraints, and clear | |
8067 * the memoized data. | |
8068 */ | |
8069 resetFit: function() { | |
8070 var info = this._fitInfo || {}; | |
8071 for (var property in info.sizerInlineStyle) { | |
8072 this.sizingTarget.style[property] = info.sizerInlineStyle[property]; | |
8073 } | |
8074 for (var property in info.inlineStyle) { | |
8075 this.style[property] = info.inlineStyle[property]; | |
8076 } | |
8077 | |
8078 this._fitInfo = null; | |
8079 }, | |
8080 | |
8081 /** | |
8082 * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after | |
8083 * the element or the `fitInto` element has been resized, or if any of the | |
8084 * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated
. | |
8085 * It preserves the scroll position of the sizingTarget. | |
8086 */ | |
8087 refit: function() { | |
8088 var scrollLeft = this.sizingTarget.scrollLeft; | |
8089 var scrollTop = this.sizingTarget.scrollTop; | |
8090 this.resetFit(); | |
8091 this.fit(); | |
8092 this.sizingTarget.scrollLeft = scrollLeft; | |
8093 this.sizingTarget.scrollTop = scrollTop; | |
8094 }, | |
8095 | |
8096 /** | |
8097 * Positions the element according to `horizontalAlign, verticalAlign`. | |
8098 */ | |
8099 position: function() { | |
8100 if (!this.horizontalAlign && !this.verticalAlign) { | |
8101 // needs to be centered, and it is done after constrain. | |
8102 return; | |
8103 } | |
8104 this._discoverInfo(); | |
8105 | |
8106 this.style.position = 'fixed'; | |
8107 // Need border-box for margin/padding. | |
8108 this.sizingTarget.style.boxSizing = 'border-box'; | |
8109 // Set to 0, 0 in order to discover any offset caused by parent stacking c
ontexts. | |
8110 this.style.left = '0px'; | |
8111 this.style.top = '0px'; | |
8112 | |
8113 var rect = this.getBoundingClientRect(); | |
8114 var positionRect = this.__getNormalizedRect(this.positionTarget); | |
8115 var fitRect = this.__getNormalizedRect(this.fitInto); | |
8116 | |
8117 var margin = this._fitInfo.margin; | |
8118 | |
8119 // Consider the margin as part of the size for position calculations. | |
8120 var size = { | |
8121 width: rect.width + margin.left + margin.right, | |
8122 height: rect.height + margin.top + margin.bottom | |
8123 }; | |
8124 | |
8125 var position = this.__getPosition(this._localeHorizontalAlign, this.vertic
alAlign, size, positionRect, fitRect); | |
8126 | |
8127 var left = position.left + margin.left; | |
8128 var top = position.top + margin.top; | |
8129 | |
8130 // Use original size (without margin). | |
8131 var right = Math.min(fitRect.right - margin.right, left + rect.width); | |
8132 var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); | |
8133 | |
8134 var minWidth = this._fitInfo.sizedBy.minWidth; | |
8135 var minHeight = this._fitInfo.sizedBy.minHeight; | |
8136 if (left < margin.left) { | |
8137 left = margin.left; | |
8138 if (right - left < minWidth) { | |
8139 left = right - minWidth; | |
8140 } | |
8141 } | |
8142 if (top < margin.top) { | |
8143 top = margin.top; | |
8144 if (bottom - top < minHeight) { | |
8145 top = bottom - minHeight; | |
8146 } | |
8147 } | |
8148 | |
8149 this.sizingTarget.style.maxWidth = (right - left) + 'px'; | |
8150 this.sizingTarget.style.maxHeight = (bottom - top) + 'px'; | |
8151 | |
8152 // Remove the offset caused by any stacking context. | |
8153 this.style.left = (left - rect.left) + 'px'; | |
8154 this.style.top = (top - rect.top) + 'px'; | |
8155 }, | |
8156 | |
8157 /** | |
8158 * Constrains the size of the element to `fitInto` by setting `max-height` | |
8159 * and/or `max-width`. | |
8160 */ | |
8161 constrain: function() { | |
8162 if (this.horizontalAlign || this.verticalAlign) { | |
8163 return; | |
8164 } | |
8165 this._discoverInfo(); | |
8166 | |
8167 var info = this._fitInfo; | |
8168 // position at (0px, 0px) if not already positioned, so we can measure the
natural size. | |
8169 if (!info.positionedBy.vertically) { | |
8170 this.style.position = 'fixed'; | |
8171 this.style.top = '0px'; | |
8172 } | |
8173 if (!info.positionedBy.horizontally) { | |
8174 this.style.position = 'fixed'; | |
8175 this.style.left = '0px'; | |
8176 } | |
8177 | |
8178 // need border-box for margin/padding | |
8179 this.sizingTarget.style.boxSizing = 'border-box'; | |
8180 // constrain the width and height if not already set | |
8181 var rect = this.getBoundingClientRect(); | |
8182 if (!info.sizedBy.height) { | |
8183 this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom'
, 'Height'); | |
8184 } | |
8185 if (!info.sizedBy.width) { | |
8186 this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'righ
t', 'Width'); | |
8187 } | |
8188 }, | |
8189 | |
8190 /** | |
8191 * @protected | |
8192 * @deprecated | |
8193 */ | |
8194 _sizeDimension: function(rect, positionedBy, start, end, extent) { | |
8195 this.__sizeDimension(rect, positionedBy, start, end, extent); | |
8196 }, | |
8197 | |
8198 /** | |
8199 * @private | |
8200 */ | |
8201 __sizeDimension: function(rect, positionedBy, start, end, extent) { | |
8202 var info = this._fitInfo; | |
8203 var fitRect = this.__getNormalizedRect(this.fitInto); | |
8204 var max = extent === 'Width' ? fitRect.width : fitRect.height; | |
8205 var flip = (positionedBy === end); | |
8206 var offset = flip ? max - rect[end] : rect[start]; | |
8207 var margin = info.margin[flip ? start : end]; | |
8208 var offsetExtent = 'offset' + extent; | |
8209 var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent]; | |
8210 this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingO
ffset) + 'px'; | |
8211 }, | |
8212 | |
8213 /** | |
8214 * Centers horizontally and vertically if not already positioned. This also
sets | |
8215 * `position:fixed`. | |
8216 */ | |
8217 center: function() { | |
8218 if (this.horizontalAlign || this.verticalAlign) { | |
8219 return; | |
8220 } | |
8221 this._discoverInfo(); | |
8222 | |
8223 var positionedBy = this._fitInfo.positionedBy; | |
8224 if (positionedBy.vertically && positionedBy.horizontally) { | |
8225 // Already positioned. | |
8226 return; | |
8227 } | |
8228 // Need position:fixed to center | |
8229 this.style.position = 'fixed'; | |
8230 // Take into account the offset caused by parents that create stacking | |
8231 // contexts (e.g. with transform: translate3d). Translate to 0,0 and | |
8232 // measure the bounding rect. | |
8233 if (!positionedBy.vertically) { | |
8234 this.style.top = '0px'; | |
8235 } | |
8236 if (!positionedBy.horizontally) { | |
8237 this.style.left = '0px'; | |
8238 } | |
8239 // It will take in consideration margins and transforms | |
8240 var rect = this.getBoundingClientRect(); | |
8241 var fitRect = this.__getNormalizedRect(this.fitInto); | |
8242 if (!positionedBy.vertically) { | |
8243 var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; | |
8244 this.style.top = top + 'px'; | |
8245 } | |
8246 if (!positionedBy.horizontally) { | |
8247 var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2; | |
8248 this.style.left = left + 'px'; | |
8249 } | |
8250 }, | |
8251 | |
8252 __getNormalizedRect: function(target) { | |
8253 if (target === document.documentElement || target === window) { | |
8254 return { | |
8255 top: 0, | |
8256 left: 0, | |
8257 width: window.innerWidth, | |
8258 height: window.innerHeight, | |
8259 right: window.innerWidth, | |
8260 bottom: window.innerHeight | |
8261 }; | |
8262 } | |
8263 return target.getBoundingClientRect(); | |
8264 }, | |
8265 | |
8266 __getCroppedArea: function(position, size, fitRect) { | |
8267 var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom
- (position.top + size.height)); | |
8268 var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.righ
t - (position.left + size.width)); | |
8269 return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * si
ze.height; | |
8270 }, | |
8271 | |
8272 | |
8273 __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { | |
8274 // All the possible configurations. | |
8275 // Ordered as top-left, top-right, bottom-left, bottom-right. | |
8276 var positions = [{ | |
8277 verticalAlign: 'top', | |
8278 horizontalAlign: 'left', | |
8279 top: positionRect.top, | |
8280 left: positionRect.left | |
8281 }, { | |
8282 verticalAlign: 'top', | |
8283 horizontalAlign: 'right', | |
8284 top: positionRect.top, | |
8285 left: positionRect.right - size.width | |
8286 }, { | |
8287 verticalAlign: 'bottom', | |
8288 horizontalAlign: 'left', | |
8289 top: positionRect.bottom - size.height, | |
8290 left: positionRect.left | |
8291 }, { | |
8292 verticalAlign: 'bottom', | |
8293 horizontalAlign: 'right', | |
8294 top: positionRect.bottom - size.height, | |
8295 left: positionRect.right - size.width | |
8296 }]; | |
8297 | |
8298 if (this.noOverlap) { | |
8299 // Duplicate. | |
8300 for (var i = 0, l = positions.length; i < l; i++) { | |
8301 var copy = {}; | |
8302 for (var key in positions[i]) { | |
8303 copy[key] = positions[i][key]; | |
8304 } | |
8305 positions.push(copy); | |
8306 } | |
8307 // Horizontal overlap only. | |
8308 positions[0].top = positions[1].top += positionRect.height; | |
8309 positions[2].top = positions[3].top -= positionRect.height; | |
8310 // Vertical overlap only. | |
8311 positions[4].left = positions[6].left += positionRect.width; | |
8312 positions[5].left = positions[7].left -= positionRect.width; | |
8313 } | |
8314 | |
8315 // Consider auto as null for coding convenience. | |
8316 vAlign = vAlign === 'auto' ? null : vAlign; | |
8317 hAlign = hAlign === 'auto' ? null : hAlign; | |
8318 | |
8319 var position; | |
8320 for (var i = 0; i < positions.length; i++) { | |
8321 var pos = positions[i]; | |
8322 | |
8323 // If both vAlign and hAlign are defined, return exact match. | |
8324 // For dynamicAlign and noOverlap we'll have more than one candidate, so | |
8325 // we'll have to check the croppedArea to make the best choice. | |
8326 if (!this.dynamicAlign && !this.noOverlap && | |
8327 pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { | |
8328 position = pos; | |
8329 break; | |
8330 } | |
8331 | |
8332 // Align is ok if alignment preferences are respected. If no preferences
, | |
8333 // it is considered ok. | |
8334 var alignOk = (!vAlign || pos.verticalAlign === vAlign) && | |
8335 (!hAlign || pos.horizontalAlign === hAlign); | |
8336 | |
8337 // Filter out elements that don't match the alignment (if defined). | |
8338 // With dynamicAlign, we need to consider all the positions to find the | |
8339 // one that minimizes the cropped area. | |
8340 if (!this.dynamicAlign && !alignOk) { | |
8341 continue; | |
8342 } | |
8343 | |
8344 position = position || pos; | |
8345 pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); | |
8346 var diff = pos.croppedArea - position.croppedArea; | |
8347 // Check which crops less. If it crops equally, check if align is ok. | |
8348 if (diff < 0 || (diff === 0 && alignOk)) { | |
8349 position = pos; | |
8350 } | |
8351 // If not cropped and respects the align requirements, keep it. | |
8352 // This allows to prefer positions overlapping horizontally over the | |
8353 // ones overlapping vertically. | |
8354 if (position.croppedArea === 0 && alignOk) { | |
8355 break; | |
8356 } | |
8357 } | |
8358 | |
8359 return position; | |
8360 } | |
8361 | |
8362 }; | |
8363 (function() { | |
8364 'use strict'; | |
8365 | |
8366 Polymer({ | |
8367 | |
8368 is: 'iron-overlay-backdrop', | |
8369 | |
8370 properties: { | |
8371 | |
8372 /** | |
8373 * Returns true if the backdrop is opened. | |
8374 */ | |
8375 opened: { | 4910 opened: { |
8376 reflectToAttribute: true, | 4911 reflectToAttribute: true, |
8377 type: Boolean, | 4912 type: Boolean, |
8378 value: false, | 4913 value: false, |
8379 observer: '_openedChanged' | 4914 observer: '_openedChanged' |
8380 } | 4915 } |
8381 | 4916 }, |
8382 }, | |
8383 | |
8384 listeners: { | 4917 listeners: { |
8385 'transitionend': '_onTransitionend' | 4918 transitionend: '_onTransitionend' |
8386 }, | 4919 }, |
8387 | |
8388 created: function() { | 4920 created: function() { |
8389 // Used to cancel previous requestAnimationFrame calls when opened changes
. | |
8390 this.__openedRaf = null; | 4921 this.__openedRaf = null; |
8391 }, | 4922 }, |
8392 | |
8393 attached: function() { | 4923 attached: function() { |
8394 this.opened && this._openedChanged(this.opened); | 4924 this.opened && this._openedChanged(this.opened); |
8395 }, | 4925 }, |
8396 | |
8397 /** | |
8398 * Appends the backdrop to document body if needed. | |
8399 */ | |
8400 prepare: function() { | 4926 prepare: function() { |
8401 if (this.opened && !this.parentNode) { | 4927 if (this.opened && !this.parentNode) { |
8402 Polymer.dom(document.body).appendChild(this); | 4928 Polymer.dom(document.body).appendChild(this); |
8403 } | 4929 } |
8404 }, | 4930 }, |
8405 | |
8406 /** | |
8407 * Shows the backdrop. | |
8408 */ | |
8409 open: function() { | 4931 open: function() { |
8410 this.opened = true; | 4932 this.opened = true; |
8411 }, | 4933 }, |
8412 | |
8413 /** | |
8414 * Hides the backdrop. | |
8415 */ | |
8416 close: function() { | 4934 close: function() { |
8417 this.opened = false; | 4935 this.opened = false; |
8418 }, | 4936 }, |
8419 | |
8420 /** | |
8421 * Removes the backdrop from document body if needed. | |
8422 */ | |
8423 complete: function() { | 4937 complete: function() { |
8424 if (!this.opened && this.parentNode === document.body) { | 4938 if (!this.opened && this.parentNode === document.body) { |
8425 Polymer.dom(this.parentNode).removeChild(this); | 4939 Polymer.dom(this.parentNode).removeChild(this); |
8426 } | 4940 } |
8427 }, | 4941 }, |
8428 | |
8429 _onTransitionend: function(event) { | 4942 _onTransitionend: function(event) { |
8430 if (event && event.target === this) { | 4943 if (event && event.target === this) { |
8431 this.complete(); | 4944 this.complete(); |
8432 } | 4945 } |
8433 }, | 4946 }, |
8434 | |
8435 /** | |
8436 * @param {boolean} opened | |
8437 * @private | |
8438 */ | |
8439 _openedChanged: function(opened) { | 4947 _openedChanged: function(opened) { |
8440 if (opened) { | 4948 if (opened) { |
8441 // Auto-attach. | |
8442 this.prepare(); | 4949 this.prepare(); |
8443 } else { | 4950 } else { |
8444 // Animation might be disabled via the mixin or opacity custom property. | |
8445 // If it is disabled in other ways, it's up to the user to call complete
. | |
8446 var cs = window.getComputedStyle(this); | 4951 var cs = window.getComputedStyle(this); |
8447 if (cs.transitionDuration === '0s' || cs.opacity == 0) { | 4952 if (cs.transitionDuration === '0s' || cs.opacity == 0) { |
8448 this.complete(); | 4953 this.complete(); |
8449 } | 4954 } |
8450 } | 4955 } |
8451 | |
8452 if (!this.isAttached) { | 4956 if (!this.isAttached) { |
8453 return; | 4957 return; |
8454 } | 4958 } |
8455 | |
8456 // Always cancel previous requestAnimationFrame. | |
8457 if (this.__openedRaf) { | 4959 if (this.__openedRaf) { |
8458 window.cancelAnimationFrame(this.__openedRaf); | 4960 window.cancelAnimationFrame(this.__openedRaf); |
8459 this.__openedRaf = null; | 4961 this.__openedRaf = null; |
8460 } | 4962 } |
8461 // Force relayout to ensure proper transitions. | |
8462 this.scrollTop = this.scrollTop; | 4963 this.scrollTop = this.scrollTop; |
8463 this.__openedRaf = window.requestAnimationFrame(function() { | 4964 this.__openedRaf = window.requestAnimationFrame(function() { |
8464 this.__openedRaf = null; | 4965 this.__openedRaf = null; |
8465 this.toggleClass('opened', this.opened); | 4966 this.toggleClass('opened', this.opened); |
8466 }.bind(this)); | 4967 }.bind(this)); |
8467 } | 4968 } |
8468 }); | 4969 }); |
8469 | |
8470 })(); | 4970 })(); |
8471 /** | 4971 |
8472 * @struct | 4972 Polymer.IronOverlayManagerClass = function() { |
8473 * @constructor | 4973 this._overlays = []; |
8474 * @private | 4974 this._minimumZ = 101; |
8475 */ | 4975 this._backdropElement = null; |
8476 Polymer.IronOverlayManagerClass = function() { | 4976 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); |
8477 /** | 4977 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); |
8478 * Used to keep track of the opened overlays. | 4978 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true); |
8479 * @private {Array<Element>} | 4979 }; |
8480 */ | 4980 |
8481 this._overlays = []; | 4981 Polymer.IronOverlayManagerClass.prototype = { |
8482 | 4982 constructor: Polymer.IronOverlayManagerClass, |
8483 /** | 4983 get backdropElement() { |
8484 * iframes have a default z-index of 100, | 4984 if (!this._backdropElement) { |
8485 * so this default should be at least that. | 4985 this._backdropElement = document.createElement('iron-overlay-backdrop'); |
8486 * @private {number} | 4986 } |
8487 */ | 4987 return this._backdropElement; |
8488 this._minimumZ = 101; | 4988 }, |
8489 | 4989 get deepActiveElement() { |
8490 /** | 4990 var active = document.activeElement || document.body; |
8491 * Memoized backdrop element. | 4991 while (active.root && Polymer.dom(active.root).activeElement) { |
8492 * @private {Element|null} | 4992 active = Polymer.dom(active.root).activeElement; |
8493 */ | 4993 } |
8494 this._backdropElement = null; | 4994 return active; |
8495 | 4995 }, |
8496 // Enable document-wide tap recognizer. | 4996 _bringOverlayAtIndexToFront: function(i) { |
8497 Polymer.Gestures.add(document, 'tap', this._onCaptureClick.bind(this)); | 4997 var overlay = this._overlays[i]; |
8498 | 4998 if (!overlay) { |
8499 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); | 4999 return; |
8500 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true
); | 5000 } |
8501 }; | 5001 var lastI = this._overlays.length - 1; |
8502 | 5002 var currentOverlay = this._overlays[lastI]; |
8503 Polymer.IronOverlayManagerClass.prototype = { | 5003 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
8504 | 5004 lastI--; |
8505 constructor: Polymer.IronOverlayManagerClass, | 5005 } |
8506 | 5006 if (i >= lastI) { |
8507 /** | 5007 return; |
8508 * The shared backdrop element. | 5008 } |
8509 * @type {!Element} backdropElement | 5009 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); |
8510 */ | 5010 if (this._getZ(overlay) <= minimumZ) { |
8511 get backdropElement() { | 5011 this._applyOverlayZ(overlay, minimumZ); |
8512 if (!this._backdropElement) { | 5012 } |
8513 this._backdropElement = document.createElement('iron-overlay-backdrop'); | 5013 while (i < lastI) { |
8514 } | 5014 this._overlays[i] = this._overlays[i + 1]; |
8515 return this._backdropElement; | 5015 i++; |
8516 }, | 5016 } |
8517 | 5017 this._overlays[lastI] = overlay; |
8518 /** | 5018 }, |
8519 * The deepest active element. | 5019 addOrRemoveOverlay: function(overlay) { |
8520 * @type {!Element} activeElement the active element | 5020 if (overlay.opened) { |
8521 */ | 5021 this.addOverlay(overlay); |
8522 get deepActiveElement() { | 5022 } else { |
8523 // document.activeElement can be null | 5023 this.removeOverlay(overlay); |
8524 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement | 5024 } |
8525 // In case of null, default it to document.body. | 5025 }, |
8526 var active = document.activeElement || document.body; | 5026 addOverlay: function(overlay) { |
8527 while (active.root && Polymer.dom(active.root).activeElement) { | 5027 var i = this._overlays.indexOf(overlay); |
8528 active = Polymer.dom(active.root).activeElement; | 5028 if (i >= 0) { |
8529 } | 5029 this._bringOverlayAtIndexToFront(i); |
8530 return active; | |
8531 }, | |
8532 | |
8533 /** | |
8534 * Brings the overlay at the specified index to the front. | |
8535 * @param {number} i | |
8536 * @private | |
8537 */ | |
8538 _bringOverlayAtIndexToFront: function(i) { | |
8539 var overlay = this._overlays[i]; | |
8540 if (!overlay) { | |
8541 return; | |
8542 } | |
8543 var lastI = this._overlays.length - 1; | |
8544 var currentOverlay = this._overlays[lastI]; | |
8545 // Ensure always-on-top overlay stays on top. | |
8546 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
8547 lastI--; | |
8548 } | |
8549 // If already the top element, return. | |
8550 if (i >= lastI) { | |
8551 return; | |
8552 } | |
8553 // Update z-index to be on top. | |
8554 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); | |
8555 if (this._getZ(overlay) <= minimumZ) { | |
8556 this._applyOverlayZ(overlay, minimumZ); | |
8557 } | |
8558 | |
8559 // Shift other overlays behind the new on top. | |
8560 while (i < lastI) { | |
8561 this._overlays[i] = this._overlays[i + 1]; | |
8562 i++; | |
8563 } | |
8564 this._overlays[lastI] = overlay; | |
8565 }, | |
8566 | |
8567 /** | |
8568 * Adds the overlay and updates its z-index if it's opened, or removes it if
it's closed. | |
8569 * Also updates the backdrop z-index. | |
8570 * @param {!Element} overlay | |
8571 */ | |
8572 addOrRemoveOverlay: function(overlay) { | |
8573 if (overlay.opened) { | |
8574 this.addOverlay(overlay); | |
8575 } else { | |
8576 this.removeOverlay(overlay); | |
8577 } | |
8578 }, | |
8579 | |
8580 /** | |
8581 * Tracks overlays for z-index and focus management. | |
8582 * Ensures the last added overlay with always-on-top remains on top. | |
8583 * @param {!Element} overlay | |
8584 */ | |
8585 addOverlay: function(overlay) { | |
8586 var i = this._overlays.indexOf(overlay); | |
8587 if (i >= 0) { | |
8588 this._bringOverlayAtIndexToFront(i); | |
8589 this.trackBackdrop(); | |
8590 return; | |
8591 } | |
8592 var insertionIndex = this._overlays.length; | |
8593 var currentOverlay = this._overlays[insertionIndex - 1]; | |
8594 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); | |
8595 var newZ = this._getZ(overlay); | |
8596 | |
8597 // Ensure always-on-top overlay stays on top. | |
8598 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)
) { | |
8599 // This bumps the z-index of +2. | |
8600 this._applyOverlayZ(currentOverlay, minimumZ); | |
8601 insertionIndex--; | |
8602 // Update minimumZ to match previous overlay's z-index. | |
8603 var previousOverlay = this._overlays[insertionIndex - 1]; | |
8604 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); | |
8605 } | |
8606 | |
8607 // Update z-index and insert overlay. | |
8608 if (newZ <= minimumZ) { | |
8609 this._applyOverlayZ(overlay, minimumZ); | |
8610 } | |
8611 this._overlays.splice(insertionIndex, 0, overlay); | |
8612 | |
8613 this.trackBackdrop(); | 5030 this.trackBackdrop(); |
8614 }, | 5031 return; |
8615 | 5032 } |
8616 /** | 5033 var insertionIndex = this._overlays.length; |
8617 * @param {!Element} overlay | 5034 var currentOverlay = this._overlays[insertionIndex - 1]; |
8618 */ | 5035 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); |
8619 removeOverlay: function(overlay) { | 5036 var newZ = this._getZ(overlay); |
8620 var i = this._overlays.indexOf(overlay); | 5037 if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay))
{ |
8621 if (i === -1) { | 5038 this._applyOverlayZ(currentOverlay, minimumZ); |
8622 return; | 5039 insertionIndex--; |
8623 } | 5040 var previousOverlay = this._overlays[insertionIndex - 1]; |
8624 this._overlays.splice(i, 1); | 5041 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); |
8625 | 5042 } |
8626 this.trackBackdrop(); | 5043 if (newZ <= minimumZ) { |
8627 }, | 5044 this._applyOverlayZ(overlay, minimumZ); |
8628 | 5045 } |
8629 /** | 5046 this._overlays.splice(insertionIndex, 0, overlay); |
8630 * Returns the current overlay. | 5047 this.trackBackdrop(); |
8631 * @return {Element|undefined} | 5048 }, |
8632 */ | 5049 removeOverlay: function(overlay) { |
8633 currentOverlay: function() { | 5050 var i = this._overlays.indexOf(overlay); |
8634 var i = this._overlays.length - 1; | 5051 if (i === -1) { |
8635 return this._overlays[i]; | 5052 return; |
8636 }, | 5053 } |
8637 | 5054 this._overlays.splice(i, 1); |
8638 /** | 5055 this.trackBackdrop(); |
8639 * Returns the current overlay z-index. | 5056 }, |
8640 * @return {number} | 5057 currentOverlay: function() { |
8641 */ | 5058 var i = this._overlays.length - 1; |
8642 currentOverlayZ: function() { | 5059 return this._overlays[i]; |
8643 return this._getZ(this.currentOverlay()); | 5060 }, |
8644 }, | 5061 currentOverlayZ: function() { |
8645 | 5062 return this._getZ(this.currentOverlay()); |
8646 /** | 5063 }, |
8647 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. | 5064 ensureMinimumZ: function(minimumZ) { |
8648 * This does not effect the z-index of any existing overlays. | 5065 this._minimumZ = Math.max(this._minimumZ, minimumZ); |
8649 * @param {number} minimumZ | 5066 }, |
8650 */ | 5067 focusOverlay: function() { |
8651 ensureMinimumZ: function(minimumZ) { | 5068 var current = this.currentOverlay(); |
8652 this._minimumZ = Math.max(this._minimumZ, minimumZ); | 5069 if (current) { |
8653 }, | 5070 current._applyFocus(); |
8654 | 5071 } |
8655 focusOverlay: function() { | 5072 }, |
8656 var current = /** @type {?} */ (this.currentOverlay()); | 5073 trackBackdrop: function() { |
8657 if (current) { | 5074 var overlay = this._overlayWithBackdrop(); |
8658 current._applyFocus(); | 5075 if (!overlay && !this._backdropElement) { |
8659 } | 5076 return; |
8660 }, | 5077 } |
8661 | 5078 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; |
8662 /** | 5079 this.backdropElement.opened = !!overlay; |
8663 * Updates the backdrop z-index. | 5080 }, |
8664 */ | 5081 getBackdrops: function() { |
8665 trackBackdrop: function() { | 5082 var backdrops = []; |
8666 var overlay = this._overlayWithBackdrop(); | 5083 for (var i = 0; i < this._overlays.length; i++) { |
8667 // Avoid creating the backdrop if there is no overlay with backdrop. | 5084 if (this._overlays[i].withBackdrop) { |
8668 if (!overlay && !this._backdropElement) { | 5085 backdrops.push(this._overlays[i]); |
8669 return; | 5086 } |
8670 } | 5087 } |
8671 this.backdropElement.style.zIndex = this._getZ(overlay) - 1; | 5088 return backdrops; |
8672 this.backdropElement.opened = !!overlay; | 5089 }, |
8673 }, | 5090 backdropZ: function() { |
8674 | 5091 return this._getZ(this._overlayWithBackdrop()) - 1; |
8675 /** | 5092 }, |
8676 * @return {Array<Element>} | 5093 _overlayWithBackdrop: function() { |
8677 */ | 5094 for (var i = 0; i < this._overlays.length; i++) { |
8678 getBackdrops: function() { | 5095 if (this._overlays[i].withBackdrop) { |
8679 var backdrops = []; | 5096 return this._overlays[i]; |
8680 for (var i = 0; i < this._overlays.length; i++) { | 5097 } |
8681 if (this._overlays[i].withBackdrop) { | 5098 } |
8682 backdrops.push(this._overlays[i]); | 5099 }, |
8683 } | 5100 _getZ: function(overlay) { |
8684 } | 5101 var z = this._minimumZ; |
8685 return backdrops; | 5102 if (overlay) { |
8686 }, | 5103 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).z
Index); |
8687 | 5104 if (z1 === z1) { |
8688 /** | 5105 z = z1; |
8689 * Returns the z-index for the backdrop. | 5106 } |
8690 * @return {number} | 5107 } |
8691 */ | 5108 return z; |
8692 backdropZ: function() { | 5109 }, |
8693 return this._getZ(this._overlayWithBackdrop()) - 1; | 5110 _setZ: function(element, z) { |
8694 }, | 5111 element.style.zIndex = z; |
8695 | 5112 }, |
8696 /** | 5113 _applyOverlayZ: function(overlay, aboveZ) { |
8697 * Returns the first opened overlay that has a backdrop. | 5114 this._setZ(overlay, aboveZ + 2); |
8698 * @return {Element|undefined} | 5115 }, |
8699 * @private | 5116 _overlayInPath: function(path) { |
8700 */ | 5117 path = path || []; |
8701 _overlayWithBackdrop: function() { | 5118 for (var i = 0; i < path.length; i++) { |
8702 for (var i = 0; i < this._overlays.length; i++) { | 5119 if (path[i]._manager === this) { |
8703 if (this._overlays[i].withBackdrop) { | 5120 return path[i]; |
8704 return this._overlays[i]; | 5121 } |
8705 } | 5122 } |
8706 } | 5123 }, |
8707 }, | 5124 _onCaptureClick: function(event) { |
8708 | 5125 var overlay = this.currentOverlay(); |
8709 /** | 5126 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { |
8710 * Calculates the minimum z-index for the overlay. | 5127 overlay._onCaptureClick(event); |
8711 * @param {Element=} overlay | 5128 } |
8712 * @private | 5129 }, |
8713 */ | 5130 _onCaptureFocus: function(event) { |
8714 _getZ: function(overlay) { | 5131 var overlay = this.currentOverlay(); |
8715 var z = this._minimumZ; | 5132 if (overlay) { |
8716 if (overlay) { | 5133 overlay._onCaptureFocus(event); |
8717 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay)
.zIndex); | 5134 } |
8718 // Check if is a number | 5135 }, |
8719 // Number.isNaN not supported in IE 10+ | 5136 _onCaptureKeyDown: function(event) { |
8720 if (z1 === z1) { | 5137 var overlay = this.currentOverlay(); |
8721 z = z1; | 5138 if (overlay) { |
8722 } | 5139 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) { |
8723 } | 5140 overlay._onCaptureEsc(event); |
8724 return z; | 5141 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 't
ab')) { |
8725 }, | 5142 overlay._onCaptureTab(event); |
8726 | 5143 } |
8727 /** | 5144 } |
8728 * @param {!Element} element | 5145 }, |
8729 * @param {number|string} z | 5146 _shouldBeBehindOverlay: function(overlay1, overlay2) { |
8730 * @private | 5147 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; |
8731 */ | 5148 } |
8732 _setZ: function(element, z) { | 5149 }; |
8733 element.style.zIndex = z; | 5150 |
8734 }, | 5151 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); |
8735 | 5152 |
8736 /** | |
8737 * @param {!Element} overlay | |
8738 * @param {number} aboveZ | |
8739 * @private | |
8740 */ | |
8741 _applyOverlayZ: function(overlay, aboveZ) { | |
8742 this._setZ(overlay, aboveZ + 2); | |
8743 }, | |
8744 | |
8745 /** | |
8746 * Returns the deepest overlay in the path. | |
8747 * @param {Array<Element>=} path | |
8748 * @return {Element|undefined} | |
8749 * @suppress {missingProperties} | |
8750 * @private | |
8751 */ | |
8752 _overlayInPath: function(path) { | |
8753 path = path || []; | |
8754 for (var i = 0; i < path.length; i++) { | |
8755 if (path[i]._manager === this) { | |
8756 return path[i]; | |
8757 } | |
8758 } | |
8759 }, | |
8760 | |
8761 /** | |
8762 * Ensures the click event is delegated to the right overlay. | |
8763 * @param {!Event} event | |
8764 * @private | |
8765 */ | |
8766 _onCaptureClick: function(event) { | |
8767 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8768 // Check if clicked outside of top overlay. | |
8769 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { | |
8770 overlay._onCaptureClick(event); | |
8771 } | |
8772 }, | |
8773 | |
8774 /** | |
8775 * Ensures the focus event is delegated to the right overlay. | |
8776 * @param {!Event} event | |
8777 * @private | |
8778 */ | |
8779 _onCaptureFocus: function(event) { | |
8780 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8781 if (overlay) { | |
8782 overlay._onCaptureFocus(event); | |
8783 } | |
8784 }, | |
8785 | |
8786 /** | |
8787 * Ensures TAB and ESC keyboard events are delegated to the right overlay. | |
8788 * @param {!Event} event | |
8789 * @private | |
8790 */ | |
8791 _onCaptureKeyDown: function(event) { | |
8792 var overlay = /** @type {?} */ (this.currentOverlay()); | |
8793 if (overlay) { | |
8794 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc'))
{ | |
8795 overlay._onCaptureEsc(event); | |
8796 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event,
'tab')) { | |
8797 overlay._onCaptureTab(event); | |
8798 } | |
8799 } | |
8800 }, | |
8801 | |
8802 /** | |
8803 * Returns if the overlay1 should be behind overlay2. | |
8804 * @param {!Element} overlay1 | |
8805 * @param {!Element} overlay2 | |
8806 * @return {boolean} | |
8807 * @suppress {missingProperties} | |
8808 * @private | |
8809 */ | |
8810 _shouldBeBehindOverlay: function(overlay1, overlay2) { | |
8811 return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; | |
8812 } | |
8813 }; | |
8814 | |
8815 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); | |
8816 (function() { | 5153 (function() { |
8817 'use strict'; | 5154 'use strict'; |
8818 | |
8819 /** | |
8820 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays | |
8821 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety | |
8822 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. | |
8823 | |
8824 See the [demo source code](https://github.com/PolymerElements/iron-overlay-behav
ior/blob/master/demo/simple-overlay.html) | |
8825 for an example. | |
8826 | |
8827 ### Closing and canceling | |
8828 | |
8829 An overlay may be hidden by closing or canceling. The difference between close a
nd cancel is user | |
8830 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, | |
8831 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is | |
8832 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click
` properties. | |
8833 `close()` should be called explicitly by the implementer when the user interacts
with a control | |
8834 in the overlay element. When the dialog is canceled, the overlay fires an 'iron-
overlay-canceled' | |
8835 event. Call `preventDefault` on this event to prevent the overlay from closing. | |
8836 | |
8837 ### Positioning | |
8838 | |
8839 By default the element is sized and positioned to fit and centered inside the wi
ndow. You can | |
8840 position and size it manually using CSS. See `Polymer.IronFitBehavior`. | |
8841 | |
8842 ### Backdrop | |
8843 | |
8844 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The
backdrop is | |
8845 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page
for styling | |
8846 options. | |
8847 | |
8848 In addition, `with-backdrop` will wrap the focus within the content in the light
DOM. | |
8849 Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_f
ocusableNodes) | |
8850 to achieve a different behavior. | |
8851 | |
8852 ### Limitations | |
8853 | |
8854 The element is styled to appear on top of other content by setting its `z-index`
property. You | |
8855 must ensure no element has a stacking context with a higher `z-index` than its p
arent stacking | |
8856 context. You should place this element as a child of `<body>` whenever possible. | |
8857 | |
8858 @demo demo/index.html | |
8859 @polymerBehavior Polymer.IronOverlayBehavior | |
8860 */ | |
8861 | |
8862 Polymer.IronOverlayBehaviorImpl = { | 5155 Polymer.IronOverlayBehaviorImpl = { |
8863 | |
8864 properties: { | 5156 properties: { |
8865 | |
8866 /** | |
8867 * True if the overlay is currently displayed. | |
8868 */ | |
8869 opened: { | 5157 opened: { |
8870 observer: '_openedChanged', | 5158 observer: '_openedChanged', |
8871 type: Boolean, | 5159 type: Boolean, |
8872 value: false, | 5160 value: false, |
8873 notify: true | 5161 notify: true |
8874 }, | 5162 }, |
8875 | |
8876 /** | |
8877 * True if the overlay was canceled when it was last closed. | |
8878 */ | |
8879 canceled: { | 5163 canceled: { |
8880 observer: '_canceledChanged', | 5164 observer: '_canceledChanged', |
8881 readOnly: true, | 5165 readOnly: true, |
8882 type: Boolean, | 5166 type: Boolean, |
8883 value: false | 5167 value: false |
8884 }, | 5168 }, |
8885 | |
8886 /** | |
8887 * Set to true to display a backdrop behind the overlay. It traps the focu
s | |
8888 * within the light DOM of the overlay. | |
8889 */ | |
8890 withBackdrop: { | 5169 withBackdrop: { |
8891 observer: '_withBackdropChanged', | 5170 observer: '_withBackdropChanged', |
8892 type: Boolean | 5171 type: Boolean |
8893 }, | 5172 }, |
8894 | |
8895 /** | |
8896 * Set to true to disable auto-focusing the overlay or child nodes with | |
8897 * the `autofocus` attribute` when the overlay is opened. | |
8898 */ | |
8899 noAutoFocus: { | 5173 noAutoFocus: { |
8900 type: Boolean, | 5174 type: Boolean, |
8901 value: false | 5175 value: false |
8902 }, | 5176 }, |
8903 | |
8904 /** | |
8905 * Set to true to disable canceling the overlay with the ESC key. | |
8906 */ | |
8907 noCancelOnEscKey: { | 5177 noCancelOnEscKey: { |
8908 type: Boolean, | 5178 type: Boolean, |
8909 value: false | 5179 value: false |
8910 }, | 5180 }, |
8911 | |
8912 /** | |
8913 * Set to true to disable canceling the overlay by clicking outside it. | |
8914 */ | |
8915 noCancelOnOutsideClick: { | 5181 noCancelOnOutsideClick: { |
8916 type: Boolean, | 5182 type: Boolean, |
8917 value: false | 5183 value: false |
8918 }, | 5184 }, |
8919 | |
8920 /** | |
8921 * Contains the reason(s) this overlay was last closed (see `iron-overlay-
closed`). | |
8922 * `IronOverlayBehavior` provides the `canceled` reason; implementers of t
he | |
8923 * behavior can provide other reasons in addition to `canceled`. | |
8924 */ | |
8925 closingReason: { | 5185 closingReason: { |
8926 // was a getter before, but needs to be a property so other | |
8927 // behaviors can override this. | |
8928 type: Object | 5186 type: Object |
8929 }, | 5187 }, |
8930 | |
8931 /** | |
8932 * Set to true to enable restoring of focus when overlay is closed. | |
8933 */ | |
8934 restoreFocusOnClose: { | 5188 restoreFocusOnClose: { |
8935 type: Boolean, | 5189 type: Boolean, |
8936 value: false | 5190 value: false |
8937 }, | 5191 }, |
8938 | |
8939 /** | |
8940 * Set to true to keep overlay always on top. | |
8941 */ | |
8942 alwaysOnTop: { | 5192 alwaysOnTop: { |
8943 type: Boolean | 5193 type: Boolean |
8944 }, | 5194 }, |
8945 | |
8946 /** | |
8947 * Shortcut to access to the overlay manager. | |
8948 * @private | |
8949 * @type {Polymer.IronOverlayManagerClass} | |
8950 */ | |
8951 _manager: { | 5195 _manager: { |
8952 type: Object, | 5196 type: Object, |
8953 value: Polymer.IronOverlayManager | 5197 value: Polymer.IronOverlayManager |
8954 }, | 5198 }, |
8955 | |
8956 /** | |
8957 * The node being focused. | |
8958 * @type {?Node} | |
8959 */ | |
8960 _focusedChild: { | 5199 _focusedChild: { |
8961 type: Object | 5200 type: Object |
8962 } | 5201 } |
8963 | 5202 }, |
8964 }, | |
8965 | |
8966 listeners: { | 5203 listeners: { |
8967 'iron-resize': '_onIronResize' | 5204 'iron-resize': '_onIronResize' |
8968 }, | 5205 }, |
8969 | |
8970 /** | |
8971 * The backdrop element. | |
8972 * @type {Element} | |
8973 */ | |
8974 get backdropElement() { | 5206 get backdropElement() { |
8975 return this._manager.backdropElement; | 5207 return this._manager.backdropElement; |
8976 }, | 5208 }, |
8977 | |
8978 /** | |
8979 * Returns the node to give focus to. | |
8980 * @type {Node} | |
8981 */ | |
8982 get _focusNode() { | 5209 get _focusNode() { |
8983 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; | 5210 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; |
8984 }, | 5211 }, |
8985 | |
8986 /** | |
8987 * Array of nodes that can receive focus (overlay included), ordered by `tab
index`. | |
8988 * This is used to retrieve which is the first and last focusable nodes in o
rder | |
8989 * to wrap the focus for overlays `with-backdrop`. | |
8990 * | |
8991 * If you know what is your content (specifically the first and last focusab
le children), | |
8992 * you can override this method to return only `[firstFocusable, lastFocusab
le];` | |
8993 * @type {Array<Node>} | |
8994 * @protected | |
8995 */ | |
8996 get _focusableNodes() { | 5212 get _focusableNodes() { |
8997 // Elements that can be focused even if they have [disabled] attribute. | 5213 var FOCUSABLE_WITH_DISABLED = [ 'a[href]', 'area[href]', 'iframe', '[tabin
dex]', '[contentEditable=true]' ]; |
8998 var FOCUSABLE_WITH_DISABLED = [ | 5214 var FOCUSABLE_WITHOUT_DISABLED = [ 'input', 'select', 'textarea', 'button'
]; |
8999 'a[href]', | 5215 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"])'; |
9000 'area[href]', | |
9001 'iframe', | |
9002 '[tabindex]', | |
9003 '[contentEditable=true]' | |
9004 ]; | |
9005 | |
9006 // Elements that cannot be focused if they have [disabled] attribute. | |
9007 var FOCUSABLE_WITHOUT_DISABLED = [ | |
9008 'input', | |
9009 'select', | |
9010 'textarea', | |
9011 'button' | |
9012 ]; | |
9013 | |
9014 // Discard elements with tabindex=-1 (makes them not focusable). | |
9015 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + | |
9016 ':not([tabindex="-1"]),' + | |
9017 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),'
) + | |
9018 ':not([disabled]):not([tabindex="-1"])'; | |
9019 | |
9020 var focusables = Polymer.dom(this).querySelectorAll(selector); | 5216 var focusables = Polymer.dom(this).querySelectorAll(selector); |
9021 if (this.tabIndex >= 0) { | 5217 if (this.tabIndex >= 0) { |
9022 // Insert at the beginning because we might have all elements with tabIn
dex = 0, | |
9023 // and the overlay should be the first of the list. | |
9024 focusables.splice(0, 0, this); | 5218 focusables.splice(0, 0, this); |
9025 } | 5219 } |
9026 // Sort by tabindex. | 5220 return focusables.sort(function(a, b) { |
9027 return focusables.sort(function (a, b) { | |
9028 if (a.tabIndex === b.tabIndex) { | 5221 if (a.tabIndex === b.tabIndex) { |
9029 return 0; | 5222 return 0; |
9030 } | 5223 } |
9031 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { | 5224 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { |
9032 return 1; | 5225 return 1; |
9033 } | 5226 } |
9034 return -1; | 5227 return -1; |
9035 }); | 5228 }); |
9036 }, | 5229 }, |
9037 | |
9038 ready: function() { | 5230 ready: function() { |
9039 // Used to skip calls to notifyResize and refit while the overlay is anima
ting. | |
9040 this.__isAnimating = false; | 5231 this.__isAnimating = false; |
9041 // with-backdrop needs tabindex to be set in order to trap the focus. | |
9042 // If it is not set, IronOverlayBehavior will set it, and remove it if wit
h-backdrop = false. | |
9043 this.__shouldRemoveTabIndex = false; | 5232 this.__shouldRemoveTabIndex = false; |
9044 // Used for wrapping the focus on TAB / Shift+TAB. | |
9045 this.__firstFocusableNode = this.__lastFocusableNode = null; | 5233 this.__firstFocusableNode = this.__lastFocusableNode = null; |
9046 // Used by __onNextAnimationFrame to cancel any previous callback. | |
9047 this.__raf = null; | 5234 this.__raf = null; |
9048 // Focused node before overlay gets opened. Can be restored on close. | |
9049 this.__restoreFocusNode = null; | 5235 this.__restoreFocusNode = null; |
9050 this._ensureSetup(); | 5236 this._ensureSetup(); |
9051 }, | 5237 }, |
9052 | |
9053 attached: function() { | 5238 attached: function() { |
9054 // Call _openedChanged here so that position can be computed correctly. | |
9055 if (this.opened) { | 5239 if (this.opened) { |
9056 this._openedChanged(this.opened); | 5240 this._openedChanged(this.opened); |
9057 } | 5241 } |
9058 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); | 5242 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); |
9059 }, | 5243 }, |
9060 | |
9061 detached: function() { | 5244 detached: function() { |
9062 Polymer.dom(this).unobserveNodes(this._observer); | 5245 Polymer.dom(this).unobserveNodes(this._observer); |
9063 this._observer = null; | 5246 this._observer = null; |
9064 if (this.__raf) { | 5247 if (this.__raf) { |
9065 window.cancelAnimationFrame(this.__raf); | 5248 window.cancelAnimationFrame(this.__raf); |
9066 this.__raf = null; | 5249 this.__raf = null; |
9067 } | 5250 } |
9068 this._manager.removeOverlay(this); | 5251 this._manager.removeOverlay(this); |
9069 }, | 5252 }, |
9070 | |
9071 /** | |
9072 * Toggle the opened state of the overlay. | |
9073 */ | |
9074 toggle: function() { | 5253 toggle: function() { |
9075 this._setCanceled(false); | 5254 this._setCanceled(false); |
9076 this.opened = !this.opened; | 5255 this.opened = !this.opened; |
9077 }, | 5256 }, |
9078 | |
9079 /** | |
9080 * Open the overlay. | |
9081 */ | |
9082 open: function() { | 5257 open: function() { |
9083 this._setCanceled(false); | 5258 this._setCanceled(false); |
9084 this.opened = true; | 5259 this.opened = true; |
9085 }, | 5260 }, |
9086 | |
9087 /** | |
9088 * Close the overlay. | |
9089 */ | |
9090 close: function() { | 5261 close: function() { |
9091 this._setCanceled(false); | 5262 this._setCanceled(false); |
9092 this.opened = false; | 5263 this.opened = false; |
9093 }, | 5264 }, |
9094 | |
9095 /** | |
9096 * Cancels the overlay. | |
9097 * @param {Event=} event The original event | |
9098 */ | |
9099 cancel: function(event) { | 5265 cancel: function(event) { |
9100 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t
rue}); | 5266 var cancelEvent = this.fire('iron-overlay-canceled', event, { |
| 5267 cancelable: true |
| 5268 }); |
9101 if (cancelEvent.defaultPrevented) { | 5269 if (cancelEvent.defaultPrevented) { |
9102 return; | 5270 return; |
9103 } | 5271 } |
9104 | |
9105 this._setCanceled(true); | 5272 this._setCanceled(true); |
9106 this.opened = false; | 5273 this.opened = false; |
9107 }, | 5274 }, |
9108 | |
9109 _ensureSetup: function() { | 5275 _ensureSetup: function() { |
9110 if (this._overlaySetup) { | 5276 if (this._overlaySetup) { |
9111 return; | 5277 return; |
9112 } | 5278 } |
9113 this._overlaySetup = true; | 5279 this._overlaySetup = true; |
9114 this.style.outline = 'none'; | 5280 this.style.outline = 'none'; |
9115 this.style.display = 'none'; | 5281 this.style.display = 'none'; |
9116 }, | 5282 }, |
9117 | |
9118 /** | |
9119 * Called when `opened` changes. | |
9120 * @param {boolean=} opened | |
9121 * @protected | |
9122 */ | |
9123 _openedChanged: function(opened) { | 5283 _openedChanged: function(opened) { |
9124 if (opened) { | 5284 if (opened) { |
9125 this.removeAttribute('aria-hidden'); | 5285 this.removeAttribute('aria-hidden'); |
9126 } else { | 5286 } else { |
9127 this.setAttribute('aria-hidden', 'true'); | 5287 this.setAttribute('aria-hidden', 'true'); |
9128 } | 5288 } |
9129 | |
9130 // Defer any animation-related code on attached | |
9131 // (_openedChanged gets called again on attached). | |
9132 if (!this.isAttached) { | 5289 if (!this.isAttached) { |
9133 return; | 5290 return; |
9134 } | 5291 } |
9135 | |
9136 this.__isAnimating = true; | 5292 this.__isAnimating = true; |
9137 | |
9138 // Use requestAnimationFrame for non-blocking rendering. | |
9139 this.__onNextAnimationFrame(this.__openedChanged); | 5293 this.__onNextAnimationFrame(this.__openedChanged); |
9140 }, | 5294 }, |
9141 | |
9142 _canceledChanged: function() { | 5295 _canceledChanged: function() { |
9143 this.closingReason = this.closingReason || {}; | 5296 this.closingReason = this.closingReason || {}; |
9144 this.closingReason.canceled = this.canceled; | 5297 this.closingReason.canceled = this.canceled; |
9145 }, | 5298 }, |
9146 | |
9147 _withBackdropChanged: function() { | 5299 _withBackdropChanged: function() { |
9148 // If tabindex is already set, no need to override it. | |
9149 if (this.withBackdrop && !this.hasAttribute('tabindex')) { | 5300 if (this.withBackdrop && !this.hasAttribute('tabindex')) { |
9150 this.setAttribute('tabindex', '-1'); | 5301 this.setAttribute('tabindex', '-1'); |
9151 this.__shouldRemoveTabIndex = true; | 5302 this.__shouldRemoveTabIndex = true; |
9152 } else if (this.__shouldRemoveTabIndex) { | 5303 } else if (this.__shouldRemoveTabIndex) { |
9153 this.removeAttribute('tabindex'); | 5304 this.removeAttribute('tabindex'); |
9154 this.__shouldRemoveTabIndex = false; | 5305 this.__shouldRemoveTabIndex = false; |
9155 } | 5306 } |
9156 if (this.opened && this.isAttached) { | 5307 if (this.opened && this.isAttached) { |
9157 this._manager.trackBackdrop(); | 5308 this._manager.trackBackdrop(); |
9158 } | 5309 } |
9159 }, | 5310 }, |
9160 | |
9161 /** | |
9162 * tasks which must occur before opening; e.g. making the element visible. | |
9163 * @protected | |
9164 */ | |
9165 _prepareRenderOpened: function() { | 5311 _prepareRenderOpened: function() { |
9166 // Store focused node. | |
9167 this.__restoreFocusNode = this._manager.deepActiveElement; | 5312 this.__restoreFocusNode = this._manager.deepActiveElement; |
9168 | |
9169 // Needed to calculate the size of the overlay so that transitions on its
size | |
9170 // will have the correct starting points. | |
9171 this._preparePositioning(); | 5313 this._preparePositioning(); |
9172 this.refit(); | 5314 this.refit(); |
9173 this._finishPositioning(); | 5315 this._finishPositioning(); |
9174 | |
9175 // Safari will apply the focus to the autofocus element when displayed | |
9176 // for the first time, so we make sure to return the focus where it was. | |
9177 if (this.noAutoFocus && document.activeElement === this._focusNode) { | 5316 if (this.noAutoFocus && document.activeElement === this._focusNode) { |
9178 this._focusNode.blur(); | 5317 this._focusNode.blur(); |
9179 this.__restoreFocusNode.focus(); | 5318 this.__restoreFocusNode.focus(); |
9180 } | 5319 } |
9181 }, | 5320 }, |
9182 | |
9183 /** | |
9184 * Tasks which cause the overlay to actually open; typically play an animati
on. | |
9185 * @protected | |
9186 */ | |
9187 _renderOpened: function() { | 5321 _renderOpened: function() { |
9188 this._finishRenderOpened(); | 5322 this._finishRenderOpened(); |
9189 }, | 5323 }, |
9190 | |
9191 /** | |
9192 * Tasks which cause the overlay to actually close; typically play an animat
ion. | |
9193 * @protected | |
9194 */ | |
9195 _renderClosed: function() { | 5324 _renderClosed: function() { |
9196 this._finishRenderClosed(); | 5325 this._finishRenderClosed(); |
9197 }, | 5326 }, |
9198 | |
9199 /** | |
9200 * Tasks to be performed at the end of open action. Will fire `iron-overlay-
opened`. | |
9201 * @protected | |
9202 */ | |
9203 _finishRenderOpened: function() { | 5327 _finishRenderOpened: function() { |
9204 this.notifyResize(); | 5328 this.notifyResize(); |
9205 this.__isAnimating = false; | 5329 this.__isAnimating = false; |
9206 | |
9207 // Store it so we don't query too much. | |
9208 var focusableNodes = this._focusableNodes; | 5330 var focusableNodes = this._focusableNodes; |
9209 this.__firstFocusableNode = focusableNodes[0]; | 5331 this.__firstFocusableNode = focusableNodes[0]; |
9210 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; | 5332 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; |
9211 | |
9212 this.fire('iron-overlay-opened'); | 5333 this.fire('iron-overlay-opened'); |
9213 }, | 5334 }, |
9214 | |
9215 /** | |
9216 * Tasks to be performed at the end of close action. Will fire `iron-overlay
-closed`. | |
9217 * @protected | |
9218 */ | |
9219 _finishRenderClosed: function() { | 5335 _finishRenderClosed: function() { |
9220 // Hide the overlay. | |
9221 this.style.display = 'none'; | 5336 this.style.display = 'none'; |
9222 // Reset z-index only at the end of the animation. | |
9223 this.style.zIndex = ''; | 5337 this.style.zIndex = ''; |
9224 this.notifyResize(); | 5338 this.notifyResize(); |
9225 this.__isAnimating = false; | 5339 this.__isAnimating = false; |
9226 this.fire('iron-overlay-closed', this.closingReason); | 5340 this.fire('iron-overlay-closed', this.closingReason); |
9227 }, | 5341 }, |
9228 | |
9229 _preparePositioning: function() { | 5342 _preparePositioning: function() { |
9230 this.style.transition = this.style.webkitTransition = 'none'; | 5343 this.style.transition = this.style.webkitTransition = 'none'; |
9231 this.style.transform = this.style.webkitTransform = 'none'; | 5344 this.style.transform = this.style.webkitTransform = 'none'; |
9232 this.style.display = ''; | 5345 this.style.display = ''; |
9233 }, | 5346 }, |
9234 | |
9235 _finishPositioning: function() { | 5347 _finishPositioning: function() { |
9236 // First, make it invisible & reactivate animations. | |
9237 this.style.display = 'none'; | 5348 this.style.display = 'none'; |
9238 // Force reflow before re-enabling animations so that they don't start. | |
9239 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
9240 this.scrollTop = this.scrollTop; | 5349 this.scrollTop = this.scrollTop; |
9241 this.style.transition = this.style.webkitTransition = ''; | 5350 this.style.transition = this.style.webkitTransition = ''; |
9242 this.style.transform = this.style.webkitTransform = ''; | 5351 this.style.transform = this.style.webkitTransform = ''; |
9243 // Now that animations are enabled, make it visible again | |
9244 this.style.display = ''; | 5352 this.style.display = ''; |
9245 // Force reflow, so that following animations are properly started. | |
9246 // Set scrollTop to itself so that Closure Compiler doesn't remove this. | |
9247 this.scrollTop = this.scrollTop; | 5353 this.scrollTop = this.scrollTop; |
9248 }, | 5354 }, |
9249 | |
9250 /** | |
9251 * Applies focus according to the opened state. | |
9252 * @protected | |
9253 */ | |
9254 _applyFocus: function() { | 5355 _applyFocus: function() { |
9255 if (this.opened) { | 5356 if (this.opened) { |
9256 if (!this.noAutoFocus) { | 5357 if (!this.noAutoFocus) { |
9257 this._focusNode.focus(); | 5358 this._focusNode.focus(); |
9258 } | 5359 } |
9259 } | 5360 } else { |
9260 else { | |
9261 this._focusNode.blur(); | 5361 this._focusNode.blur(); |
9262 this._focusedChild = null; | 5362 this._focusedChild = null; |
9263 // Restore focus. | |
9264 if (this.restoreFocusOnClose && this.__restoreFocusNode) { | 5363 if (this.restoreFocusOnClose && this.__restoreFocusNode) { |
9265 this.__restoreFocusNode.focus(); | 5364 this.__restoreFocusNode.focus(); |
9266 } | 5365 } |
9267 this.__restoreFocusNode = null; | 5366 this.__restoreFocusNode = null; |
9268 // If many overlays get closed at the same time, one of them would still | |
9269 // be the currentOverlay even if already closed, and would call _applyFo
cus | |
9270 // infinitely, so we check for this not to be the current overlay. | |
9271 var currentOverlay = this._manager.currentOverlay(); | 5367 var currentOverlay = this._manager.currentOverlay(); |
9272 if (currentOverlay && this !== currentOverlay) { | 5368 if (currentOverlay && this !== currentOverlay) { |
9273 currentOverlay._applyFocus(); | 5369 currentOverlay._applyFocus(); |
9274 } | 5370 } |
9275 } | 5371 } |
9276 }, | 5372 }, |
9277 | |
9278 /** | |
9279 * Cancels (closes) the overlay. Call when click happens outside the overlay
. | |
9280 * @param {!Event} event | |
9281 * @protected | |
9282 */ | |
9283 _onCaptureClick: function(event) { | 5373 _onCaptureClick: function(event) { |
9284 if (!this.noCancelOnOutsideClick) { | 5374 if (!this.noCancelOnOutsideClick) { |
9285 this.cancel(event); | 5375 this.cancel(event); |
9286 } | 5376 } |
9287 }, | 5377 }, |
9288 | 5378 _onCaptureFocus: function(event) { |
9289 /** | |
9290 * Keeps track of the focused child. If withBackdrop, traps focus within ove
rlay. | |
9291 * @param {!Event} event | |
9292 * @protected | |
9293 */ | |
9294 _onCaptureFocus: function (event) { | |
9295 if (!this.withBackdrop) { | 5379 if (!this.withBackdrop) { |
9296 return; | 5380 return; |
9297 } | 5381 } |
9298 var path = Polymer.dom(event).path; | 5382 var path = Polymer.dom(event).path; |
9299 if (path.indexOf(this) === -1) { | 5383 if (path.indexOf(this) === -1) { |
9300 event.stopPropagation(); | 5384 event.stopPropagation(); |
9301 this._applyFocus(); | 5385 this._applyFocus(); |
9302 } else { | 5386 } else { |
9303 this._focusedChild = path[0]; | 5387 this._focusedChild = path[0]; |
9304 } | 5388 } |
9305 }, | 5389 }, |
9306 | |
9307 /** | |
9308 * Handles the ESC key event and cancels (closes) the overlay. | |
9309 * @param {!Event} event | |
9310 * @protected | |
9311 */ | |
9312 _onCaptureEsc: function(event) { | 5390 _onCaptureEsc: function(event) { |
9313 if (!this.noCancelOnEscKey) { | 5391 if (!this.noCancelOnEscKey) { |
9314 this.cancel(event); | 5392 this.cancel(event); |
9315 } | 5393 } |
9316 }, | 5394 }, |
9317 | |
9318 /** | |
9319 * Handles TAB key events to track focus changes. | |
9320 * Will wrap focus for overlays withBackdrop. | |
9321 * @param {!Event} event | |
9322 * @protected | |
9323 */ | |
9324 _onCaptureTab: function(event) { | 5395 _onCaptureTab: function(event) { |
9325 if (!this.withBackdrop) { | 5396 if (!this.withBackdrop) { |
9326 return; | 5397 return; |
9327 } | 5398 } |
9328 // TAB wraps from last to first focusable. | |
9329 // Shift + TAB wraps from first to last focusable. | |
9330 var shift = event.shiftKey; | 5399 var shift = event.shiftKey; |
9331 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; | 5400 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; |
9332 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; | 5401 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; |
9333 var shouldWrap = false; | 5402 var shouldWrap = false; |
9334 if (nodeToCheck === nodeToSet) { | 5403 if (nodeToCheck === nodeToSet) { |
9335 // If nodeToCheck is the same as nodeToSet, it means we have an overlay | |
9336 // with 0 or 1 focusables; in either case we still need to trap the | |
9337 // focus within the overlay. | |
9338 shouldWrap = true; | 5404 shouldWrap = true; |
9339 } else { | 5405 } else { |
9340 // In dom=shadow, the manager will receive focus changes on the main | |
9341 // root but not the ones within other shadow roots, so we can't rely on | |
9342 // _focusedChild, but we should check the deepest active element. | |
9343 var focusedNode = this._manager.deepActiveElement; | 5406 var focusedNode = this._manager.deepActiveElement; |
9344 // If the active element is not the nodeToCheck but the overlay itself, | 5407 shouldWrap = focusedNode === nodeToCheck || focusedNode === this; |
9345 // it means the focus is about to go outside the overlay, hence we | 5408 } |
9346 // should prevent that (e.g. user opens the overlay and hit Shift+TAB). | |
9347 shouldWrap = (focusedNode === nodeToCheck || focusedNode === this); | |
9348 } | |
9349 | |
9350 if (shouldWrap) { | 5409 if (shouldWrap) { |
9351 // When the overlay contains the last focusable element of the document | |
9352 // and it's already focused, pressing TAB would move the focus outside | |
9353 // the document (e.g. to the browser search bar). Similarly, when the | |
9354 // overlay contains the first focusable element of the document and it's | |
9355 // already focused, pressing Shift+TAB would move the focus outside the | |
9356 // document (e.g. to the browser search bar). | |
9357 // In both cases, we would not receive a focus event, but only a blur. | |
9358 // In order to achieve focus wrapping, we prevent this TAB event and | |
9359 // force the focus. This will also prevent the focus to temporarily move | |
9360 // outside the overlay, which might cause scrolling. | |
9361 event.preventDefault(); | 5410 event.preventDefault(); |
9362 this._focusedChild = nodeToSet; | 5411 this._focusedChild = nodeToSet; |
9363 this._applyFocus(); | 5412 this._applyFocus(); |
9364 } | 5413 } |
9365 }, | 5414 }, |
9366 | |
9367 /** | |
9368 * Refits if the overlay is opened and not animating. | |
9369 * @protected | |
9370 */ | |
9371 _onIronResize: function() { | 5415 _onIronResize: function() { |
9372 if (this.opened && !this.__isAnimating) { | 5416 if (this.opened && !this.__isAnimating) { |
9373 this.__onNextAnimationFrame(this.refit); | 5417 this.__onNextAnimationFrame(this.refit); |
9374 } | 5418 } |
9375 }, | 5419 }, |
9376 | |
9377 /** | |
9378 * Will call notifyResize if overlay is opened. | |
9379 * Can be overridden in order to avoid multiple observers on the same node. | |
9380 * @protected | |
9381 */ | |
9382 _onNodesChange: function() { | 5420 _onNodesChange: function() { |
9383 if (this.opened && !this.__isAnimating) { | 5421 if (this.opened && !this.__isAnimating) { |
9384 this.notifyResize(); | 5422 this.notifyResize(); |
9385 } | 5423 } |
9386 }, | 5424 }, |
9387 | |
9388 /** | |
9389 * Tasks executed when opened changes: prepare for the opening, move the | |
9390 * focus, update the manager, render opened/closed. | |
9391 * @private | |
9392 */ | |
9393 __openedChanged: function() { | 5425 __openedChanged: function() { |
9394 if (this.opened) { | 5426 if (this.opened) { |
9395 // Make overlay visible, then add it to the manager. | |
9396 this._prepareRenderOpened(); | 5427 this._prepareRenderOpened(); |
9397 this._manager.addOverlay(this); | 5428 this._manager.addOverlay(this); |
9398 // Move the focus to the child node with [autofocus]. | |
9399 this._applyFocus(); | 5429 this._applyFocus(); |
9400 | |
9401 this._renderOpened(); | 5430 this._renderOpened(); |
9402 } else { | 5431 } else { |
9403 // Remove overlay, then restore the focus before actually closing. | |
9404 this._manager.removeOverlay(this); | 5432 this._manager.removeOverlay(this); |
9405 this._applyFocus(); | 5433 this._applyFocus(); |
9406 | |
9407 this._renderClosed(); | 5434 this._renderClosed(); |
9408 } | 5435 } |
9409 }, | 5436 }, |
9410 | |
9411 /** | |
9412 * Executes a callback on the next animation frame, overriding any previous | |
9413 * callback awaiting for the next animation frame. e.g. | |
9414 * `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`; | |
9415 * `callback1` will never be invoked. | |
9416 * @param {!Function} callback Its `this` parameter is the overlay itself. | |
9417 * @private | |
9418 */ | |
9419 __onNextAnimationFrame: function(callback) { | 5437 __onNextAnimationFrame: function(callback) { |
9420 if (this.__raf) { | 5438 if (this.__raf) { |
9421 window.cancelAnimationFrame(this.__raf); | 5439 window.cancelAnimationFrame(this.__raf); |
9422 } | 5440 } |
9423 var self = this; | 5441 var self = this; |
9424 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { | 5442 this.__raf = window.requestAnimationFrame(function nextAnimationFrame() { |
9425 self.__raf = null; | 5443 self.__raf = null; |
9426 callback.call(self); | 5444 callback.call(self); |
9427 }); | 5445 }); |
9428 } | 5446 } |
9429 | |
9430 }; | 5447 }; |
9431 | 5448 Polymer.IronOverlayBehavior = [ Polymer.IronFitBehavior, Polymer.IronResizable
Behavior, Polymer.IronOverlayBehaviorImpl ]; |
9432 /** @polymerBehavior */ | |
9433 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; | |
9434 | |
9435 /** | |
9436 * Fired after the overlay opens. | |
9437 * @event iron-overlay-opened | |
9438 */ | |
9439 | |
9440 /** | |
9441 * Fired when the overlay is canceled, but before it is closed. | |
9442 * @event iron-overlay-canceled | |
9443 * @param {Event} event The closing of the overlay can be prevented | |
9444 * by calling `event.preventDefault()`. The `event.detail` is the original eve
nt that | |
9445 * originated the canceling (e.g. ESC keyboard event or click event outside th
e overlay). | |
9446 */ | |
9447 | |
9448 /** | |
9449 * Fired after the overlay closes. | |
9450 * @event iron-overlay-closed | |
9451 * @param {Event} event The `event.detail` is the `closingReason` property | |
9452 * (contains `canceled`, whether the overlay was canceled). | |
9453 */ | |
9454 | |
9455 })(); | 5449 })(); |
9456 /** | 5450 |
9457 * `Polymer.NeonAnimatableBehavior` is implemented by elements containing anim
ations for use with | 5451 Polymer.NeonAnimatableBehavior = { |
9458 * elements implementing `Polymer.NeonAnimationRunnerBehavior`. | 5452 properties: { |
9459 * @polymerBehavior | 5453 animationConfig: { |
9460 */ | 5454 type: Object |
9461 Polymer.NeonAnimatableBehavior = { | 5455 }, |
9462 | 5456 entryAnimation: { |
| 5457 observer: '_entryAnimationChanged', |
| 5458 type: String |
| 5459 }, |
| 5460 exitAnimation: { |
| 5461 observer: '_exitAnimationChanged', |
| 5462 type: String |
| 5463 } |
| 5464 }, |
| 5465 _entryAnimationChanged: function() { |
| 5466 this.animationConfig = this.animationConfig || {}; |
| 5467 this.animationConfig['entry'] = [ { |
| 5468 name: this.entryAnimation, |
| 5469 node: this |
| 5470 } ]; |
| 5471 }, |
| 5472 _exitAnimationChanged: function() { |
| 5473 this.animationConfig = this.animationConfig || {}; |
| 5474 this.animationConfig['exit'] = [ { |
| 5475 name: this.exitAnimation, |
| 5476 node: this |
| 5477 } ]; |
| 5478 }, |
| 5479 _copyProperties: function(config1, config2) { |
| 5480 for (var property in config2) { |
| 5481 config1[property] = config2[property]; |
| 5482 } |
| 5483 }, |
| 5484 _cloneConfig: function(config) { |
| 5485 var clone = { |
| 5486 isClone: true |
| 5487 }; |
| 5488 this._copyProperties(clone, config); |
| 5489 return clone; |
| 5490 }, |
| 5491 _getAnimationConfigRecursive: function(type, map, allConfigs) { |
| 5492 if (!this.animationConfig) { |
| 5493 return; |
| 5494 } |
| 5495 if (this.animationConfig.value && typeof this.animationConfig.value === 'fun
ction') { |
| 5496 this._warn(this._logf('playAnimation', "Please put 'animationConfig' insid
e of your components 'properties' object instead of outside of it.")); |
| 5497 return; |
| 5498 } |
| 5499 var thisConfig; |
| 5500 if (type) { |
| 5501 thisConfig = this.animationConfig[type]; |
| 5502 } else { |
| 5503 thisConfig = this.animationConfig; |
| 5504 } |
| 5505 if (!Array.isArray(thisConfig)) { |
| 5506 thisConfig = [ thisConfig ]; |
| 5507 } |
| 5508 if (thisConfig) { |
| 5509 for (var config, index = 0; config = thisConfig[index]; index++) { |
| 5510 if (config.animatable) { |
| 5511 config.animatable._getAnimationConfigRecursive(config.type || type, ma
p, allConfigs); |
| 5512 } else { |
| 5513 if (config.id) { |
| 5514 var cachedConfig = map[config.id]; |
| 5515 if (cachedConfig) { |
| 5516 if (!cachedConfig.isClone) { |
| 5517 map[config.id] = this._cloneConfig(cachedConfig); |
| 5518 cachedConfig = map[config.id]; |
| 5519 } |
| 5520 this._copyProperties(cachedConfig, config); |
| 5521 } else { |
| 5522 map[config.id] = config; |
| 5523 } |
| 5524 } else { |
| 5525 allConfigs.push(config); |
| 5526 } |
| 5527 } |
| 5528 } |
| 5529 } |
| 5530 }, |
| 5531 getAnimationConfig: function(type) { |
| 5532 var map = {}; |
| 5533 var allConfigs = []; |
| 5534 this._getAnimationConfigRecursive(type, map, allConfigs); |
| 5535 for (var key in map) { |
| 5536 allConfigs.push(map[key]); |
| 5537 } |
| 5538 return allConfigs; |
| 5539 } |
| 5540 }; |
| 5541 |
| 5542 Polymer.NeonAnimationRunnerBehaviorImpl = { |
| 5543 _configureAnimations: function(configs) { |
| 5544 var results = []; |
| 5545 if (configs.length > 0) { |
| 5546 for (var config, index = 0; config = configs[index]; index++) { |
| 5547 var neonAnimation = document.createElement(config.name); |
| 5548 if (neonAnimation.isNeonAnimation) { |
| 5549 var result = null; |
| 5550 try { |
| 5551 result = neonAnimation.configure(config); |
| 5552 if (typeof result.cancel != 'function') { |
| 5553 result = document.timeline.play(result); |
| 5554 } |
| 5555 } catch (e) { |
| 5556 result = null; |
| 5557 console.warn('Couldnt play', '(', config.name, ').', e); |
| 5558 } |
| 5559 if (result) { |
| 5560 results.push({ |
| 5561 neonAnimation: neonAnimation, |
| 5562 config: config, |
| 5563 animation: result |
| 5564 }); |
| 5565 } |
| 5566 } else { |
| 5567 console.warn(this.is + ':', config.name, 'not found!'); |
| 5568 } |
| 5569 } |
| 5570 } |
| 5571 return results; |
| 5572 }, |
| 5573 _shouldComplete: function(activeEntries) { |
| 5574 var finished = true; |
| 5575 for (var i = 0; i < activeEntries.length; i++) { |
| 5576 if (activeEntries[i].animation.playState != 'finished') { |
| 5577 finished = false; |
| 5578 break; |
| 5579 } |
| 5580 } |
| 5581 return finished; |
| 5582 }, |
| 5583 _complete: function(activeEntries) { |
| 5584 for (var i = 0; i < activeEntries.length; i++) { |
| 5585 activeEntries[i].neonAnimation.complete(activeEntries[i].config); |
| 5586 } |
| 5587 for (var i = 0; i < activeEntries.length; i++) { |
| 5588 activeEntries[i].animation.cancel(); |
| 5589 } |
| 5590 }, |
| 5591 playAnimation: function(type, cookie) { |
| 5592 var configs = this.getAnimationConfig(type); |
| 5593 if (!configs) { |
| 5594 return; |
| 5595 } |
| 5596 this._active = this._active || {}; |
| 5597 if (this._active[type]) { |
| 5598 this._complete(this._active[type]); |
| 5599 delete this._active[type]; |
| 5600 } |
| 5601 var activeEntries = this._configureAnimations(configs); |
| 5602 if (activeEntries.length == 0) { |
| 5603 this.fire('neon-animation-finish', cookie, { |
| 5604 bubbles: false |
| 5605 }); |
| 5606 return; |
| 5607 } |
| 5608 this._active[type] = activeEntries; |
| 5609 for (var i = 0; i < activeEntries.length; i++) { |
| 5610 activeEntries[i].animation.onfinish = function() { |
| 5611 if (this._shouldComplete(activeEntries)) { |
| 5612 this._complete(activeEntries); |
| 5613 delete this._active[type]; |
| 5614 this.fire('neon-animation-finish', cookie, { |
| 5615 bubbles: false |
| 5616 }); |
| 5617 } |
| 5618 }.bind(this); |
| 5619 } |
| 5620 }, |
| 5621 cancelAnimation: function() { |
| 5622 for (var k in this._animations) { |
| 5623 this._animations[k].cancel(); |
| 5624 } |
| 5625 this._animations = {}; |
| 5626 } |
| 5627 }; |
| 5628 |
| 5629 Polymer.NeonAnimationRunnerBehavior = [ Polymer.NeonAnimatableBehavior, Polymer.
NeonAnimationRunnerBehaviorImpl ]; |
| 5630 |
| 5631 Polymer.NeonAnimationBehavior = { |
| 5632 properties: { |
| 5633 animationTiming: { |
| 5634 type: Object, |
| 5635 value: function() { |
| 5636 return { |
| 5637 duration: 500, |
| 5638 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', |
| 5639 fill: 'both' |
| 5640 }; |
| 5641 } |
| 5642 } |
| 5643 }, |
| 5644 isNeonAnimation: true, |
| 5645 timingFromConfig: function(config) { |
| 5646 if (config.timing) { |
| 5647 for (var property in config.timing) { |
| 5648 this.animationTiming[property] = config.timing[property]; |
| 5649 } |
| 5650 } |
| 5651 return this.animationTiming; |
| 5652 }, |
| 5653 setPrefixedProperty: function(node, property, value) { |
| 5654 var map = { |
| 5655 transform: [ 'webkitTransform' ], |
| 5656 transformOrigin: [ 'mozTransformOrigin', 'webkitTransformOrigin' ] |
| 5657 }; |
| 5658 var prefixes = map[property]; |
| 5659 for (var prefix, index = 0; prefix = prefixes[index]; index++) { |
| 5660 node.style[prefix] = value; |
| 5661 } |
| 5662 node.style[property] = value; |
| 5663 }, |
| 5664 complete: function() {} |
| 5665 }; |
| 5666 |
| 5667 Polymer({ |
| 5668 is: 'opaque-animation', |
| 5669 behaviors: [ Polymer.NeonAnimationBehavior ], |
| 5670 configure: function(config) { |
| 5671 var node = config.node; |
| 5672 this._effect = new KeyframeEffect(node, [ { |
| 5673 opacity: '1' |
| 5674 }, { |
| 5675 opacity: '1' |
| 5676 } ], this.timingFromConfig(config)); |
| 5677 node.style.opacity = '0'; |
| 5678 return this._effect; |
| 5679 }, |
| 5680 complete: function(config) { |
| 5681 config.node.style.opacity = ''; |
| 5682 } |
| 5683 }); |
| 5684 |
| 5685 (function() { |
| 5686 'use strict'; |
| 5687 var LAST_TOUCH_POSITION = { |
| 5688 pageX: 0, |
| 5689 pageY: 0 |
| 5690 }; |
| 5691 var ROOT_TARGET = null; |
| 5692 var SCROLLABLE_NODES = []; |
| 5693 Polymer.IronDropdownScrollManager = { |
| 5694 get currentLockingElement() { |
| 5695 return this._lockingElements[this._lockingElements.length - 1]; |
| 5696 }, |
| 5697 elementIsScrollLocked: function(element) { |
| 5698 var currentLockingElement = this.currentLockingElement; |
| 5699 if (currentLockingElement === undefined) return false; |
| 5700 var scrollLocked; |
| 5701 if (this._hasCachedLockedElement(element)) { |
| 5702 return true; |
| 5703 } |
| 5704 if (this._hasCachedUnlockedElement(element)) { |
| 5705 return false; |
| 5706 } |
| 5707 scrollLocked = !!currentLockingElement && currentLockingElement !== elemen
t && !this._composedTreeContains(currentLockingElement, element); |
| 5708 if (scrollLocked) { |
| 5709 this._lockedElementCache.push(element); |
| 5710 } else { |
| 5711 this._unlockedElementCache.push(element); |
| 5712 } |
| 5713 return scrollLocked; |
| 5714 }, |
| 5715 pushScrollLock: function(element) { |
| 5716 if (this._lockingElements.indexOf(element) >= 0) { |
| 5717 return; |
| 5718 } |
| 5719 if (this._lockingElements.length === 0) { |
| 5720 this._lockScrollInteractions(); |
| 5721 } |
| 5722 this._lockingElements.push(element); |
| 5723 this._lockedElementCache = []; |
| 5724 this._unlockedElementCache = []; |
| 5725 }, |
| 5726 removeScrollLock: function(element) { |
| 5727 var index = this._lockingElements.indexOf(element); |
| 5728 if (index === -1) { |
| 5729 return; |
| 5730 } |
| 5731 this._lockingElements.splice(index, 1); |
| 5732 this._lockedElementCache = []; |
| 5733 this._unlockedElementCache = []; |
| 5734 if (this._lockingElements.length === 0) { |
| 5735 this._unlockScrollInteractions(); |
| 5736 } |
| 5737 }, |
| 5738 _lockingElements: [], |
| 5739 _lockedElementCache: null, |
| 5740 _unlockedElementCache: null, |
| 5741 _hasCachedLockedElement: function(element) { |
| 5742 return this._lockedElementCache.indexOf(element) > -1; |
| 5743 }, |
| 5744 _hasCachedUnlockedElement: function(element) { |
| 5745 return this._unlockedElementCache.indexOf(element) > -1; |
| 5746 }, |
| 5747 _composedTreeContains: function(element, child) { |
| 5748 var contentElements; |
| 5749 var distributedNodes; |
| 5750 var contentIndex; |
| 5751 var nodeIndex; |
| 5752 if (element.contains(child)) { |
| 5753 return true; |
| 5754 } |
| 5755 contentElements = Polymer.dom(element).querySelectorAll('content'); |
| 5756 for (contentIndex = 0; contentIndex < contentElements.length; ++contentInd
ex) { |
| 5757 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistrib
utedNodes(); |
| 5758 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { |
| 5759 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) { |
| 5760 return true; |
| 5761 } |
| 5762 } |
| 5763 } |
| 5764 return false; |
| 5765 }, |
| 5766 _scrollInteractionHandler: function(event) { |
| 5767 if (event.cancelable && this._shouldPreventScrolling(event)) { |
| 5768 event.preventDefault(); |
| 5769 } |
| 5770 if (event.targetTouches) { |
| 5771 var touch = event.targetTouches[0]; |
| 5772 LAST_TOUCH_POSITION.pageX = touch.pageX; |
| 5773 LAST_TOUCH_POSITION.pageY = touch.pageY; |
| 5774 } |
| 5775 }, |
| 5776 _lockScrollInteractions: function() { |
| 5777 this._boundScrollHandler = this._boundScrollHandler || this._scrollInterac
tionHandler.bind(this); |
| 5778 document.addEventListener('wheel', this._boundScrollHandler, true); |
| 5779 document.addEventListener('mousewheel', this._boundScrollHandler, true); |
| 5780 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true
); |
| 5781 document.addEventListener('touchstart', this._boundScrollHandler, true); |
| 5782 document.addEventListener('touchmove', this._boundScrollHandler, true); |
| 5783 }, |
| 5784 _unlockScrollInteractions: function() { |
| 5785 document.removeEventListener('wheel', this._boundScrollHandler, true); |
| 5786 document.removeEventListener('mousewheel', this._boundScrollHandler, true)
; |
| 5787 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, t
rue); |
| 5788 document.removeEventListener('touchstart', this._boundScrollHandler, true)
; |
| 5789 document.removeEventListener('touchmove', this._boundScrollHandler, true); |
| 5790 }, |
| 5791 _shouldPreventScrolling: function(event) { |
| 5792 var target = Polymer.dom(event).rootTarget; |
| 5793 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { |
| 5794 ROOT_TARGET = target; |
| 5795 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); |
| 5796 } |
| 5797 if (!SCROLLABLE_NODES.length) { |
| 5798 return true; |
| 5799 } |
| 5800 if (event.type === 'touchstart') { |
| 5801 return false; |
| 5802 } |
| 5803 var info = this._getScrollInfo(event); |
| 5804 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY)
; |
| 5805 }, |
| 5806 _getScrollableNodes: function(nodes) { |
| 5807 var scrollables = []; |
| 5808 var lockingIndex = nodes.indexOf(this.currentLockingElement); |
| 5809 for (var i = 0; i <= lockingIndex; i++) { |
| 5810 var node = nodes[i]; |
| 5811 if (node.nodeType === 11) { |
| 5812 continue; |
| 5813 } |
| 5814 var style = node.style; |
| 5815 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { |
| 5816 style = window.getComputedStyle(node); |
| 5817 } |
| 5818 if (style.overflow === 'scroll' || style.overflow === 'auto') { |
| 5819 scrollables.push(node); |
| 5820 } |
| 5821 } |
| 5822 return scrollables; |
| 5823 }, |
| 5824 _getScrollingNode: function(nodes, deltaX, deltaY) { |
| 5825 if (!deltaX && !deltaY) { |
| 5826 return; |
| 5827 } |
| 5828 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); |
| 5829 for (var i = 0; i < nodes.length; i++) { |
| 5830 var node = nodes[i]; |
| 5831 var canScroll = false; |
| 5832 if (verticalScroll) { |
| 5833 canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.sc
rollHeight - node.clientHeight; |
| 5834 } else { |
| 5835 canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node.
scrollWidth - node.clientWidth; |
| 5836 } |
| 5837 if (canScroll) { |
| 5838 return node; |
| 5839 } |
| 5840 } |
| 5841 }, |
| 5842 _getScrollInfo: function(event) { |
| 5843 var info = { |
| 5844 deltaX: event.deltaX, |
| 5845 deltaY: event.deltaY |
| 5846 }; |
| 5847 if ('deltaX' in event) {} else if ('wheelDeltaX' in event) { |
| 5848 info.deltaX = -event.wheelDeltaX; |
| 5849 info.deltaY = -event.wheelDeltaY; |
| 5850 } else if ('axis' in event) { |
| 5851 info.deltaX = event.axis === 1 ? event.detail : 0; |
| 5852 info.deltaY = event.axis === 2 ? event.detail : 0; |
| 5853 } else if (event.targetTouches) { |
| 5854 var touch = event.targetTouches[0]; |
| 5855 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; |
| 5856 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; |
| 5857 } |
| 5858 return info; |
| 5859 } |
| 5860 }; |
| 5861 })(); |
| 5862 |
| 5863 (function() { |
| 5864 'use strict'; |
| 5865 Polymer({ |
| 5866 is: 'iron-dropdown', |
| 5867 behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer
.IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ], |
9463 properties: { | 5868 properties: { |
9464 | 5869 horizontalAlign: { |
9465 /** | 5870 type: String, |
9466 * Animation configuration. See README for more info. | 5871 value: 'left', |
9467 */ | 5872 reflectToAttribute: true |
9468 animationConfig: { | 5873 }, |
| 5874 verticalAlign: { |
| 5875 type: String, |
| 5876 value: 'top', |
| 5877 reflectToAttribute: true |
| 5878 }, |
| 5879 openAnimationConfig: { |
9469 type: Object | 5880 type: Object |
9470 }, | 5881 }, |
9471 | 5882 closeAnimationConfig: { |
9472 /** | 5883 type: Object |
9473 * Convenience property for setting an 'entry' animation. Do not set `anim
ationConfig.entry` | |
9474 * manually if using this. The animated node is set to `this` if using thi
s property. | |
9475 */ | |
9476 entryAnimation: { | |
9477 observer: '_entryAnimationChanged', | |
9478 type: String | |
9479 }, | 5884 }, |
9480 | 5885 focusTarget: { |
9481 /** | 5886 type: Object |
9482 * Convenience property for setting an 'exit' animation. Do not set `anima
tionConfig.exit` | 5887 }, |
9483 * manually if using this. The animated node is set to `this` if using thi
s property. | 5888 noAnimations: { |
9484 */ | 5889 type: Boolean, |
9485 exitAnimation: { | 5890 value: false |
9486 observer: '_exitAnimationChanged', | 5891 }, |
9487 type: String | 5892 allowOutsideScroll: { |
9488 } | 5893 type: Boolean, |
9489 | 5894 value: false |
9490 }, | 5895 }, |
9491 | 5896 _boundOnCaptureScroll: { |
9492 _entryAnimationChanged: function() { | 5897 type: Function, |
9493 this.animationConfig = this.animationConfig || {}; | 5898 value: function() { |
9494 this.animationConfig['entry'] = [{ | 5899 return this._onCaptureScroll.bind(this); |
9495 name: this.entryAnimation, | 5900 } |
9496 node: this | 5901 } |
9497 }]; | 5902 }, |
9498 }, | 5903 listeners: { |
9499 | 5904 'neon-animation-finish': '_onNeonAnimationFinish' |
9500 _exitAnimationChanged: function() { | 5905 }, |
9501 this.animationConfig = this.animationConfig || {}; | 5906 observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizont
alAlign, verticalOffset, horizontalOffset)' ], |
9502 this.animationConfig['exit'] = [{ | 5907 get containedElement() { |
9503 name: this.exitAnimation, | 5908 return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
9504 node: this | 5909 }, |
9505 }]; | 5910 get _focusTarget() { |
9506 }, | 5911 return this.focusTarget || this.containedElement; |
9507 | 5912 }, |
9508 _copyProperties: function(config1, config2) { | 5913 ready: function() { |
9509 // shallowly copy properties from config2 to config1 | 5914 this._scrollTop = 0; |
9510 for (var property in config2) { | 5915 this._scrollLeft = 0; |
9511 config1[property] = config2[property]; | 5916 this._refitOnScrollRAF = null; |
9512 } | 5917 }, |
9513 }, | 5918 detached: function() { |
9514 | 5919 this.cancelAnimation(); |
9515 _cloneConfig: function(config) { | 5920 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
9516 var clone = { | 5921 }, |
9517 isClone: true | 5922 _openedChanged: function() { |
| 5923 if (this.opened && this.disabled) { |
| 5924 this.cancel(); |
| 5925 } else { |
| 5926 this.cancelAnimation(); |
| 5927 this.sizingTarget = this.containedElement || this.sizingTarget; |
| 5928 this._updateAnimationConfig(); |
| 5929 this._saveScrollPosition(); |
| 5930 if (this.opened) { |
| 5931 document.addEventListener('scroll', this._boundOnCaptureScroll); |
| 5932 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScro
llLock(this); |
| 5933 } else { |
| 5934 document.removeEventListener('scroll', this._boundOnCaptureScroll); |
| 5935 Polymer.IronDropdownScrollManager.removeScrollLock(this); |
| 5936 } |
| 5937 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments); |
| 5938 } |
| 5939 }, |
| 5940 _renderOpened: function() { |
| 5941 if (!this.noAnimations && this.animationConfig.open) { |
| 5942 this.$.contentWrapper.classList.add('animating'); |
| 5943 this.playAnimation('open'); |
| 5944 } else { |
| 5945 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments); |
| 5946 } |
| 5947 }, |
| 5948 _renderClosed: function() { |
| 5949 if (!this.noAnimations && this.animationConfig.close) { |
| 5950 this.$.contentWrapper.classList.add('animating'); |
| 5951 this.playAnimation('close'); |
| 5952 } else { |
| 5953 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments); |
| 5954 } |
| 5955 }, |
| 5956 _onNeonAnimationFinish: function() { |
| 5957 this.$.contentWrapper.classList.remove('animating'); |
| 5958 if (this.opened) { |
| 5959 this._finishRenderOpened(); |
| 5960 } else { |
| 5961 this._finishRenderClosed(); |
| 5962 } |
| 5963 }, |
| 5964 _onCaptureScroll: function() { |
| 5965 if (!this.allowOutsideScroll) { |
| 5966 this._restoreScrollPosition(); |
| 5967 } else { |
| 5968 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrol
lRAF); |
| 5969 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(th
is)); |
| 5970 } |
| 5971 }, |
| 5972 _saveScrollPosition: function() { |
| 5973 if (document.scrollingElement) { |
| 5974 this._scrollTop = document.scrollingElement.scrollTop; |
| 5975 this._scrollLeft = document.scrollingElement.scrollLeft; |
| 5976 } else { |
| 5977 this._scrollTop = Math.max(document.documentElement.scrollTop, document.
body.scrollTop); |
| 5978 this._scrollLeft = Math.max(document.documentElement.scrollLeft, documen
t.body.scrollLeft); |
| 5979 } |
| 5980 }, |
| 5981 _restoreScrollPosition: function() { |
| 5982 if (document.scrollingElement) { |
| 5983 document.scrollingElement.scrollTop = this._scrollTop; |
| 5984 document.scrollingElement.scrollLeft = this._scrollLeft; |
| 5985 } else { |
| 5986 document.documentElement.scrollTop = this._scrollTop; |
| 5987 document.documentElement.scrollLeft = this._scrollLeft; |
| 5988 document.body.scrollTop = this._scrollTop; |
| 5989 document.body.scrollLeft = this._scrollLeft; |
| 5990 } |
| 5991 }, |
| 5992 _updateAnimationConfig: function() { |
| 5993 var animations = (this.openAnimationConfig || []).concat(this.closeAnimati
onConfig || []); |
| 5994 for (var i = 0; i < animations.length; i++) { |
| 5995 animations[i].node = this.containedElement; |
| 5996 } |
| 5997 this.animationConfig = { |
| 5998 open: this.openAnimationConfig, |
| 5999 close: this.closeAnimationConfig |
9518 }; | 6000 }; |
9519 this._copyProperties(clone, config); | 6001 }, |
9520 return clone; | 6002 _updateOverlayPosition: function() { |
9521 }, | 6003 if (this.isAttached) { |
9522 | 6004 this.notifyResize(); |
9523 _getAnimationConfigRecursive: function(type, map, allConfigs) { | 6005 } |
9524 if (!this.animationConfig) { | 6006 }, |
9525 return; | 6007 _applyFocus: function() { |
9526 } | 6008 var focusTarget = this.focusTarget || this.containedElement; |
9527 | 6009 if (focusTarget && this.opened && !this.noAutoFocus) { |
9528 if(this.animationConfig.value && typeof this.animationConfig.value === 'fu
nction') { | 6010 focusTarget.focus(); |
9529 » this._warn(this._logf('playAnimation', "Please put 'animationConfig' ins
ide of your components 'properties' object instead of outside of it.")); | 6011 } else { |
9530 » return; | 6012 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); |
9531 } | 6013 } |
9532 | 6014 } |
9533 // type is optional | 6015 }); |
9534 var thisConfig; | 6016 })(); |
9535 if (type) { | 6017 |
9536 thisConfig = this.animationConfig[type]; | |
9537 } else { | |
9538 thisConfig = this.animationConfig; | |
9539 } | |
9540 | |
9541 if (!Array.isArray(thisConfig)) { | |
9542 thisConfig = [thisConfig]; | |
9543 } | |
9544 | |
9545 // iterate animations and recurse to process configurations from child nod
es | |
9546 if (thisConfig) { | |
9547 for (var config, index = 0; config = thisConfig[index]; index++) { | |
9548 if (config.animatable) { | |
9549 config.animatable._getAnimationConfigRecursive(config.type || type,
map, allConfigs); | |
9550 } else { | |
9551 if (config.id) { | |
9552 var cachedConfig = map[config.id]; | |
9553 if (cachedConfig) { | |
9554 // merge configurations with the same id, making a clone lazily | |
9555 if (!cachedConfig.isClone) { | |
9556 map[config.id] = this._cloneConfig(cachedConfig) | |
9557 cachedConfig = map[config.id]; | |
9558 } | |
9559 this._copyProperties(cachedConfig, config); | |
9560 } else { | |
9561 // put any configs with an id into a map | |
9562 map[config.id] = config; | |
9563 } | |
9564 } else { | |
9565 allConfigs.push(config); | |
9566 } | |
9567 } | |
9568 } | |
9569 } | |
9570 }, | |
9571 | |
9572 /** | |
9573 * An element implementing `Polymer.NeonAnimationRunnerBehavior` calls this
method to configure | |
9574 * an animation with an optional type. Elements implementing `Polymer.NeonAn
imatableBehavior` | |
9575 * should define the property `animationConfig`, which is either a configura
tion object | |
9576 * or a map of animation type to array of configuration objects. | |
9577 */ | |
9578 getAnimationConfig: function(type) { | |
9579 var map = {}; | |
9580 var allConfigs = []; | |
9581 this._getAnimationConfigRecursive(type, map, allConfigs); | |
9582 // append the configurations saved in the map to the array | |
9583 for (var key in map) { | |
9584 allConfigs.push(map[key]); | |
9585 } | |
9586 return allConfigs; | |
9587 } | |
9588 | |
9589 }; | |
9590 /** | |
9591 * `Polymer.NeonAnimationRunnerBehavior` adds a method to run animations. | |
9592 * | |
9593 * @polymerBehavior Polymer.NeonAnimationRunnerBehavior | |
9594 */ | |
9595 Polymer.NeonAnimationRunnerBehaviorImpl = { | |
9596 | |
9597 _configureAnimations: function(configs) { | |
9598 var results = []; | |
9599 if (configs.length > 0) { | |
9600 for (var config, index = 0; config = configs[index]; index++) { | |
9601 var neonAnimation = document.createElement(config.name); | |
9602 // is this element actually a neon animation? | |
9603 if (neonAnimation.isNeonAnimation) { | |
9604 var result = null; | |
9605 // configuration or play could fail if polyfills aren't loaded | |
9606 try { | |
9607 result = neonAnimation.configure(config); | |
9608 // Check if we have an Effect rather than an Animation | |
9609 if (typeof result.cancel != 'function') { | |
9610 result = document.timeline.play(result); | |
9611 } | |
9612 } catch (e) { | |
9613 result = null; | |
9614 console.warn('Couldnt play', '(', config.name, ').', e); | |
9615 } | |
9616 if (result) { | |
9617 results.push({ | |
9618 neonAnimation: neonAnimation, | |
9619 config: config, | |
9620 animation: result, | |
9621 }); | |
9622 } | |
9623 } else { | |
9624 console.warn(this.is + ':', config.name, 'not found!'); | |
9625 } | |
9626 } | |
9627 } | |
9628 return results; | |
9629 }, | |
9630 | |
9631 _shouldComplete: function(activeEntries) { | |
9632 var finished = true; | |
9633 for (var i = 0; i < activeEntries.length; i++) { | |
9634 if (activeEntries[i].animation.playState != 'finished') { | |
9635 finished = false; | |
9636 break; | |
9637 } | |
9638 } | |
9639 return finished; | |
9640 }, | |
9641 | |
9642 _complete: function(activeEntries) { | |
9643 for (var i = 0; i < activeEntries.length; i++) { | |
9644 activeEntries[i].neonAnimation.complete(activeEntries[i].config); | |
9645 } | |
9646 for (var i = 0; i < activeEntries.length; i++) { | |
9647 activeEntries[i].animation.cancel(); | |
9648 } | |
9649 }, | |
9650 | |
9651 /** | |
9652 * Plays an animation with an optional `type`. | |
9653 * @param {string=} type | |
9654 * @param {!Object=} cookie | |
9655 */ | |
9656 playAnimation: function(type, cookie) { | |
9657 var configs = this.getAnimationConfig(type); | |
9658 if (!configs) { | |
9659 return; | |
9660 } | |
9661 this._active = this._active || {}; | |
9662 if (this._active[type]) { | |
9663 this._complete(this._active[type]); | |
9664 delete this._active[type]; | |
9665 } | |
9666 | |
9667 var activeEntries = this._configureAnimations(configs); | |
9668 | |
9669 if (activeEntries.length == 0) { | |
9670 this.fire('neon-animation-finish', cookie, {bubbles: false}); | |
9671 return; | |
9672 } | |
9673 | |
9674 this._active[type] = activeEntries; | |
9675 | |
9676 for (var i = 0; i < activeEntries.length; i++) { | |
9677 activeEntries[i].animation.onfinish = function() { | |
9678 if (this._shouldComplete(activeEntries)) { | |
9679 this._complete(activeEntries); | |
9680 delete this._active[type]; | |
9681 this.fire('neon-animation-finish', cookie, {bubbles: false}); | |
9682 } | |
9683 }.bind(this); | |
9684 } | |
9685 }, | |
9686 | |
9687 /** | |
9688 * Cancels the currently running animations. | |
9689 */ | |
9690 cancelAnimation: function() { | |
9691 for (var k in this._animations) { | |
9692 this._animations[k].cancel(); | |
9693 } | |
9694 this._animations = {}; | |
9695 } | |
9696 }; | |
9697 | |
9698 /** @polymerBehavior Polymer.NeonAnimationRunnerBehavior */ | |
9699 Polymer.NeonAnimationRunnerBehavior = [ | |
9700 Polymer.NeonAnimatableBehavior, | |
9701 Polymer.NeonAnimationRunnerBehaviorImpl | |
9702 ]; | |
9703 /** | |
9704 * Use `Polymer.NeonAnimationBehavior` to implement an animation. | |
9705 * @polymerBehavior | |
9706 */ | |
9707 Polymer.NeonAnimationBehavior = { | |
9708 | |
9709 properties: { | |
9710 | |
9711 /** | |
9712 * Defines the animation timing. | |
9713 */ | |
9714 animationTiming: { | |
9715 type: Object, | |
9716 value: function() { | |
9717 return { | |
9718 duration: 500, | |
9719 easing: 'cubic-bezier(0.4, 0, 0.2, 1)', | |
9720 fill: 'both' | |
9721 } | |
9722 } | |
9723 } | |
9724 | |
9725 }, | |
9726 | |
9727 /** | |
9728 * Can be used to determine that elements implement this behavior. | |
9729 */ | |
9730 isNeonAnimation: true, | |
9731 | |
9732 /** | |
9733 * Do any animation configuration here. | |
9734 */ | |
9735 // configure: function(config) { | |
9736 // }, | |
9737 | |
9738 /** | |
9739 * Returns the animation timing by mixing in properties from `config` to the
defaults defined | |
9740 * by the animation. | |
9741 */ | |
9742 timingFromConfig: function(config) { | |
9743 if (config.timing) { | |
9744 for (var property in config.timing) { | |
9745 this.animationTiming[property] = config.timing[property]; | |
9746 } | |
9747 } | |
9748 return this.animationTiming; | |
9749 }, | |
9750 | |
9751 /** | |
9752 * Sets `transform` and `transformOrigin` properties along with the prefixed
versions. | |
9753 */ | |
9754 setPrefixedProperty: function(node, property, value) { | |
9755 var map = { | |
9756 'transform': ['webkitTransform'], | |
9757 'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin'] | |
9758 }; | |
9759 var prefixes = map[property]; | |
9760 for (var prefix, index = 0; prefix = prefixes[index]; index++) { | |
9761 node.style[prefix] = value; | |
9762 } | |
9763 node.style[property] = value; | |
9764 }, | |
9765 | |
9766 /** | |
9767 * Called when the animation finishes. | |
9768 */ | |
9769 complete: function() {} | |
9770 | |
9771 }; | |
9772 Polymer({ | 6018 Polymer({ |
9773 | 6019 is: 'fade-in-animation', |
9774 is: 'opaque-animation', | 6020 behaviors: [ Polymer.NeonAnimationBehavior ], |
9775 | 6021 configure: function(config) { |
9776 behaviors: [ | 6022 var node = config.node; |
9777 Polymer.NeonAnimationBehavior | 6023 this._effect = new KeyframeEffect(node, [ { |
9778 ], | 6024 opacity: '0' |
9779 | 6025 }, { |
9780 configure: function(config) { | 6026 opacity: '1' |
9781 var node = config.node; | 6027 } ], this.timingFromConfig(config)); |
9782 this._effect = new KeyframeEffect(node, [ | 6028 return this._effect; |
9783 {'opacity': '1'}, | 6029 } |
9784 {'opacity': '1'} | 6030 }); |
9785 ], this.timingFromConfig(config)); | 6031 |
9786 node.style.opacity = '0'; | |
9787 return this._effect; | |
9788 }, | |
9789 | |
9790 complete: function(config) { | |
9791 config.node.style.opacity = ''; | |
9792 } | |
9793 | |
9794 }); | |
9795 (function() { | |
9796 'use strict'; | |
9797 // Used to calculate the scroll direction during touch events. | |
9798 var LAST_TOUCH_POSITION = { | |
9799 pageX: 0, | |
9800 pageY: 0 | |
9801 }; | |
9802 // Used to avoid computing event.path and filter scrollable nodes (better pe
rf). | |
9803 var ROOT_TARGET = null; | |
9804 var SCROLLABLE_NODES = []; | |
9805 | |
9806 /** | |
9807 * The IronDropdownScrollManager is intended to provide a central source | |
9808 * of authority and control over which elements in a document are currently | |
9809 * allowed to scroll. | |
9810 */ | |
9811 | |
9812 Polymer.IronDropdownScrollManager = { | |
9813 | |
9814 /** | |
9815 * The current element that defines the DOM boundaries of the | |
9816 * scroll lock. This is always the most recently locking element. | |
9817 */ | |
9818 get currentLockingElement() { | |
9819 return this._lockingElements[this._lockingElements.length - 1]; | |
9820 }, | |
9821 | |
9822 /** | |
9823 * Returns true if the provided element is "scroll locked", which is to | |
9824 * say that it cannot be scrolled via pointer or keyboard interactions. | |
9825 * | |
9826 * @param {HTMLElement} element An HTML element instance which may or may | |
9827 * not be scroll locked. | |
9828 */ | |
9829 elementIsScrollLocked: function(element) { | |
9830 var currentLockingElement = this.currentLockingElement; | |
9831 | |
9832 if (currentLockingElement === undefined) | |
9833 return false; | |
9834 | |
9835 var scrollLocked; | |
9836 | |
9837 if (this._hasCachedLockedElement(element)) { | |
9838 return true; | |
9839 } | |
9840 | |
9841 if (this._hasCachedUnlockedElement(element)) { | |
9842 return false; | |
9843 } | |
9844 | |
9845 scrollLocked = !!currentLockingElement && | |
9846 currentLockingElement !== element && | |
9847 !this._composedTreeContains(currentLockingElement, element); | |
9848 | |
9849 if (scrollLocked) { | |
9850 this._lockedElementCache.push(element); | |
9851 } else { | |
9852 this._unlockedElementCache.push(element); | |
9853 } | |
9854 | |
9855 return scrollLocked; | |
9856 }, | |
9857 | |
9858 /** | |
9859 * Push an element onto the current scroll lock stack. The most recently | |
9860 * pushed element and its children will be considered scrollable. All | |
9861 * other elements will not be scrollable. | |
9862 * | |
9863 * Scroll locking is implemented as a stack so that cases such as | |
9864 * dropdowns within dropdowns are handled well. | |
9865 * | |
9866 * @param {HTMLElement} element The element that should lock scroll. | |
9867 */ | |
9868 pushScrollLock: function(element) { | |
9869 // Prevent pushing the same element twice | |
9870 if (this._lockingElements.indexOf(element) >= 0) { | |
9871 return; | |
9872 } | |
9873 | |
9874 if (this._lockingElements.length === 0) { | |
9875 this._lockScrollInteractions(); | |
9876 } | |
9877 | |
9878 this._lockingElements.push(element); | |
9879 | |
9880 this._lockedElementCache = []; | |
9881 this._unlockedElementCache = []; | |
9882 }, | |
9883 | |
9884 /** | |
9885 * Remove an element from the scroll lock stack. The element being | |
9886 * removed does not need to be the most recently pushed element. However, | |
9887 * the scroll lock constraints only change when the most recently pushed | |
9888 * element is removed. | |
9889 * | |
9890 * @param {HTMLElement} element The element to remove from the scroll | |
9891 * lock stack. | |
9892 */ | |
9893 removeScrollLock: function(element) { | |
9894 var index = this._lockingElements.indexOf(element); | |
9895 | |
9896 if (index === -1) { | |
9897 return; | |
9898 } | |
9899 | |
9900 this._lockingElements.splice(index, 1); | |
9901 | |
9902 this._lockedElementCache = []; | |
9903 this._unlockedElementCache = []; | |
9904 | |
9905 if (this._lockingElements.length === 0) { | |
9906 this._unlockScrollInteractions(); | |
9907 } | |
9908 }, | |
9909 | |
9910 _lockingElements: [], | |
9911 | |
9912 _lockedElementCache: null, | |
9913 | |
9914 _unlockedElementCache: null, | |
9915 | |
9916 _hasCachedLockedElement: function(element) { | |
9917 return this._lockedElementCache.indexOf(element) > -1; | |
9918 }, | |
9919 | |
9920 _hasCachedUnlockedElement: function(element) { | |
9921 return this._unlockedElementCache.indexOf(element) > -1; | |
9922 }, | |
9923 | |
9924 _composedTreeContains: function(element, child) { | |
9925 // NOTE(cdata): This method iterates over content elements and their | |
9926 // corresponding distributed nodes to implement a contains-like method | |
9927 // that pierces through the composed tree of the ShadowDOM. Results of | |
9928 // this operation are cached (elsewhere) on a per-scroll-lock basis, to | |
9929 // guard against potentially expensive lookups happening repeatedly as | |
9930 // a user scrolls / touchmoves. | |
9931 var contentElements; | |
9932 var distributedNodes; | |
9933 var contentIndex; | |
9934 var nodeIndex; | |
9935 | |
9936 if (element.contains(child)) { | |
9937 return true; | |
9938 } | |
9939 | |
9940 contentElements = Polymer.dom(element).querySelectorAll('content'); | |
9941 | |
9942 for (contentIndex = 0; contentIndex < contentElements.length; ++contentI
ndex) { | |
9943 | |
9944 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistr
ibutedNodes(); | |
9945 | |
9946 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex)
{ | |
9947 | |
9948 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ | |
9949 return true; | |
9950 } | |
9951 } | |
9952 } | |
9953 | |
9954 return false; | |
9955 }, | |
9956 | |
9957 _scrollInteractionHandler: function(event) { | |
9958 // Avoid canceling an event with cancelable=false, e.g. scrolling is in | |
9959 // progress and cannot be interrupted. | |
9960 if (event.cancelable && this._shouldPreventScrolling(event)) { | |
9961 event.preventDefault(); | |
9962 } | |
9963 // If event has targetTouches (touch event), update last touch position. | |
9964 if (event.targetTouches) { | |
9965 var touch = event.targetTouches[0]; | |
9966 LAST_TOUCH_POSITION.pageX = touch.pageX; | |
9967 LAST_TOUCH_POSITION.pageY = touch.pageY; | |
9968 } | |
9969 }, | |
9970 | |
9971 _lockScrollInteractions: function() { | |
9972 this._boundScrollHandler = this._boundScrollHandler || | |
9973 this._scrollInteractionHandler.bind(this); | |
9974 // Modern `wheel` event for mouse wheel scrolling: | |
9975 document.addEventListener('wheel', this._boundScrollHandler, true); | |
9976 // Older, non-standard `mousewheel` event for some FF: | |
9977 document.addEventListener('mousewheel', this._boundScrollHandler, true); | |
9978 // IE: | |
9979 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr
ue); | |
9980 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove. | |
9981 document.addEventListener('touchstart', this._boundScrollHandler, true); | |
9982 // Mobile devices can scroll on touch move: | |
9983 document.addEventListener('touchmove', this._boundScrollHandler, true); | |
9984 }, | |
9985 | |
9986 _unlockScrollInteractions: function() { | |
9987 document.removeEventListener('wheel', this._boundScrollHandler, true); | |
9988 document.removeEventListener('mousewheel', this._boundScrollHandler, tru
e); | |
9989 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler,
true); | |
9990 document.removeEventListener('touchstart', this._boundScrollHandler, tru
e); | |
9991 document.removeEventListener('touchmove', this._boundScrollHandler, true
); | |
9992 }, | |
9993 | |
9994 /** | |
9995 * Returns true if the event causes scroll outside the current locking | |
9996 * element, e.g. pointer/keyboard interactions, or scroll "leaking" | |
9997 * outside the locking element when it is already at its scroll boundaries
. | |
9998 * @param {!Event} event | |
9999 * @return {boolean} | |
10000 * @private | |
10001 */ | |
10002 _shouldPreventScrolling: function(event) { | |
10003 | |
10004 // Update if root target changed. For touch events, ensure we don't | |
10005 // update during touchmove. | |
10006 var target = Polymer.dom(event).rootTarget; | |
10007 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { | |
10008 ROOT_TARGET = target; | |
10009 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); | |
10010 } | |
10011 | |
10012 // Prevent event if no scrollable nodes. | |
10013 if (!SCROLLABLE_NODES.length) { | |
10014 return true; | |
10015 } | |
10016 // Don't prevent touchstart event inside the locking element when it has | |
10017 // scrollable nodes. | |
10018 if (event.type === 'touchstart') { | |
10019 return false; | |
10020 } | |
10021 // Get deltaX/Y. | |
10022 var info = this._getScrollInfo(event); | |
10023 // Prevent if there is no child that can scroll. | |
10024 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta
Y); | |
10025 }, | |
10026 | |
10027 /** | |
10028 * Returns an array of scrollable nodes up to the current locking element, | |
10029 * which is included too if scrollable. | |
10030 * @param {!Array<Node>} nodes | |
10031 * @return {Array<Node>} scrollables | |
10032 * @private | |
10033 */ | |
10034 _getScrollableNodes: function(nodes) { | |
10035 var scrollables = []; | |
10036 var lockingIndex = nodes.indexOf(this.currentLockingElement); | |
10037 // Loop from root target to locking element (included). | |
10038 for (var i = 0; i <= lockingIndex; i++) { | |
10039 var node = nodes[i]; | |
10040 // Skip document fragments. | |
10041 if (node.nodeType === 11) { | |
10042 continue; | |
10043 } | |
10044 // Check inline style before checking computed style. | |
10045 var style = node.style; | |
10046 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { | |
10047 style = window.getComputedStyle(node); | |
10048 } | |
10049 if (style.overflow === 'scroll' || style.overflow === 'auto') { | |
10050 scrollables.push(node); | |
10051 } | |
10052 } | |
10053 return scrollables; | |
10054 }, | |
10055 | |
10056 /** | |
10057 * Returns the node that is scrolling. If there is no scrolling, | |
10058 * returns undefined. | |
10059 * @param {!Array<Node>} nodes | |
10060 * @param {number} deltaX Scroll delta on the x-axis | |
10061 * @param {number} deltaY Scroll delta on the y-axis | |
10062 * @return {Node|undefined} | |
10063 * @private | |
10064 */ | |
10065 _getScrollingNode: function(nodes, deltaX, deltaY) { | |
10066 // No scroll. | |
10067 if (!deltaX && !deltaY) { | |
10068 return; | |
10069 } | |
10070 // Check only one axis according to where there is more scroll. | |
10071 // Prefer vertical to horizontal. | |
10072 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); | |
10073 for (var i = 0; i < nodes.length; i++) { | |
10074 var node = nodes[i]; | |
10075 var canScroll = false; | |
10076 if (verticalScroll) { | |
10077 // delta < 0 is scroll up, delta > 0 is scroll down. | |
10078 canScroll = deltaY < 0 ? node.scrollTop > 0 : | |
10079 node.scrollTop < node.scrollHeight - node.clientHeight; | |
10080 } else { | |
10081 // delta < 0 is scroll left, delta > 0 is scroll right. | |
10082 canScroll = deltaX < 0 ? node.scrollLeft > 0 : | |
10083 node.scrollLeft < node.scrollWidth - node.clientWidth; | |
10084 } | |
10085 if (canScroll) { | |
10086 return node; | |
10087 } | |
10088 } | |
10089 }, | |
10090 | |
10091 /** | |
10092 * Returns scroll `deltaX` and `deltaY`. | |
10093 * @param {!Event} event The scroll event | |
10094 * @return {{ | |
10095 * deltaX: number The x-axis scroll delta (positive: scroll right, | |
10096 * negative: scroll left, 0: no scroll), | |
10097 * deltaY: number The y-axis scroll delta (positive: scroll down, | |
10098 * negative: scroll up, 0: no scroll) | |
10099 * }} info | |
10100 * @private | |
10101 */ | |
10102 _getScrollInfo: function(event) { | |
10103 var info = { | |
10104 deltaX: event.deltaX, | |
10105 deltaY: event.deltaY | |
10106 }; | |
10107 // Already available. | |
10108 if ('deltaX' in event) { | |
10109 // do nothing, values are already good. | |
10110 } | |
10111 // Safari has scroll info in `wheelDeltaX/Y`. | |
10112 else if ('wheelDeltaX' in event) { | |
10113 info.deltaX = -event.wheelDeltaX; | |
10114 info.deltaY = -event.wheelDeltaY; | |
10115 } | |
10116 // Firefox has scroll info in `detail` and `axis`. | |
10117 else if ('axis' in event) { | |
10118 info.deltaX = event.axis === 1 ? event.detail : 0; | |
10119 info.deltaY = event.axis === 2 ? event.detail : 0; | |
10120 } | |
10121 // On mobile devices, calculate scroll direction. | |
10122 else if (event.targetTouches) { | |
10123 var touch = event.targetTouches[0]; | |
10124 // Touch moves from right to left => scrolling goes right. | |
10125 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; | |
10126 // Touch moves from down to up => scrolling goes down. | |
10127 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; | |
10128 } | |
10129 return info; | |
10130 } | |
10131 }; | |
10132 })(); | |
10133 (function() { | |
10134 'use strict'; | |
10135 | |
10136 Polymer({ | |
10137 is: 'iron-dropdown', | |
10138 | |
10139 behaviors: [ | |
10140 Polymer.IronControlState, | |
10141 Polymer.IronA11yKeysBehavior, | |
10142 Polymer.IronOverlayBehavior, | |
10143 Polymer.NeonAnimationRunnerBehavior | |
10144 ], | |
10145 | |
10146 properties: { | |
10147 /** | |
10148 * The orientation against which to align the dropdown content | |
10149 * horizontally relative to the dropdown trigger. | |
10150 * Overridden from `Polymer.IronFitBehavior`. | |
10151 */ | |
10152 horizontalAlign: { | |
10153 type: String, | |
10154 value: 'left', | |
10155 reflectToAttribute: true | |
10156 }, | |
10157 | |
10158 /** | |
10159 * The orientation against which to align the dropdown content | |
10160 * vertically relative to the dropdown trigger. | |
10161 * Overridden from `Polymer.IronFitBehavior`. | |
10162 */ | |
10163 verticalAlign: { | |
10164 type: String, | |
10165 value: 'top', | |
10166 reflectToAttribute: true | |
10167 }, | |
10168 | |
10169 /** | |
10170 * An animation config. If provided, this will be used to animate the | |
10171 * opening of the dropdown. | |
10172 */ | |
10173 openAnimationConfig: { | |
10174 type: Object | |
10175 }, | |
10176 | |
10177 /** | |
10178 * An animation config. If provided, this will be used to animate the | |
10179 * closing of the dropdown. | |
10180 */ | |
10181 closeAnimationConfig: { | |
10182 type: Object | |
10183 }, | |
10184 | |
10185 /** | |
10186 * If provided, this will be the element that will be focused when | |
10187 * the dropdown opens. | |
10188 */ | |
10189 focusTarget: { | |
10190 type: Object | |
10191 }, | |
10192 | |
10193 /** | |
10194 * Set to true to disable animations when opening and closing the | |
10195 * dropdown. | |
10196 */ | |
10197 noAnimations: { | |
10198 type: Boolean, | |
10199 value: false | |
10200 }, | |
10201 | |
10202 /** | |
10203 * By default, the dropdown will constrain scrolling on the page | |
10204 * to itself when opened. | |
10205 * Set to true in order to prevent scroll from being constrained | |
10206 * to the dropdown when it opens. | |
10207 */ | |
10208 allowOutsideScroll: { | |
10209 type: Boolean, | |
10210 value: false | |
10211 }, | |
10212 | |
10213 /** | |
10214 * Callback for scroll events. | |
10215 * @type {Function} | |
10216 * @private | |
10217 */ | |
10218 _boundOnCaptureScroll: { | |
10219 type: Function, | |
10220 value: function() { | |
10221 return this._onCaptureScroll.bind(this); | |
10222 } | |
10223 } | |
10224 }, | |
10225 | |
10226 listeners: { | |
10227 'neon-animation-finish': '_onNeonAnimationFinish' | |
10228 }, | |
10229 | |
10230 observers: [ | |
10231 '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign
, verticalOffset, horizontalOffset)' | |
10232 ], | |
10233 | |
10234 /** | |
10235 * The element that is contained by the dropdown, if any. | |
10236 */ | |
10237 get containedElement() { | |
10238 return Polymer.dom(this.$.content).getDistributedNodes()[0]; | |
10239 }, | |
10240 | |
10241 /** | |
10242 * The element that should be focused when the dropdown opens. | |
10243 * @deprecated | |
10244 */ | |
10245 get _focusTarget() { | |
10246 return this.focusTarget || this.containedElement; | |
10247 }, | |
10248 | |
10249 ready: function() { | |
10250 // Memoized scrolling position, used to block scrolling outside. | |
10251 this._scrollTop = 0; | |
10252 this._scrollLeft = 0; | |
10253 // Used to perform a non-blocking refit on scroll. | |
10254 this._refitOnScrollRAF = null; | |
10255 }, | |
10256 | |
10257 detached: function() { | |
10258 this.cancelAnimation(); | |
10259 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
10260 }, | |
10261 | |
10262 /** | |
10263 * Called when the value of `opened` changes. | |
10264 * Overridden from `IronOverlayBehavior` | |
10265 */ | |
10266 _openedChanged: function() { | |
10267 if (this.opened && this.disabled) { | |
10268 this.cancel(); | |
10269 } else { | |
10270 this.cancelAnimation(); | |
10271 this.sizingTarget = this.containedElement || this.sizingTarget; | |
10272 this._updateAnimationConfig(); | |
10273 this._saveScrollPosition(); | |
10274 if (this.opened) { | |
10275 document.addEventListener('scroll', this._boundOnCaptureScroll); | |
10276 !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.push
ScrollLock(this); | |
10277 } else { | |
10278 document.removeEventListener('scroll', this._boundOnCaptureScroll)
; | |
10279 Polymer.IronDropdownScrollManager.removeScrollLock(this); | |
10280 } | |
10281 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments
); | |
10282 } | |
10283 }, | |
10284 | |
10285 /** | |
10286 * Overridden from `IronOverlayBehavior`. | |
10287 */ | |
10288 _renderOpened: function() { | |
10289 if (!this.noAnimations && this.animationConfig.open) { | |
10290 this.$.contentWrapper.classList.add('animating'); | |
10291 this.playAnimation('open'); | |
10292 } else { | |
10293 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments)
; | |
10294 } | |
10295 }, | |
10296 | |
10297 /** | |
10298 * Overridden from `IronOverlayBehavior`. | |
10299 */ | |
10300 _renderClosed: function() { | |
10301 | |
10302 if (!this.noAnimations && this.animationConfig.close) { | |
10303 this.$.contentWrapper.classList.add('animating'); | |
10304 this.playAnimation('close'); | |
10305 } else { | |
10306 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments)
; | |
10307 } | |
10308 }, | |
10309 | |
10310 /** | |
10311 * Called when animation finishes on the dropdown (when opening or | |
10312 * closing). Responsible for "completing" the process of opening or | |
10313 * closing the dropdown by positioning it or setting its display to | |
10314 * none. | |
10315 */ | |
10316 _onNeonAnimationFinish: function() { | |
10317 this.$.contentWrapper.classList.remove('animating'); | |
10318 if (this.opened) { | |
10319 this._finishRenderOpened(); | |
10320 } else { | |
10321 this._finishRenderClosed(); | |
10322 } | |
10323 }, | |
10324 | |
10325 _onCaptureScroll: function() { | |
10326 if (!this.allowOutsideScroll) { | |
10327 this._restoreScrollPosition(); | |
10328 } else { | |
10329 this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnS
crollRAF); | |
10330 this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bin
d(this)); | |
10331 } | |
10332 }, | |
10333 | |
10334 /** | |
10335 * Memoizes the scroll position of the outside scrolling element. | |
10336 * @private | |
10337 */ | |
10338 _saveScrollPosition: function() { | |
10339 if (document.scrollingElement) { | |
10340 this._scrollTop = document.scrollingElement.scrollTop; | |
10341 this._scrollLeft = document.scrollingElement.scrollLeft; | |
10342 } else { | |
10343 // Since we don't know if is the body or html, get max. | |
10344 this._scrollTop = Math.max(document.documentElement.scrollTop, docum
ent.body.scrollTop); | |
10345 this._scrollLeft = Math.max(document.documentElement.scrollLeft, doc
ument.body.scrollLeft); | |
10346 } | |
10347 }, | |
10348 | |
10349 /** | |
10350 * Resets the scroll position of the outside scrolling element. | |
10351 * @private | |
10352 */ | |
10353 _restoreScrollPosition: function() { | |
10354 if (document.scrollingElement) { | |
10355 document.scrollingElement.scrollTop = this._scrollTop; | |
10356 document.scrollingElement.scrollLeft = this._scrollLeft; | |
10357 } else { | |
10358 // Since we don't know if is the body or html, set both. | |
10359 document.documentElement.scrollTop = this._scrollTop; | |
10360 document.documentElement.scrollLeft = this._scrollLeft; | |
10361 document.body.scrollTop = this._scrollTop; | |
10362 document.body.scrollLeft = this._scrollLeft; | |
10363 } | |
10364 }, | |
10365 | |
10366 /** | |
10367 * Constructs the final animation config from different properties used | |
10368 * to configure specific parts of the opening and closing animations. | |
10369 */ | |
10370 _updateAnimationConfig: function() { | |
10371 var animations = (this.openAnimationConfig || []).concat(this.closeAni
mationConfig || []); | |
10372 for (var i = 0; i < animations.length; i++) { | |
10373 animations[i].node = this.containedElement; | |
10374 } | |
10375 this.animationConfig = { | |
10376 open: this.openAnimationConfig, | |
10377 close: this.closeAnimationConfig | |
10378 }; | |
10379 }, | |
10380 | |
10381 /** | |
10382 * Updates the overlay position based on configured horizontal | |
10383 * and vertical alignment. | |
10384 */ | |
10385 _updateOverlayPosition: function() { | |
10386 if (this.isAttached) { | |
10387 // This triggers iron-resize, and iron-overlay-behavior will call re
fit if needed. | |
10388 this.notifyResize(); | |
10389 } | |
10390 }, | |
10391 | |
10392 /** | |
10393 * Apply focus to focusTarget or containedElement | |
10394 */ | |
10395 _applyFocus: function () { | |
10396 var focusTarget = this.focusTarget || this.containedElement; | |
10397 if (focusTarget && this.opened && !this.noAutoFocus) { | |
10398 focusTarget.focus(); | |
10399 } else { | |
10400 Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); | |
10401 } | |
10402 } | |
10403 }); | |
10404 })(); | |
10405 Polymer({ | 6032 Polymer({ |
10406 | 6033 is: 'fade-out-animation', |
10407 is: 'fade-in-animation', | 6034 behaviors: [ Polymer.NeonAnimationBehavior ], |
10408 | 6035 configure: function(config) { |
10409 behaviors: [ | 6036 var node = config.node; |
10410 Polymer.NeonAnimationBehavior | 6037 this._effect = new KeyframeEffect(node, [ { |
10411 ], | 6038 opacity: '1' |
10412 | 6039 }, { |
10413 configure: function(config) { | 6040 opacity: '0' |
10414 var node = config.node; | 6041 } ], this.timingFromConfig(config)); |
10415 this._effect = new KeyframeEffect(node, [ | 6042 return this._effect; |
10416 {'opacity': '0'}, | 6043 } |
10417 {'opacity': '1'} | 6044 }); |
10418 ], this.timingFromConfig(config)); | 6045 |
10419 return this._effect; | |
10420 } | |
10421 | |
10422 }); | |
10423 Polymer({ | 6046 Polymer({ |
10424 | 6047 is: 'paper-menu-grow-height-animation', |
10425 is: 'fade-out-animation', | 6048 behaviors: [ Polymer.NeonAnimationBehavior ], |
10426 | 6049 configure: function(config) { |
10427 behaviors: [ | 6050 var node = config.node; |
10428 Polymer.NeonAnimationBehavior | 6051 var rect = node.getBoundingClientRect(); |
10429 ], | 6052 var height = rect.height; |
10430 | 6053 this._effect = new KeyframeEffect(node, [ { |
10431 configure: function(config) { | 6054 height: height / 2 + 'px' |
10432 var node = config.node; | 6055 }, { |
10433 this._effect = new KeyframeEffect(node, [ | 6056 height: height + 'px' |
10434 {'opacity': '1'}, | 6057 } ], this.timingFromConfig(config)); |
10435 {'opacity': '0'} | 6058 return this._effect; |
10436 ], this.timingFromConfig(config)); | 6059 } |
10437 return this._effect; | 6060 }); |
10438 } | 6061 |
10439 | |
10440 }); | |
10441 Polymer({ | 6062 Polymer({ |
10442 is: 'paper-menu-grow-height-animation', | 6063 is: 'paper-menu-grow-width-animation', |
10443 | 6064 behaviors: [ Polymer.NeonAnimationBehavior ], |
10444 behaviors: [ | 6065 configure: function(config) { |
10445 Polymer.NeonAnimationBehavior | 6066 var node = config.node; |
10446 ], | 6067 var rect = node.getBoundingClientRect(); |
10447 | 6068 var width = rect.width; |
10448 configure: function(config) { | 6069 this._effect = new KeyframeEffect(node, [ { |
10449 var node = config.node; | 6070 width: width / 2 + 'px' |
10450 var rect = node.getBoundingClientRect(); | 6071 }, { |
10451 var height = rect.height; | 6072 width: width + 'px' |
10452 | 6073 } ], this.timingFromConfig(config)); |
10453 this._effect = new KeyframeEffect(node, [{ | 6074 return this._effect; |
10454 height: (height / 2) + 'px' | 6075 } |
10455 }, { | 6076 }); |
10456 height: height + 'px' | 6077 |
10457 }], this.timingFromConfig(config)); | 6078 Polymer({ |
10458 | 6079 is: 'paper-menu-shrink-width-animation', |
10459 return this._effect; | 6080 behaviors: [ Polymer.NeonAnimationBehavior ], |
10460 } | 6081 configure: function(config) { |
10461 }); | 6082 var node = config.node; |
10462 | 6083 var rect = node.getBoundingClientRect(); |
10463 Polymer({ | 6084 var width = rect.width; |
10464 is: 'paper-menu-grow-width-animation', | 6085 this._effect = new KeyframeEffect(node, [ { |
10465 | 6086 width: width + 'px' |
10466 behaviors: [ | 6087 }, { |
10467 Polymer.NeonAnimationBehavior | 6088 width: width - width / 20 + 'px' |
10468 ], | 6089 } ], this.timingFromConfig(config)); |
10469 | 6090 return this._effect; |
10470 configure: function(config) { | 6091 } |
10471 var node = config.node; | 6092 }); |
10472 var rect = node.getBoundingClientRect(); | 6093 |
10473 var width = rect.width; | 6094 Polymer({ |
10474 | 6095 is: 'paper-menu-shrink-height-animation', |
10475 this._effect = new KeyframeEffect(node, [{ | 6096 behaviors: [ Polymer.NeonAnimationBehavior ], |
10476 width: (width / 2) + 'px' | 6097 configure: function(config) { |
10477 }, { | 6098 var node = config.node; |
10478 width: width + 'px' | 6099 var rect = node.getBoundingClientRect(); |
10479 }], this.timingFromConfig(config)); | 6100 var height = rect.height; |
10480 | 6101 var top = rect.top; |
10481 return this._effect; | 6102 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); |
10482 } | 6103 this._effect = new KeyframeEffect(node, [ { |
10483 }); | 6104 height: height + 'px', |
10484 | 6105 transform: 'translateY(0)' |
10485 Polymer({ | 6106 }, { |
10486 is: 'paper-menu-shrink-width-animation', | 6107 height: height / 2 + 'px', |
10487 | 6108 transform: 'translateY(-20px)' |
10488 behaviors: [ | 6109 } ], this.timingFromConfig(config)); |
10489 Polymer.NeonAnimationBehavior | 6110 return this._effect; |
10490 ], | 6111 } |
10491 | 6112 }); |
10492 configure: function(config) { | 6113 |
10493 var node = config.node; | |
10494 var rect = node.getBoundingClientRect(); | |
10495 var width = rect.width; | |
10496 | |
10497 this._effect = new KeyframeEffect(node, [{ | |
10498 width: width + 'px' | |
10499 }, { | |
10500 width: width - (width / 20) + 'px' | |
10501 }], this.timingFromConfig(config)); | |
10502 | |
10503 return this._effect; | |
10504 } | |
10505 }); | |
10506 | |
10507 Polymer({ | |
10508 is: 'paper-menu-shrink-height-animation', | |
10509 | |
10510 behaviors: [ | |
10511 Polymer.NeonAnimationBehavior | |
10512 ], | |
10513 | |
10514 configure: function(config) { | |
10515 var node = config.node; | |
10516 var rect = node.getBoundingClientRect(); | |
10517 var height = rect.height; | |
10518 var top = rect.top; | |
10519 | |
10520 this.setPrefixedProperty(node, 'transformOrigin', '0 0'); | |
10521 | |
10522 this._effect = new KeyframeEffect(node, [{ | |
10523 height: height + 'px', | |
10524 transform: 'translateY(0)' | |
10525 }, { | |
10526 height: height / 2 + 'px', | |
10527 transform: 'translateY(-20px)' | |
10528 }], this.timingFromConfig(config)); | |
10529 | |
10530 return this._effect; | |
10531 } | |
10532 }); | |
10533 // Copyright 2016 The Chromium Authors. All rights reserved. | 6114 // Copyright 2016 The Chromium Authors. All rights reserved. |
10534 // Use of this source code is governed by a BSD-style license that can be | 6115 // Use of this source code is governed by a BSD-style license that can be |
10535 // found in the LICENSE file. | 6116 // found in the LICENSE file. |
10536 | |
10537 /** Same as paper-menu-button's custom easing cubic-bezier param. */ | |
10538 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; | 6117 var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)'; |
10539 | 6118 |
10540 Polymer({ | 6119 Polymer({ |
10541 is: 'cr-shared-menu', | 6120 is: 'cr-shared-menu', |
10542 | 6121 behaviors: [ Polymer.IronA11yKeysBehavior ], |
10543 behaviors: [Polymer.IronA11yKeysBehavior], | |
10544 | |
10545 properties: { | 6122 properties: { |
10546 menuOpen: { | 6123 menuOpen: { |
10547 type: Boolean, | 6124 type: Boolean, |
10548 observer: 'menuOpenChanged_', | 6125 observer: 'menuOpenChanged_', |
10549 value: false, | 6126 value: false, |
10550 notify: true, | 6127 notify: true |
10551 }, | 6128 }, |
10552 | |
10553 /** | |
10554 * The contextual item that this menu was clicked for. | |
10555 * e.g. the data used to render an item in an <iron-list> or <dom-repeat> | |
10556 * @type {?Object} | |
10557 */ | |
10558 itemData: { | 6129 itemData: { |
10559 type: Object, | 6130 type: Object, |
10560 value: null, | 6131 value: null |
10561 }, | 6132 }, |
10562 | |
10563 /** @override */ | |
10564 keyEventTarget: { | 6133 keyEventTarget: { |
10565 type: Object, | 6134 type: Object, |
10566 value: function() { | 6135 value: function() { |
10567 return this.$.menu; | 6136 return this.$.menu; |
10568 } | 6137 } |
10569 }, | 6138 }, |
10570 | |
10571 openAnimationConfig: { | 6139 openAnimationConfig: { |
10572 type: Object, | 6140 type: Object, |
10573 value: function() { | 6141 value: function() { |
10574 return [{ | 6142 return [ { |
10575 name: 'fade-in-animation', | 6143 name: 'fade-in-animation', |
10576 timing: { | 6144 timing: { |
10577 delay: 50, | 6145 delay: 50, |
10578 duration: 200 | 6146 duration: 200 |
10579 } | 6147 } |
10580 }, { | 6148 }, { |
10581 name: 'paper-menu-grow-width-animation', | 6149 name: 'paper-menu-grow-width-animation', |
10582 timing: { | 6150 timing: { |
10583 delay: 50, | 6151 delay: 50, |
10584 duration: 150, | 6152 duration: 150, |
10585 easing: SLIDE_CUBIC_BEZIER | 6153 easing: SLIDE_CUBIC_BEZIER |
10586 } | 6154 } |
10587 }, { | 6155 }, { |
10588 name: 'paper-menu-grow-height-animation', | 6156 name: 'paper-menu-grow-height-animation', |
10589 timing: { | 6157 timing: { |
10590 delay: 100, | 6158 delay: 100, |
10591 duration: 275, | 6159 duration: 275, |
10592 easing: SLIDE_CUBIC_BEZIER | 6160 easing: SLIDE_CUBIC_BEZIER |
10593 } | 6161 } |
10594 }]; | 6162 } ]; |
10595 } | 6163 } |
10596 }, | 6164 }, |
10597 | |
10598 closeAnimationConfig: { | 6165 closeAnimationConfig: { |
10599 type: Object, | 6166 type: Object, |
10600 value: function() { | 6167 value: function() { |
10601 return [{ | 6168 return [ { |
10602 name: 'fade-out-animation', | 6169 name: 'fade-out-animation', |
10603 timing: { | 6170 timing: { |
10604 duration: 150 | 6171 duration: 150 |
10605 } | 6172 } |
10606 }]; | 6173 } ]; |
10607 } | 6174 } |
10608 } | 6175 } |
10609 }, | 6176 }, |
10610 | |
10611 keyBindings: { | 6177 keyBindings: { |
10612 'tab': 'onTabPressed_', | 6178 tab: 'onTabPressed_' |
10613 }, | 6179 }, |
10614 | |
10615 listeners: { | 6180 listeners: { |
10616 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_', | 6181 'dropdown.iron-overlay-canceled': 'onOverlayCanceled_' |
10617 }, | 6182 }, |
10618 | |
10619 /** | |
10620 * The last anchor that was used to open a menu. It's necessary for toggling. | |
10621 * @private {?Element} | |
10622 */ | |
10623 lastAnchor_: null, | 6183 lastAnchor_: null, |
10624 | |
10625 /** | |
10626 * The first focusable child in the menu's light DOM. | |
10627 * @private {?Element} | |
10628 */ | |
10629 firstFocus_: null, | 6184 firstFocus_: null, |
10630 | |
10631 /** | |
10632 * The last focusable child in the menu's light DOM. | |
10633 * @private {?Element} | |
10634 */ | |
10635 lastFocus_: null, | 6185 lastFocus_: null, |
10636 | |
10637 /** @override */ | |
10638 attached: function() { | 6186 attached: function() { |
10639 window.addEventListener('resize', this.closeMenu.bind(this)); | 6187 window.addEventListener('resize', this.closeMenu.bind(this)); |
10640 }, | 6188 }, |
10641 | |
10642 /** Closes the menu. */ | |
10643 closeMenu: function() { | 6189 closeMenu: function() { |
10644 if (this.root.activeElement == null) { | 6190 if (this.root.activeElement == null) { |
10645 // Something else has taken focus away from the menu. Do not attempt to | |
10646 // restore focus to the button which opened the menu. | |
10647 this.$.dropdown.restoreFocusOnClose = false; | 6191 this.$.dropdown.restoreFocusOnClose = false; |
10648 } | 6192 } |
10649 this.menuOpen = false; | 6193 this.menuOpen = false; |
10650 }, | 6194 }, |
10651 | |
10652 /** | |
10653 * Opens the menu at the anchor location. | |
10654 * @param {!Element} anchor The location to display the menu. | |
10655 * @param {!Object} itemData The contextual item's data. | |
10656 */ | |
10657 openMenu: function(anchor, itemData) { | 6195 openMenu: function(anchor, itemData) { |
10658 if (this.lastAnchor_ == anchor && this.menuOpen) | 6196 if (this.lastAnchor_ == anchor && this.menuOpen) return; |
10659 return; | 6197 if (this.menuOpen) this.closeMenu(); |
10660 | |
10661 if (this.menuOpen) | |
10662 this.closeMenu(); | |
10663 | |
10664 this.itemData = itemData; | 6198 this.itemData = itemData; |
10665 this.lastAnchor_ = anchor; | 6199 this.lastAnchor_ = anchor; |
10666 this.$.dropdown.restoreFocusOnClose = true; | 6200 this.$.dropdown.restoreFocusOnClose = true; |
10667 | 6201 var focusableChildren = Polymer.dom(this).querySelectorAll('[tabindex]:not([
hidden]),button:not([hidden])'); |
10668 var focusableChildren = Polymer.dom(this).querySelectorAll( | |
10669 '[tabindex]:not([hidden]),button:not([hidden])'); | |
10670 if (focusableChildren.length > 0) { | 6202 if (focusableChildren.length > 0) { |
10671 this.$.dropdown.focusTarget = focusableChildren[0]; | 6203 this.$.dropdown.focusTarget = focusableChildren[0]; |
10672 this.firstFocus_ = focusableChildren[0]; | 6204 this.firstFocus_ = focusableChildren[0]; |
10673 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; | 6205 this.lastFocus_ = focusableChildren[focusableChildren.length - 1]; |
10674 } | 6206 } |
10675 | |
10676 // Move the menu to the anchor. | |
10677 this.$.dropdown.positionTarget = anchor; | 6207 this.$.dropdown.positionTarget = anchor; |
10678 this.menuOpen = true; | 6208 this.menuOpen = true; |
10679 }, | 6209 }, |
10680 | |
10681 /** | |
10682 * Toggles the menu for the anchor that is passed in. | |
10683 * @param {!Element} anchor The location to display the menu. | |
10684 * @param {!Object} itemData The contextual item's data. | |
10685 */ | |
10686 toggleMenu: function(anchor, itemData) { | 6210 toggleMenu: function(anchor, itemData) { |
10687 if (anchor == this.lastAnchor_ && this.menuOpen) | 6211 if (anchor == this.lastAnchor_ && this.menuOpen) this.closeMenu(); else this
.openMenu(anchor, itemData); |
10688 this.closeMenu(); | 6212 }, |
10689 else | |
10690 this.openMenu(anchor, itemData); | |
10691 }, | |
10692 | |
10693 /** | |
10694 * Trap focus inside the menu. As a very basic heuristic, will wrap focus from | |
10695 * the first element with a nonzero tabindex to the last such element. | |
10696 * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available | |
10697 * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179). | |
10698 * @param {CustomEvent} e | |
10699 */ | |
10700 onTabPressed_: function(e) { | 6213 onTabPressed_: function(e) { |
10701 if (!this.firstFocus_ || !this.lastFocus_) | 6214 if (!this.firstFocus_ || !this.lastFocus_) return; |
10702 return; | |
10703 | |
10704 var toFocus; | 6215 var toFocus; |
10705 var keyEvent = e.detail.keyboardEvent; | 6216 var keyEvent = e.detail.keyboardEvent; |
10706 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) | 6217 if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_) toFocus = this
.lastFocus_; else if (keyEvent.target == this.lastFocus_) toFocus = this.firstFo
cus_; |
10707 toFocus = this.lastFocus_; | 6218 if (!toFocus) return; |
10708 else if (keyEvent.target == this.lastFocus_) | |
10709 toFocus = this.firstFocus_; | |
10710 | |
10711 if (!toFocus) | |
10712 return; | |
10713 | |
10714 e.preventDefault(); | 6219 e.preventDefault(); |
10715 toFocus.focus(); | 6220 toFocus.focus(); |
10716 }, | 6221 }, |
10717 | |
10718 /** | |
10719 * Ensure the menu is reset properly when it is closed by the dropdown (eg, | |
10720 * clicking outside). | |
10721 * @private | |
10722 */ | |
10723 menuOpenChanged_: function() { | 6222 menuOpenChanged_: function() { |
10724 if (!this.menuOpen) { | 6223 if (!this.menuOpen) { |
10725 this.itemData = null; | 6224 this.itemData = null; |
10726 this.lastAnchor_ = null; | 6225 this.lastAnchor_ = null; |
10727 } | 6226 } |
10728 }, | 6227 }, |
10729 | |
10730 /** | |
10731 * Prevent focus restoring when tapping outside the menu. This stops the | |
10732 * focus moving around unexpectedly when closing the menu with the mouse. | |
10733 * @param {CustomEvent} e | |
10734 * @private | |
10735 */ | |
10736 onOverlayCanceled_: function(e) { | 6228 onOverlayCanceled_: function(e) { |
10737 if (e.detail.type == 'tap') | 6229 if (e.detail.type == 'tap') this.$.dropdown.restoreFocusOnClose = false; |
10738 this.$.dropdown.restoreFocusOnClose = false; | 6230 } |
10739 }, | 6231 }); |
10740 }); | 6232 |
10741 /** @polymerBehavior Polymer.PaperItemBehavior */ | 6233 Polymer.PaperItemBehaviorImpl = { |
10742 Polymer.PaperItemBehaviorImpl = { | 6234 hostAttributes: { |
10743 hostAttributes: { | 6235 role: 'option', |
10744 role: 'option', | 6236 tabindex: '0' |
10745 tabindex: '0' | 6237 } |
10746 } | 6238 }; |
10747 }; | 6239 |
10748 | 6240 Polymer.PaperItemBehavior = [ Polymer.IronButtonState, Polymer.IronControlState,
Polymer.PaperItemBehaviorImpl ]; |
10749 /** @polymerBehavior */ | 6241 |
10750 Polymer.PaperItemBehavior = [ | |
10751 Polymer.IronButtonState, | |
10752 Polymer.IronControlState, | |
10753 Polymer.PaperItemBehaviorImpl | |
10754 ]; | |
10755 Polymer({ | 6242 Polymer({ |
10756 is: 'paper-item', | 6243 is: 'paper-item', |
10757 | 6244 behaviors: [ Polymer.PaperItemBehavior ] |
10758 behaviors: [ | 6245 }); |
10759 Polymer.PaperItemBehavior | 6246 |
10760 ] | |
10761 }); | |
10762 Polymer({ | 6247 Polymer({ |
10763 | 6248 is: 'iron-collapse', |
10764 is: 'iron-collapse', | 6249 behaviors: [ Polymer.IronResizableBehavior ], |
10765 | 6250 properties: { |
10766 behaviors: [ | 6251 horizontal: { |
10767 Polymer.IronResizableBehavior | 6252 type: Boolean, |
10768 ], | 6253 value: false, |
10769 | 6254 observer: '_horizontalChanged' |
10770 properties: { | 6255 }, |
10771 | 6256 opened: { |
10772 /** | 6257 type: Boolean, |
10773 * If true, the orientation is horizontal; otherwise is vertical. | 6258 value: false, |
10774 * | 6259 notify: true, |
10775 * @attribute horizontal | 6260 observer: '_openedChanged' |
10776 */ | 6261 }, |
10777 horizontal: { | 6262 noAnimation: { |
10778 type: Boolean, | 6263 type: Boolean |
10779 value: false, | 6264 } |
10780 observer: '_horizontalChanged' | 6265 }, |
10781 }, | 6266 get dimension() { |
10782 | 6267 return this.horizontal ? 'width' : 'height'; |
10783 /** | 6268 }, |
10784 * Set opened to true to show the collapse element and to false to hide it
. | 6269 get _dimensionMax() { |
10785 * | 6270 return this.horizontal ? 'maxWidth' : 'maxHeight'; |
10786 * @attribute opened | 6271 }, |
10787 */ | 6272 get _dimensionMaxCss() { |
10788 opened: { | 6273 return this.horizontal ? 'max-width' : 'max-height'; |
10789 type: Boolean, | 6274 }, |
10790 value: false, | 6275 hostAttributes: { |
10791 notify: true, | 6276 role: 'group', |
10792 observer: '_openedChanged' | 6277 'aria-hidden': 'true', |
10793 }, | 6278 'aria-expanded': 'false' |
10794 | 6279 }, |
10795 /** | 6280 listeners: { |
10796 * Set noAnimation to true to disable animations | 6281 transitionend: '_transitionEnd' |
10797 * | 6282 }, |
10798 * @attribute noAnimation | 6283 attached: function() { |
10799 */ | 6284 this._transitionEnd(); |
10800 noAnimation: { | 6285 }, |
10801 type: Boolean | 6286 toggle: function() { |
10802 }, | 6287 this.opened = !this.opened; |
10803 | 6288 }, |
10804 }, | 6289 show: function() { |
10805 | 6290 this.opened = true; |
10806 get dimension() { | 6291 }, |
10807 return this.horizontal ? 'width' : 'height'; | 6292 hide: function() { |
10808 }, | 6293 this.opened = false; |
10809 | 6294 }, |
10810 /** | 6295 updateSize: function(size, animated) { |
10811 * `maxWidth` or `maxHeight`. | 6296 var curSize = this.style[this._dimensionMax]; |
10812 * @private | 6297 if (curSize === size || size === 'auto' && !curSize) { |
10813 */ | 6298 return; |
10814 get _dimensionMax() { | 6299 } |
10815 return this.horizontal ? 'maxWidth' : 'maxHeight'; | 6300 this._updateTransition(false); |
10816 }, | 6301 if (animated && !this.noAnimation && this._isDisplayed) { |
10817 | 6302 var startSize = this._calcSize(); |
10818 /** | |
10819 * `max-width` or `max-height`. | |
10820 * @private | |
10821 */ | |
10822 get _dimensionMaxCss() { | |
10823 return this.horizontal ? 'max-width' : 'max-height'; | |
10824 }, | |
10825 | |
10826 hostAttributes: { | |
10827 role: 'group', | |
10828 'aria-hidden': 'true', | |
10829 'aria-expanded': 'false' | |
10830 }, | |
10831 | |
10832 listeners: { | |
10833 transitionend: '_transitionEnd' | |
10834 }, | |
10835 | |
10836 attached: function() { | |
10837 // It will take care of setting correct classes and styles. | |
10838 this._transitionEnd(); | |
10839 }, | |
10840 | |
10841 /** | |
10842 * Toggle the opened state. | |
10843 * | |
10844 * @method toggle | |
10845 */ | |
10846 toggle: function() { | |
10847 this.opened = !this.opened; | |
10848 }, | |
10849 | |
10850 show: function() { | |
10851 this.opened = true; | |
10852 }, | |
10853 | |
10854 hide: function() { | |
10855 this.opened = false; | |
10856 }, | |
10857 | |
10858 /** | |
10859 * Updates the size of the element. | |
10860 * @param {string} size The new value for `maxWidth`/`maxHeight` as css prop
erty value, usually `auto` or `0px`. | |
10861 * @param {boolean=} animated if `true` updates the size with an animation,
otherwise without. | |
10862 */ | |
10863 updateSize: function(size, animated) { | |
10864 // No change! | |
10865 var curSize = this.style[this._dimensionMax]; | |
10866 if (curSize === size || (size === 'auto' && !curSize)) { | |
10867 return; | |
10868 } | |
10869 | |
10870 this._updateTransition(false); | |
10871 // If we can animate, must do some prep work. | |
10872 if (animated && !this.noAnimation && this._isDisplayed) { | |
10873 // Animation will start at the current size. | |
10874 var startSize = this._calcSize(); | |
10875 // For `auto` we must calculate what is the final size for the animation
. | |
10876 // After the transition is done, _transitionEnd will set the size back t
o `auto`. | |
10877 if (size === 'auto') { | |
10878 this.style[this._dimensionMax] = ''; | |
10879 size = this._calcSize(); | |
10880 } | |
10881 // Go to startSize without animation. | |
10882 this.style[this._dimensionMax] = startSize; | |
10883 // Force layout to ensure transition will go. Set scrollTop to itself | |
10884 // so that compilers won't remove it. | |
10885 this.scrollTop = this.scrollTop; | |
10886 // Enable animation. | |
10887 this._updateTransition(true); | |
10888 } | |
10889 // Set the final size. | |
10890 if (size === 'auto') { | 6303 if (size === 'auto') { |
10891 this.style[this._dimensionMax] = ''; | 6304 this.style[this._dimensionMax] = ''; |
| 6305 size = this._calcSize(); |
| 6306 } |
| 6307 this.style[this._dimensionMax] = startSize; |
| 6308 this.scrollTop = this.scrollTop; |
| 6309 this._updateTransition(true); |
| 6310 } |
| 6311 if (size === 'auto') { |
| 6312 this.style[this._dimensionMax] = ''; |
| 6313 } else { |
| 6314 this.style[this._dimensionMax] = size; |
| 6315 } |
| 6316 }, |
| 6317 enableTransition: function(enabled) { |
| 6318 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` in
stead.'); |
| 6319 this.noAnimation = !enabled; |
| 6320 }, |
| 6321 _updateTransition: function(enabled) { |
| 6322 this.style.transitionDuration = enabled && !this.noAnimation ? '' : '0s'; |
| 6323 }, |
| 6324 _horizontalChanged: function() { |
| 6325 this.style.transitionProperty = this._dimensionMaxCss; |
| 6326 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'maxW
idth'; |
| 6327 this.style[otherDimension] = ''; |
| 6328 this.updateSize(this.opened ? 'auto' : '0px', false); |
| 6329 }, |
| 6330 _openedChanged: function() { |
| 6331 this.setAttribute('aria-expanded', this.opened); |
| 6332 this.setAttribute('aria-hidden', !this.opened); |
| 6333 this.toggleClass('iron-collapse-closed', false); |
| 6334 this.toggleClass('iron-collapse-opened', false); |
| 6335 this.updateSize(this.opened ? 'auto' : '0px', true); |
| 6336 if (this.opened) { |
| 6337 this.focus(); |
| 6338 } |
| 6339 if (this.noAnimation) { |
| 6340 this._transitionEnd(); |
| 6341 } |
| 6342 }, |
| 6343 _transitionEnd: function() { |
| 6344 if (this.opened) { |
| 6345 this.style[this._dimensionMax] = ''; |
| 6346 } |
| 6347 this.toggleClass('iron-collapse-closed', !this.opened); |
| 6348 this.toggleClass('iron-collapse-opened', this.opened); |
| 6349 this._updateTransition(false); |
| 6350 this.notifyResize(); |
| 6351 }, |
| 6352 get _isDisplayed() { |
| 6353 var rect = this.getBoundingClientRect(); |
| 6354 for (var prop in rect) { |
| 6355 if (rect[prop] !== 0) return true; |
| 6356 } |
| 6357 return false; |
| 6358 }, |
| 6359 _calcSize: function() { |
| 6360 return this.getBoundingClientRect()[this.dimension] + 'px'; |
| 6361 } |
| 6362 }); |
| 6363 |
| 6364 Polymer.IronFormElementBehavior = { |
| 6365 properties: { |
| 6366 name: { |
| 6367 type: String |
| 6368 }, |
| 6369 value: { |
| 6370 notify: true, |
| 6371 type: String |
| 6372 }, |
| 6373 required: { |
| 6374 type: Boolean, |
| 6375 value: false |
| 6376 }, |
| 6377 _parentForm: { |
| 6378 type: Object |
| 6379 } |
| 6380 }, |
| 6381 attached: function() { |
| 6382 this.fire('iron-form-element-register'); |
| 6383 }, |
| 6384 detached: function() { |
| 6385 if (this._parentForm) { |
| 6386 this._parentForm.fire('iron-form-element-unregister', { |
| 6387 target: this |
| 6388 }); |
| 6389 } |
| 6390 } |
| 6391 }; |
| 6392 |
| 6393 Polymer.IronCheckedElementBehaviorImpl = { |
| 6394 properties: { |
| 6395 checked: { |
| 6396 type: Boolean, |
| 6397 value: false, |
| 6398 reflectToAttribute: true, |
| 6399 notify: true, |
| 6400 observer: '_checkedChanged' |
| 6401 }, |
| 6402 toggles: { |
| 6403 type: Boolean, |
| 6404 value: true, |
| 6405 reflectToAttribute: true |
| 6406 }, |
| 6407 value: { |
| 6408 type: String, |
| 6409 value: 'on', |
| 6410 observer: '_valueChanged' |
| 6411 } |
| 6412 }, |
| 6413 observers: [ '_requiredChanged(required)' ], |
| 6414 created: function() { |
| 6415 this._hasIronCheckedElementBehavior = true; |
| 6416 }, |
| 6417 _getValidity: function(_value) { |
| 6418 return this.disabled || !this.required || this.checked; |
| 6419 }, |
| 6420 _requiredChanged: function() { |
| 6421 if (this.required) { |
| 6422 this.setAttribute('aria-required', 'true'); |
| 6423 } else { |
| 6424 this.removeAttribute('aria-required'); |
| 6425 } |
| 6426 }, |
| 6427 _checkedChanged: function() { |
| 6428 this.active = this.checked; |
| 6429 this.fire('iron-change'); |
| 6430 }, |
| 6431 _valueChanged: function() { |
| 6432 if (this.value === undefined || this.value === null) { |
| 6433 this.value = 'on'; |
| 6434 } |
| 6435 } |
| 6436 }; |
| 6437 |
| 6438 Polymer.IronCheckedElementBehavior = [ Polymer.IronFormElementBehavior, Polymer.
IronValidatableBehavior, Polymer.IronCheckedElementBehaviorImpl ]; |
| 6439 |
| 6440 Polymer.PaperCheckedElementBehaviorImpl = { |
| 6441 _checkedChanged: function() { |
| 6442 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this); |
| 6443 if (this.hasRipple()) { |
| 6444 if (this.checked) { |
| 6445 this._ripple.setAttribute('checked', ''); |
10892 } else { | 6446 } else { |
10893 this.style[this._dimensionMax] = size; | 6447 this._ripple.removeAttribute('checked'); |
10894 } | 6448 } |
10895 }, | 6449 } |
10896 | 6450 }, |
10897 /** | 6451 _buttonStateChanged: function() { |
10898 * enableTransition() is deprecated, but left over so it doesn't break exist
ing code. | 6452 Polymer.PaperRippleBehavior._buttonStateChanged.call(this); |
10899 * Please use `noAnimation` property instead. | 6453 if (this.disabled) { |
10900 * | 6454 return; |
10901 * @method enableTransition | 6455 } |
10902 * @deprecated since version 1.0.4 | 6456 if (this.isAttached) { |
10903 */ | 6457 this.checked = this.active; |
10904 enableTransition: function(enabled) { | 6458 } |
10905 Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation`
instead.'); | 6459 } |
10906 this.noAnimation = !enabled; | 6460 }; |
10907 }, | 6461 |
10908 | 6462 Polymer.PaperCheckedElementBehavior = [ Polymer.PaperInkyFocusBehavior, Polymer.
IronCheckedElementBehavior, Polymer.PaperCheckedElementBehaviorImpl ]; |
10909 _updateTransition: function(enabled) { | 6463 |
10910 this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s'
; | |
10911 }, | |
10912 | |
10913 _horizontalChanged: function() { | |
10914 this.style.transitionProperty = this._dimensionMaxCss; | |
10915 var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'ma
xWidth'; | |
10916 this.style[otherDimension] = ''; | |
10917 this.updateSize(this.opened ? 'auto' : '0px', false); | |
10918 }, | |
10919 | |
10920 _openedChanged: function() { | |
10921 this.setAttribute('aria-expanded', this.opened); | |
10922 this.setAttribute('aria-hidden', !this.opened); | |
10923 | |
10924 this.toggleClass('iron-collapse-closed', false); | |
10925 this.toggleClass('iron-collapse-opened', false); | |
10926 this.updateSize(this.opened ? 'auto' : '0px', true); | |
10927 | |
10928 // Focus the current collapse. | |
10929 if (this.opened) { | |
10930 this.focus(); | |
10931 } | |
10932 if (this.noAnimation) { | |
10933 this._transitionEnd(); | |
10934 } | |
10935 }, | |
10936 | |
10937 _transitionEnd: function() { | |
10938 if (this.opened) { | |
10939 this.style[this._dimensionMax] = ''; | |
10940 } | |
10941 this.toggleClass('iron-collapse-closed', !this.opened); | |
10942 this.toggleClass('iron-collapse-opened', this.opened); | |
10943 this._updateTransition(false); | |
10944 this.notifyResize(); | |
10945 }, | |
10946 | |
10947 /** | |
10948 * Simplistic heuristic to detect if element has a parent with display: none | |
10949 * | |
10950 * @private | |
10951 */ | |
10952 get _isDisplayed() { | |
10953 var rect = this.getBoundingClientRect(); | |
10954 for (var prop in rect) { | |
10955 if (rect[prop] !== 0) return true; | |
10956 } | |
10957 return false; | |
10958 }, | |
10959 | |
10960 _calcSize: function() { | |
10961 return this.getBoundingClientRect()[this.dimension] + 'px'; | |
10962 } | |
10963 | |
10964 }); | |
10965 /** | |
10966 Polymer.IronFormElementBehavior enables a custom element to be included | |
10967 in an `iron-form`. | |
10968 | |
10969 @demo demo/index.html | |
10970 @polymerBehavior | |
10971 */ | |
10972 Polymer.IronFormElementBehavior = { | |
10973 | |
10974 properties: { | |
10975 /** | |
10976 * Fired when the element is added to an `iron-form`. | |
10977 * | |
10978 * @event iron-form-element-register | |
10979 */ | |
10980 | |
10981 /** | |
10982 * Fired when the element is removed from an `iron-form`. | |
10983 * | |
10984 * @event iron-form-element-unregister | |
10985 */ | |
10986 | |
10987 /** | |
10988 * The name of this element. | |
10989 */ | |
10990 name: { | |
10991 type: String | |
10992 }, | |
10993 | |
10994 /** | |
10995 * The value for this element. | |
10996 */ | |
10997 value: { | |
10998 notify: true, | |
10999 type: String | |
11000 }, | |
11001 | |
11002 /** | |
11003 * Set to true to mark the input as required. If used in a form, a | |
11004 * custom element that uses this behavior should also use | |
11005 * Polymer.IronValidatableBehavior and define a custom validation method. | |
11006 * Otherwise, a `required` element will always be considered valid. | |
11007 * It's also strongly recommended to provide a visual style for the elemen
t | |
11008 * when its value is invalid. | |
11009 */ | |
11010 required: { | |
11011 type: Boolean, | |
11012 value: false | |
11013 }, | |
11014 | |
11015 /** | |
11016 * The form that the element is registered to. | |
11017 */ | |
11018 _parentForm: { | |
11019 type: Object | |
11020 } | |
11021 }, | |
11022 | |
11023 attached: function() { | |
11024 // Note: the iron-form that this element belongs to will set this | |
11025 // element's _parentForm property when handling this event. | |
11026 this.fire('iron-form-element-register'); | |
11027 }, | |
11028 | |
11029 detached: function() { | |
11030 if (this._parentForm) { | |
11031 this._parentForm.fire('iron-form-element-unregister', {target: this}); | |
11032 } | |
11033 } | |
11034 | |
11035 }; | |
11036 /** | |
11037 * Use `Polymer.IronCheckedElementBehavior` to implement a custom element | |
11038 * that has a `checked` property, which can be used for validation if the | |
11039 * element is also `required`. Element instances implementing this behavior | |
11040 * will also be registered for use in an `iron-form` element. | |
11041 * | |
11042 * @demo demo/index.html | |
11043 * @polymerBehavior Polymer.IronCheckedElementBehavior | |
11044 */ | |
11045 Polymer.IronCheckedElementBehaviorImpl = { | |
11046 | |
11047 properties: { | |
11048 /** | |
11049 * Fired when the checked state changes. | |
11050 * | |
11051 * @event iron-change | |
11052 */ | |
11053 | |
11054 /** | |
11055 * Gets or sets the state, `true` is checked and `false` is unchecked. | |
11056 */ | |
11057 checked: { | |
11058 type: Boolean, | |
11059 value: false, | |
11060 reflectToAttribute: true, | |
11061 notify: true, | |
11062 observer: '_checkedChanged' | |
11063 }, | |
11064 | |
11065 /** | |
11066 * If true, the button toggles the active state with each tap or press | |
11067 * of the spacebar. | |
11068 */ | |
11069 toggles: { | |
11070 type: Boolean, | |
11071 value: true, | |
11072 reflectToAttribute: true | |
11073 }, | |
11074 | |
11075 /* Overriden from Polymer.IronFormElementBehavior */ | |
11076 value: { | |
11077 type: String, | |
11078 value: 'on', | |
11079 observer: '_valueChanged' | |
11080 } | |
11081 }, | |
11082 | |
11083 observers: [ | |
11084 '_requiredChanged(required)' | |
11085 ], | |
11086 | |
11087 created: function() { | |
11088 // Used by `iron-form` to handle the case that an element with this behavi
or | |
11089 // doesn't have a role of 'checkbox' or 'radio', but should still only be | |
11090 // included when the form is serialized if `this.checked === true`. | |
11091 this._hasIronCheckedElementBehavior = true; | |
11092 }, | |
11093 | |
11094 /** | |
11095 * Returns false if the element is required and not checked, and true otherw
ise. | |
11096 * @param {*=} _value Ignored. | |
11097 * @return {boolean} true if `required` is false or if `checked` is true. | |
11098 */ | |
11099 _getValidity: function(_value) { | |
11100 return this.disabled || !this.required || this.checked; | |
11101 }, | |
11102 | |
11103 /** | |
11104 * Update the aria-required label when `required` is changed. | |
11105 */ | |
11106 _requiredChanged: function() { | |
11107 if (this.required) { | |
11108 this.setAttribute('aria-required', 'true'); | |
11109 } else { | |
11110 this.removeAttribute('aria-required'); | |
11111 } | |
11112 }, | |
11113 | |
11114 /** | |
11115 * Fire `iron-changed` when the checked state changes. | |
11116 */ | |
11117 _checkedChanged: function() { | |
11118 this.active = this.checked; | |
11119 this.fire('iron-change'); | |
11120 }, | |
11121 | |
11122 /** | |
11123 * Reset value to 'on' if it is set to `undefined`. | |
11124 */ | |
11125 _valueChanged: function() { | |
11126 if (this.value === undefined || this.value === null) { | |
11127 this.value = 'on'; | |
11128 } | |
11129 } | |
11130 }; | |
11131 | |
11132 /** @polymerBehavior Polymer.IronCheckedElementBehavior */ | |
11133 Polymer.IronCheckedElementBehavior = [ | |
11134 Polymer.IronFormElementBehavior, | |
11135 Polymer.IronValidatableBehavior, | |
11136 Polymer.IronCheckedElementBehaviorImpl | |
11137 ]; | |
11138 /** | |
11139 * Use `Polymer.PaperCheckedElementBehavior` to implement a custom element | |
11140 * that has a `checked` property similar to `Polymer.IronCheckedElementBehavio
r` | |
11141 * and is compatible with having a ripple effect. | |
11142 * @polymerBehavior Polymer.PaperCheckedElementBehavior | |
11143 */ | |
11144 Polymer.PaperCheckedElementBehaviorImpl = { | |
11145 /** | |
11146 * Synchronizes the element's checked state with its ripple effect. | |
11147 */ | |
11148 _checkedChanged: function() { | |
11149 Polymer.IronCheckedElementBehaviorImpl._checkedChanged.call(this); | |
11150 if (this.hasRipple()) { | |
11151 if (this.checked) { | |
11152 this._ripple.setAttribute('checked', ''); | |
11153 } else { | |
11154 this._ripple.removeAttribute('checked'); | |
11155 } | |
11156 } | |
11157 }, | |
11158 | |
11159 /** | |
11160 * Synchronizes the element's `active` and `checked` state. | |
11161 */ | |
11162 _buttonStateChanged: function() { | |
11163 Polymer.PaperRippleBehavior._buttonStateChanged.call(this); | |
11164 if (this.disabled) { | |
11165 return; | |
11166 } | |
11167 if (this.isAttached) { | |
11168 this.checked = this.active; | |
11169 } | |
11170 } | |
11171 }; | |
11172 | |
11173 /** @polymerBehavior Polymer.PaperCheckedElementBehavior */ | |
11174 Polymer.PaperCheckedElementBehavior = [ | |
11175 Polymer.PaperInkyFocusBehavior, | |
11176 Polymer.IronCheckedElementBehavior, | |
11177 Polymer.PaperCheckedElementBehaviorImpl | |
11178 ]; | |
11179 Polymer({ | 6464 Polymer({ |
11180 is: 'paper-checkbox', | 6465 is: 'paper-checkbox', |
11181 | 6466 behaviors: [ Polymer.PaperCheckedElementBehavior ], |
11182 behaviors: [ | 6467 hostAttributes: { |
11183 Polymer.PaperCheckedElementBehavior | 6468 role: 'checkbox', |
11184 ], | 6469 'aria-checked': false, |
11185 | 6470 tabindex: 0 |
11186 hostAttributes: { | 6471 }, |
11187 role: 'checkbox', | 6472 properties: { |
11188 'aria-checked': false, | 6473 ariaActiveAttribute: { |
11189 tabindex: 0 | 6474 type: String, |
11190 }, | 6475 value: 'aria-checked' |
11191 | 6476 } |
11192 properties: { | 6477 }, |
11193 /** | 6478 _computeCheckboxClass: function(checked, invalid) { |
11194 * Fired when the checked state changes due to user interaction. | 6479 var className = ''; |
11195 * | 6480 if (checked) { |
11196 * @event change | 6481 className += 'checked '; |
11197 */ | 6482 } |
11198 | 6483 if (invalid) { |
11199 /** | 6484 className += 'invalid'; |
11200 * Fired when the checked state changes. | 6485 } |
11201 * | 6486 return className; |
11202 * @event iron-change | 6487 }, |
11203 */ | 6488 _computeCheckmarkClass: function(checked) { |
11204 ariaActiveAttribute: { | 6489 return checked ? '' : 'hidden'; |
11205 type: String, | 6490 }, |
11206 value: 'aria-checked' | 6491 _createRipple: function() { |
11207 } | 6492 this._rippleContainer = this.$.checkboxContainer; |
11208 }, | 6493 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); |
11209 | 6494 } |
11210 _computeCheckboxClass: function(checked, invalid) { | 6495 }); |
11211 var className = ''; | 6496 |
11212 if (checked) { | |
11213 className += 'checked '; | |
11214 } | |
11215 if (invalid) { | |
11216 className += 'invalid'; | |
11217 } | |
11218 return className; | |
11219 }, | |
11220 | |
11221 _computeCheckmarkClass: function(checked) { | |
11222 return checked ? '' : 'hidden'; | |
11223 }, | |
11224 | |
11225 // create ripple inside the checkboxContainer | |
11226 _createRipple: function() { | |
11227 this._rippleContainer = this.$.checkboxContainer; | |
11228 return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); | |
11229 } | |
11230 | |
11231 }); | |
11232 Polymer({ | 6497 Polymer({ |
11233 is: 'paper-icon-button-light', | 6498 is: 'paper-icon-button-light', |
11234 extends: 'button', | 6499 "extends": 'button', |
11235 | 6500 behaviors: [ Polymer.PaperRippleBehavior ], |
11236 behaviors: [ | 6501 listeners: { |
11237 Polymer.PaperRippleBehavior | 6502 down: '_rippleDown', |
11238 ], | 6503 up: '_rippleUp', |
11239 | 6504 focus: '_rippleDown', |
11240 listeners: { | 6505 blur: '_rippleUp' |
11241 'down': '_rippleDown', | 6506 }, |
11242 'up': '_rippleUp', | 6507 _rippleDown: function() { |
11243 'focus': '_rippleDown', | 6508 this.getRipple().downAction(); |
11244 'blur': '_rippleUp', | 6509 }, |
11245 }, | 6510 _rippleUp: function() { |
11246 | 6511 this.getRipple().upAction(); |
11247 _rippleDown: function() { | 6512 }, |
11248 this.getRipple().downAction(); | 6513 ensureRipple: function(var_args) { |
11249 }, | 6514 var lastRipple = this._ripple; |
11250 | 6515 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); |
11251 _rippleUp: function() { | 6516 if (this._ripple && this._ripple !== lastRipple) { |
11252 this.getRipple().upAction(); | 6517 this._ripple.center = true; |
11253 }, | 6518 this._ripple.classList.add('circle'); |
11254 | 6519 } |
11255 /** | 6520 } |
11256 * @param {...*} var_args | 6521 }); |
11257 */ | 6522 |
11258 ensureRipple: function(var_args) { | |
11259 var lastRipple = this._ripple; | |
11260 Polymer.PaperRippleBehavior.ensureRipple.apply(this, arguments); | |
11261 if (this._ripple && this._ripple !== lastRipple) { | |
11262 this._ripple.center = true; | |
11263 this._ripple.classList.add('circle'); | |
11264 } | |
11265 } | |
11266 }); | |
11267 // Copyright 2016 The Chromium Authors. All rights reserved. | 6523 // Copyright 2016 The Chromium Authors. All rights reserved. |
11268 // Use of this source code is governed by a BSD-style license that can be | 6524 // Use of this source code is governed by a BSD-style license that can be |
11269 // found in the LICENSE file. | 6525 // found in the LICENSE file. |
11270 | |
11271 cr.define('cr.icon', function() { | 6526 cr.define('cr.icon', function() { |
11272 /** | |
11273 * @return {!Array<number>} The scale factors supported by this platform for | |
11274 * webui resources. | |
11275 */ | |
11276 function getSupportedScaleFactors() { | 6527 function getSupportedScaleFactors() { |
11277 var supportedScaleFactors = []; | 6528 var supportedScaleFactors = []; |
11278 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) { | 6529 if (cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux) { |
11279 // All desktop platforms support zooming which also updates the | |
11280 // renderer's device scale factors (a.k.a devicePixelRatio), and | |
11281 // these platforms has high DPI assets for 2.0x. Use 1x and 2x in | |
11282 // image-set on these platforms so that the renderer can pick the | |
11283 // closest image for the current device scale factor. | |
11284 supportedScaleFactors.push(1); | 6530 supportedScaleFactors.push(1); |
11285 supportedScaleFactors.push(2); | 6531 supportedScaleFactors.push(2); |
11286 } else { | 6532 } else { |
11287 // For other platforms that use fixed device scale factor, use | |
11288 // the window's device pixel ratio. | |
11289 // TODO(oshima): Investigate if Android/iOS need to use image-set. | |
11290 supportedScaleFactors.push(window.devicePixelRatio); | 6533 supportedScaleFactors.push(window.devicePixelRatio); |
11291 } | 6534 } |
11292 return supportedScaleFactors; | 6535 return supportedScaleFactors; |
11293 } | 6536 } |
11294 | |
11295 /** | |
11296 * Returns the URL of the image, or an image set of URLs for the profile | |
11297 * avatar. Default avatars have resources available for multiple scalefactors, | |
11298 * whereas the GAIA profile image only comes in one size. | |
11299 * | |
11300 * @param {string} path The path of the image. | |
11301 * @return {string} The url, or an image set of URLs of the avatar image. | |
11302 */ | |
11303 function getProfileAvatarIcon(path) { | 6537 function getProfileAvatarIcon(path) { |
11304 var chromeThemePath = 'chrome://theme'; | 6538 var chromeThemePath = 'chrome://theme'; |
11305 var isDefaultAvatar = | 6539 var isDefaultAvatar = path.slice(0, chromeThemePath.length) == chromeThemePa
th; |
11306 (path.slice(0, chromeThemePath.length) == chromeThemePath); | 6540 return isDefaultAvatar ? imageset(path + '@scalefactorx') : url(path); |
11307 return isDefaultAvatar ? imageset(path + '@scalefactorx'): url(path); | 6541 } |
11308 } | |
11309 | |
11310 /** | |
11311 * Generates a CSS -webkit-image-set for a chrome:// url. | |
11312 * An entry in the image set is added for each of getSupportedScaleFactors(). | |
11313 * The scale-factor-specific url is generated by replacing the first instance | |
11314 * of 'scalefactor' in |path| with the numeric scale factor. | |
11315 * @param {string} path The URL to generate an image set for. | |
11316 * 'scalefactor' should be a substring of |path|. | |
11317 * @return {string} The CSS -webkit-image-set. | |
11318 */ | |
11319 function imageset(path) { | 6542 function imageset(path) { |
11320 var supportedScaleFactors = getSupportedScaleFactors(); | 6543 var supportedScaleFactors = getSupportedScaleFactors(); |
11321 | |
11322 var replaceStartIndex = path.indexOf('scalefactor'); | 6544 var replaceStartIndex = path.indexOf('scalefactor'); |
11323 if (replaceStartIndex < 0) | 6545 if (replaceStartIndex < 0) return url(path); |
11324 return url(path); | |
11325 | |
11326 var s = ''; | 6546 var s = ''; |
11327 for (var i = 0; i < supportedScaleFactors.length; ++i) { | 6547 for (var i = 0; i < supportedScaleFactors.length; ++i) { |
11328 var scaleFactor = supportedScaleFactors[i]; | 6548 var scaleFactor = supportedScaleFactors[i]; |
11329 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + | 6549 var pathWithScaleFactor = path.substr(0, replaceStartIndex) + scaleFactor
+ path.substr(replaceStartIndex + 'scalefactor'.length); |
11330 scaleFactor + path.substr(replaceStartIndex + 'scalefactor'.length); | |
11331 | |
11332 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; | 6550 s += url(pathWithScaleFactor) + ' ' + scaleFactor + 'x'; |
11333 | 6551 if (i != supportedScaleFactors.length - 1) s += ', '; |
11334 if (i != supportedScaleFactors.length - 1) | |
11335 s += ', '; | |
11336 } | 6552 } |
11337 return '-webkit-image-set(' + s + ')'; | 6553 return '-webkit-image-set(' + s + ')'; |
11338 } | 6554 } |
11339 | |
11340 /** | |
11341 * A regular expression for identifying favicon URLs. | |
11342 * @const {!RegExp} | |
11343 */ | |
11344 var FAVICON_URL_REGEX = /\.ico$/i; | 6555 var FAVICON_URL_REGEX = /\.ico$/i; |
11345 | |
11346 /** | |
11347 * Creates a CSS -webkit-image-set for a favicon request. | |
11348 * @param {string} url Either the URL of the original page or of the favicon | |
11349 * itself. | |
11350 * @param {number=} opt_size Optional preferred size of the favicon. | |
11351 * @param {string=} opt_type Optional type of favicon to request. Valid values | |
11352 * are 'favicon' and 'touch-icon'. Default is 'favicon'. | |
11353 * @return {string} -webkit-image-set for the favicon. | |
11354 */ | |
11355 function getFaviconImageSet(url, opt_size, opt_type) { | 6556 function getFaviconImageSet(url, opt_size, opt_type) { |
11356 var size = opt_size || 16; | 6557 var size = opt_size || 16; |
11357 var type = opt_type || 'favicon'; | 6558 var type = opt_type || 'favicon'; |
11358 | 6559 return imageset('chrome://' + type + '/size/' + size + '@scalefactorx/' + (F
AVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url); |
11359 return imageset( | 6560 } |
11360 'chrome://' + type + '/size/' + size + '@scalefactorx/' + | |
11361 // Note: Literal 'iconurl' must match |kIconURLParameter| in | |
11362 // components/favicon_base/favicon_url_parser.cc. | |
11363 (FAVICON_URL_REGEX.test(url) ? 'iconurl/' : '') + url); | |
11364 } | |
11365 | |
11366 return { | 6561 return { |
11367 getSupportedScaleFactors: getSupportedScaleFactors, | 6562 getSupportedScaleFactors: getSupportedScaleFactors, |
11368 getProfileAvatarIcon: getProfileAvatarIcon, | 6563 getProfileAvatarIcon: getProfileAvatarIcon, |
11369 getFaviconImageSet: getFaviconImageSet, | 6564 getFaviconImageSet: getFaviconImageSet |
11370 }; | 6565 }; |
11371 }); | 6566 }); |
| 6567 |
11372 // Copyright 2016 The Chromium Authors. All rights reserved. | 6568 // Copyright 2016 The Chromium Authors. All rights reserved. |
11373 // Use of this source code is governed by a BSD-style license that can be | 6569 // Use of this source code is governed by a BSD-style license that can be |
11374 // found in the LICENSE file. | 6570 // found in the LICENSE file. |
11375 | |
11376 /** | |
11377 * @fileoverview Defines a singleton object, md_history.BrowserService, which | |
11378 * provides access to chrome.send APIs. | |
11379 */ | |
11380 | |
11381 cr.define('md_history', function() { | 6571 cr.define('md_history', function() { |
11382 /** @constructor */ | |
11383 function BrowserService() { | 6572 function BrowserService() { |
11384 /** @private {Array<!HistoryEntry>} */ | |
11385 this.pendingDeleteItems_ = null; | 6573 this.pendingDeleteItems_ = null; |
11386 /** @private {PromiseResolver} */ | |
11387 this.pendingDeletePromise_ = null; | 6574 this.pendingDeletePromise_ = null; |
11388 } | 6575 } |
11389 | |
11390 BrowserService.prototype = { | 6576 BrowserService.prototype = { |
11391 /** | |
11392 * @param {!Array<!HistoryEntry>} items | |
11393 * @return {Promise<!Array<!HistoryEntry>>} | |
11394 */ | |
11395 deleteItems: function(items) { | 6577 deleteItems: function(items) { |
11396 if (this.pendingDeleteItems_ != null) { | 6578 if (this.pendingDeleteItems_ != null) { |
11397 // There's already a deletion in progress, reject immediately. | 6579 return new Promise(function(resolve, reject) { |
11398 return new Promise(function(resolve, reject) { reject(items); }); | 6580 reject(items); |
| 6581 }); |
11399 } | 6582 } |
11400 | |
11401 var removalList = items.map(function(item) { | 6583 var removalList = items.map(function(item) { |
11402 return { | 6584 return { |
11403 url: item.url, | 6585 url: item.url, |
11404 timestamps: item.allTimestamps | 6586 timestamps: item.allTimestamps |
11405 }; | 6587 }; |
11406 }); | 6588 }); |
11407 | |
11408 this.pendingDeleteItems_ = items; | 6589 this.pendingDeleteItems_ = items; |
11409 this.pendingDeletePromise_ = new PromiseResolver(); | 6590 this.pendingDeletePromise_ = new PromiseResolver(); |
11410 | |
11411 chrome.send('removeVisits', removalList); | 6591 chrome.send('removeVisits', removalList); |
11412 | |
11413 return this.pendingDeletePromise_.promise; | 6592 return this.pendingDeletePromise_.promise; |
11414 }, | 6593 }, |
11415 | |
11416 /** | |
11417 * @param {!string} url | |
11418 */ | |
11419 removeBookmark: function(url) { | 6594 removeBookmark: function(url) { |
11420 chrome.send('removeBookmark', [url]); | 6595 chrome.send('removeBookmark', [ url ]); |
11421 }, | 6596 }, |
11422 | |
11423 /** | |
11424 * @param {string} sessionTag | |
11425 */ | |
11426 openForeignSessionAllTabs: function(sessionTag) { | 6597 openForeignSessionAllTabs: function(sessionTag) { |
11427 chrome.send('openForeignSession', [sessionTag]); | 6598 chrome.send('openForeignSession', [ sessionTag ]); |
11428 }, | 6599 }, |
11429 | |
11430 /** | |
11431 * @param {string} sessionTag | |
11432 * @param {number} windowId | |
11433 * @param {number} tabId | |
11434 * @param {MouseEvent} e | |
11435 */ | |
11436 openForeignSessionTab: function(sessionTag, windowId, tabId, e) { | 6600 openForeignSessionTab: function(sessionTag, windowId, tabId, e) { |
11437 chrome.send('openForeignSession', [ | 6601 chrome.send('openForeignSession', [ sessionTag, String(windowId), String(t
abId), e.button || 0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey ]); |
11438 sessionTag, String(windowId), String(tabId), e.button || 0, e.altKey, | 6602 }, |
11439 e.ctrlKey, e.metaKey, e.shiftKey | |
11440 ]); | |
11441 }, | |
11442 | |
11443 /** | |
11444 * @param {string} sessionTag | |
11445 */ | |
11446 deleteForeignSession: function(sessionTag) { | 6603 deleteForeignSession: function(sessionTag) { |
11447 chrome.send('deleteForeignSession', [sessionTag]); | 6604 chrome.send('deleteForeignSession', [ sessionTag ]); |
11448 }, | 6605 }, |
11449 | |
11450 openClearBrowsingData: function() { | 6606 openClearBrowsingData: function() { |
11451 chrome.send('clearBrowsingData'); | 6607 chrome.send('clearBrowsingData'); |
11452 }, | 6608 }, |
11453 | |
11454 /** | |
11455 * Record an action in UMA. | |
11456 * @param {string} actionDesc The name of the action to be logged. | |
11457 */ | |
11458 recordAction: function(actionDesc) { | 6609 recordAction: function(actionDesc) { |
11459 chrome.send('metricsHandler:recordAction', [actionDesc]); | 6610 chrome.send('metricsHandler:recordAction', [ actionDesc ]); |
11460 }, | 6611 }, |
11461 | |
11462 /** | |
11463 * @param {boolean} successful | |
11464 * @private | |
11465 */ | |
11466 resolveDelete_: function(successful) { | 6612 resolveDelete_: function(successful) { |
11467 if (this.pendingDeleteItems_ == null || | 6613 if (this.pendingDeleteItems_ == null || this.pendingDeletePromise_ == null
) { |
11468 this.pendingDeletePromise_ == null) { | |
11469 return; | 6614 return; |
11470 } | 6615 } |
11471 | 6616 if (successful) this.pendingDeletePromise_.resolve(this.pendingDeleteItems
_); else this.pendingDeletePromise_.reject(this.pendingDeleteItems_); |
11472 if (successful) | |
11473 this.pendingDeletePromise_.resolve(this.pendingDeleteItems_); | |
11474 else | |
11475 this.pendingDeletePromise_.reject(this.pendingDeleteItems_); | |
11476 | |
11477 this.pendingDeleteItems_ = null; | 6617 this.pendingDeleteItems_ = null; |
11478 this.pendingDeletePromise_ = null; | 6618 this.pendingDeletePromise_ = null; |
11479 }, | 6619 } |
11480 }; | 6620 }; |
11481 | |
11482 cr.addSingletonGetter(BrowserService); | 6621 cr.addSingletonGetter(BrowserService); |
11483 | 6622 return { |
11484 return {BrowserService: BrowserService}; | 6623 BrowserService: BrowserService |
11485 }); | 6624 }; |
11486 | 6625 }); |
11487 /** | 6626 |
11488 * Called by the history backend when deletion was succesful. | |
11489 */ | |
11490 function deleteComplete() { | 6627 function deleteComplete() { |
11491 md_history.BrowserService.getInstance().resolveDelete_(true); | 6628 md_history.BrowserService.getInstance().resolveDelete_(true); |
11492 } | 6629 } |
11493 | 6630 |
11494 /** | |
11495 * Called by the history backend when the deletion failed. | |
11496 */ | |
11497 function deleteFailed() { | 6631 function deleteFailed() { |
11498 md_history.BrowserService.getInstance().resolveDelete_(false); | 6632 md_history.BrowserService.getInstance().resolveDelete_(false); |
11499 }; | 6633 } |
| 6634 |
11500 // Copyright 2016 The Chromium Authors. All rights reserved. | 6635 // Copyright 2016 The Chromium Authors. All rights reserved. |
11501 // Use of this source code is governed by a BSD-style license that can be | 6636 // Use of this source code is governed by a BSD-style license that can be |
11502 // found in the LICENSE file. | 6637 // found in the LICENSE file. |
11503 | |
11504 Polymer({ | 6638 Polymer({ |
11505 is: 'history-searched-label', | 6639 is: 'history-searched-label', |
11506 | |
11507 properties: { | 6640 properties: { |
11508 // The text to show in this label. | |
11509 title: String, | 6641 title: String, |
11510 | 6642 searchTerm: String |
11511 // The search term to bold within the title. | 6643 }, |
11512 searchTerm: String, | 6644 observers: [ 'setSearchedTextToBold_(title, searchTerm)' ], |
11513 }, | |
11514 | |
11515 observers: ['setSearchedTextToBold_(title, searchTerm)'], | |
11516 | |
11517 /** | |
11518 * Updates the page title. If a search term is specified, highlights any | |
11519 * occurrences of the search term in bold. | |
11520 * @private | |
11521 */ | |
11522 setSearchedTextToBold_: function() { | 6645 setSearchedTextToBold_: function() { |
11523 var i = 0; | 6646 var i = 0; |
11524 var titleElem = this.$.container; | 6647 var titleElem = this.$.container; |
11525 var titleText = this.title; | 6648 var titleText = this.title; |
11526 | |
11527 if (this.searchTerm == '' || this.searchTerm == null) { | 6649 if (this.searchTerm == '' || this.searchTerm == null) { |
11528 titleElem.textContent = titleText; | 6650 titleElem.textContent = titleText; |
11529 return; | 6651 return; |
11530 } | 6652 } |
11531 | |
11532 var re = new RegExp(quoteString(this.searchTerm), 'gim'); | 6653 var re = new RegExp(quoteString(this.searchTerm), 'gim'); |
11533 var match; | 6654 var match; |
11534 titleElem.textContent = ''; | 6655 titleElem.textContent = ''; |
11535 while (match = re.exec(titleText)) { | 6656 while (match = re.exec(titleText)) { |
11536 if (match.index > i) | 6657 if (match.index > i) titleElem.appendChild(document.createTextNode(titleTe
xt.slice(i, match.index))); |
11537 titleElem.appendChild(document.createTextNode( | |
11538 titleText.slice(i, match.index))); | |
11539 i = re.lastIndex; | 6658 i = re.lastIndex; |
11540 // Mark the highlighted text in bold. | |
11541 var b = document.createElement('b'); | 6659 var b = document.createElement('b'); |
11542 b.textContent = titleText.substring(match.index, i); | 6660 b.textContent = titleText.substring(match.index, i); |
11543 titleElem.appendChild(b); | 6661 titleElem.appendChild(b); |
11544 } | 6662 } |
11545 if (i < titleText.length) | 6663 if (i < titleText.length) titleElem.appendChild(document.createTextNode(titl
eText.slice(i))); |
11546 titleElem.appendChild( | 6664 } |
11547 document.createTextNode(titleText.slice(i))); | 6665 }); |
11548 }, | 6666 |
11549 }); | |
11550 // Copyright 2015 The Chromium Authors. All rights reserved. | 6667 // Copyright 2015 The Chromium Authors. All rights reserved. |
11551 // Use of this source code is governed by a BSD-style license that can be | 6668 // Use of this source code is governed by a BSD-style license that can be |
11552 // found in the LICENSE file. | 6669 // found in the LICENSE file. |
11553 | |
11554 cr.define('md_history', function() { | 6670 cr.define('md_history', function() { |
11555 var HistoryItem = Polymer({ | 6671 var HistoryItem = Polymer({ |
11556 is: 'history-item', | 6672 is: 'history-item', |
11557 | |
11558 properties: { | 6673 properties: { |
11559 // Underlying HistoryEntry data for this item. Contains read-only fields | 6674 item: { |
11560 // from the history backend, as well as fields computed by history-list. | 6675 type: Object, |
11561 item: {type: Object, observer: 'showIcon_'}, | 6676 observer: 'showIcon_' |
11562 | 6677 }, |
11563 // Search term used to obtain this history-item. | 6678 searchTerm: { |
11564 searchTerm: {type: String}, | 6679 type: String |
11565 | 6680 }, |
11566 selected: {type: Boolean, notify: true}, | 6681 selected: { |
11567 | 6682 type: Boolean, |
11568 isFirstItem: {type: Boolean, reflectToAttribute: true}, | 6683 notify: true |
11569 | 6684 }, |
11570 isCardStart: {type: Boolean, reflectToAttribute: true}, | 6685 isFirstItem: { |
11571 | 6686 type: Boolean, |
11572 isCardEnd: {type: Boolean, reflectToAttribute: true}, | 6687 reflectToAttribute: true |
11573 | 6688 }, |
11574 // True if the item is being displayed embedded in another element and | 6689 isCardStart: { |
11575 // should not manage its own borders or size. | 6690 type: Boolean, |
11576 embedded: {type: Boolean, reflectToAttribute: true}, | 6691 reflectToAttribute: true |
11577 | 6692 }, |
11578 hasTimeGap: {type: Boolean}, | 6693 isCardEnd: { |
11579 | 6694 type: Boolean, |
11580 numberOfItems: {type: Number}, | 6695 reflectToAttribute: true |
11581 | 6696 }, |
11582 // The path of this history item inside its parent. | 6697 embedded: { |
11583 path: String, | 6698 type: Boolean, |
11584 }, | 6699 reflectToAttribute: true |
11585 | 6700 }, |
11586 /** | 6701 hasTimeGap: { |
11587 * When a history-item is selected the toolbar is notified and increases | 6702 type: Boolean |
11588 * or decreases its count of selected items accordingly. | 6703 }, |
11589 * @param {MouseEvent} e | 6704 numberOfItems: { |
11590 * @private | 6705 type: Number |
11591 */ | 6706 }, |
| 6707 path: String |
| 6708 }, |
11592 onCheckboxSelected_: function(e) { | 6709 onCheckboxSelected_: function(e) { |
11593 // TODO(calamity): Fire this event whenever |selected| changes. | |
11594 this.fire('history-checkbox-select', { | 6710 this.fire('history-checkbox-select', { |
11595 element: this, | 6711 element: this, |
11596 shiftKey: e.shiftKey, | 6712 shiftKey: e.shiftKey |
11597 }); | 6713 }); |
11598 e.preventDefault(); | 6714 e.preventDefault(); |
11599 }, | 6715 }, |
11600 | |
11601 /** | |
11602 * @param {MouseEvent} e | |
11603 * @private | |
11604 */ | |
11605 onCheckboxMousedown_: function(e) { | 6716 onCheckboxMousedown_: function(e) { |
11606 // Prevent shift clicking a checkbox from selecting text. | 6717 if (e.shiftKey) e.preventDefault(); |
11607 if (e.shiftKey) | 6718 }, |
11608 e.preventDefault(); | |
11609 }, | |
11610 | |
11611 /** | |
11612 * Remove bookmark of current item when bookmark-star is clicked. | |
11613 * @private | |
11614 */ | |
11615 onRemoveBookmarkTap_: function() { | 6719 onRemoveBookmarkTap_: function() { |
11616 if (!this.item.starred) | 6720 if (!this.item.starred) return; |
11617 return; | 6721 if (this.$$('#bookmark-star') == this.root.activeElement) this.$['menu-but
ton'].focus(); |
11618 | 6722 md_history.BrowserService.getInstance().removeBookmark(this.item.url); |
11619 if (this.$$('#bookmark-star') == this.root.activeElement) | |
11620 this.$['menu-button'].focus(); | |
11621 | |
11622 md_history.BrowserService.getInstance() | |
11623 .removeBookmark(this.item.url); | |
11624 this.fire('remove-bookmark-stars', this.item.url); | 6723 this.fire('remove-bookmark-stars', this.item.url); |
11625 }, | 6724 }, |
11626 | |
11627 /** | |
11628 * Fires a custom event when the menu button is clicked. Sends the details | |
11629 * of the history item and where the menu should appear. | |
11630 */ | |
11631 onMenuButtonTap_: function(e) { | 6725 onMenuButtonTap_: function(e) { |
11632 this.fire('toggle-menu', { | 6726 this.fire('toggle-menu', { |
11633 target: Polymer.dom(e).localTarget, | 6727 target: Polymer.dom(e).localTarget, |
11634 item: this.item, | 6728 item: this.item, |
11635 path: this.path, | 6729 path: this.path |
11636 }); | 6730 }); |
11637 | |
11638 // Stops the 'tap' event from closing the menu when it opens. | |
11639 e.stopPropagation(); | 6731 e.stopPropagation(); |
11640 }, | 6732 }, |
11641 | |
11642 /** | |
11643 * Set the favicon image, based on the URL of the history item. | |
11644 * @private | |
11645 */ | |
11646 showIcon_: function() { | 6733 showIcon_: function() { |
11647 this.$.icon.style.backgroundImage = | 6734 this.$.icon.style.backgroundImage = cr.icon.getFaviconImageSet(this.item.u
rl); |
11648 cr.icon.getFaviconImageSet(this.item.url); | 6735 }, |
11649 }, | |
11650 | |
11651 selectionNotAllowed_: function() { | 6736 selectionNotAllowed_: function() { |
11652 return !loadTimeData.getBoolean('allowDeletingHistory'); | 6737 return !loadTimeData.getBoolean('allowDeletingHistory'); |
11653 }, | 6738 }, |
11654 | |
11655 /** | |
11656 * Generates the title for this history card. | |
11657 * @param {number} numberOfItems The number of items in the card. | |
11658 * @param {string} search The search term associated with these results. | |
11659 * @private | |
11660 */ | |
11661 cardTitle_: function(numberOfItems, historyDate, search) { | 6739 cardTitle_: function(numberOfItems, historyDate, search) { |
11662 if (!search) | 6740 if (!search) return this.item.dateRelativeDay; |
11663 return this.item.dateRelativeDay; | |
11664 | |
11665 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults'; | 6741 var resultId = numberOfItems == 1 ? 'searchResult' : 'searchResults'; |
11666 return loadTimeData.getStringF('foundSearchResults', numberOfItems, | 6742 return loadTimeData.getStringF('foundSearchResults', numberOfItems, loadTi
meData.getString(resultId), search); |
11667 loadTimeData.getString(resultId), search); | 6743 }, |
11668 }, | |
11669 | |
11670 /** | |
11671 * Crop long item titles to reduce their effect on layout performance. See | |
11672 * crbug.com/621347. | |
11673 * @param {string} title | |
11674 * @return {string} | |
11675 */ | |
11676 cropItemTitle_: function(title) { | 6744 cropItemTitle_: function(title) { |
11677 return (title.length > TITLE_MAX_LENGTH) ? | 6745 return title.length > TITLE_MAX_LENGTH ? title.substr(0, TITLE_MAX_LENGTH)
: title; |
11678 title.substr(0, TITLE_MAX_LENGTH) : | |
11679 title; | |
11680 } | 6746 } |
11681 }); | 6747 }); |
11682 | |
11683 /** | |
11684 * Check whether the time difference between the given history item and the | |
11685 * next one is large enough for a spacer to be required. | |
11686 * @param {Array<HistoryEntry>} visits | |
11687 * @param {number} currentIndex | |
11688 * @param {string} searchedTerm | |
11689 * @return {boolean} Whether or not time gap separator is required. | |
11690 * @private | |
11691 */ | |
11692 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) { | 6748 HistoryItem.needsTimeGap = function(visits, currentIndex, searchedTerm) { |
11693 if (currentIndex >= visits.length - 1 || visits.length == 0) | 6749 if (currentIndex >= visits.length - 1 || visits.length == 0) return false; |
11694 return false; | |
11695 | |
11696 var currentItem = visits[currentIndex]; | 6750 var currentItem = visits[currentIndex]; |
11697 var nextItem = visits[currentIndex + 1]; | 6751 var nextItem = visits[currentIndex + 1]; |
11698 | 6752 if (searchedTerm) return currentItem.dateShort != nextItem.dateShort; |
11699 if (searchedTerm) | 6753 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && currentItem.d
ateRelativeDay == nextItem.dateRelativeDay; |
11700 return currentItem.dateShort != nextItem.dateShort; | |
11701 | |
11702 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && | |
11703 currentItem.dateRelativeDay == nextItem.dateRelativeDay; | |
11704 }; | 6754 }; |
11705 | 6755 return { |
11706 return { HistoryItem: HistoryItem }; | 6756 HistoryItem: HistoryItem |
11707 }); | 6757 }; |
| 6758 }); |
| 6759 |
11708 // Copyright 2016 The Chromium Authors. All rights reserved. | 6760 // Copyright 2016 The Chromium Authors. All rights reserved. |
11709 // Use of this source code is governed by a BSD-style license that can be | 6761 // Use of this source code is governed by a BSD-style license that can be |
11710 // found in the LICENSE file. | 6762 // found in the LICENSE file. |
11711 | |
11712 /** | |
11713 * @constructor | |
11714 * @param {string} currentPath | |
11715 */ | |
11716 var SelectionTreeNode = function(currentPath) { | 6763 var SelectionTreeNode = function(currentPath) { |
11717 /** @type {string} */ | |
11718 this.currentPath = currentPath; | 6764 this.currentPath = currentPath; |
11719 /** @type {boolean} */ | |
11720 this.leaf = false; | 6765 this.leaf = false; |
11721 /** @type {Array<number>} */ | |
11722 this.indexes = []; | 6766 this.indexes = []; |
11723 /** @type {Array<SelectionTreeNode>} */ | |
11724 this.children = []; | 6767 this.children = []; |
11725 }; | 6768 }; |
11726 | 6769 |
11727 /** | |
11728 * @param {number} index | |
11729 * @param {string} path | |
11730 */ | |
11731 SelectionTreeNode.prototype.addChild = function(index, path) { | 6770 SelectionTreeNode.prototype.addChild = function(index, path) { |
11732 this.indexes.push(index); | 6771 this.indexes.push(index); |
11733 this.children[index] = new SelectionTreeNode(path); | 6772 this.children[index] = new SelectionTreeNode(path); |
11734 }; | 6773 }; |
11735 | 6774 |
11736 /** @polymerBehavior */ | |
11737 var HistoryListBehavior = { | 6775 var HistoryListBehavior = { |
11738 properties: { | 6776 properties: { |
11739 /** | |
11740 * Polymer paths to the history items contained in this list. | |
11741 * @type {!Set<string>} selectedPaths | |
11742 */ | |
11743 selectedPaths: { | 6777 selectedPaths: { |
11744 type: Object, | 6778 type: Object, |
11745 value: /** @return {!Set<string>} */ function() { return new Set(); } | 6779 value: function() { |
11746 }, | 6780 return new Set(); |
11747 | 6781 } |
11748 lastSelectedPath: String, | 6782 }, |
11749 }, | 6783 lastSelectedPath: String |
11750 | 6784 }, |
11751 listeners: { | 6785 listeners: { |
11752 'history-checkbox-select': 'itemSelected_', | 6786 'history-checkbox-select': 'itemSelected_' |
11753 }, | 6787 }, |
11754 | 6788 hasResults: function(historyDataLength) { |
11755 /** | 6789 return historyDataLength > 0; |
11756 * @param {number} historyDataLength | 6790 }, |
11757 * @return {boolean} | |
11758 * @private | |
11759 */ | |
11760 hasResults: function(historyDataLength) { return historyDataLength > 0; }, | |
11761 | |
11762 /** | |
11763 * @param {string} searchedTerm | |
11764 * @param {boolean} isLoading | |
11765 * @return {string} | |
11766 * @private | |
11767 */ | |
11768 noResultsMessage: function(searchedTerm, isLoading) { | 6791 noResultsMessage: function(searchedTerm, isLoading) { |
11769 if (isLoading) | 6792 if (isLoading) return ''; |
11770 return ''; | |
11771 | |
11772 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; | 6793 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; |
11773 return loadTimeData.getString(messageId); | 6794 return loadTimeData.getString(messageId); |
11774 }, | 6795 }, |
11775 | |
11776 /** | |
11777 * Deselect each item in |selectedPaths|. | |
11778 */ | |
11779 unselectAllItems: function() { | 6796 unselectAllItems: function() { |
11780 this.selectedPaths.forEach(function(path) { | 6797 this.selectedPaths.forEach(function(path) { |
11781 this.set(path + '.selected', false); | 6798 this.set(path + '.selected', false); |
11782 }.bind(this)); | 6799 }.bind(this)); |
11783 | |
11784 this.selectedPaths.clear(); | 6800 this.selectedPaths.clear(); |
11785 }, | 6801 }, |
11786 | |
11787 /** | |
11788 * Performs a request to the backend to delete all selected items. If | |
11789 * successful, removes them from the view. Does not prompt the user before | |
11790 * deleting -- see <history-list-container> for a version of this method which | |
11791 * does prompt. | |
11792 */ | |
11793 deleteSelected: function() { | 6802 deleteSelected: function() { |
11794 var toBeRemoved = | 6803 var toBeRemoved = Array.from(this.selectedPaths.values()).map(function(path)
{ |
11795 Array.from(this.selectedPaths.values()).map(function(path) { | 6804 return this.get(path); |
11796 return this.get(path); | 6805 }.bind(this)); |
11797 }.bind(this)); | 6806 md_history.BrowserService.getInstance().deleteItems(toBeRemoved).then(functi
on() { |
11798 | 6807 this.removeItemsByPath(Array.from(this.selectedPaths)); |
11799 md_history.BrowserService.getInstance() | 6808 this.fire('unselect-all'); |
11800 .deleteItems(toBeRemoved) | 6809 }.bind(this)); |
11801 .then(function() { | 6810 }, |
11802 this.removeItemsByPath(Array.from(this.selectedPaths)); | |
11803 this.fire('unselect-all'); | |
11804 }.bind(this)); | |
11805 }, | |
11806 | |
11807 /** | |
11808 * Removes the history items in |paths|. Assumes paths are of a.0.b.0... | |
11809 * structure. | |
11810 * | |
11811 * We want to use notifySplices to update the arrays for performance reasons | |
11812 * which requires manually batching and sending the notifySplices for each | |
11813 * level. To do this, we build a tree where each node is an array and then | |
11814 * depth traverse it to remove items. Each time a node has all children | |
11815 * deleted, we can also remove the node. | |
11816 * | |
11817 * @param {Array<string>} paths | |
11818 * @private | |
11819 */ | |
11820 removeItemsByPath: function(paths) { | 6811 removeItemsByPath: function(paths) { |
11821 if (paths.length == 0) | 6812 if (paths.length == 0) return; |
11822 return; | |
11823 | |
11824 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths)); | 6813 this.removeItemsBeneathNode_(this.buildRemovalTree_(paths)); |
11825 }, | 6814 }, |
11826 | |
11827 /** | |
11828 * Creates the tree to traverse in order to remove |paths| from this list. | |
11829 * Assumes paths are of a.0.b.0... | |
11830 * structure. | |
11831 * | |
11832 * @param {Array<string>} paths | |
11833 * @return {SelectionTreeNode} | |
11834 * @private | |
11835 */ | |
11836 buildRemovalTree_: function(paths) { | 6815 buildRemovalTree_: function(paths) { |
11837 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]); | 6816 var rootNode = new SelectionTreeNode(paths[0].split('.')[0]); |
11838 | |
11839 // Build a tree to each history item specified in |paths|. | |
11840 paths.forEach(function(path) { | 6817 paths.forEach(function(path) { |
11841 var components = path.split('.'); | 6818 var components = path.split('.'); |
11842 var node = rootNode; | 6819 var node = rootNode; |
11843 components.shift(); | 6820 components.shift(); |
11844 while (components.length > 1) { | 6821 while (components.length > 1) { |
11845 var index = Number(components.shift()); | 6822 var index = Number(components.shift()); |
11846 var arrayName = components.shift(); | 6823 var arrayName = components.shift(); |
11847 | 6824 if (!node.children[index]) node.addChild(index, [ node.currentPath, inde
x, arrayName ].join('.')); |
11848 if (!node.children[index]) | |
11849 node.addChild(index, [node.currentPath, index, arrayName].join('.')); | |
11850 | |
11851 node = node.children[index]; | 6825 node = node.children[index]; |
11852 } | 6826 } |
11853 node.leaf = true; | 6827 node.leaf = true; |
11854 node.indexes.push(Number(components.shift())); | 6828 node.indexes.push(Number(components.shift())); |
11855 }); | 6829 }); |
11856 | |
11857 return rootNode; | 6830 return rootNode; |
11858 }, | 6831 }, |
11859 | |
11860 /** | |
11861 * Removes the history items underneath |node| and deletes container arrays as | |
11862 * they become empty. | |
11863 * @param {SelectionTreeNode} node | |
11864 * @return {boolean} Whether this node's array should be deleted. | |
11865 * @private | |
11866 */ | |
11867 removeItemsBeneathNode_: function(node) { | 6832 removeItemsBeneathNode_: function(node) { |
11868 var array = this.get(node.currentPath); | 6833 var array = this.get(node.currentPath); |
11869 var splices = []; | 6834 var splices = []; |
11870 | 6835 node.indexes.sort(function(a, b) { |
11871 node.indexes.sort(function(a, b) { return b - a; }); | 6836 return b - a; |
| 6837 }); |
11872 node.indexes.forEach(function(index) { | 6838 node.indexes.forEach(function(index) { |
11873 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) { | 6839 if (node.leaf || this.removeItemsBeneathNode_(node.children[index])) { |
11874 var item = array.splice(index, 1); | 6840 var item = array.splice(index, 1); |
11875 splices.push({ | 6841 splices.push({ |
11876 index: index, | 6842 index: index, |
11877 removed: [item], | 6843 removed: [ item ], |
11878 addedCount: 0, | 6844 addedCount: 0, |
11879 object: array, | 6845 object: array, |
11880 type: 'splice' | 6846 type: 'splice' |
11881 }); | 6847 }); |
11882 } | 6848 } |
11883 }.bind(this)); | 6849 }.bind(this)); |
11884 | 6850 if (array.length == 0) return true; |
11885 if (array.length == 0) | |
11886 return true; | |
11887 | |
11888 // notifySplices gives better performance than individually splicing as it | |
11889 // batches all of the updates together. | |
11890 this.notifySplices(node.currentPath, splices); | 6851 this.notifySplices(node.currentPath, splices); |
11891 return false; | 6852 return false; |
11892 }, | 6853 }, |
11893 | |
11894 /** | |
11895 * @param {Event} e | |
11896 * @private | |
11897 */ | |
11898 itemSelected_: function(e) { | 6854 itemSelected_: function(e) { |
11899 var item = e.detail.element; | 6855 var item = e.detail.element; |
11900 var paths = []; | 6856 var paths = []; |
11901 var itemPath = item.path; | 6857 var itemPath = item.path; |
11902 | |
11903 // Handle shift selection. Change the selection state of all items between | |
11904 // |path| and |lastSelected| to the selection state of |item|. | |
11905 if (e.detail.shiftKey && this.lastSelectedPath) { | 6858 if (e.detail.shiftKey && this.lastSelectedPath) { |
11906 var itemPathComponents = itemPath.split('.'); | 6859 var itemPathComponents = itemPath.split('.'); |
11907 var itemIndex = Number(itemPathComponents.pop()); | 6860 var itemIndex = Number(itemPathComponents.pop()); |
11908 var itemArrayPath = itemPathComponents.join('.'); | 6861 var itemArrayPath = itemPathComponents.join('.'); |
11909 | |
11910 var lastItemPathComponents = this.lastSelectedPath.split('.'); | 6862 var lastItemPathComponents = this.lastSelectedPath.split('.'); |
11911 var lastItemIndex = Number(lastItemPathComponents.pop()); | 6863 var lastItemIndex = Number(lastItemPathComponents.pop()); |
11912 if (itemArrayPath == lastItemPathComponents.join('.')) { | 6864 if (itemArrayPath == lastItemPathComponents.join('.')) { |
11913 for (var i = Math.min(itemIndex, lastItemIndex); | 6865 for (var i = Math.min(itemIndex, lastItemIndex); i <= Math.max(itemIndex
, lastItemIndex); i++) { |
11914 i <= Math.max(itemIndex, lastItemIndex); i++) { | |
11915 paths.push(itemArrayPath + '.' + i); | 6866 paths.push(itemArrayPath + '.' + i); |
11916 } | 6867 } |
11917 } | 6868 } |
11918 } | 6869 } |
11919 | 6870 if (paths.length == 0) paths.push(item.path); |
11920 if (paths.length == 0) | |
11921 paths.push(item.path); | |
11922 | |
11923 paths.forEach(function(path) { | 6871 paths.forEach(function(path) { |
11924 this.set(path + '.selected', item.selected); | 6872 this.set(path + '.selected', item.selected); |
11925 | |
11926 if (item.selected) { | 6873 if (item.selected) { |
11927 this.selectedPaths.add(path); | 6874 this.selectedPaths.add(path); |
11928 return; | 6875 return; |
11929 } | 6876 } |
11930 | |
11931 this.selectedPaths.delete(path); | 6877 this.selectedPaths.delete(path); |
11932 }.bind(this)); | 6878 }.bind(this)); |
| 6879 this.lastSelectedPath = itemPath; |
| 6880 } |
| 6881 }; |
11933 | 6882 |
11934 this.lastSelectedPath = itemPath; | |
11935 }, | |
11936 }; | |
11937 // Copyright 2016 The Chromium Authors. All rights reserved. | 6883 // Copyright 2016 The Chromium Authors. All rights reserved. |
11938 // Use of this source code is governed by a BSD-style license that can be | 6884 // Use of this source code is governed by a BSD-style license that can be |
11939 // found in the LICENSE file. | 6885 // found in the LICENSE file. |
11940 | |
11941 /** | |
11942 * @typedef {{domain: string, | |
11943 * visits: !Array<HistoryEntry>, | |
11944 * rendered: boolean, | |
11945 * expanded: boolean}} | |
11946 */ | |
11947 var HistoryDomain; | 6886 var HistoryDomain; |
11948 | 6887 |
11949 /** | |
11950 * @typedef {{title: string, | |
11951 * domains: !Array<HistoryDomain>}} | |
11952 */ | |
11953 var HistoryGroup; | 6888 var HistoryGroup; |
11954 | 6889 |
11955 Polymer({ | 6890 Polymer({ |
11956 is: 'history-grouped-list', | 6891 is: 'history-grouped-list', |
11957 | 6892 behaviors: [ HistoryListBehavior ], |
11958 behaviors: [HistoryListBehavior], | |
11959 | |
11960 properties: { | 6893 properties: { |
11961 // An array of history entries in reverse chronological order. | |
11962 historyData: { | 6894 historyData: { |
11963 type: Array, | 6895 type: Array |
11964 }, | 6896 }, |
11965 | |
11966 /** | |
11967 * @type {Array<HistoryGroup>} | |
11968 */ | |
11969 groupedHistoryData_: { | 6897 groupedHistoryData_: { |
11970 type: Array, | 6898 type: Array |
11971 }, | 6899 }, |
11972 | |
11973 searchedTerm: { | 6900 searchedTerm: { |
11974 type: String, | 6901 type: String, |
11975 value: '' | 6902 value: '' |
11976 }, | 6903 }, |
11977 | |
11978 range: { | 6904 range: { |
11979 type: Number, | 6905 type: Number |
11980 }, | 6906 }, |
11981 | |
11982 queryStartTime: String, | 6907 queryStartTime: String, |
11983 queryEndTime: String, | 6908 queryEndTime: String |
11984 }, | 6909 }, |
11985 | 6910 observers: [ 'updateGroupedHistoryData_(range, historyData)' ], |
11986 observers: [ | |
11987 'updateGroupedHistoryData_(range, historyData)' | |
11988 ], | |
11989 | |
11990 /** | |
11991 * Make a list of domains from visits. | |
11992 * @param {!Array<!HistoryEntry>} visits | |
11993 * @return {!Array<!HistoryDomain>} | |
11994 */ | |
11995 createHistoryDomains_: function(visits) { | 6911 createHistoryDomains_: function(visits) { |
11996 var domainIndexes = {}; | 6912 var domainIndexes = {}; |
11997 var domains = []; | 6913 var domains = []; |
11998 | |
11999 // Group the visits into a dictionary and generate a list of domains. | |
12000 for (var i = 0, visit; visit = visits[i]; i++) { | 6914 for (var i = 0, visit; visit = visits[i]; i++) { |
12001 var domain = visit.domain; | 6915 var domain = visit.domain; |
12002 if (domainIndexes[domain] == undefined) { | 6916 if (domainIndexes[domain] == undefined) { |
12003 domainIndexes[domain] = domains.length; | 6917 domainIndexes[domain] = domains.length; |
12004 domains.push({ | 6918 domains.push({ |
12005 domain: domain, | 6919 domain: domain, |
12006 visits: [], | 6920 visits: [], |
12007 expanded: false, | 6921 expanded: false, |
12008 rendered: false, | 6922 rendered: false |
12009 }); | 6923 }); |
12010 } | 6924 } |
12011 domains[domainIndexes[domain]].visits.push(visit); | 6925 domains[domainIndexes[domain]].visits.push(visit); |
12012 } | 6926 } |
12013 var sortByVisits = function(a, b) { | 6927 var sortByVisits = function(a, b) { |
12014 return b.visits.length - a.visits.length; | 6928 return b.visits.length - a.visits.length; |
12015 }; | 6929 }; |
12016 domains.sort(sortByVisits); | 6930 domains.sort(sortByVisits); |
12017 | |
12018 return domains; | 6931 return domains; |
12019 }, | 6932 }, |
12020 | |
12021 updateGroupedHistoryData_: function() { | 6933 updateGroupedHistoryData_: function() { |
12022 if (this.historyData.length == 0) { | 6934 if (this.historyData.length == 0) { |
12023 this.groupedHistoryData_ = []; | 6935 this.groupedHistoryData_ = []; |
12024 return; | 6936 return; |
12025 } | 6937 } |
12026 | |
12027 if (this.range == HistoryRange.WEEK) { | 6938 if (this.range == HistoryRange.WEEK) { |
12028 // Group each day into a list of results. | |
12029 var days = []; | 6939 var days = []; |
12030 var currentDayVisits = [this.historyData[0]]; | 6940 var currentDayVisits = [ this.historyData[0] ]; |
12031 | |
12032 var pushCurrentDay = function() { | 6941 var pushCurrentDay = function() { |
12033 days.push({ | 6942 days.push({ |
12034 title: this.searchedTerm ? currentDayVisits[0].dateShort : | 6943 title: this.searchedTerm ? currentDayVisits[0].dateShort : currentDayV
isits[0].dateRelativeDay, |
12035 currentDayVisits[0].dateRelativeDay, | 6944 domains: this.createHistoryDomains_(currentDayVisits) |
12036 domains: this.createHistoryDomains_(currentDayVisits), | |
12037 }); | 6945 }); |
12038 }.bind(this); | 6946 }.bind(this); |
12039 | |
12040 var visitsSameDay = function(a, b) { | 6947 var visitsSameDay = function(a, b) { |
12041 if (this.searchedTerm) | 6948 if (this.searchedTerm) return a.dateShort == b.dateShort; |
12042 return a.dateShort == b.dateShort; | |
12043 | |
12044 return a.dateRelativeDay == b.dateRelativeDay; | 6949 return a.dateRelativeDay == b.dateRelativeDay; |
12045 }.bind(this); | 6950 }.bind(this); |
12046 | |
12047 for (var i = 1; i < this.historyData.length; i++) { | 6951 for (var i = 1; i < this.historyData.length; i++) { |
12048 var visit = this.historyData[i]; | 6952 var visit = this.historyData[i]; |
12049 if (!visitsSameDay(visit, currentDayVisits[0])) { | 6953 if (!visitsSameDay(visit, currentDayVisits[0])) { |
12050 pushCurrentDay(); | 6954 pushCurrentDay(); |
12051 currentDayVisits = []; | 6955 currentDayVisits = []; |
12052 } | 6956 } |
12053 currentDayVisits.push(visit); | 6957 currentDayVisits.push(visit); |
12054 } | 6958 } |
12055 pushCurrentDay(); | 6959 pushCurrentDay(); |
12056 | |
12057 this.groupedHistoryData_ = days; | 6960 this.groupedHistoryData_ = days; |
12058 } else if (this.range == HistoryRange.MONTH) { | 6961 } else if (this.range == HistoryRange.MONTH) { |
12059 // Group each all visits into a single list. | 6962 this.groupedHistoryData_ = [ { |
12060 this.groupedHistoryData_ = [{ | |
12061 title: this.queryStartTime + ' – ' + this.queryEndTime, | 6963 title: this.queryStartTime + ' – ' + this.queryEndTime, |
12062 domains: this.createHistoryDomains_(this.historyData) | 6964 domains: this.createHistoryDomains_(this.historyData) |
12063 }]; | 6965 } ]; |
12064 } | 6966 } |
12065 }, | 6967 }, |
12066 | |
12067 /** | |
12068 * @param {{model:Object, currentTarget:IronCollapseElement}} e | |
12069 */ | |
12070 toggleDomainExpanded_: function(e) { | 6968 toggleDomainExpanded_: function(e) { |
12071 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse'); | 6969 var collapse = e.currentTarget.parentNode.querySelector('iron-collapse'); |
12072 e.model.set('domain.rendered', true); | 6970 e.model.set('domain.rendered', true); |
12073 | 6971 setTimeout(function() { |
12074 // Give the history-items time to render. | 6972 collapse.toggle(); |
12075 setTimeout(function() { collapse.toggle() }, 0); | 6973 }, 0); |
12076 }, | 6974 }, |
12077 | |
12078 /** | |
12079 * Check whether the time difference between the given history item and the | |
12080 * next one is large enough for a spacer to be required. | |
12081 * @param {number} groupIndex | |
12082 * @param {number} domainIndex | |
12083 * @param {number} itemIndex | |
12084 * @return {boolean} Whether or not time gap separator is required. | |
12085 * @private | |
12086 */ | |
12087 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) { | 6975 needsTimeGap_: function(groupIndex, domainIndex, itemIndex) { |
12088 var visits = | 6976 var visits = this.groupedHistoryData_[groupIndex].domains[domainIndex].visit
s; |
12089 this.groupedHistoryData_[groupIndex].domains[domainIndex].visits; | 6977 return md_history.HistoryItem.needsTimeGap(visits, itemIndex, this.searchedT
erm); |
12090 | 6978 }, |
12091 return md_history.HistoryItem.needsTimeGap( | |
12092 visits, itemIndex, this.searchedTerm); | |
12093 }, | |
12094 | |
12095 /** | |
12096 * @param {number} groupIndex | |
12097 * @param {number} domainIndex | |
12098 * @param {number} itemIndex | |
12099 * @return {string} | |
12100 * @private | |
12101 */ | |
12102 pathForItem_: function(groupIndex, domainIndex, itemIndex) { | 6979 pathForItem_: function(groupIndex, domainIndex, itemIndex) { |
12103 return [ | 6980 return [ 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits'
, itemIndex ].join('.'); |
12104 'groupedHistoryData_', groupIndex, 'domains', domainIndex, 'visits', | 6981 }, |
12105 itemIndex | |
12106 ].join('.'); | |
12107 }, | |
12108 | |
12109 /** | |
12110 * @param {HistoryDomain} domain | |
12111 * @return {string} | |
12112 * @private | |
12113 */ | |
12114 getWebsiteIconStyle_: function(domain) { | 6982 getWebsiteIconStyle_: function(domain) { |
12115 return 'background-image: ' + | 6983 return 'background-image: ' + cr.icon.getFaviconImageSet(domain.visits[0].ur
l); |
12116 cr.icon.getFaviconImageSet(domain.visits[0].url); | 6984 }, |
12117 }, | |
12118 | |
12119 /** | |
12120 * @param {boolean} expanded | |
12121 * @return {string} | |
12122 * @private | |
12123 */ | |
12124 getDropdownIcon_: function(expanded) { | 6985 getDropdownIcon_: function(expanded) { |
12125 return expanded ? 'cr:expand-less' : 'cr:expand-more'; | 6986 return expanded ? 'cr:expand-less' : 'cr:expand-more'; |
12126 }, | 6987 } |
12127 }); | 6988 }); |
12128 /** | |
12129 * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll e
vents from a | |
12130 * designated scroll target. | |
12131 * | |
12132 * Elements that consume this behavior can override the `_scrollHandler` | |
12133 * method to add logic on the scroll event. | |
12134 * | |
12135 * @demo demo/scrolling-region.html Scrolling Region | |
12136 * @demo demo/document.html Document Element | |
12137 * @polymerBehavior | |
12138 */ | |
12139 Polymer.IronScrollTargetBehavior = { | |
12140 | 6989 |
12141 properties: { | 6990 Polymer.IronScrollTargetBehavior = { |
| 6991 properties: { |
| 6992 scrollTarget: { |
| 6993 type: HTMLElement, |
| 6994 value: function() { |
| 6995 return this._defaultScrollTarget; |
| 6996 } |
| 6997 } |
| 6998 }, |
| 6999 observers: [ '_scrollTargetChanged(scrollTarget, isAttached)' ], |
| 7000 _scrollTargetChanged: function(scrollTarget, isAttached) { |
| 7001 var eventTarget; |
| 7002 if (this._oldScrollTarget) { |
| 7003 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScro
llTarget; |
| 7004 eventTarget.removeEventListener('scroll', this._boundScrollHandler); |
| 7005 this._oldScrollTarget = null; |
| 7006 } |
| 7007 if (!isAttached) { |
| 7008 return; |
| 7009 } |
| 7010 if (scrollTarget === 'document') { |
| 7011 this.scrollTarget = this._doc; |
| 7012 } else if (typeof scrollTarget === 'string') { |
| 7013 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : Polymer.
dom(this.ownerDocument).querySelector('#' + scrollTarget); |
| 7014 } else if (this._isValidScrollTarget()) { |
| 7015 eventTarget = scrollTarget === this._doc ? window : scrollTarget; |
| 7016 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler
.bind(this); |
| 7017 this._oldScrollTarget = scrollTarget; |
| 7018 eventTarget.addEventListener('scroll', this._boundScrollHandler); |
| 7019 } |
| 7020 }, |
| 7021 _scrollHandler: function scrollHandler() {}, |
| 7022 get _defaultScrollTarget() { |
| 7023 return this._doc; |
| 7024 }, |
| 7025 get _doc() { |
| 7026 return this.ownerDocument.documentElement; |
| 7027 }, |
| 7028 get _scrollTop() { |
| 7029 if (this._isValidScrollTarget()) { |
| 7030 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollT
arget.scrollTop; |
| 7031 } |
| 7032 return 0; |
| 7033 }, |
| 7034 get _scrollLeft() { |
| 7035 if (this._isValidScrollTarget()) { |
| 7036 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollT
arget.scrollLeft; |
| 7037 } |
| 7038 return 0; |
| 7039 }, |
| 7040 set _scrollTop(top) { |
| 7041 if (this.scrollTarget === this._doc) { |
| 7042 window.scrollTo(window.pageXOffset, top); |
| 7043 } else if (this._isValidScrollTarget()) { |
| 7044 this.scrollTarget.scrollTop = top; |
| 7045 } |
| 7046 }, |
| 7047 set _scrollLeft(left) { |
| 7048 if (this.scrollTarget === this._doc) { |
| 7049 window.scrollTo(left, window.pageYOffset); |
| 7050 } else if (this._isValidScrollTarget()) { |
| 7051 this.scrollTarget.scrollLeft = left; |
| 7052 } |
| 7053 }, |
| 7054 scroll: function(left, top) { |
| 7055 if (this.scrollTarget === this._doc) { |
| 7056 window.scrollTo(left, top); |
| 7057 } else if (this._isValidScrollTarget()) { |
| 7058 this.scrollTarget.scrollLeft = left; |
| 7059 this.scrollTarget.scrollTop = top; |
| 7060 } |
| 7061 }, |
| 7062 get _scrollTargetWidth() { |
| 7063 if (this._isValidScrollTarget()) { |
| 7064 return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTa
rget.offsetWidth; |
| 7065 } |
| 7066 return 0; |
| 7067 }, |
| 7068 get _scrollTargetHeight() { |
| 7069 if (this._isValidScrollTarget()) { |
| 7070 return this.scrollTarget === this._doc ? window.innerHeight : this.scrollT
arget.offsetHeight; |
| 7071 } |
| 7072 return 0; |
| 7073 }, |
| 7074 _isValidScrollTarget: function() { |
| 7075 return this.scrollTarget instanceof HTMLElement; |
| 7076 } |
| 7077 }; |
12142 | 7078 |
12143 /** | |
12144 * Specifies the element that will handle the scroll event | |
12145 * on the behalf of the current element. This is typically a reference to
an element, | |
12146 * but there are a few more posibilities: | |
12147 * | |
12148 * ### Elements id | |
12149 * | |
12150 *```html | |
12151 * <div id="scrollable-element" style="overflow: auto;"> | |
12152 * <x-element scroll-target="scrollable-element"> | |
12153 * \x3c!-- Content--\x3e | |
12154 * </x-element> | |
12155 * </div> | |
12156 *``` | |
12157 * In this case, the `scrollTarget` will point to the outer div element. | |
12158 * | |
12159 * ### Document scrolling | |
12160 * | |
12161 * For document scrolling, you can use the reserved word `document`: | |
12162 * | |
12163 *```html | |
12164 * <x-element scroll-target="document"> | |
12165 * \x3c!-- Content --\x3e | |
12166 * </x-element> | |
12167 *``` | |
12168 * | |
12169 * ### Elements reference | |
12170 * | |
12171 *```js | |
12172 * appHeader.scrollTarget = document.querySelector('#scrollable-element'); | |
12173 *``` | |
12174 * | |
12175 * @type {HTMLElement} | |
12176 */ | |
12177 scrollTarget: { | |
12178 type: HTMLElement, | |
12179 value: function() { | |
12180 return this._defaultScrollTarget; | |
12181 } | |
12182 } | |
12183 }, | |
12184 | |
12185 observers: [ | |
12186 '_scrollTargetChanged(scrollTarget, isAttached)' | |
12187 ], | |
12188 | |
12189 _scrollTargetChanged: function(scrollTarget, isAttached) { | |
12190 var eventTarget; | |
12191 | |
12192 if (this._oldScrollTarget) { | |
12193 eventTarget = this._oldScrollTarget === this._doc ? window : this._oldSc
rollTarget; | |
12194 eventTarget.removeEventListener('scroll', this._boundScrollHandler); | |
12195 this._oldScrollTarget = null; | |
12196 } | |
12197 | |
12198 if (!isAttached) { | |
12199 return; | |
12200 } | |
12201 // Support element id references | |
12202 if (scrollTarget === 'document') { | |
12203 | |
12204 this.scrollTarget = this._doc; | |
12205 | |
12206 } else if (typeof scrollTarget === 'string') { | |
12207 | |
12208 this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] : | |
12209 Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); | |
12210 | |
12211 } else if (this._isValidScrollTarget()) { | |
12212 | |
12213 eventTarget = scrollTarget === this._doc ? window : scrollTarget; | |
12214 this._boundScrollHandler = this._boundScrollHandler || this._scrollHandl
er.bind(this); | |
12215 this._oldScrollTarget = scrollTarget; | |
12216 | |
12217 eventTarget.addEventListener('scroll', this._boundScrollHandler); | |
12218 } | |
12219 }, | |
12220 | |
12221 /** | |
12222 * Runs on every scroll event. Consumer of this behavior may override this m
ethod. | |
12223 * | |
12224 * @protected | |
12225 */ | |
12226 _scrollHandler: function scrollHandler() {}, | |
12227 | |
12228 /** | |
12229 * The default scroll target. Consumers of this behavior may want to customi
ze | |
12230 * the default scroll target. | |
12231 * | |
12232 * @type {Element} | |
12233 */ | |
12234 get _defaultScrollTarget() { | |
12235 return this._doc; | |
12236 }, | |
12237 | |
12238 /** | |
12239 * Shortcut for the document element | |
12240 * | |
12241 * @type {Element} | |
12242 */ | |
12243 get _doc() { | |
12244 return this.ownerDocument.documentElement; | |
12245 }, | |
12246 | |
12247 /** | |
12248 * Gets the number of pixels that the content of an element is scrolled upwa
rd. | |
12249 * | |
12250 * @type {number} | |
12251 */ | |
12252 get _scrollTop() { | |
12253 if (this._isValidScrollTarget()) { | |
12254 return this.scrollTarget === this._doc ? window.pageYOffset : this.scrol
lTarget.scrollTop; | |
12255 } | |
12256 return 0; | |
12257 }, | |
12258 | |
12259 /** | |
12260 * Gets the number of pixels that the content of an element is scrolled to t
he left. | |
12261 * | |
12262 * @type {number} | |
12263 */ | |
12264 get _scrollLeft() { | |
12265 if (this._isValidScrollTarget()) { | |
12266 return this.scrollTarget === this._doc ? window.pageXOffset : this.scrol
lTarget.scrollLeft; | |
12267 } | |
12268 return 0; | |
12269 }, | |
12270 | |
12271 /** | |
12272 * Sets the number of pixels that the content of an element is scrolled upwa
rd. | |
12273 * | |
12274 * @type {number} | |
12275 */ | |
12276 set _scrollTop(top) { | |
12277 if (this.scrollTarget === this._doc) { | |
12278 window.scrollTo(window.pageXOffset, top); | |
12279 } else if (this._isValidScrollTarget()) { | |
12280 this.scrollTarget.scrollTop = top; | |
12281 } | |
12282 }, | |
12283 | |
12284 /** | |
12285 * Sets the number of pixels that the content of an element is scrolled to t
he left. | |
12286 * | |
12287 * @type {number} | |
12288 */ | |
12289 set _scrollLeft(left) { | |
12290 if (this.scrollTarget === this._doc) { | |
12291 window.scrollTo(left, window.pageYOffset); | |
12292 } else if (this._isValidScrollTarget()) { | |
12293 this.scrollTarget.scrollLeft = left; | |
12294 } | |
12295 }, | |
12296 | |
12297 /** | |
12298 * Scrolls the content to a particular place. | |
12299 * | |
12300 * @method scroll | |
12301 * @param {number} left The left position | |
12302 * @param {number} top The top position | |
12303 */ | |
12304 scroll: function(left, top) { | |
12305 if (this.scrollTarget === this._doc) { | |
12306 window.scrollTo(left, top); | |
12307 } else if (this._isValidScrollTarget()) { | |
12308 this.scrollTarget.scrollLeft = left; | |
12309 this.scrollTarget.scrollTop = top; | |
12310 } | |
12311 }, | |
12312 | |
12313 /** | |
12314 * Gets the width of the scroll target. | |
12315 * | |
12316 * @type {number} | |
12317 */ | |
12318 get _scrollTargetWidth() { | |
12319 if (this._isValidScrollTarget()) { | |
12320 return this.scrollTarget === this._doc ? window.innerWidth : this.scroll
Target.offsetWidth; | |
12321 } | |
12322 return 0; | |
12323 }, | |
12324 | |
12325 /** | |
12326 * Gets the height of the scroll target. | |
12327 * | |
12328 * @type {number} | |
12329 */ | |
12330 get _scrollTargetHeight() { | |
12331 if (this._isValidScrollTarget()) { | |
12332 return this.scrollTarget === this._doc ? window.innerHeight : this.scrol
lTarget.offsetHeight; | |
12333 } | |
12334 return 0; | |
12335 }, | |
12336 | |
12337 /** | |
12338 * Returns true if the scroll target is a valid HTMLElement. | |
12339 * | |
12340 * @return {boolean} | |
12341 */ | |
12342 _isValidScrollTarget: function() { | |
12343 return this.scrollTarget instanceof HTMLElement; | |
12344 } | |
12345 }; | |
12346 (function() { | 7079 (function() { |
12347 | |
12348 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); | 7080 var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
12349 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; | 7081 var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
12350 var DEFAULT_PHYSICAL_COUNT = 3; | 7082 var DEFAULT_PHYSICAL_COUNT = 3; |
12351 var HIDDEN_Y = '-10000px'; | 7083 var HIDDEN_Y = '-10000px'; |
12352 var DEFAULT_GRID_SIZE = 200; | 7084 var DEFAULT_GRID_SIZE = 200; |
12353 var SECRET_TABINDEX = -100; | 7085 var SECRET_TABINDEX = -100; |
12354 | |
12355 Polymer({ | 7086 Polymer({ |
12356 | |
12357 is: 'iron-list', | 7087 is: 'iron-list', |
12358 | |
12359 properties: { | 7088 properties: { |
12360 | |
12361 /** | |
12362 * An array containing items determining how many instances of the templat
e | |
12363 * to stamp and that that each template instance should bind to. | |
12364 */ | |
12365 items: { | 7089 items: { |
12366 type: Array | 7090 type: Array |
12367 }, | 7091 }, |
12368 | |
12369 /** | |
12370 * The max count of physical items the pool can extend to. | |
12371 */ | |
12372 maxPhysicalCount: { | 7092 maxPhysicalCount: { |
12373 type: Number, | 7093 type: Number, |
12374 value: 500 | 7094 value: 500 |
12375 }, | 7095 }, |
12376 | |
12377 /** | |
12378 * The name of the variable to add to the binding scope for the array | |
12379 * element associated with a given template instance. | |
12380 */ | |
12381 as: { | 7096 as: { |
12382 type: String, | 7097 type: String, |
12383 value: 'item' | 7098 value: 'item' |
12384 }, | 7099 }, |
12385 | |
12386 /** | |
12387 * The name of the variable to add to the binding scope with the index | |
12388 * for the row. | |
12389 */ | |
12390 indexAs: { | 7100 indexAs: { |
12391 type: String, | 7101 type: String, |
12392 value: 'index' | 7102 value: 'index' |
12393 }, | 7103 }, |
12394 | |
12395 /** | |
12396 * The name of the variable to add to the binding scope to indicate | |
12397 * if the row is selected. | |
12398 */ | |
12399 selectedAs: { | 7104 selectedAs: { |
12400 type: String, | 7105 type: String, |
12401 value: 'selected' | 7106 value: 'selected' |
12402 }, | 7107 }, |
12403 | |
12404 /** | |
12405 * When true, the list is rendered as a grid. Grid items must have | |
12406 * fixed width and height set via CSS. e.g. | |
12407 * | |
12408 * ```html | |
12409 * <iron-list grid> | |
12410 * <template> | |
12411 * <div style="width: 100px; height: 100px;"> 100x100 </div> | |
12412 * </template> | |
12413 * </iron-list> | |
12414 * ``` | |
12415 */ | |
12416 grid: { | 7108 grid: { |
12417 type: Boolean, | 7109 type: Boolean, |
12418 value: false, | 7110 value: false, |
12419 reflectToAttribute: true | 7111 reflectToAttribute: true |
12420 }, | 7112 }, |
12421 | |
12422 /** | |
12423 * When true, tapping a row will select the item, placing its data model | |
12424 * in the set of selected items retrievable via the selection property. | |
12425 * | |
12426 * Note that tapping focusable elements within the list item will not | |
12427 * result in selection, since they are presumed to have their * own action
. | |
12428 */ | |
12429 selectionEnabled: { | 7113 selectionEnabled: { |
12430 type: Boolean, | 7114 type: Boolean, |
12431 value: false | 7115 value: false |
12432 }, | 7116 }, |
12433 | |
12434 /** | |
12435 * When `multiSelection` is false, this is the currently selected item, or
`null` | |
12436 * if no item is selected. | |
12437 */ | |
12438 selectedItem: { | 7117 selectedItem: { |
12439 type: Object, | 7118 type: Object, |
12440 notify: true | 7119 notify: true |
12441 }, | 7120 }, |
12442 | |
12443 /** | |
12444 * When `multiSelection` is true, this is an array that contains the selec
ted items. | |
12445 */ | |
12446 selectedItems: { | 7121 selectedItems: { |
12447 type: Object, | 7122 type: Object, |
12448 notify: true | 7123 notify: true |
12449 }, | 7124 }, |
12450 | |
12451 /** | |
12452 * When `true`, multiple items may be selected at once (in this case, | |
12453 * `selected` is an array of currently selected items). When `false`, | |
12454 * only one item may be selected at a time. | |
12455 */ | |
12456 multiSelection: { | 7125 multiSelection: { |
12457 type: Boolean, | 7126 type: Boolean, |
12458 value: false | 7127 value: false |
12459 } | 7128 } |
12460 }, | 7129 }, |
12461 | 7130 observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEn
abled)', '_multiSelectionChanged(multiSelection)', '_setOverflow(scrollTarget)'
], |
12462 observers: [ | 7131 behaviors: [ Polymer.Templatizer, Polymer.IronResizableBehavior, Polymer.Iro
nA11yKeysBehavior, Polymer.IronScrollTargetBehavior ], |
12463 '_itemsChanged(items.*)', | |
12464 '_selectionEnabledChanged(selectionEnabled)', | |
12465 '_multiSelectionChanged(multiSelection)', | |
12466 '_setOverflow(scrollTarget)' | |
12467 ], | |
12468 | |
12469 behaviors: [ | |
12470 Polymer.Templatizer, | |
12471 Polymer.IronResizableBehavior, | |
12472 Polymer.IronA11yKeysBehavior, | |
12473 Polymer.IronScrollTargetBehavior | |
12474 ], | |
12475 | |
12476 keyBindings: { | 7132 keyBindings: { |
12477 'up': '_didMoveUp', | 7133 up: '_didMoveUp', |
12478 'down': '_didMoveDown', | 7134 down: '_didMoveDown', |
12479 'enter': '_didEnter' | 7135 enter: '_didEnter' |
12480 }, | 7136 }, |
12481 | 7137 _ratio: .5, |
12482 /** | |
12483 * The ratio of hidden tiles that should remain in the scroll direction. | |
12484 * Recommended value ~0.5, so it will distribute tiles evely in both directi
ons. | |
12485 */ | |
12486 _ratio: 0.5, | |
12487 | |
12488 /** | |
12489 * The padding-top value for the list. | |
12490 */ | |
12491 _scrollerPaddingTop: 0, | 7138 _scrollerPaddingTop: 0, |
12492 | |
12493 /** | |
12494 * This value is the same as `scrollTop`. | |
12495 */ | |
12496 _scrollPosition: 0, | 7139 _scrollPosition: 0, |
12497 | |
12498 /** | |
12499 * The sum of the heights of all the tiles in the DOM. | |
12500 */ | |
12501 _physicalSize: 0, | 7140 _physicalSize: 0, |
12502 | |
12503 /** | |
12504 * The average `offsetHeight` of the tiles observed till now. | |
12505 */ | |
12506 _physicalAverage: 0, | 7141 _physicalAverage: 0, |
12507 | |
12508 /** | |
12509 * The number of tiles which `offsetHeight` > 0 observed until now. | |
12510 */ | |
12511 _physicalAverageCount: 0, | 7142 _physicalAverageCount: 0, |
12512 | |
12513 /** | |
12514 * The Y position of the item rendered in the `_physicalStart` | |
12515 * tile relative to the scrolling list. | |
12516 */ | |
12517 _physicalTop: 0, | 7143 _physicalTop: 0, |
12518 | |
12519 /** | |
12520 * The number of items in the list. | |
12521 */ | |
12522 _virtualCount: 0, | 7144 _virtualCount: 0, |
12523 | |
12524 /** | |
12525 * A map between an item key and its physical item index | |
12526 */ | |
12527 _physicalIndexForKey: null, | 7145 _physicalIndexForKey: null, |
12528 | |
12529 /** | |
12530 * The estimated scroll height based on `_physicalAverage` | |
12531 */ | |
12532 _estScrollHeight: 0, | 7146 _estScrollHeight: 0, |
12533 | |
12534 /** | |
12535 * The scroll height of the dom node | |
12536 */ | |
12537 _scrollHeight: 0, | 7147 _scrollHeight: 0, |
12538 | |
12539 /** | |
12540 * The height of the list. This is referred as the viewport in the context o
f list. | |
12541 */ | |
12542 _viewportHeight: 0, | 7148 _viewportHeight: 0, |
12543 | |
12544 /** | |
12545 * The width of the list. This is referred as the viewport in the context of
list. | |
12546 */ | |
12547 _viewportWidth: 0, | 7149 _viewportWidth: 0, |
12548 | |
12549 /** | |
12550 * An array of DOM nodes that are currently in the tree | |
12551 * @type {?Array<!TemplatizerNode>} | |
12552 */ | |
12553 _physicalItems: null, | 7150 _physicalItems: null, |
12554 | |
12555 /** | |
12556 * An array of heights for each item in `_physicalItems` | |
12557 * @type {?Array<number>} | |
12558 */ | |
12559 _physicalSizes: null, | 7151 _physicalSizes: null, |
12560 | |
12561 /** | |
12562 * A cached value for the first visible index. | |
12563 * See `firstVisibleIndex` | |
12564 * @type {?number} | |
12565 */ | |
12566 _firstVisibleIndexVal: null, | 7152 _firstVisibleIndexVal: null, |
12567 | |
12568 /** | |
12569 * A cached value for the last visible index. | |
12570 * See `lastVisibleIndex` | |
12571 * @type {?number} | |
12572 */ | |
12573 _lastVisibleIndexVal: null, | 7153 _lastVisibleIndexVal: null, |
12574 | |
12575 /** | |
12576 * A Polymer collection for the items. | |
12577 * @type {?Polymer.Collection} | |
12578 */ | |
12579 _collection: null, | 7154 _collection: null, |
12580 | |
12581 /** | |
12582 * True if the current item list was rendered for the first time | |
12583 * after attached. | |
12584 */ | |
12585 _itemsRendered: false, | 7155 _itemsRendered: false, |
12586 | |
12587 /** | |
12588 * The page that is currently rendered. | |
12589 */ | |
12590 _lastPage: null, | 7156 _lastPage: null, |
12591 | |
12592 /** | |
12593 * The max number of pages to render. One page is equivalent to the height o
f the list. | |
12594 */ | |
12595 _maxPages: 3, | 7157 _maxPages: 3, |
12596 | |
12597 /** | |
12598 * The currently focused physical item. | |
12599 */ | |
12600 _focusedItem: null, | 7158 _focusedItem: null, |
12601 | |
12602 /** | |
12603 * The index of the `_focusedItem`. | |
12604 */ | |
12605 _focusedIndex: -1, | 7159 _focusedIndex: -1, |
12606 | |
12607 /** | |
12608 * The the item that is focused if it is moved offscreen. | |
12609 * @private {?TemplatizerNode} | |
12610 */ | |
12611 _offscreenFocusedItem: null, | 7160 _offscreenFocusedItem: null, |
12612 | |
12613 /** | |
12614 * The item that backfills the `_offscreenFocusedItem` in the physical items | |
12615 * list when that item is moved offscreen. | |
12616 */ | |
12617 _focusBackfillItem: null, | 7161 _focusBackfillItem: null, |
12618 | |
12619 /** | |
12620 * The maximum items per row | |
12621 */ | |
12622 _itemsPerRow: 1, | 7162 _itemsPerRow: 1, |
12623 | |
12624 /** | |
12625 * The width of each grid item | |
12626 */ | |
12627 _itemWidth: 0, | 7163 _itemWidth: 0, |
12628 | |
12629 /** | |
12630 * The height of the row in grid layout. | |
12631 */ | |
12632 _rowHeight: 0, | 7164 _rowHeight: 0, |
12633 | |
12634 /** | |
12635 * The bottom of the physical content. | |
12636 */ | |
12637 get _physicalBottom() { | 7165 get _physicalBottom() { |
12638 return this._physicalTop + this._physicalSize; | 7166 return this._physicalTop + this._physicalSize; |
12639 }, | 7167 }, |
12640 | |
12641 /** | |
12642 * The bottom of the scroll. | |
12643 */ | |
12644 get _scrollBottom() { | 7168 get _scrollBottom() { |
12645 return this._scrollPosition + this._viewportHeight; | 7169 return this._scrollPosition + this._viewportHeight; |
12646 }, | 7170 }, |
12647 | |
12648 /** | |
12649 * The n-th item rendered in the last physical item. | |
12650 */ | |
12651 get _virtualEnd() { | 7171 get _virtualEnd() { |
12652 return this._virtualStart + this._physicalCount - 1; | 7172 return this._virtualStart + this._physicalCount - 1; |
12653 }, | 7173 }, |
12654 | |
12655 /** | |
12656 * The height of the physical content that isn't on the screen. | |
12657 */ | |
12658 get _hiddenContentSize() { | 7174 get _hiddenContentSize() { |
12659 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; | 7175 var size = this.grid ? this._physicalRows * this._rowHeight : this._physic
alSize; |
12660 return size - this._viewportHeight; | 7176 return size - this._viewportHeight; |
12661 }, | 7177 }, |
12662 | |
12663 /** | |
12664 * The maximum scroll top value. | |
12665 */ | |
12666 get _maxScrollTop() { | 7178 get _maxScrollTop() { |
12667 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; | 7179 return this._estScrollHeight - this._viewportHeight + this._scrollerPaddin
gTop; |
12668 }, | 7180 }, |
12669 | |
12670 /** | |
12671 * The lowest n-th value for an item such that it can be rendered in `_physi
calStart`. | |
12672 */ | |
12673 _minVirtualStart: 0, | 7181 _minVirtualStart: 0, |
12674 | |
12675 /** | |
12676 * The largest n-th value for an item such that it can be rendered in `_phys
icalStart`. | |
12677 */ | |
12678 get _maxVirtualStart() { | 7182 get _maxVirtualStart() { |
12679 return Math.max(0, this._virtualCount - this._physicalCount); | 7183 return Math.max(0, this._virtualCount - this._physicalCount); |
12680 }, | 7184 }, |
12681 | |
12682 /** | |
12683 * The n-th item rendered in the `_physicalStart` tile. | |
12684 */ | |
12685 _virtualStartVal: 0, | 7185 _virtualStartVal: 0, |
12686 | |
12687 set _virtualStart(val) { | 7186 set _virtualStart(val) { |
12688 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); | 7187 this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._min
VirtualStart, val)); |
12689 }, | 7188 }, |
12690 | |
12691 get _virtualStart() { | 7189 get _virtualStart() { |
12692 return this._virtualStartVal || 0; | 7190 return this._virtualStartVal || 0; |
12693 }, | 7191 }, |
12694 | |
12695 /** | |
12696 * The k-th tile that is at the top of the scrolling list. | |
12697 */ | |
12698 _physicalStartVal: 0, | 7192 _physicalStartVal: 0, |
12699 | |
12700 set _physicalStart(val) { | 7193 set _physicalStart(val) { |
12701 this._physicalStartVal = val % this._physicalCount; | 7194 this._physicalStartVal = val % this._physicalCount; |
12702 if (this._physicalStartVal < 0) { | 7195 if (this._physicalStartVal < 0) { |
12703 this._physicalStartVal = this._physicalCount + this._physicalStartVal; | 7196 this._physicalStartVal = this._physicalCount + this._physicalStartVal; |
12704 } | 7197 } |
12705 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 7198 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
12706 }, | 7199 }, |
12707 | |
12708 get _physicalStart() { | 7200 get _physicalStart() { |
12709 return this._physicalStartVal || 0; | 7201 return this._physicalStartVal || 0; |
12710 }, | 7202 }, |
12711 | |
12712 /** | |
12713 * The number of tiles in the DOM. | |
12714 */ | |
12715 _physicalCountVal: 0, | 7203 _physicalCountVal: 0, |
12716 | |
12717 set _physicalCount(val) { | 7204 set _physicalCount(val) { |
12718 this._physicalCountVal = val; | 7205 this._physicalCountVal = val; |
12719 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; | 7206 this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this
._physicalCount; |
12720 }, | 7207 }, |
12721 | |
12722 get _physicalCount() { | 7208 get _physicalCount() { |
12723 return this._physicalCountVal; | 7209 return this._physicalCountVal; |
12724 }, | 7210 }, |
12725 | |
12726 /** | |
12727 * The k-th tile that is at the bottom of the scrolling list. | |
12728 */ | |
12729 _physicalEnd: 0, | 7211 _physicalEnd: 0, |
12730 | |
12731 /** | |
12732 * An optimal physical size such that we will have enough physical items | |
12733 * to fill up the viewport and recycle when the user scrolls. | |
12734 * | |
12735 * This default value assumes that we will at least have the equivalent | |
12736 * to a viewport of physical items above and below the user's viewport. | |
12737 */ | |
12738 get _optPhysicalSize() { | 7212 get _optPhysicalSize() { |
12739 if (this.grid) { | 7213 if (this.grid) { |
12740 return this._estRowsInView * this._rowHeight * this._maxPages; | 7214 return this._estRowsInView * this._rowHeight * this._maxPages; |
12741 } | 7215 } |
12742 return this._viewportHeight * this._maxPages; | 7216 return this._viewportHeight * this._maxPages; |
12743 }, | 7217 }, |
12744 | |
12745 get _optPhysicalCount() { | 7218 get _optPhysicalCount() { |
12746 return this._estRowsInView * this._itemsPerRow * this._maxPages; | 7219 return this._estRowsInView * this._itemsPerRow * this._maxPages; |
12747 }, | 7220 }, |
12748 | |
12749 /** | |
12750 * True if the current list is visible. | |
12751 */ | |
12752 get _isVisible() { | 7221 get _isVisible() { |
12753 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); | 7222 return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.
scrollTarget.offsetHeight); |
12754 }, | 7223 }, |
12755 | |
12756 /** | |
12757 * Gets the index of the first visible item in the viewport. | |
12758 * | |
12759 * @type {number} | |
12760 */ | |
12761 get firstVisibleIndex() { | 7224 get firstVisibleIndex() { |
12762 if (this._firstVisibleIndexVal === null) { | 7225 if (this._firstVisibleIndexVal === null) { |
12763 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); | 7226 var physicalOffset = Math.floor(this._physicalTop + this._scrollerPaddin
gTop); |
12764 | 7227 this._firstVisibleIndexVal = this._iterateItems(function(pidx, vidx) { |
12765 this._firstVisibleIndexVal = this._iterateItems( | 7228 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
12766 function(pidx, vidx) { | 7229 if (physicalOffset > this._scrollPosition) { |
12767 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 7230 return this.grid ? vidx - vidx % this._itemsPerRow : vidx; |
12768 | 7231 } |
12769 if (physicalOffset > this._scrollPosition) { | 7232 if (this.grid && this._virtualCount - 1 === vidx) { |
12770 return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx; | 7233 return vidx - vidx % this._itemsPerRow; |
12771 } | 7234 } |
12772 // Handle a partially rendered final row in grid mode | 7235 }) || 0; |
12773 if (this.grid && this._virtualCount - 1 === vidx) { | |
12774 return vidx - (vidx % this._itemsPerRow); | |
12775 } | |
12776 }) || 0; | |
12777 } | 7236 } |
12778 return this._firstVisibleIndexVal; | 7237 return this._firstVisibleIndexVal; |
12779 }, | 7238 }, |
12780 | |
12781 /** | |
12782 * Gets the index of the last visible item in the viewport. | |
12783 * | |
12784 * @type {number} | |
12785 */ | |
12786 get lastVisibleIndex() { | 7239 get lastVisibleIndex() { |
12787 if (this._lastVisibleIndexVal === null) { | 7240 if (this._lastVisibleIndexVal === null) { |
12788 if (this.grid) { | 7241 if (this.grid) { |
12789 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; | 7242 var lastIndex = this.firstVisibleIndex + this._estRowsInView * this._i
temsPerRow - 1; |
12790 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); | 7243 this._lastVisibleIndexVal = Math.min(this._virtualCount, lastIndex); |
12791 } else { | 7244 } else { |
12792 var physicalOffset = this._physicalTop; | 7245 var physicalOffset = this._physicalTop; |
12793 this._iterateItems(function(pidx, vidx) { | 7246 this._iterateItems(function(pidx, vidx) { |
12794 if (physicalOffset < this._scrollBottom) { | 7247 if (physicalOffset < this._scrollBottom) { |
12795 this._lastVisibleIndexVal = vidx; | 7248 this._lastVisibleIndexVal = vidx; |
12796 } else { | 7249 } else { |
12797 // Break _iterateItems | |
12798 return true; | 7250 return true; |
12799 } | 7251 } |
12800 physicalOffset += this._getPhysicalSizeIncrement(pidx); | 7252 physicalOffset += this._getPhysicalSizeIncrement(pidx); |
12801 }); | 7253 }); |
12802 } | 7254 } |
12803 } | 7255 } |
12804 return this._lastVisibleIndexVal; | 7256 return this._lastVisibleIndexVal; |
12805 }, | 7257 }, |
12806 | |
12807 get _defaultScrollTarget() { | 7258 get _defaultScrollTarget() { |
12808 return this; | 7259 return this; |
12809 }, | 7260 }, |
12810 get _virtualRowCount() { | 7261 get _virtualRowCount() { |
12811 return Math.ceil(this._virtualCount / this._itemsPerRow); | 7262 return Math.ceil(this._virtualCount / this._itemsPerRow); |
12812 }, | 7263 }, |
12813 | |
12814 get _estRowsInView() { | 7264 get _estRowsInView() { |
12815 return Math.ceil(this._viewportHeight / this._rowHeight); | 7265 return Math.ceil(this._viewportHeight / this._rowHeight); |
12816 }, | 7266 }, |
12817 | |
12818 get _physicalRows() { | 7267 get _physicalRows() { |
12819 return Math.ceil(this._physicalCount / this._itemsPerRow); | 7268 return Math.ceil(this._physicalCount / this._itemsPerRow); |
12820 }, | 7269 }, |
12821 | |
12822 ready: function() { | 7270 ready: function() { |
12823 this.addEventListener('focus', this._didFocus.bind(this), true); | 7271 this.addEventListener('focus', this._didFocus.bind(this), true); |
12824 }, | 7272 }, |
12825 | |
12826 attached: function() { | 7273 attached: function() { |
12827 this.updateViewportBoundaries(); | 7274 this.updateViewportBoundaries(); |
12828 this._render(); | 7275 this._render(); |
12829 // `iron-resize` is fired when the list is attached if the event is added | |
12830 // before attached causing unnecessary work. | |
12831 this.listen(this, 'iron-resize', '_resizeHandler'); | 7276 this.listen(this, 'iron-resize', '_resizeHandler'); |
12832 }, | 7277 }, |
12833 | |
12834 detached: function() { | 7278 detached: function() { |
12835 this._itemsRendered = false; | 7279 this._itemsRendered = false; |
12836 this.unlisten(this, 'iron-resize', '_resizeHandler'); | 7280 this.unlisten(this, 'iron-resize', '_resizeHandler'); |
12837 }, | 7281 }, |
12838 | |
12839 /** | |
12840 * Set the overflow property if this element has its own scrolling region | |
12841 */ | |
12842 _setOverflow: function(scrollTarget) { | 7282 _setOverflow: function(scrollTarget) { |
12843 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; | 7283 this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; |
12844 this.style.overflow = scrollTarget === this ? 'auto' : ''; | 7284 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
12845 }, | 7285 }, |
12846 | |
12847 /** | |
12848 * Invoke this method if you dynamically update the viewport's | |
12849 * size or CSS padding. | |
12850 * | |
12851 * @method updateViewportBoundaries | |
12852 */ | |
12853 updateViewportBoundaries: function() { | 7286 updateViewportBoundaries: function() { |
12854 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : | 7287 this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(windo
w.getComputedStyle(this)['padding-top'], 10); |
12855 parseInt(window.getComputedStyle(this)['padding-top'], 10); | |
12856 | |
12857 this._viewportHeight = this._scrollTargetHeight; | 7288 this._viewportHeight = this._scrollTargetHeight; |
12858 if (this.grid) { | 7289 if (this.grid) { |
12859 this._updateGridMetrics(); | 7290 this._updateGridMetrics(); |
12860 } | 7291 } |
12861 }, | 7292 }, |
12862 | |
12863 /** | |
12864 * Update the models, the position of the | |
12865 * items in the viewport and recycle tiles as needed. | |
12866 */ | |
12867 _scrollHandler: function() { | 7293 _scrollHandler: function() { |
12868 // clamp the `scrollTop` value | |
12869 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; | 7294 var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop))
; |
12870 var delta = scrollTop - this._scrollPosition; | 7295 var delta = scrollTop - this._scrollPosition; |
12871 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; | 7296 var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBotto
m; |
12872 var ratio = this._ratio; | 7297 var ratio = this._ratio; |
12873 var recycledTiles = 0; | 7298 var recycledTiles = 0; |
12874 var hiddenContentSize = this._hiddenContentSize; | 7299 var hiddenContentSize = this._hiddenContentSize; |
12875 var currentRatio = ratio; | 7300 var currentRatio = ratio; |
12876 var movingUp = []; | 7301 var movingUp = []; |
12877 | |
12878 // track the last `scrollTop` | |
12879 this._scrollPosition = scrollTop; | 7302 this._scrollPosition = scrollTop; |
12880 | |
12881 // clear cached visible indexes | |
12882 this._firstVisibleIndexVal = null; | 7303 this._firstVisibleIndexVal = null; |
12883 this._lastVisibleIndexVal = null; | 7304 this._lastVisibleIndexVal = null; |
12884 | |
12885 scrollBottom = this._scrollBottom; | 7305 scrollBottom = this._scrollBottom; |
12886 physicalBottom = this._physicalBottom; | 7306 physicalBottom = this._physicalBottom; |
12887 | |
12888 // random access | |
12889 if (Math.abs(delta) > this._physicalSize) { | 7307 if (Math.abs(delta) > this._physicalSize) { |
12890 this._physicalTop += delta; | 7308 this._physicalTop += delta; |
12891 recycledTiles = Math.round(delta / this._physicalAverage); | 7309 recycledTiles = Math.round(delta / this._physicalAverage); |
12892 } | 7310 } else if (delta < 0) { |
12893 // scroll up | |
12894 else if (delta < 0) { | |
12895 var topSpace = scrollTop - this._physicalTop; | 7311 var topSpace = scrollTop - this._physicalTop; |
12896 var virtualStart = this._virtualStart; | 7312 var virtualStart = this._virtualStart; |
12897 | |
12898 recycledTileSet = []; | 7313 recycledTileSet = []; |
12899 | |
12900 kth = this._physicalEnd; | 7314 kth = this._physicalEnd; |
12901 currentRatio = topSpace / hiddenContentSize; | 7315 currentRatio = topSpace / hiddenContentSize; |
12902 | 7316 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualStart - recycledTiles > 0 && physicalBottom - this._getPhysicalSizeIncremen
t(kth) > scrollBottom) { |
12903 // move tiles from bottom to top | |
12904 while ( | |
12905 // approximate `currentRatio` to `ratio` | |
12906 currentRatio < ratio && | |
12907 // recycle less physical items than the total | |
12908 recycledTiles < this._physicalCount && | |
12909 // ensure that these recycled tiles are needed | |
12910 virtualStart - recycledTiles > 0 && | |
12911 // ensure that the tile is not visible | |
12912 physicalBottom - this._getPhysicalSizeIncrement(kth) > scrollBottom | |
12913 ) { | |
12914 | |
12915 tileHeight = this._getPhysicalSizeIncrement(kth); | 7317 tileHeight = this._getPhysicalSizeIncrement(kth); |
12916 currentRatio += tileHeight / hiddenContentSize; | 7318 currentRatio += tileHeight / hiddenContentSize; |
12917 physicalBottom -= tileHeight; | 7319 physicalBottom -= tileHeight; |
12918 recycledTileSet.push(kth); | 7320 recycledTileSet.push(kth); |
12919 recycledTiles++; | 7321 recycledTiles++; |
12920 kth = (kth === 0) ? this._physicalCount - 1 : kth - 1; | 7322 kth = kth === 0 ? this._physicalCount - 1 : kth - 1; |
12921 } | 7323 } |
12922 | |
12923 movingUp = recycledTileSet; | 7324 movingUp = recycledTileSet; |
12924 recycledTiles = -recycledTiles; | 7325 recycledTiles = -recycledTiles; |
12925 } | 7326 } else if (delta > 0) { |
12926 // scroll down | |
12927 else if (delta > 0) { | |
12928 var bottomSpace = physicalBottom - scrollBottom; | 7327 var bottomSpace = physicalBottom - scrollBottom; |
12929 var virtualEnd = this._virtualEnd; | 7328 var virtualEnd = this._virtualEnd; |
12930 var lastVirtualItemIndex = this._virtualCount-1; | 7329 var lastVirtualItemIndex = this._virtualCount - 1; |
12931 | |
12932 recycledTileSet = []; | 7330 recycledTileSet = []; |
12933 | |
12934 kth = this._physicalStart; | 7331 kth = this._physicalStart; |
12935 currentRatio = bottomSpace / hiddenContentSize; | 7332 currentRatio = bottomSpace / hiddenContentSize; |
12936 | 7333 while (currentRatio < ratio && recycledTiles < this._physicalCount && vi
rtualEnd + recycledTiles < lastVirtualItemIndex && this._physicalTop + this._get
PhysicalSizeIncrement(kth) < scrollTop) { |
12937 // move tiles from top to bottom | |
12938 while ( | |
12939 // approximate `currentRatio` to `ratio` | |
12940 currentRatio < ratio && | |
12941 // recycle less physical items than the total | |
12942 recycledTiles < this._physicalCount && | |
12943 // ensure that these recycled tiles are needed | |
12944 virtualEnd + recycledTiles < lastVirtualItemIndex && | |
12945 // ensure that the tile is not visible | |
12946 this._physicalTop + this._getPhysicalSizeIncrement(kth) < scrollTop | |
12947 ) { | |
12948 | |
12949 tileHeight = this._getPhysicalSizeIncrement(kth); | 7334 tileHeight = this._getPhysicalSizeIncrement(kth); |
12950 currentRatio += tileHeight / hiddenContentSize; | 7335 currentRatio += tileHeight / hiddenContentSize; |
12951 | |
12952 this._physicalTop += tileHeight; | 7336 this._physicalTop += tileHeight; |
12953 recycledTileSet.push(kth); | 7337 recycledTileSet.push(kth); |
12954 recycledTiles++; | 7338 recycledTiles++; |
12955 kth = (kth + 1) % this._physicalCount; | 7339 kth = (kth + 1) % this._physicalCount; |
12956 } | 7340 } |
12957 } | 7341 } |
12958 | |
12959 if (recycledTiles === 0) { | 7342 if (recycledTiles === 0) { |
12960 // Try to increase the pool if the list's client height isn't filled up
with physical items | |
12961 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { | 7343 if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) { |
12962 this._increasePoolIfNeeded(); | 7344 this._increasePoolIfNeeded(); |
12963 } | 7345 } |
12964 } else { | 7346 } else { |
12965 this._virtualStart = this._virtualStart + recycledTiles; | 7347 this._virtualStart = this._virtualStart + recycledTiles; |
12966 this._physicalStart = this._physicalStart + recycledTiles; | 7348 this._physicalStart = this._physicalStart + recycledTiles; |
12967 this._update(recycledTileSet, movingUp); | 7349 this._update(recycledTileSet, movingUp); |
12968 } | 7350 } |
12969 }, | 7351 }, |
12970 | |
12971 /** | |
12972 * Update the list of items, starting from the `_virtualStart` item. | |
12973 * @param {!Array<number>=} itemSet | |
12974 * @param {!Array<number>=} movingUp | |
12975 */ | |
12976 _update: function(itemSet, movingUp) { | 7352 _update: function(itemSet, movingUp) { |
12977 // manage focus | |
12978 this._manageFocus(); | 7353 this._manageFocus(); |
12979 // update models | |
12980 this._assignModels(itemSet); | 7354 this._assignModels(itemSet); |
12981 // measure heights | |
12982 this._updateMetrics(itemSet); | 7355 this._updateMetrics(itemSet); |
12983 // adjust offset after measuring | |
12984 if (movingUp) { | 7356 if (movingUp) { |
12985 while (movingUp.length) { | 7357 while (movingUp.length) { |
12986 var idx = movingUp.pop(); | 7358 var idx = movingUp.pop(); |
12987 this._physicalTop -= this._getPhysicalSizeIncrement(idx); | 7359 this._physicalTop -= this._getPhysicalSizeIncrement(idx); |
12988 } | 7360 } |
12989 } | 7361 } |
12990 // update the position of the items | |
12991 this._positionItems(); | 7362 this._positionItems(); |
12992 // set the scroller size | |
12993 this._updateScrollerSize(); | 7363 this._updateScrollerSize(); |
12994 // increase the pool of physical items | |
12995 this._increasePoolIfNeeded(); | 7364 this._increasePoolIfNeeded(); |
12996 }, | 7365 }, |
12997 | |
12998 /** | |
12999 * Creates a pool of DOM elements and attaches them to the local dom. | |
13000 */ | |
13001 _createPool: function(size) { | 7366 _createPool: function(size) { |
13002 var physicalItems = new Array(size); | 7367 var physicalItems = new Array(size); |
13003 | |
13004 this._ensureTemplatized(); | 7368 this._ensureTemplatized(); |
13005 | |
13006 for (var i = 0; i < size; i++) { | 7369 for (var i = 0; i < size; i++) { |
13007 var inst = this.stamp(null); | 7370 var inst = this.stamp(null); |
13008 // First element child is item; Safari doesn't support children[0] | |
13009 // on a doc fragment | |
13010 physicalItems[i] = inst.root.querySelector('*'); | 7371 physicalItems[i] = inst.root.querySelector('*'); |
13011 Polymer.dom(this).appendChild(inst.root); | 7372 Polymer.dom(this).appendChild(inst.root); |
13012 } | 7373 } |
13013 return physicalItems; | 7374 return physicalItems; |
13014 }, | 7375 }, |
13015 | |
13016 /** | |
13017 * Increases the pool of physical items only if needed. | |
13018 * | |
13019 * @return {boolean} True if the pool was increased. | |
13020 */ | |
13021 _increasePoolIfNeeded: function() { | 7376 _increasePoolIfNeeded: function() { |
13022 // Base case 1: the list has no height. | |
13023 if (this._viewportHeight === 0) { | 7377 if (this._viewportHeight === 0) { |
13024 return false; | 7378 return false; |
13025 } | 7379 } |
13026 // Base case 2: If the physical size is optimal and the list's client heig
ht is full | |
13027 // with physical items, don't increase the pool. | |
13028 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; | 7380 var isClientHeightFull = this._physicalBottom >= this._scrollBottom && thi
s._physicalTop <= this._scrollPosition; |
13029 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { | 7381 if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) { |
13030 return false; | 7382 return false; |
13031 } | 7383 } |
13032 // this value should range between [0 <= `currentPage` <= `_maxPages`] | |
13033 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); | 7384 var currentPage = Math.floor(this._physicalSize / this._viewportHeight); |
13034 | |
13035 if (currentPage === 0) { | 7385 if (currentPage === 0) { |
13036 // fill the first page | 7386 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * .5))); |
13037 this._debounceTemplate(this._increasePool.bind(this, Math.round(this._ph
ysicalCount * 0.5))); | |
13038 } else if (this._lastPage !== currentPage && isClientHeightFull) { | 7387 } else if (this._lastPage !== currentPage && isClientHeightFull) { |
13039 // paint the page and defer the next increase | |
13040 // wait 16ms which is rough enough to get paint cycle. | |
13041 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); | 7388 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increa
sePool.bind(this, this._itemsPerRow), 16)); |
13042 } else { | 7389 } else { |
13043 // fill the rest of the pages | |
13044 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; | 7390 this._debounceTemplate(this._increasePool.bind(this, this._itemsPerRow))
; |
13045 } | 7391 } |
13046 | |
13047 this._lastPage = currentPage; | 7392 this._lastPage = currentPage; |
13048 | |
13049 return true; | 7393 return true; |
13050 }, | 7394 }, |
13051 | |
13052 /** | |
13053 * Increases the pool size. | |
13054 */ | |
13055 _increasePool: function(missingItems) { | 7395 _increasePool: function(missingItems) { |
13056 var nextPhysicalCount = Math.min( | 7396 var nextPhysicalCount = Math.min(this._physicalCount + missingItems, this.
_virtualCount - this._virtualStart, Math.max(this.maxPhysicalCount, DEFAULT_PHYS
ICAL_COUNT)); |
13057 this._physicalCount + missingItems, | |
13058 this._virtualCount - this._virtualStart, | |
13059 Math.max(this.maxPhysicalCount, DEFAULT_PHYSICAL_COUNT) | |
13060 ); | |
13061 var prevPhysicalCount = this._physicalCount; | 7397 var prevPhysicalCount = this._physicalCount; |
13062 var delta = nextPhysicalCount - prevPhysicalCount; | 7398 var delta = nextPhysicalCount - prevPhysicalCount; |
13063 | |
13064 if (delta <= 0) { | 7399 if (delta <= 0) { |
13065 return; | 7400 return; |
13066 } | 7401 } |
13067 | |
13068 [].push.apply(this._physicalItems, this._createPool(delta)); | 7402 [].push.apply(this._physicalItems, this._createPool(delta)); |
13069 [].push.apply(this._physicalSizes, new Array(delta)); | 7403 [].push.apply(this._physicalSizes, new Array(delta)); |
13070 | |
13071 this._physicalCount = prevPhysicalCount + delta; | 7404 this._physicalCount = prevPhysicalCount + delta; |
13072 | 7405 if (this._physicalStart > this._physicalEnd && this._isIndexRendered(this.
_focusedIndex) && this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd
) { |
13073 // update the physical start if we need to preserve the model of the focus
ed item. | |
13074 // In this situation, the focused item is currently rendered and its model
would | |
13075 // have changed after increasing the pool if the physical start remained u
nchanged. | |
13076 if (this._physicalStart > this._physicalEnd && | |
13077 this._isIndexRendered(this._focusedIndex) && | |
13078 this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) { | |
13079 this._physicalStart = this._physicalStart + delta; | 7406 this._physicalStart = this._physicalStart + delta; |
13080 } | 7407 } |
13081 this._update(); | 7408 this._update(); |
13082 }, | 7409 }, |
13083 | |
13084 /** | |
13085 * Render a new list of items. This method does exactly the same as `update`
, | |
13086 * but it also ensures that only one `update` cycle is created. | |
13087 */ | |
13088 _render: function() { | 7410 _render: function() { |
13089 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; | 7411 var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0; |
13090 | |
13091 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { | 7412 if (this.isAttached && !this._itemsRendered && this._isVisible && requires
Update) { |
13092 this._lastPage = 0; | 7413 this._lastPage = 0; |
13093 this._update(); | 7414 this._update(); |
13094 this._itemsRendered = true; | 7415 this._itemsRendered = true; |
13095 } | 7416 } |
13096 }, | 7417 }, |
13097 | |
13098 /** | |
13099 * Templetizes the user template. | |
13100 */ | |
13101 _ensureTemplatized: function() { | 7418 _ensureTemplatized: function() { |
13102 if (!this.ctor) { | 7419 if (!this.ctor) { |
13103 // Template instance props that should be excluded from forwarding | |
13104 var props = {}; | 7420 var props = {}; |
13105 props.__key__ = true; | 7421 props.__key__ = true; |
13106 props[this.as] = true; | 7422 props[this.as] = true; |
13107 props[this.indexAs] = true; | 7423 props[this.indexAs] = true; |
13108 props[this.selectedAs] = true; | 7424 props[this.selectedAs] = true; |
13109 props.tabIndex = true; | 7425 props.tabIndex = true; |
13110 | |
13111 this._instanceProps = props; | 7426 this._instanceProps = props; |
13112 this._userTemplate = Polymer.dom(this).querySelector('template'); | 7427 this._userTemplate = Polymer.dom(this).querySelector('template'); |
13113 | |
13114 if (this._userTemplate) { | 7428 if (this._userTemplate) { |
13115 this.templatize(this._userTemplate); | 7429 this.templatize(this._userTemplate); |
13116 } else { | 7430 } else { |
13117 console.warn('iron-list requires a template to be provided in light-do
m'); | 7431 console.warn('iron-list requires a template to be provided in light-do
m'); |
13118 } | 7432 } |
13119 } | 7433 } |
13120 }, | 7434 }, |
13121 | |
13122 /** | |
13123 * Implements extension point from Templatizer mixin. | |
13124 */ | |
13125 _getStampedChildren: function() { | 7435 _getStampedChildren: function() { |
13126 return this._physicalItems; | 7436 return this._physicalItems; |
13127 }, | 7437 }, |
13128 | |
13129 /** | |
13130 * Implements extension point from Templatizer | |
13131 * Called as a side effect of a template instance path change, responsible | |
13132 * for notifying items.<key-for-instance>.<path> change up to host. | |
13133 */ | |
13134 _forwardInstancePath: function(inst, path, value) { | 7438 _forwardInstancePath: function(inst, path, value) { |
13135 if (path.indexOf(this.as + '.') === 0) { | 7439 if (path.indexOf(this.as + '.') === 0) { |
13136 this.notifyPath('items.' + inst.__key__ + '.' + | 7440 this.notifyPath('items.' + inst.__key__ + '.' + path.slice(this.as.lengt
h + 1), value); |
13137 path.slice(this.as.length + 1), value); | |
13138 } | 7441 } |
13139 }, | 7442 }, |
13140 | |
13141 /** | |
13142 * Implements extension point from Templatizer mixin | |
13143 * Called as side-effect of a host property change, responsible for | |
13144 * notifying parent path change on each row. | |
13145 */ | |
13146 _forwardParentProp: function(prop, value) { | 7443 _forwardParentProp: function(prop, value) { |
13147 if (this._physicalItems) { | 7444 if (this._physicalItems) { |
13148 this._physicalItems.forEach(function(item) { | 7445 this._physicalItems.forEach(function(item) { |
13149 item._templateInstance[prop] = value; | 7446 item._templateInstance[prop] = value; |
13150 }, this); | 7447 }, this); |
13151 } | 7448 } |
13152 }, | 7449 }, |
13153 | |
13154 /** | |
13155 * Implements extension point from Templatizer | |
13156 * Called as side-effect of a host path change, responsible for | |
13157 * notifying parent.<path> path change on each row. | |
13158 */ | |
13159 _forwardParentPath: function(path, value) { | 7450 _forwardParentPath: function(path, value) { |
13160 if (this._physicalItems) { | 7451 if (this._physicalItems) { |
13161 this._physicalItems.forEach(function(item) { | 7452 this._physicalItems.forEach(function(item) { |
13162 item._templateInstance.notifyPath(path, value, true); | 7453 item._templateInstance.notifyPath(path, value, true); |
13163 }, this); | 7454 }, this); |
13164 } | 7455 } |
13165 }, | 7456 }, |
13166 | |
13167 /** | |
13168 * Called as a side effect of a host items.<key>.<path> path change, | |
13169 * responsible for notifying item.<path> changes. | |
13170 */ | |
13171 _forwardItemPath: function(path, value) { | 7457 _forwardItemPath: function(path, value) { |
13172 if (!this._physicalIndexForKey) { | 7458 if (!this._physicalIndexForKey) { |
13173 return; | 7459 return; |
13174 } | 7460 } |
13175 var dot = path.indexOf('.'); | 7461 var dot = path.indexOf('.'); |
13176 var key = path.substring(0, dot < 0 ? path.length : dot); | 7462 var key = path.substring(0, dot < 0 ? path.length : dot); |
13177 var idx = this._physicalIndexForKey[key]; | 7463 var idx = this._physicalIndexForKey[key]; |
13178 var offscreenItem = this._offscreenFocusedItem; | 7464 var offscreenItem = this._offscreenFocusedItem; |
13179 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? | 7465 var el = offscreenItem && offscreenItem._templateInstance.__key__ === key
? offscreenItem : this._physicalItems[idx]; |
13180 offscreenItem : this._physicalItems[idx]; | |
13181 | |
13182 if (!el || el._templateInstance.__key__ !== key) { | 7466 if (!el || el._templateInstance.__key__ !== key) { |
13183 return; | 7467 return; |
13184 } | 7468 } |
13185 if (dot >= 0) { | 7469 if (dot >= 0) { |
13186 path = this.as + '.' + path.substring(dot+1); | 7470 path = this.as + '.' + path.substring(dot + 1); |
13187 el._templateInstance.notifyPath(path, value, true); | 7471 el._templateInstance.notifyPath(path, value, true); |
13188 } else { | 7472 } else { |
13189 // Update selection if needed | |
13190 var currentItem = el._templateInstance[this.as]; | 7473 var currentItem = el._templateInstance[this.as]; |
13191 if (Array.isArray(this.selectedItems)) { | 7474 if (Array.isArray(this.selectedItems)) { |
13192 for (var i = 0; i < this.selectedItems.length; i++) { | 7475 for (var i = 0; i < this.selectedItems.length; i++) { |
13193 if (this.selectedItems[i] === currentItem) { | 7476 if (this.selectedItems[i] === currentItem) { |
13194 this.set('selectedItems.' + i, value); | 7477 this.set('selectedItems.' + i, value); |
13195 break; | 7478 break; |
13196 } | 7479 } |
13197 } | 7480 } |
13198 } else if (this.selectedItem === currentItem) { | 7481 } else if (this.selectedItem === currentItem) { |
13199 this.set('selectedItem', value); | 7482 this.set('selectedItem', value); |
13200 } | 7483 } |
13201 el._templateInstance[this.as] = value; | 7484 el._templateInstance[this.as] = value; |
13202 } | 7485 } |
13203 }, | 7486 }, |
13204 | |
13205 /** | |
13206 * Called when the items have changed. That is, ressignments | |
13207 * to `items`, splices or updates to a single item. | |
13208 */ | |
13209 _itemsChanged: function(change) { | 7487 _itemsChanged: function(change) { |
13210 if (change.path === 'items') { | 7488 if (change.path === 'items') { |
13211 // reset items | |
13212 this._virtualStart = 0; | 7489 this._virtualStart = 0; |
13213 this._physicalTop = 0; | 7490 this._physicalTop = 0; |
13214 this._virtualCount = this.items ? this.items.length : 0; | 7491 this._virtualCount = this.items ? this.items.length : 0; |
13215 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; | 7492 this._collection = this.items ? Polymer.Collection.get(this.items) : nul
l; |
13216 this._physicalIndexForKey = {}; | 7493 this._physicalIndexForKey = {}; |
13217 this._firstVisibleIndexVal = null; | 7494 this._firstVisibleIndexVal = null; |
13218 this._lastVisibleIndexVal = null; | 7495 this._lastVisibleIndexVal = null; |
13219 | |
13220 this._resetScrollPosition(0); | 7496 this._resetScrollPosition(0); |
13221 this._removeFocusedItem(); | 7497 this._removeFocusedItem(); |
13222 // create the initial physical items | |
13223 if (!this._physicalItems) { | 7498 if (!this._physicalItems) { |
13224 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); | 7499 this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, thi
s._virtualCount)); |
13225 this._physicalItems = this._createPool(this._physicalCount); | 7500 this._physicalItems = this._createPool(this._physicalCount); |
13226 this._physicalSizes = new Array(this._physicalCount); | 7501 this._physicalSizes = new Array(this._physicalCount); |
13227 } | 7502 } |
13228 | |
13229 this._physicalStart = 0; | 7503 this._physicalStart = 0; |
13230 | |
13231 } else if (change.path === 'items.splices') { | 7504 } else if (change.path === 'items.splices') { |
13232 | |
13233 this._adjustVirtualIndex(change.value.indexSplices); | 7505 this._adjustVirtualIndex(change.value.indexSplices); |
13234 this._virtualCount = this.items ? this.items.length : 0; | 7506 this._virtualCount = this.items ? this.items.length : 0; |
13235 | |
13236 } else { | 7507 } else { |
13237 // update a single item | |
13238 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); | 7508 this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.
value); |
13239 return; | 7509 return; |
13240 } | 7510 } |
13241 | |
13242 this._itemsRendered = false; | 7511 this._itemsRendered = false; |
13243 this._debounceTemplate(this._render); | 7512 this._debounceTemplate(this._render); |
13244 }, | 7513 }, |
13245 | |
13246 /** | |
13247 * @param {!Array<!PolymerSplice>} splices | |
13248 */ | |
13249 _adjustVirtualIndex: function(splices) { | 7514 _adjustVirtualIndex: function(splices) { |
13250 splices.forEach(function(splice) { | 7515 splices.forEach(function(splice) { |
13251 // deselect removed items | |
13252 splice.removed.forEach(this._removeItem, this); | 7516 splice.removed.forEach(this._removeItem, this); |
13253 // We only need to care about changes happening above the current positi
on | |
13254 if (splice.index < this._virtualStart) { | 7517 if (splice.index < this._virtualStart) { |
13255 var delta = Math.max( | 7518 var delta = Math.max(splice.addedCount - splice.removed.length, splice
.index - this._virtualStart); |
13256 splice.addedCount - splice.removed.length, | |
13257 splice.index - this._virtualStart); | |
13258 | |
13259 this._virtualStart = this._virtualStart + delta; | 7519 this._virtualStart = this._virtualStart + delta; |
13260 | |
13261 if (this._focusedIndex >= 0) { | 7520 if (this._focusedIndex >= 0) { |
13262 this._focusedIndex = this._focusedIndex + delta; | 7521 this._focusedIndex = this._focusedIndex + delta; |
13263 } | 7522 } |
13264 } | 7523 } |
13265 }, this); | 7524 }, this); |
13266 }, | 7525 }, |
13267 | |
13268 _removeItem: function(item) { | 7526 _removeItem: function(item) { |
13269 this.$.selector.deselect(item); | 7527 this.$.selector.deselect(item); |
13270 // remove the current focused item | |
13271 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { | 7528 if (this._focusedItem && this._focusedItem._templateInstance[this.as] ===
item) { |
13272 this._removeFocusedItem(); | 7529 this._removeFocusedItem(); |
13273 } | 7530 } |
13274 }, | 7531 }, |
13275 | |
13276 /** | |
13277 * Executes a provided function per every physical index in `itemSet` | |
13278 * `itemSet` default value is equivalent to the entire set of physical index
es. | |
13279 * | |
13280 * @param {!function(number, number)} fn | |
13281 * @param {!Array<number>=} itemSet | |
13282 */ | |
13283 _iterateItems: function(fn, itemSet) { | 7532 _iterateItems: function(fn, itemSet) { |
13284 var pidx, vidx, rtn, i; | 7533 var pidx, vidx, rtn, i; |
13285 | |
13286 if (arguments.length === 2 && itemSet) { | 7534 if (arguments.length === 2 && itemSet) { |
13287 for (i = 0; i < itemSet.length; i++) { | 7535 for (i = 0; i < itemSet.length; i++) { |
13288 pidx = itemSet[i]; | 7536 pidx = itemSet[i]; |
13289 vidx = this._computeVidx(pidx); | 7537 vidx = this._computeVidx(pidx); |
13290 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 7538 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
13291 return rtn; | 7539 return rtn; |
13292 } | 7540 } |
13293 } | 7541 } |
13294 } else { | 7542 } else { |
13295 pidx = this._physicalStart; | 7543 pidx = this._physicalStart; |
13296 vidx = this._virtualStart; | 7544 vidx = this._virtualStart; |
13297 | 7545 for (;pidx < this._physicalCount; pidx++, vidx++) { |
13298 for (; pidx < this._physicalCount; pidx++, vidx++) { | |
13299 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 7546 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
13300 return rtn; | 7547 return rtn; |
13301 } | 7548 } |
13302 } | 7549 } |
13303 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { | 7550 for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { |
13304 if ((rtn = fn.call(this, pidx, vidx)) != null) { | 7551 if ((rtn = fn.call(this, pidx, vidx)) != null) { |
13305 return rtn; | 7552 return rtn; |
13306 } | 7553 } |
13307 } | 7554 } |
13308 } | 7555 } |
13309 }, | 7556 }, |
13310 | |
13311 /** | |
13312 * Returns the virtual index for a given physical index | |
13313 * | |
13314 * @param {number} pidx Physical index | |
13315 * @return {number} | |
13316 */ | |
13317 _computeVidx: function(pidx) { | 7557 _computeVidx: function(pidx) { |
13318 if (pidx >= this._physicalStart) { | 7558 if (pidx >= this._physicalStart) { |
13319 return this._virtualStart + (pidx - this._physicalStart); | 7559 return this._virtualStart + (pidx - this._physicalStart); |
13320 } | 7560 } |
13321 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; | 7561 return this._virtualStart + (this._physicalCount - this._physicalStart) +
pidx; |
13322 }, | 7562 }, |
13323 | |
13324 /** | |
13325 * Assigns the data models to a given set of items. | |
13326 * @param {!Array<number>=} itemSet | |
13327 */ | |
13328 _assignModels: function(itemSet) { | 7563 _assignModels: function(itemSet) { |
13329 this._iterateItems(function(pidx, vidx) { | 7564 this._iterateItems(function(pidx, vidx) { |
13330 var el = this._physicalItems[pidx]; | 7565 var el = this._physicalItems[pidx]; |
13331 var inst = el._templateInstance; | 7566 var inst = el._templateInstance; |
13332 var item = this.items && this.items[vidx]; | 7567 var item = this.items && this.items[vidx]; |
13333 | |
13334 if (item != null) { | 7568 if (item != null) { |
13335 inst[this.as] = item; | 7569 inst[this.as] = item; |
13336 inst.__key__ = this._collection.getKey(item); | 7570 inst.__key__ = this._collection.getKey(item); |
13337 inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.s
elector).isSelected(item); | 7571 inst[this.selectedAs] = this.$.selector.isSelected(item); |
13338 inst[this.indexAs] = vidx; | 7572 inst[this.indexAs] = vidx; |
13339 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; | 7573 inst.tabIndex = this._focusedIndex === vidx ? 0 : -1; |
13340 this._physicalIndexForKey[inst.__key__] = pidx; | 7574 this._physicalIndexForKey[inst.__key__] = pidx; |
13341 el.removeAttribute('hidden'); | 7575 el.removeAttribute('hidden'); |
13342 } else { | 7576 } else { |
13343 inst.__key__ = null; | 7577 inst.__key__ = null; |
13344 el.setAttribute('hidden', ''); | 7578 el.setAttribute('hidden', ''); |
13345 } | 7579 } |
13346 }, itemSet); | 7580 }, itemSet); |
13347 }, | 7581 }, |
13348 | 7582 _updateMetrics: function(itemSet) { |
13349 /** | |
13350 * Updates the height for a given set of items. | |
13351 * | |
13352 * @param {!Array<number>=} itemSet | |
13353 */ | |
13354 _updateMetrics: function(itemSet) { | |
13355 // Make sure we distributed all the physical items | |
13356 // so we can measure them | |
13357 Polymer.dom.flush(); | 7583 Polymer.dom.flush(); |
13358 | |
13359 var newPhysicalSize = 0; | 7584 var newPhysicalSize = 0; |
13360 var oldPhysicalSize = 0; | 7585 var oldPhysicalSize = 0; |
13361 var prevAvgCount = this._physicalAverageCount; | 7586 var prevAvgCount = this._physicalAverageCount; |
13362 var prevPhysicalAvg = this._physicalAverage; | 7587 var prevPhysicalAvg = this._physicalAverage; |
13363 | |
13364 this._iterateItems(function(pidx, vidx) { | 7588 this._iterateItems(function(pidx, vidx) { |
13365 | |
13366 oldPhysicalSize += this._physicalSizes[pidx] || 0; | 7589 oldPhysicalSize += this._physicalSizes[pidx] || 0; |
13367 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; | 7590 this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; |
13368 newPhysicalSize += this._physicalSizes[pidx]; | 7591 newPhysicalSize += this._physicalSizes[pidx]; |
13369 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; | 7592 this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; |
13370 | |
13371 }, itemSet); | 7593 }, itemSet); |
13372 | |
13373 this._viewportHeight = this._scrollTargetHeight; | 7594 this._viewportHeight = this._scrollTargetHeight; |
13374 if (this.grid) { | 7595 if (this.grid) { |
13375 this._updateGridMetrics(); | 7596 this._updateGridMetrics(); |
13376 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; | 7597 this._physicalSize = Math.ceil(this._physicalCount / this._itemsPerRow)
* this._rowHeight; |
13377 } else { | 7598 } else { |
13378 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; | 7599 this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalS
ize; |
13379 } | 7600 } |
13380 | |
13381 // update the average if we measured something | |
13382 if (this._physicalAverageCount !== prevAvgCount) { | 7601 if (this._physicalAverageCount !== prevAvgCount) { |
13383 this._physicalAverage = Math.round( | 7602 this._physicalAverage = Math.round((prevPhysicalAvg * prevAvgCount + new
PhysicalSize) / this._physicalAverageCount); |
13384 ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) / | |
13385 this._physicalAverageCount); | |
13386 } | 7603 } |
13387 }, | 7604 }, |
13388 | |
13389 _updateGridMetrics: function() { | 7605 _updateGridMetrics: function() { |
13390 this._viewportWidth = this.$.items.offsetWidth; | 7606 this._viewportWidth = this.$.items.offsetWidth; |
13391 // Set item width to the value of the _physicalItems offsetWidth | |
13392 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; | 7607 this._itemWidth = this._physicalCount > 0 ? this._physicalItems[0].getBoun
dingClientRect().width : DEFAULT_GRID_SIZE; |
13393 // Set row height to the value of the _physicalItems offsetHeight | |
13394 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; | 7608 this._rowHeight = this._physicalCount > 0 ? this._physicalItems[0].offsetH
eight : DEFAULT_GRID_SIZE; |
13395 // If in grid mode compute how many items with exist in each row | |
13396 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; | 7609 this._itemsPerRow = this._itemWidth ? Math.floor(this._viewportWidth / thi
s._itemWidth) : this._itemsPerRow; |
13397 }, | 7610 }, |
13398 | |
13399 /** | |
13400 * Updates the position of the physical items. | |
13401 */ | |
13402 _positionItems: function() { | 7611 _positionItems: function() { |
13403 this._adjustScrollPosition(); | 7612 this._adjustScrollPosition(); |
13404 | |
13405 var y = this._physicalTop; | 7613 var y = this._physicalTop; |
13406 | |
13407 if (this.grid) { | 7614 if (this.grid) { |
13408 var totalItemWidth = this._itemsPerRow * this._itemWidth; | 7615 var totalItemWidth = this._itemsPerRow * this._itemWidth; |
13409 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; | 7616 var rowOffset = (this._viewportWidth - totalItemWidth) / 2; |
13410 | |
13411 this._iterateItems(function(pidx, vidx) { | 7617 this._iterateItems(function(pidx, vidx) { |
13412 | |
13413 var modulus = vidx % this._itemsPerRow; | 7618 var modulus = vidx % this._itemsPerRow; |
13414 var x = Math.floor((modulus * this._itemWidth) + rowOffset); | 7619 var x = Math.floor(modulus * this._itemWidth + rowOffset); |
13415 | |
13416 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); | 7620 this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]); |
13417 | |
13418 if (this._shouldRenderNextRow(vidx)) { | 7621 if (this._shouldRenderNextRow(vidx)) { |
13419 y += this._rowHeight; | 7622 y += this._rowHeight; |
13420 } | 7623 } |
13421 | |
13422 }); | 7624 }); |
13423 } else { | 7625 } else { |
13424 this._iterateItems(function(pidx, vidx) { | 7626 this._iterateItems(function(pidx, vidx) { |
13425 | |
13426 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); | 7627 this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); |
13427 y += this._physicalSizes[pidx]; | 7628 y += this._physicalSizes[pidx]; |
13428 | |
13429 }); | 7629 }); |
13430 } | 7630 } |
13431 }, | 7631 }, |
13432 | |
13433 _getPhysicalSizeIncrement: function(pidx) { | 7632 _getPhysicalSizeIncrement: function(pidx) { |
13434 if (!this.grid) { | 7633 if (!this.grid) { |
13435 return this._physicalSizes[pidx]; | 7634 return this._physicalSizes[pidx]; |
13436 } | 7635 } |
13437 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ | 7636 if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1)
{ |
13438 return 0; | 7637 return 0; |
13439 } | 7638 } |
13440 return this._rowHeight; | 7639 return this._rowHeight; |
13441 }, | 7640 }, |
13442 | |
13443 /** | |
13444 * Returns, based on the current index, | |
13445 * whether or not the next index will need | |
13446 * to be rendered on a new row. | |
13447 * | |
13448 * @param {number} vidx Virtual index | |
13449 * @return {boolean} | |
13450 */ | |
13451 _shouldRenderNextRow: function(vidx) { | 7641 _shouldRenderNextRow: function(vidx) { |
13452 return vidx % this._itemsPerRow === this._itemsPerRow - 1; | 7642 return vidx % this._itemsPerRow === this._itemsPerRow - 1; |
13453 }, | 7643 }, |
13454 | |
13455 /** | |
13456 * Adjusts the scroll position when it was overestimated. | |
13457 */ | |
13458 _adjustScrollPosition: function() { | 7644 _adjustScrollPosition: function() { |
13459 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : | 7645 var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min(
this._scrollPosition + this._physicalTop, 0); |
13460 Math.min(this._scrollPosition + this._physicalTop, 0); | |
13461 | |
13462 if (deltaHeight) { | 7646 if (deltaHeight) { |
13463 this._physicalTop = this._physicalTop - deltaHeight; | 7647 this._physicalTop = this._physicalTop - deltaHeight; |
13464 // juking scroll position during interial scrolling on iOS is no bueno | |
13465 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { | 7648 if (!IOS_TOUCH_SCROLLING && this._physicalTop !== 0) { |
13466 this._resetScrollPosition(this._scrollTop - deltaHeight); | 7649 this._resetScrollPosition(this._scrollTop - deltaHeight); |
13467 } | 7650 } |
13468 } | 7651 } |
13469 }, | 7652 }, |
13470 | |
13471 /** | |
13472 * Sets the position of the scroll. | |
13473 */ | |
13474 _resetScrollPosition: function(pos) { | 7653 _resetScrollPosition: function(pos) { |
13475 if (this.scrollTarget) { | 7654 if (this.scrollTarget) { |
13476 this._scrollTop = pos; | 7655 this._scrollTop = pos; |
13477 this._scrollPosition = this._scrollTop; | 7656 this._scrollPosition = this._scrollTop; |
13478 } | 7657 } |
13479 }, | 7658 }, |
13480 | |
13481 /** | |
13482 * Sets the scroll height, that's the height of the content, | |
13483 * | |
13484 * @param {boolean=} forceUpdate If true, updates the height no matter what. | |
13485 */ | |
13486 _updateScrollerSize: function(forceUpdate) { | 7659 _updateScrollerSize: function(forceUpdate) { |
13487 if (this.grid) { | 7660 if (this.grid) { |
13488 this._estScrollHeight = this._virtualRowCount * this._rowHeight; | 7661 this._estScrollHeight = this._virtualRowCount * this._rowHeight; |
13489 } else { | 7662 } else { |
13490 this._estScrollHeight = (this._physicalBottom + | 7663 this._estScrollHeight = this._physicalBottom + Math.max(this._virtualCou
nt - this._physicalCount - this._virtualStart, 0) * this._physicalAverage; |
13491 Math.max(this._virtualCount - this._physicalCount - this._virtualSta
rt, 0) * this._physicalAverage); | |
13492 } | 7664 } |
13493 | |
13494 forceUpdate = forceUpdate || this._scrollHeight === 0; | 7665 forceUpdate = forceUpdate || this._scrollHeight === 0; |
13495 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; | 7666 forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight
- this._physicalSize; |
13496 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; | 7667 forceUpdate = forceUpdate || this.grid && this.$.items.style.height < this
._estScrollHeight; |
13497 | |
13498 // amortize height adjustment, so it won't trigger repaints very often | |
13499 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { | 7668 if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >=
this._optPhysicalSize) { |
13500 this.$.items.style.height = this._estScrollHeight + 'px'; | 7669 this.$.items.style.height = this._estScrollHeight + 'px'; |
13501 this._scrollHeight = this._estScrollHeight; | 7670 this._scrollHeight = this._estScrollHeight; |
13502 } | 7671 } |
13503 }, | 7672 }, |
13504 | 7673 scrollToItem: function(item) { |
13505 /** | |
13506 * Scroll to a specific item in the virtual list regardless | |
13507 * of the physical items in the DOM tree. | |
13508 * | |
13509 * @method scrollToItem | |
13510 * @param {(Object)} item The item to be scrolled to | |
13511 */ | |
13512 scrollToItem: function(item){ | |
13513 return this.scrollToIndex(this.items.indexOf(item)); | 7674 return this.scrollToIndex(this.items.indexOf(item)); |
13514 }, | 7675 }, |
13515 | |
13516 /** | |
13517 * Scroll to a specific index in the virtual list regardless | |
13518 * of the physical items in the DOM tree. | |
13519 * | |
13520 * @method scrollToIndex | |
13521 * @param {number} idx The index of the item | |
13522 */ | |
13523 scrollToIndex: function(idx) { | 7676 scrollToIndex: function(idx) { |
13524 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { | 7677 if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) { |
13525 return; | 7678 return; |
13526 } | 7679 } |
13527 | |
13528 Polymer.dom.flush(); | 7680 Polymer.dom.flush(); |
13529 | 7681 idx = Math.min(Math.max(idx, 0), this._virtualCount - 1); |
13530 idx = Math.min(Math.max(idx, 0), this._virtualCount-1); | |
13531 // update the virtual start only when needed | |
13532 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { | 7682 if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) { |
13533 this._virtualStart = this.grid ? (idx - this._itemsPerRow * 2) : (idx -
1); | 7683 this._virtualStart = this.grid ? idx - this._itemsPerRow * 2 : idx - 1; |
13534 } | 7684 } |
13535 // manage focus | |
13536 this._manageFocus(); | 7685 this._manageFocus(); |
13537 // assign new models | |
13538 this._assignModels(); | 7686 this._assignModels(); |
13539 // measure the new sizes | |
13540 this._updateMetrics(); | 7687 this._updateMetrics(); |
13541 | 7688 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; |
13542 // estimate new physical offset | |
13543 var estPhysicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
this._physicalAverage; | |
13544 this._physicalTop = estPhysicalTop; | 7689 this._physicalTop = estPhysicalTop; |
13545 | |
13546 var currentTopItem = this._physicalStart; | 7690 var currentTopItem = this._physicalStart; |
13547 var currentVirtualItem = this._virtualStart; | 7691 var currentVirtualItem = this._virtualStart; |
13548 var targetOffsetTop = 0; | 7692 var targetOffsetTop = 0; |
13549 var hiddenContentSize = this._hiddenContentSize; | 7693 var hiddenContentSize = this._hiddenContentSize; |
13550 | |
13551 // scroll to the item as much as we can | |
13552 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { | 7694 while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) { |
13553 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); | 7695 targetOffsetTop = targetOffsetTop + this._getPhysicalSizeIncrement(curre
ntTopItem); |
13554 currentTopItem = (currentTopItem + 1) % this._physicalCount; | 7696 currentTopItem = (currentTopItem + 1) % this._physicalCount; |
13555 currentVirtualItem++; | 7697 currentVirtualItem++; |
13556 } | 7698 } |
13557 // update the scroller size | |
13558 this._updateScrollerSize(true); | 7699 this._updateScrollerSize(true); |
13559 // update the position of the items | |
13560 this._positionItems(); | 7700 this._positionItems(); |
13561 // set the new scroll position | |
13562 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); | 7701 this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + t
argetOffsetTop); |
13563 // increase the pool of physical items if needed | |
13564 this._increasePoolIfNeeded(); | 7702 this._increasePoolIfNeeded(); |
13565 // clear cached visible index | |
13566 this._firstVisibleIndexVal = null; | 7703 this._firstVisibleIndexVal = null; |
13567 this._lastVisibleIndexVal = null; | 7704 this._lastVisibleIndexVal = null; |
13568 }, | 7705 }, |
13569 | |
13570 /** | |
13571 * Reset the physical average and the average count. | |
13572 */ | |
13573 _resetAverage: function() { | 7706 _resetAverage: function() { |
13574 this._physicalAverage = 0; | 7707 this._physicalAverage = 0; |
13575 this._physicalAverageCount = 0; | 7708 this._physicalAverageCount = 0; |
13576 }, | 7709 }, |
13577 | |
13578 /** | |
13579 * A handler for the `iron-resize` event triggered by `IronResizableBehavior
` | |
13580 * when the element is resized. | |
13581 */ | |
13582 _resizeHandler: function() { | 7710 _resizeHandler: function() { |
13583 // iOS fires the resize event when the address bar slides up | |
13584 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { | 7711 if (IOS && Math.abs(this._viewportHeight - this._scrollTargetHeight) < 100
) { |
13585 return; | 7712 return; |
13586 } | 7713 } |
13587 // In Desktop Safari 9.0.3, if the scroll bars are always shown, | |
13588 // changing the scroll position from a resize handler would result in | |
13589 // the scroll position being reset. Waiting 1ms fixes the issue. | |
13590 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { | 7714 Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', function() { |
13591 this.updateViewportBoundaries(); | 7715 this.updateViewportBoundaries(); |
13592 this._render(); | 7716 this._render(); |
13593 | |
13594 if (this._itemsRendered && this._physicalItems && this._isVisible) { | 7717 if (this._itemsRendered && this._physicalItems && this._isVisible) { |
13595 this._resetAverage(); | 7718 this._resetAverage(); |
13596 this.scrollToIndex(this.firstVisibleIndex); | 7719 this.scrollToIndex(this.firstVisibleIndex); |
13597 } | 7720 } |
13598 }.bind(this), 1)); | 7721 }.bind(this), 1)); |
13599 }, | 7722 }, |
13600 | |
13601 _getModelFromItem: function(item) { | 7723 _getModelFromItem: function(item) { |
13602 var key = this._collection.getKey(item); | 7724 var key = this._collection.getKey(item); |
13603 var pidx = this._physicalIndexForKey[key]; | 7725 var pidx = this._physicalIndexForKey[key]; |
13604 | |
13605 if (pidx != null) { | 7726 if (pidx != null) { |
13606 return this._physicalItems[pidx]._templateInstance; | 7727 return this._physicalItems[pidx]._templateInstance; |
13607 } | 7728 } |
13608 return null; | 7729 return null; |
13609 }, | 7730 }, |
13610 | |
13611 /** | |
13612 * Gets a valid item instance from its index or the object value. | |
13613 * | |
13614 * @param {(Object|number)} item The item object or its index | |
13615 */ | |
13616 _getNormalizedItem: function(item) { | 7731 _getNormalizedItem: function(item) { |
13617 if (this._collection.getKey(item) === undefined) { | 7732 if (this._collection.getKey(item) === undefined) { |
13618 if (typeof item === 'number') { | 7733 if (typeof item === 'number') { |
13619 item = this.items[item]; | 7734 item = this.items[item]; |
13620 if (!item) { | 7735 if (!item) { |
13621 throw new RangeError('<item> not found'); | 7736 throw new RangeError('<item> not found'); |
13622 } | 7737 } |
13623 return item; | 7738 return item; |
13624 } | 7739 } |
13625 throw new TypeError('<item> should be a valid item'); | 7740 throw new TypeError('<item> should be a valid item'); |
13626 } | 7741 } |
13627 return item; | 7742 return item; |
13628 }, | 7743 }, |
13629 | |
13630 /** | |
13631 * Select the list item at the given index. | |
13632 * | |
13633 * @method selectItem | |
13634 * @param {(Object|number)} item The item object or its index | |
13635 */ | |
13636 selectItem: function(item) { | 7744 selectItem: function(item) { |
13637 item = this._getNormalizedItem(item); | 7745 item = this._getNormalizedItem(item); |
13638 var model = this._getModelFromItem(item); | 7746 var model = this._getModelFromItem(item); |
13639 | |
13640 if (!this.multiSelection && this.selectedItem) { | 7747 if (!this.multiSelection && this.selectedItem) { |
13641 this.deselectItem(this.selectedItem); | 7748 this.deselectItem(this.selectedItem); |
13642 } | 7749 } |
13643 if (model) { | 7750 if (model) { |
13644 model[this.selectedAs] = true; | 7751 model[this.selectedAs] = true; |
13645 } | 7752 } |
13646 this.$.selector.select(item); | 7753 this.$.selector.select(item); |
13647 this.updateSizeForItem(item); | 7754 this.updateSizeForItem(item); |
13648 }, | 7755 }, |
13649 | |
13650 /** | |
13651 * Deselects the given item list if it is already selected. | |
13652 * | |
13653 | |
13654 * @method deselect | |
13655 * @param {(Object|number)} item The item object or its index | |
13656 */ | |
13657 deselectItem: function(item) { | 7756 deselectItem: function(item) { |
13658 item = this._getNormalizedItem(item); | 7757 item = this._getNormalizedItem(item); |
13659 var model = this._getModelFromItem(item); | 7758 var model = this._getModelFromItem(item); |
13660 | |
13661 if (model) { | 7759 if (model) { |
13662 model[this.selectedAs] = false; | 7760 model[this.selectedAs] = false; |
13663 } | 7761 } |
13664 this.$.selector.deselect(item); | 7762 this.$.selector.deselect(item); |
13665 this.updateSizeForItem(item); | 7763 this.updateSizeForItem(item); |
13666 }, | 7764 }, |
13667 | |
13668 /** | |
13669 * Select or deselect a given item depending on whether the item | |
13670 * has already been selected. | |
13671 * | |
13672 * @method toggleSelectionForItem | |
13673 * @param {(Object|number)} item The item object or its index | |
13674 */ | |
13675 toggleSelectionForItem: function(item) { | 7765 toggleSelectionForItem: function(item) { |
13676 item = this._getNormalizedItem(item); | 7766 item = this._getNormalizedItem(item); |
13677 if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item
)) { | 7767 if (this.$.selector.isSelected(item)) { |
13678 this.deselectItem(item); | 7768 this.deselectItem(item); |
13679 } else { | 7769 } else { |
13680 this.selectItem(item); | 7770 this.selectItem(item); |
13681 } | 7771 } |
13682 }, | 7772 }, |
13683 | |
13684 /** | |
13685 * Clears the current selection state of the list. | |
13686 * | |
13687 * @method clearSelection | |
13688 */ | |
13689 clearSelection: function() { | 7773 clearSelection: function() { |
13690 function unselect(item) { | 7774 function unselect(item) { |
13691 var model = this._getModelFromItem(item); | 7775 var model = this._getModelFromItem(item); |
13692 if (model) { | 7776 if (model) { |
13693 model[this.selectedAs] = false; | 7777 model[this.selectedAs] = false; |
13694 } | 7778 } |
13695 } | 7779 } |
13696 | |
13697 if (Array.isArray(this.selectedItems)) { | 7780 if (Array.isArray(this.selectedItems)) { |
13698 this.selectedItems.forEach(unselect, this); | 7781 this.selectedItems.forEach(unselect, this); |
13699 } else if (this.selectedItem) { | 7782 } else if (this.selectedItem) { |
13700 unselect.call(this, this.selectedItem); | 7783 unselect.call(this, this.selectedItem); |
13701 } | 7784 } |
13702 | 7785 this.$.selector.clearSelection(); |
13703 /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection(); | |
13704 }, | 7786 }, |
13705 | |
13706 /** | |
13707 * Add an event listener to `tap` if `selectionEnabled` is true, | |
13708 * it will remove the listener otherwise. | |
13709 */ | |
13710 _selectionEnabledChanged: function(selectionEnabled) { | 7787 _selectionEnabledChanged: function(selectionEnabled) { |
13711 var handler = selectionEnabled ? this.listen : this.unlisten; | 7788 var handler = selectionEnabled ? this.listen : this.unlisten; |
13712 handler.call(this, this, 'tap', '_selectionHandler'); | 7789 handler.call(this, this, 'tap', '_selectionHandler'); |
13713 }, | 7790 }, |
13714 | |
13715 /** | |
13716 * Select an item from an event object. | |
13717 */ | |
13718 _selectionHandler: function(e) { | 7791 _selectionHandler: function(e) { |
13719 var model = this.modelForElement(e.target); | 7792 var model = this.modelForElement(e.target); |
13720 if (!model) { | 7793 if (!model) { |
13721 return; | 7794 return; |
13722 } | 7795 } |
13723 var modelTabIndex, activeElTabIndex; | 7796 var modelTabIndex, activeElTabIndex; |
13724 var target = Polymer.dom(e).path[0]; | 7797 var target = Polymer.dom(e).path[0]; |
13725 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; | 7798 var activeEl = Polymer.dom(this.domHost ? this.domHost.root : document).ac
tiveElement; |
13726 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; | 7799 var physicalItem = this._physicalItems[this._getPhysicalIndex(model[this.i
ndexAs])]; |
13727 // Safari does not focus certain form controls via mouse | 7800 if (target.localName === 'input' || target.localName === 'button' || targe
t.localName === 'select') { |
13728 // https://bugs.webkit.org/show_bug.cgi?id=118043 | |
13729 if (target.localName === 'input' || | |
13730 target.localName === 'button' || | |
13731 target.localName === 'select') { | |
13732 return; | 7801 return; |
13733 } | 7802 } |
13734 // Set a temporary tabindex | |
13735 modelTabIndex = model.tabIndex; | 7803 modelTabIndex = model.tabIndex; |
13736 model.tabIndex = SECRET_TABINDEX; | 7804 model.tabIndex = SECRET_TABINDEX; |
13737 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; | 7805 activeElTabIndex = activeEl ? activeEl.tabIndex : -1; |
13738 model.tabIndex = modelTabIndex; | 7806 model.tabIndex = modelTabIndex; |
13739 // Only select the item if the tap wasn't on a focusable child | |
13740 // or the element bound to `tabIndex` | |
13741 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { | 7807 if (activeEl && physicalItem.contains(activeEl) && activeElTabIndex !== SE
CRET_TABINDEX) { |
13742 return; | 7808 return; |
13743 } | 7809 } |
13744 this.toggleSelectionForItem(model[this.as]); | 7810 this.toggleSelectionForItem(model[this.as]); |
13745 }, | 7811 }, |
13746 | |
13747 _multiSelectionChanged: function(multiSelection) { | 7812 _multiSelectionChanged: function(multiSelection) { |
13748 this.clearSelection(); | 7813 this.clearSelection(); |
13749 this.$.selector.multi = multiSelection; | 7814 this.$.selector.multi = multiSelection; |
13750 }, | 7815 }, |
13751 | |
13752 /** | |
13753 * Updates the size of an item. | |
13754 * | |
13755 * @method updateSizeForItem | |
13756 * @param {(Object|number)} item The item object or its index | |
13757 */ | |
13758 updateSizeForItem: function(item) { | 7816 updateSizeForItem: function(item) { |
13759 item = this._getNormalizedItem(item); | 7817 item = this._getNormalizedItem(item); |
13760 var key = this._collection.getKey(item); | 7818 var key = this._collection.getKey(item); |
13761 var pidx = this._physicalIndexForKey[key]; | 7819 var pidx = this._physicalIndexForKey[key]; |
13762 | |
13763 if (pidx != null) { | 7820 if (pidx != null) { |
13764 this._updateMetrics([pidx]); | 7821 this._updateMetrics([ pidx ]); |
13765 this._positionItems(); | 7822 this._positionItems(); |
13766 } | 7823 } |
13767 }, | 7824 }, |
13768 | |
13769 /** | |
13770 * Creates a temporary backfill item in the rendered pool of physical items | |
13771 * to replace the main focused item. The focused item has tabIndex = 0 | |
13772 * and might be currently focused by the user. | |
13773 * | |
13774 * This dynamic replacement helps to preserve the focus state. | |
13775 */ | |
13776 _manageFocus: function() { | 7825 _manageFocus: function() { |
13777 var fidx = this._focusedIndex; | 7826 var fidx = this._focusedIndex; |
13778 | |
13779 if (fidx >= 0 && fidx < this._virtualCount) { | 7827 if (fidx >= 0 && fidx < this._virtualCount) { |
13780 // if it's a valid index, check if that index is rendered | |
13781 // in a physical item. | |
13782 if (this._isIndexRendered(fidx)) { | 7828 if (this._isIndexRendered(fidx)) { |
13783 this._restoreFocusedItem(); | 7829 this._restoreFocusedItem(); |
13784 } else { | 7830 } else { |
13785 this._createFocusBackfillItem(); | 7831 this._createFocusBackfillItem(); |
13786 } | 7832 } |
13787 } else if (this._virtualCount > 0 && this._physicalCount > 0) { | 7833 } else if (this._virtualCount > 0 && this._physicalCount > 0) { |
13788 // otherwise, assign the initial focused index. | |
13789 this._focusedIndex = this._virtualStart; | 7834 this._focusedIndex = this._virtualStart; |
13790 this._focusedItem = this._physicalItems[this._physicalStart]; | 7835 this._focusedItem = this._physicalItems[this._physicalStart]; |
13791 } | 7836 } |
13792 }, | 7837 }, |
13793 | |
13794 _isIndexRendered: function(idx) { | 7838 _isIndexRendered: function(idx) { |
13795 return idx >= this._virtualStart && idx <= this._virtualEnd; | 7839 return idx >= this._virtualStart && idx <= this._virtualEnd; |
13796 }, | 7840 }, |
13797 | |
13798 _isIndexVisible: function(idx) { | 7841 _isIndexVisible: function(idx) { |
13799 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; | 7842 return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex; |
13800 }, | 7843 }, |
13801 | |
13802 _getPhysicalIndex: function(idx) { | 7844 _getPhysicalIndex: function(idx) { |
13803 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; | 7845 return this._physicalIndexForKey[this._collection.getKey(this._getNormaliz
edItem(idx))]; |
13804 }, | 7846 }, |
13805 | |
13806 _focusPhysicalItem: function(idx) { | 7847 _focusPhysicalItem: function(idx) { |
13807 if (idx < 0 || idx >= this._virtualCount) { | 7848 if (idx < 0 || idx >= this._virtualCount) { |
13808 return; | 7849 return; |
13809 } | 7850 } |
13810 this._restoreFocusedItem(); | 7851 this._restoreFocusedItem(); |
13811 // scroll to index to make sure it's rendered | |
13812 if (!this._isIndexRendered(idx)) { | 7852 if (!this._isIndexRendered(idx)) { |
13813 this.scrollToIndex(idx); | 7853 this.scrollToIndex(idx); |
13814 } | 7854 } |
13815 | |
13816 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; | 7855 var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)]; |
13817 var model = physicalItem._templateInstance; | 7856 var model = physicalItem._templateInstance; |
13818 var focusable; | 7857 var focusable; |
13819 | |
13820 // set a secret tab index | |
13821 model.tabIndex = SECRET_TABINDEX; | 7858 model.tabIndex = SECRET_TABINDEX; |
13822 // check if focusable element is the physical item | |
13823 if (physicalItem.tabIndex === SECRET_TABINDEX) { | 7859 if (physicalItem.tabIndex === SECRET_TABINDEX) { |
13824 focusable = physicalItem; | 7860 focusable = physicalItem; |
13825 } | 7861 } |
13826 // search for the element which tabindex is bound to the secret tab index | |
13827 if (!focusable) { | 7862 if (!focusable) { |
13828 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); | 7863 focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECR
ET_TABINDEX + '"]'); |
13829 } | 7864 } |
13830 // restore the tab index | |
13831 model.tabIndex = 0; | 7865 model.tabIndex = 0; |
13832 // focus the focusable element | |
13833 this._focusedIndex = idx; | 7866 this._focusedIndex = idx; |
13834 focusable && focusable.focus(); | 7867 focusable && focusable.focus(); |
13835 }, | 7868 }, |
13836 | |
13837 _removeFocusedItem: function() { | 7869 _removeFocusedItem: function() { |
13838 if (this._offscreenFocusedItem) { | 7870 if (this._offscreenFocusedItem) { |
13839 Polymer.dom(this).removeChild(this._offscreenFocusedItem); | 7871 Polymer.dom(this).removeChild(this._offscreenFocusedItem); |
13840 } | 7872 } |
13841 this._offscreenFocusedItem = null; | 7873 this._offscreenFocusedItem = null; |
13842 this._focusBackfillItem = null; | 7874 this._focusBackfillItem = null; |
13843 this._focusedItem = null; | 7875 this._focusedItem = null; |
13844 this._focusedIndex = -1; | 7876 this._focusedIndex = -1; |
13845 }, | 7877 }, |
13846 | |
13847 _createFocusBackfillItem: function() { | 7878 _createFocusBackfillItem: function() { |
13848 var pidx, fidx = this._focusedIndex; | 7879 var pidx, fidx = this._focusedIndex; |
13849 if (this._offscreenFocusedItem || fidx < 0) { | 7880 if (this._offscreenFocusedItem || fidx < 0) { |
13850 return; | 7881 return; |
13851 } | 7882 } |
13852 if (!this._focusBackfillItem) { | 7883 if (!this._focusBackfillItem) { |
13853 // create a physical item, so that it backfills the focused item. | |
13854 var stampedTemplate = this.stamp(null); | 7884 var stampedTemplate = this.stamp(null); |
13855 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); | 7885 this._focusBackfillItem = stampedTemplate.root.querySelector('*'); |
13856 Polymer.dom(this).appendChild(stampedTemplate.root); | 7886 Polymer.dom(this).appendChild(stampedTemplate.root); |
13857 } | 7887 } |
13858 // get the physical index for the focused index | |
13859 pidx = this._getPhysicalIndex(fidx); | 7888 pidx = this._getPhysicalIndex(fidx); |
13860 | |
13861 if (pidx != null) { | 7889 if (pidx != null) { |
13862 // set the offcreen focused physical item | |
13863 this._offscreenFocusedItem = this._physicalItems[pidx]; | 7890 this._offscreenFocusedItem = this._physicalItems[pidx]; |
13864 // backfill the focused physical item | |
13865 this._physicalItems[pidx] = this._focusBackfillItem; | 7891 this._physicalItems[pidx] = this._focusBackfillItem; |
13866 // hide the focused physical | |
13867 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); | 7892 this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); |
13868 } | 7893 } |
13869 }, | 7894 }, |
13870 | |
13871 _restoreFocusedItem: function() { | 7895 _restoreFocusedItem: function() { |
13872 var pidx, fidx = this._focusedIndex; | 7896 var pidx, fidx = this._focusedIndex; |
13873 | |
13874 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { | 7897 if (!this._offscreenFocusedItem || this._focusedIndex < 0) { |
13875 return; | 7898 return; |
13876 } | 7899 } |
13877 // assign models to the focused index | |
13878 this._assignModels(); | 7900 this._assignModels(); |
13879 // get the new physical index for the focused index | |
13880 pidx = this._getPhysicalIndex(fidx); | 7901 pidx = this._getPhysicalIndex(fidx); |
13881 | |
13882 if (pidx != null) { | 7902 if (pidx != null) { |
13883 // flip the focus backfill | |
13884 this._focusBackfillItem = this._physicalItems[pidx]; | 7903 this._focusBackfillItem = this._physicalItems[pidx]; |
13885 // restore the focused physical item | |
13886 this._physicalItems[pidx] = this._offscreenFocusedItem; | 7904 this._physicalItems[pidx] = this._offscreenFocusedItem; |
13887 // reset the offscreen focused item | |
13888 this._offscreenFocusedItem = null; | 7905 this._offscreenFocusedItem = null; |
13889 // hide the physical item that backfills | |
13890 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); | 7906 this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem); |
13891 } | 7907 } |
13892 }, | 7908 }, |
13893 | |
13894 _didFocus: function(e) { | 7909 _didFocus: function(e) { |
13895 var targetModel = this.modelForElement(e.target); | 7910 var targetModel = this.modelForElement(e.target); |
13896 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; | 7911 var focusedModel = this._focusedItem ? this._focusedItem._templateInstance
: null; |
13897 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; | 7912 var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null; |
13898 var fidx = this._focusedIndex; | 7913 var fidx = this._focusedIndex; |
13899 | |
13900 if (!targetModel || !focusedModel) { | 7914 if (!targetModel || !focusedModel) { |
13901 return; | 7915 return; |
13902 } | 7916 } |
13903 if (focusedModel === targetModel) { | 7917 if (focusedModel === targetModel) { |
13904 // if the user focused the same item, then bring it into view if it's no
t visible | |
13905 if (!this._isIndexVisible(fidx)) { | 7918 if (!this._isIndexVisible(fidx)) { |
13906 this.scrollToIndex(fidx); | 7919 this.scrollToIndex(fidx); |
13907 } | 7920 } |
13908 } else { | 7921 } else { |
13909 this._restoreFocusedItem(); | 7922 this._restoreFocusedItem(); |
13910 // restore tabIndex for the currently focused item | |
13911 focusedModel.tabIndex = -1; | 7923 focusedModel.tabIndex = -1; |
13912 // set the tabIndex for the next focused item | |
13913 targetModel.tabIndex = 0; | 7924 targetModel.tabIndex = 0; |
13914 fidx = targetModel[this.indexAs]; | 7925 fidx = targetModel[this.indexAs]; |
13915 this._focusedIndex = fidx; | 7926 this._focusedIndex = fidx; |
13916 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; | 7927 this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)]; |
13917 | |
13918 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { | 7928 if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) { |
13919 this._update(); | 7929 this._update(); |
13920 } | 7930 } |
13921 } | 7931 } |
13922 }, | 7932 }, |
13923 | |
13924 _didMoveUp: function() { | 7933 _didMoveUp: function() { |
13925 this._focusPhysicalItem(this._focusedIndex - 1); | 7934 this._focusPhysicalItem(this._focusedIndex - 1); |
13926 }, | 7935 }, |
13927 | |
13928 _didMoveDown: function(e) { | 7936 _didMoveDown: function(e) { |
13929 // disable scroll when pressing the down key | |
13930 e.detail.keyboardEvent.preventDefault(); | 7937 e.detail.keyboardEvent.preventDefault(); |
13931 this._focusPhysicalItem(this._focusedIndex + 1); | 7938 this._focusPhysicalItem(this._focusedIndex + 1); |
13932 }, | 7939 }, |
13933 | |
13934 _didEnter: function(e) { | 7940 _didEnter: function(e) { |
13935 this._focusPhysicalItem(this._focusedIndex); | 7941 this._focusPhysicalItem(this._focusedIndex); |
13936 this._selectionHandler(e.detail.keyboardEvent); | 7942 this._selectionHandler(e.detail.keyboardEvent); |
13937 } | 7943 } |
13938 }); | 7944 }); |
| 7945 })(); |
13939 | 7946 |
13940 })(); | |
13941 Polymer({ | 7947 Polymer({ |
| 7948 is: 'iron-scroll-threshold', |
| 7949 properties: { |
| 7950 upperThreshold: { |
| 7951 type: Number, |
| 7952 value: 100 |
| 7953 }, |
| 7954 lowerThreshold: { |
| 7955 type: Number, |
| 7956 value: 100 |
| 7957 }, |
| 7958 upperTriggered: { |
| 7959 type: Boolean, |
| 7960 value: false, |
| 7961 notify: true, |
| 7962 readOnly: true |
| 7963 }, |
| 7964 lowerTriggered: { |
| 7965 type: Boolean, |
| 7966 value: false, |
| 7967 notify: true, |
| 7968 readOnly: true |
| 7969 }, |
| 7970 horizontal: { |
| 7971 type: Boolean, |
| 7972 value: false |
| 7973 } |
| 7974 }, |
| 7975 behaviors: [ Polymer.IronScrollTargetBehavior ], |
| 7976 observers: [ '_setOverflow(scrollTarget)', '_initCheck(horizontal, isAttached)
' ], |
| 7977 get _defaultScrollTarget() { |
| 7978 return this; |
| 7979 }, |
| 7980 _setOverflow: function(scrollTarget) { |
| 7981 this.style.overflow = scrollTarget === this ? 'auto' : ''; |
| 7982 }, |
| 7983 _scrollHandler: function() { |
| 7984 var THROTTLE_THRESHOLD = 200; |
| 7985 if (!this.isDebouncerActive('_checkTheshold')) { |
| 7986 this.debounce('_checkTheshold', function() { |
| 7987 this.checkScrollThesholds(); |
| 7988 }, THROTTLE_THRESHOLD); |
| 7989 } |
| 7990 }, |
| 7991 _initCheck: function(horizontal, isAttached) { |
| 7992 if (isAttached) { |
| 7993 this.debounce('_init', function() { |
| 7994 this.clearTriggers(); |
| 7995 this.checkScrollThesholds(); |
| 7996 }); |
| 7997 } |
| 7998 }, |
| 7999 checkScrollThesholds: function() { |
| 8000 if (!this.scrollTarget || this.lowerTriggered && this.upperTriggered) { |
| 8001 return; |
| 8002 } |
| 8003 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTop; |
| 8004 var lowerScrollValue = this.horizontal ? this.scrollTarget.scrollWidth - thi
s._scrollTargetWidth - this._scrollLeft : this.scrollTarget.scrollHeight - this.
_scrollTargetHeight - this._scrollTop; |
| 8005 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) { |
| 8006 this._setUpperTriggered(true); |
| 8007 this.fire('upper-threshold'); |
| 8008 } |
| 8009 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) { |
| 8010 this._setLowerTriggered(true); |
| 8011 this.fire('lower-threshold'); |
| 8012 } |
| 8013 }, |
| 8014 clearTriggers: function() { |
| 8015 this._setUpperTriggered(false); |
| 8016 this._setLowerTriggered(false); |
| 8017 } |
| 8018 }); |
13942 | 8019 |
13943 is: 'iron-scroll-threshold', | |
13944 | |
13945 properties: { | |
13946 | |
13947 /** | |
13948 * Distance from the top (or left, for horizontal) bound of the scroller | |
13949 * where the "upper trigger" will fire. | |
13950 */ | |
13951 upperThreshold: { | |
13952 type: Number, | |
13953 value: 100 | |
13954 }, | |
13955 | |
13956 /** | |
13957 * Distance from the bottom (or right, for horizontal) bound of the scroll
er | |
13958 * where the "lower trigger" will fire. | |
13959 */ | |
13960 lowerThreshold: { | |
13961 type: Number, | |
13962 value: 100 | |
13963 }, | |
13964 | |
13965 /** | |
13966 * Read-only value that tracks the triggered state of the upper threshold. | |
13967 */ | |
13968 upperTriggered: { | |
13969 type: Boolean, | |
13970 value: false, | |
13971 notify: true, | |
13972 readOnly: true | |
13973 }, | |
13974 | |
13975 /** | |
13976 * Read-only value that tracks the triggered state of the lower threshold. | |
13977 */ | |
13978 lowerTriggered: { | |
13979 type: Boolean, | |
13980 value: false, | |
13981 notify: true, | |
13982 readOnly: true | |
13983 }, | |
13984 | |
13985 /** | |
13986 * True if the orientation of the scroller is horizontal. | |
13987 */ | |
13988 horizontal: { | |
13989 type: Boolean, | |
13990 value: false | |
13991 } | |
13992 }, | |
13993 | |
13994 behaviors: [ | |
13995 Polymer.IronScrollTargetBehavior | |
13996 ], | |
13997 | |
13998 observers: [ | |
13999 '_setOverflow(scrollTarget)', | |
14000 '_initCheck(horizontal, isAttached)' | |
14001 ], | |
14002 | |
14003 get _defaultScrollTarget() { | |
14004 return this; | |
14005 }, | |
14006 | |
14007 _setOverflow: function(scrollTarget) { | |
14008 this.style.overflow = scrollTarget === this ? 'auto' : ''; | |
14009 }, | |
14010 | |
14011 _scrollHandler: function() { | |
14012 // throttle the work on the scroll event | |
14013 var THROTTLE_THRESHOLD = 200; | |
14014 if (!this.isDebouncerActive('_checkTheshold')) { | |
14015 this.debounce('_checkTheshold', function() { | |
14016 this.checkScrollThesholds(); | |
14017 }, THROTTLE_THRESHOLD); | |
14018 } | |
14019 }, | |
14020 | |
14021 _initCheck: function(horizontal, isAttached) { | |
14022 if (isAttached) { | |
14023 this.debounce('_init', function() { | |
14024 this.clearTriggers(); | |
14025 this.checkScrollThesholds(); | |
14026 }); | |
14027 } | |
14028 }, | |
14029 | |
14030 /** | |
14031 * Checks the scroll thresholds. | |
14032 * This method is automatically called by iron-scroll-threshold. | |
14033 * | |
14034 * @method checkScrollThesholds | |
14035 */ | |
14036 checkScrollThesholds: function() { | |
14037 if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) { | |
14038 return; | |
14039 } | |
14040 var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTo
p; | |
14041 var lowerScrollValue = this.horizontal ? | |
14042 this.scrollTarget.scrollWidth - this._scrollTargetWidth - this._scroll
Left : | |
14043 this.scrollTarget.scrollHeight - this._scrollTargetHeight - this._
scrollTop; | |
14044 | |
14045 // Detect upper threshold | |
14046 if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) { | |
14047 this._setUpperTriggered(true); | |
14048 this.fire('upper-threshold'); | |
14049 } | |
14050 // Detect lower threshold | |
14051 if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) { | |
14052 this._setLowerTriggered(true); | |
14053 this.fire('lower-threshold'); | |
14054 } | |
14055 }, | |
14056 | |
14057 /** | |
14058 * Clear the upper and lower threshold states. | |
14059 * | |
14060 * @method clearTriggers | |
14061 */ | |
14062 clearTriggers: function() { | |
14063 this._setUpperTriggered(false); | |
14064 this._setLowerTriggered(false); | |
14065 } | |
14066 | |
14067 /** | |
14068 * Fires when the lower threshold has been reached. | |
14069 * | |
14070 * @event lower-threshold | |
14071 */ | |
14072 | |
14073 /** | |
14074 * Fires when the upper threshold has been reached. | |
14075 * | |
14076 * @event upper-threshold | |
14077 */ | |
14078 | |
14079 }); | |
14080 // Copyright 2015 The Chromium Authors. All rights reserved. | 8020 // Copyright 2015 The Chromium Authors. All rights reserved. |
14081 // Use of this source code is governed by a BSD-style license that can be | 8021 // Use of this source code is governed by a BSD-style license that can be |
14082 // found in the LICENSE file. | 8022 // found in the LICENSE file. |
14083 | |
14084 Polymer({ | 8023 Polymer({ |
14085 is: 'history-list', | 8024 is: 'history-list', |
14086 | 8025 behaviors: [ HistoryListBehavior ], |
14087 behaviors: [HistoryListBehavior], | |
14088 | |
14089 properties: { | 8026 properties: { |
14090 // The search term for the current query. Set when the query returns. | |
14091 searchedTerm: { | 8027 searchedTerm: { |
14092 type: String, | 8028 type: String, |
14093 value: '', | 8029 value: '' |
14094 }, | 8030 }, |
14095 | |
14096 querying: Boolean, | 8031 querying: Boolean, |
14097 | |
14098 // An array of history entries in reverse chronological order. | |
14099 historyData_: Array, | 8032 historyData_: Array, |
14100 | |
14101 resultLoadingDisabled_: { | 8033 resultLoadingDisabled_: { |
14102 type: Boolean, | 8034 type: Boolean, |
14103 value: false, | 8035 value: false |
14104 }, | 8036 } |
14105 }, | 8037 }, |
14106 | |
14107 listeners: { | 8038 listeners: { |
14108 'scroll': 'notifyListScroll_', | 8039 scroll: 'notifyListScroll_', |
14109 'remove-bookmark-stars': 'removeBookmarkStars_', | 8040 'remove-bookmark-stars': 'removeBookmarkStars_' |
14110 }, | 8041 }, |
14111 | |
14112 /** @override */ | |
14113 attached: function() { | 8042 attached: function() { |
14114 // It is possible (eg, when middle clicking the reload button) for all other | 8043 this.$['infinite-list'].notifyResize(); |
14115 // resize events to fire before the list is attached and can be measured. | |
14116 // Adding another resize here ensures it will get sized correctly. | |
14117 /** @type {IronListElement} */(this.$['infinite-list']).notifyResize(); | |
14118 this.$['infinite-list'].scrollTarget = this; | 8044 this.$['infinite-list'].scrollTarget = this; |
14119 this.$['scroll-threshold'].scrollTarget = this; | 8045 this.$['scroll-threshold'].scrollTarget = this; |
14120 }, | 8046 }, |
14121 | |
14122 /** | |
14123 * Remove bookmark star for history items with matching URLs. | |
14124 * @param {{detail: !string}} e | |
14125 * @private | |
14126 */ | |
14127 removeBookmarkStars_: function(e) { | 8047 removeBookmarkStars_: function(e) { |
14128 var url = e.detail; | 8048 var url = e.detail; |
14129 | 8049 if (this.historyData_ === undefined) return; |
14130 if (this.historyData_ === undefined) | |
14131 return; | |
14132 | |
14133 for (var i = 0; i < this.historyData_.length; i++) { | 8050 for (var i = 0; i < this.historyData_.length; i++) { |
14134 if (this.historyData_[i].url == url) | 8051 if (this.historyData_[i].url == url) this.set('historyData_.' + i + '.star
red', false); |
14135 this.set('historyData_.' + i + '.starred', false); | 8052 } |
14136 } | 8053 }, |
14137 }, | |
14138 | |
14139 /** | |
14140 * Disables history result loading when there are no more history results. | |
14141 */ | |
14142 disableResultLoading: function() { | 8054 disableResultLoading: function() { |
14143 this.resultLoadingDisabled_ = true; | 8055 this.resultLoadingDisabled_ = true; |
14144 }, | 8056 }, |
14145 | |
14146 /** | |
14147 * Adds the newly updated history results into historyData_. Adds new fields | |
14148 * for each result. | |
14149 * @param {!Array<!HistoryEntry>} historyResults The new history results. | |
14150 * @param {boolean} incremental Whether the result is from loading more | |
14151 * history, or a new search/list reload. | |
14152 */ | |
14153 addNewResults: function(historyResults, incremental) { | 8057 addNewResults: function(historyResults, incremental) { |
14154 var results = historyResults.slice(); | 8058 var results = historyResults.slice(); |
14155 /** @type {IronScrollThresholdElement} */(this.$['scroll-threshold']) | 8059 this.$['scroll-threshold'].clearTriggers(); |
14156 .clearTriggers(); | |
14157 | |
14158 if (!incremental) { | 8060 if (!incremental) { |
14159 this.resultLoadingDisabled_ = false; | 8061 this.resultLoadingDisabled_ = false; |
14160 if (this.historyData_) | 8062 if (this.historyData_) this.splice('historyData_', 0, this.historyData_.le
ngth); |
14161 this.splice('historyData_', 0, this.historyData_.length); | |
14162 this.fire('unselect-all'); | 8063 this.fire('unselect-all'); |
14163 } | 8064 } |
14164 | |
14165 if (this.historyData_) { | 8065 if (this.historyData_) { |
14166 // If we have previously received data, push the new items onto the | |
14167 // existing array. | |
14168 results.unshift('historyData_'); | 8066 results.unshift('historyData_'); |
14169 this.push.apply(this, results); | 8067 this.push.apply(this, results); |
14170 } else { | 8068 } else { |
14171 // The first time we receive data, use set() to ensure the iron-list is | |
14172 // initialized correctly. | |
14173 this.set('historyData_', results); | 8069 this.set('historyData_', results); |
14174 } | 8070 } |
14175 }, | 8071 }, |
14176 | |
14177 /** | |
14178 * Called when the page is scrolled to near the bottom of the list. | |
14179 * @private | |
14180 */ | |
14181 loadMoreData_: function() { | 8072 loadMoreData_: function() { |
14182 if (this.resultLoadingDisabled_ || this.querying) | 8073 if (this.resultLoadingDisabled_ || this.querying) return; |
14183 return; | |
14184 | |
14185 this.fire('load-more-history'); | 8074 this.fire('load-more-history'); |
14186 }, | 8075 }, |
14187 | |
14188 /** | |
14189 * Check whether the time difference between the given history item and the | |
14190 * next one is large enough for a spacer to be required. | |
14191 * @param {HistoryEntry} item | |
14192 * @param {number} index The index of |item| in |historyData_|. | |
14193 * @param {number} length The length of |historyData_|. | |
14194 * @return {boolean} Whether or not time gap separator is required. | |
14195 * @private | |
14196 */ | |
14197 needsTimeGap_: function(item, index, length) { | 8076 needsTimeGap_: function(item, index, length) { |
14198 return md_history.HistoryItem.needsTimeGap( | 8077 return md_history.HistoryItem.needsTimeGap(this.historyData_, index, this.se
archedTerm); |
14199 this.historyData_, index, this.searchedTerm); | 8078 }, |
14200 }, | |
14201 | |
14202 /** | |
14203 * True if the given item is the beginning of a new card. | |
14204 * @param {HistoryEntry} item | |
14205 * @param {number} i Index of |item| within |historyData_|. | |
14206 * @param {number} length | |
14207 * @return {boolean} | |
14208 * @private | |
14209 */ | |
14210 isCardStart_: function(item, i, length) { | 8079 isCardStart_: function(item, i, length) { |
14211 if (length == 0 || i > length - 1) | 8080 if (length == 0 || i > length - 1) return false; |
14212 return false; | 8081 return i == 0 || this.historyData_[i].dateRelativeDay != this.historyData_[i
- 1].dateRelativeDay; |
14213 return i == 0 || | 8082 }, |
14214 this.historyData_[i].dateRelativeDay != | |
14215 this.historyData_[i - 1].dateRelativeDay; | |
14216 }, | |
14217 | |
14218 /** | |
14219 * True if the given item is the end of a card. | |
14220 * @param {HistoryEntry} item | |
14221 * @param {number} i Index of |item| within |historyData_|. | |
14222 * @param {number} length | |
14223 * @return {boolean} | |
14224 * @private | |
14225 */ | |
14226 isCardEnd_: function(item, i, length) { | 8083 isCardEnd_: function(item, i, length) { |
14227 if (length == 0 || i > length - 1) | 8084 if (length == 0 || i > length - 1) return false; |
14228 return false; | 8085 return i == length - 1 || this.historyData_[i].dateRelativeDay != this.histo
ryData_[i + 1].dateRelativeDay; |
14229 return i == length - 1 || | 8086 }, |
14230 this.historyData_[i].dateRelativeDay != | |
14231 this.historyData_[i + 1].dateRelativeDay; | |
14232 }, | |
14233 | |
14234 /** | |
14235 * @param {number} index | |
14236 * @return {boolean} | |
14237 * @private | |
14238 */ | |
14239 isFirstItem_: function(index) { | 8087 isFirstItem_: function(index) { |
14240 return index == 0; | 8088 return index == 0; |
14241 }, | 8089 }, |
14242 | |
14243 /** | |
14244 * @private | |
14245 */ | |
14246 notifyListScroll_: function() { | 8090 notifyListScroll_: function() { |
14247 this.fire('history-list-scrolled'); | 8091 this.fire('history-list-scrolled'); |
14248 }, | 8092 }, |
14249 | |
14250 /** | |
14251 * @param {number} index | |
14252 * @return {string} | |
14253 * @private | |
14254 */ | |
14255 pathForItem_: function(index) { | 8093 pathForItem_: function(index) { |
14256 return 'historyData_.' + index; | 8094 return 'historyData_.' + index; |
14257 }, | 8095 } |
14258 }); | 8096 }); |
| 8097 |
14259 // Copyright 2016 The Chromium Authors. All rights reserved. | 8098 // Copyright 2016 The Chromium Authors. All rights reserved. |
14260 // Use of this source code is governed by a BSD-style license that can be | 8099 // Use of this source code is governed by a BSD-style license that can be |
14261 // found in the LICENSE file. | 8100 // found in the LICENSE file. |
14262 | |
14263 /** | |
14264 * @fileoverview | |
14265 * history-lazy-render is a simple variant of dom-if designed for lazy rendering | |
14266 * of elements that are accessed imperatively. | |
14267 * Usage: | |
14268 * <template is="history-lazy-render" id="menu"> | |
14269 * <heavy-menu></heavy-menu> | |
14270 * </template> | |
14271 * | |
14272 * this.$.menu.get().then(function(menu) { | |
14273 * menu.show(); | |
14274 * }); | |
14275 */ | |
14276 | |
14277 Polymer({ | 8101 Polymer({ |
14278 is: 'history-lazy-render', | 8102 is: 'history-lazy-render', |
14279 extends: 'template', | 8103 "extends": 'template', |
14280 | 8104 behaviors: [ Polymer.Templatizer ], |
14281 behaviors: [ | |
14282 Polymer.Templatizer | |
14283 ], | |
14284 | |
14285 /** @private {Promise<Element>} */ | |
14286 _renderPromise: null, | 8105 _renderPromise: null, |
14287 | |
14288 /** @private {TemplateInstance} */ | |
14289 _instance: null, | 8106 _instance: null, |
14290 | |
14291 /** | |
14292 * Stamp the template into the DOM tree asynchronously | |
14293 * @return {Promise<Element>} Promise which resolves when the template has | |
14294 * been stamped. | |
14295 */ | |
14296 get: function() { | 8107 get: function() { |
14297 if (!this._renderPromise) { | 8108 if (!this._renderPromise) { |
14298 this._renderPromise = new Promise(function(resolve) { | 8109 this._renderPromise = new Promise(function(resolve) { |
14299 this._debounceTemplate(function() { | 8110 this._debounceTemplate(function() { |
14300 this._render(); | 8111 this._render(); |
14301 this._renderPromise = null; | 8112 this._renderPromise = null; |
14302 resolve(this.getIfExists()); | 8113 resolve(this.getIfExists()); |
14303 }.bind(this)); | 8114 }.bind(this)); |
14304 }.bind(this)); | 8115 }.bind(this)); |
14305 } | 8116 } |
14306 return this._renderPromise; | 8117 return this._renderPromise; |
14307 }, | 8118 }, |
14308 | |
14309 /** | |
14310 * @return {?Element} The element contained in the template, if it has | |
14311 * already been stamped. | |
14312 */ | |
14313 getIfExists: function() { | 8119 getIfExists: function() { |
14314 if (this._instance) { | 8120 if (this._instance) { |
14315 var children = this._instance._children; | 8121 var children = this._instance._children; |
14316 | |
14317 for (var i = 0; i < children.length; i++) { | 8122 for (var i = 0; i < children.length; i++) { |
14318 if (children[i].nodeType == Node.ELEMENT_NODE) | 8123 if (children[i].nodeType == Node.ELEMENT_NODE) return children[i]; |
14319 return children[i]; | |
14320 } | 8124 } |
14321 } | 8125 } |
14322 return null; | 8126 return null; |
14323 }, | 8127 }, |
14324 | |
14325 _render: function() { | 8128 _render: function() { |
14326 if (!this.ctor) | 8129 if (!this.ctor) this.templatize(this); |
14327 this.templatize(this); | |
14328 var parentNode = this.parentNode; | 8130 var parentNode = this.parentNode; |
14329 if (parentNode && !this._instance) { | 8131 if (parentNode && !this._instance) { |
14330 this._instance = /** @type {TemplateInstance} */(this.stamp({})); | 8132 this._instance = this.stamp({}); |
14331 var root = this._instance.root; | 8133 var root = this._instance.root; |
14332 parentNode.insertBefore(root, this); | 8134 parentNode.insertBefore(root, this); |
14333 } | 8135 } |
14334 }, | 8136 }, |
14335 | |
14336 /** | |
14337 * @param {string} prop | |
14338 * @param {Object} value | |
14339 */ | |
14340 _forwardParentProp: function(prop, value) { | 8137 _forwardParentProp: function(prop, value) { |
14341 if (this._instance) | 8138 if (this._instance) this._instance.__setProperty(prop, value, true); |
14342 this._instance.__setProperty(prop, value, true); | 8139 }, |
14343 }, | |
14344 | |
14345 /** | |
14346 * @param {string} path | |
14347 * @param {Object} value | |
14348 */ | |
14349 _forwardParentPath: function(path, value) { | 8140 _forwardParentPath: function(path, value) { |
14350 if (this._instance) | 8141 if (this._instance) this._instance._notifyPath(path, value, true); |
14351 this._instance._notifyPath(path, value, true); | |
14352 } | 8142 } |
14353 }); | 8143 }); |
| 8144 |
14354 // Copyright 2016 The Chromium Authors. All rights reserved. | 8145 // Copyright 2016 The Chromium Authors. All rights reserved. |
14355 // Use of this source code is governed by a BSD-style license that can be | 8146 // Use of this source code is governed by a BSD-style license that can be |
14356 // found in the LICENSE file. | 8147 // found in the LICENSE file. |
14357 | |
14358 Polymer({ | 8148 Polymer({ |
14359 is: 'history-list-container', | 8149 is: 'history-list-container', |
14360 | |
14361 properties: { | 8150 properties: { |
14362 // The path of the currently selected page. | |
14363 selectedPage_: String, | 8151 selectedPage_: String, |
14364 | |
14365 // Whether domain-grouped history is enabled. | |
14366 grouped: Boolean, | 8152 grouped: Boolean, |
14367 | |
14368 /** @type {!QueryState} */ | |
14369 queryState: Object, | 8153 queryState: Object, |
14370 | 8154 queryResult: Object |
14371 /** @type {!QueryResult} */ | 8155 }, |
14372 queryResult: Object, | 8156 observers: [ 'groupedRangeChanged_(queryState.range)' ], |
14373 }, | |
14374 | |
14375 observers: [ | |
14376 'groupedRangeChanged_(queryState.range)', | |
14377 ], | |
14378 | |
14379 listeners: { | 8157 listeners: { |
14380 'history-list-scrolled': 'closeMenu_', | 8158 'history-list-scrolled': 'closeMenu_', |
14381 'load-more-history': 'loadMoreHistory_', | 8159 'load-more-history': 'loadMoreHistory_', |
14382 'toggle-menu': 'toggleMenu_', | 8160 'toggle-menu': 'toggleMenu_' |
14383 }, | 8161 }, |
14384 | |
14385 /** | |
14386 * @param {HistoryQuery} info An object containing information about the | |
14387 * query. | |
14388 * @param {!Array<HistoryEntry>} results A list of results. | |
14389 */ | |
14390 historyResult: function(info, results) { | 8162 historyResult: function(info, results) { |
14391 this.initializeResults_(info, results); | 8163 this.initializeResults_(info, results); |
14392 this.closeMenu_(); | 8164 this.closeMenu_(); |
14393 | |
14394 if (this.selectedPage_ == 'grouped-list') { | 8165 if (this.selectedPage_ == 'grouped-list') { |
14395 this.$$('#grouped-list').historyData = results; | 8166 this.$$('#grouped-list').historyData = results; |
14396 return; | 8167 return; |
14397 } | 8168 } |
14398 | 8169 var list = this.$['infinite-list']; |
14399 var list = /** @type {HistoryListElement} */(this.$['infinite-list']); | |
14400 list.addNewResults(results, this.queryState.incremental); | 8170 list.addNewResults(results, this.queryState.incremental); |
14401 if (info.finished) | 8171 if (info.finished) list.disableResultLoading(); |
14402 list.disableResultLoading(); | 8172 }, |
14403 }, | |
14404 | |
14405 /** | |
14406 * Queries the history backend for results based on queryState. | |
14407 * @param {boolean} incremental Whether the new query should continue where | |
14408 * the previous query stopped. | |
14409 */ | |
14410 queryHistory: function(incremental) { | 8173 queryHistory: function(incremental) { |
14411 var queryState = this.queryState; | 8174 var queryState = this.queryState; |
14412 // Disable querying until the first set of results have been returned. If | |
14413 // there is a search, query immediately to support search query params from | |
14414 // the URL. | |
14415 var noResults = !this.queryResult || this.queryResult.results == null; | 8175 var noResults = !this.queryResult || this.queryResult.results == null; |
14416 if (queryState.queryingDisabled || | 8176 if (queryState.queryingDisabled || !this.queryState.searchTerm && noResults)
{ |
14417 (!this.queryState.searchTerm && noResults)) { | |
14418 return; | 8177 return; |
14419 } | 8178 } |
14420 | |
14421 // Close any open dialog if a new query is initiated. | |
14422 var dialog = this.$.dialog.getIfExists(); | 8179 var dialog = this.$.dialog.getIfExists(); |
14423 if (!incremental && dialog && dialog.open) | 8180 if (!incremental && dialog && dialog.open) dialog.close(); |
14424 dialog.close(); | |
14425 | |
14426 this.set('queryState.querying', true); | 8181 this.set('queryState.querying', true); |
14427 this.set('queryState.incremental', incremental); | 8182 this.set('queryState.incremental', incremental); |
14428 | |
14429 var lastVisitTime = 0; | 8183 var lastVisitTime = 0; |
14430 if (incremental) { | 8184 if (incremental) { |
14431 var lastVisit = this.queryResult.results.slice(-1)[0]; | 8185 var lastVisit = this.queryResult.results.slice(-1)[0]; |
14432 lastVisitTime = lastVisit ? lastVisit.time : 0; | 8186 lastVisitTime = lastVisit ? lastVisit.time : 0; |
14433 } | 8187 } |
14434 | 8188 var maxResults = queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAG
E : 0; |
14435 var maxResults = | 8189 chrome.send('queryHistory', [ queryState.searchTerm, queryState.groupedOffse
t, queryState.range, lastVisitTime, maxResults ]); |
14436 queryState.range == HistoryRange.ALL_TIME ? RESULTS_PER_PAGE : 0; | 8190 }, |
14437 chrome.send('queryHistory', [ | |
14438 queryState.searchTerm, queryState.groupedOffset, queryState.range, | |
14439 lastVisitTime, maxResults | |
14440 ]); | |
14441 }, | |
14442 | |
14443 historyDeleted: function() { | 8191 historyDeleted: function() { |
14444 // Do not reload the list when there are items checked. | 8192 if (this.getSelectedItemCount() > 0) return; |
14445 if (this.getSelectedItemCount() > 0) | |
14446 return; | |
14447 | |
14448 // Reload the list with current search state. | |
14449 this.queryHistory(false); | 8193 this.queryHistory(false); |
14450 }, | 8194 }, |
14451 | |
14452 /** @return {number} */ | |
14453 getSelectedItemCount: function() { | 8195 getSelectedItemCount: function() { |
14454 return this.getSelectedList_().selectedPaths.size; | 8196 return this.getSelectedList_().selectedPaths.size; |
14455 }, | 8197 }, |
14456 | |
14457 unselectAllItems: function(count) { | 8198 unselectAllItems: function(count) { |
14458 var selectedList = this.getSelectedList_(); | 8199 var selectedList = this.getSelectedList_(); |
14459 if (selectedList) | 8200 if (selectedList) selectedList.unselectAllItems(count); |
14460 selectedList.unselectAllItems(count); | 8201 }, |
14461 }, | |
14462 | |
14463 /** | |
14464 * Delete all the currently selected history items. Will prompt the user with | |
14465 * a dialog to confirm that the deletion should be performed. | |
14466 */ | |
14467 deleteSelectedWithPrompt: function() { | 8202 deleteSelectedWithPrompt: function() { |
14468 if (!loadTimeData.getBoolean('allowDeletingHistory')) | 8203 if (!loadTimeData.getBoolean('allowDeletingHistory')) return; |
14469 return; | |
14470 this.$.dialog.get().then(function(dialog) { | 8204 this.$.dialog.get().then(function(dialog) { |
14471 dialog.showModal(); | 8205 dialog.showModal(); |
14472 }); | 8206 }); |
14473 }, | 8207 }, |
14474 | |
14475 /** | |
14476 * @param {HistoryRange} range | |
14477 * @private | |
14478 */ | |
14479 groupedRangeChanged_: function(range) { | 8208 groupedRangeChanged_: function(range) { |
14480 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? | 8209 this.selectedPage_ = this.queryState.range == HistoryRange.ALL_TIME ? 'infin
ite-list' : 'grouped-list'; |
14481 'infinite-list' : 'grouped-list'; | |
14482 | |
14483 this.queryHistory(false); | 8210 this.queryHistory(false); |
14484 }, | 8211 }, |
14485 | 8212 loadMoreHistory_: function() { |
14486 /** @private */ | 8213 this.queryHistory(true); |
14487 loadMoreHistory_: function() { this.queryHistory(true); }, | 8214 }, |
14488 | |
14489 /** | |
14490 * @param {HistoryQuery} info | |
14491 * @param {!Array<HistoryEntry>} results | |
14492 * @private | |
14493 */ | |
14494 initializeResults_: function(info, results) { | 8215 initializeResults_: function(info, results) { |
14495 if (results.length == 0) | 8216 if (results.length == 0) return; |
14496 return; | |
14497 | |
14498 var currentDate = results[0].dateRelativeDay; | 8217 var currentDate = results[0].dateRelativeDay; |
14499 | |
14500 for (var i = 0; i < results.length; i++) { | 8218 for (var i = 0; i < results.length; i++) { |
14501 // Sets the default values for these fields to prevent undefined types. | |
14502 results[i].selected = false; | 8219 results[i].selected = false; |
14503 results[i].readableTimestamp = | 8220 results[i].readableTimestamp = info.term == '' ? results[i].dateTimeOfDay
: results[i].dateShort; |
14504 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort; | |
14505 | |
14506 if (results[i].dateRelativeDay != currentDate) { | 8221 if (results[i].dateRelativeDay != currentDate) { |
14507 currentDate = results[i].dateRelativeDay; | 8222 currentDate = results[i].dateRelativeDay; |
14508 } | 8223 } |
14509 } | 8224 } |
14510 }, | 8225 }, |
14511 | |
14512 /** @private */ | |
14513 onDialogConfirmTap_: function() { | 8226 onDialogConfirmTap_: function() { |
14514 this.getSelectedList_().deleteSelected(); | 8227 this.getSelectedList_().deleteSelected(); |
14515 var dialog = assert(this.$.dialog.getIfExists()); | 8228 var dialog = assert(this.$.dialog.getIfExists()); |
14516 dialog.close(); | 8229 dialog.close(); |
14517 }, | 8230 }, |
14518 | |
14519 /** @private */ | |
14520 onDialogCancelTap_: function() { | 8231 onDialogCancelTap_: function() { |
14521 var dialog = assert(this.$.dialog.getIfExists()); | 8232 var dialog = assert(this.$.dialog.getIfExists()); |
14522 dialog.close(); | 8233 dialog.close(); |
14523 }, | 8234 }, |
14524 | |
14525 /** | |
14526 * Closes the overflow menu. | |
14527 * @private | |
14528 */ | |
14529 closeMenu_: function() { | 8235 closeMenu_: function() { |
14530 var menu = this.$.sharedMenu.getIfExists(); | 8236 var menu = this.$.sharedMenu.getIfExists(); |
14531 if (menu) | 8237 if (menu) menu.closeMenu(); |
14532 menu.closeMenu(); | 8238 }, |
14533 }, | |
14534 | |
14535 /** | |
14536 * Opens the overflow menu unless the menu is already open and the same button | |
14537 * is pressed. | |
14538 * @param {{detail: {item: !HistoryEntry, target: !HTMLElement}}} e | |
14539 * @return {Promise<Element>} | |
14540 * @private | |
14541 */ | |
14542 toggleMenu_: function(e) { | 8239 toggleMenu_: function(e) { |
14543 var target = e.detail.target; | 8240 var target = e.detail.target; |
14544 return this.$.sharedMenu.get().then(function(menu) { | 8241 return this.$.sharedMenu.get().then(function(menu) { |
14545 /** @type {CrSharedMenuElement} */(menu).toggleMenu( | 8242 menu.toggleMenu(target, e.detail); |
14546 target, e.detail); | 8243 }); |
14547 }); | 8244 }, |
14548 }, | |
14549 | |
14550 /** @private */ | |
14551 onMoreFromSiteTap_: function() { | 8245 onMoreFromSiteTap_: function() { |
14552 var menu = assert(this.$.sharedMenu.getIfExists()); | 8246 var menu = assert(this.$.sharedMenu.getIfExists()); |
14553 this.fire('search-domain', {domain: menu.itemData.item.domain}); | 8247 this.fire('search-domain', { |
| 8248 domain: menu.itemData.item.domain |
| 8249 }); |
14554 menu.closeMenu(); | 8250 menu.closeMenu(); |
14555 }, | 8251 }, |
14556 | |
14557 /** @private */ | |
14558 onRemoveFromHistoryTap_: function() { | 8252 onRemoveFromHistoryTap_: function() { |
14559 var menu = assert(this.$.sharedMenu.getIfExists()); | 8253 var menu = assert(this.$.sharedMenu.getIfExists()); |
14560 var itemData = menu.itemData; | 8254 var itemData = menu.itemData; |
14561 md_history.BrowserService.getInstance() | 8255 md_history.BrowserService.getInstance().deleteItems([ itemData.item ]).then(
function(items) { |
14562 .deleteItems([itemData.item]) | 8256 this.getSelectedList_().removeItemsByPath([ itemData.path ]); |
14563 .then(function(items) { | 8257 this.fire('unselect-all'); |
14564 this.getSelectedList_().removeItemsByPath([itemData.path]); | 8258 }.bind(this)); |
14565 // This unselect-all is to reset the toolbar when deleting a selected | |
14566 // item. TODO(tsergeant): Make this automatic based on observing list | |
14567 // modifications. | |
14568 this.fire('unselect-all'); | |
14569 }.bind(this)); | |
14570 menu.closeMenu(); | 8259 menu.closeMenu(); |
14571 }, | 8260 }, |
14572 | |
14573 /** | |
14574 * @return {HTMLElement} | |
14575 * @private | |
14576 */ | |
14577 getSelectedList_: function() { | 8261 getSelectedList_: function() { |
14578 return this.$.content.selectedItem; | 8262 return this.$.content.selectedItem; |
14579 }, | 8263 } |
14580 }); | 8264 }); |
| 8265 |
14581 // Copyright 2016 The Chromium Authors. All rights reserved. | 8266 // Copyright 2016 The Chromium Authors. All rights reserved. |
14582 // Use of this source code is governed by a BSD-style license that can be | 8267 // Use of this source code is governed by a BSD-style license that can be |
14583 // found in the LICENSE file. | 8268 // found in the LICENSE file. |
14584 | |
14585 Polymer({ | 8269 Polymer({ |
14586 is: 'history-synced-device-card', | 8270 is: 'history-synced-device-card', |
14587 | |
14588 properties: { | 8271 properties: { |
14589 // Name of the synced device. | |
14590 device: String, | 8272 device: String, |
14591 | |
14592 // When the device information was last updated. | |
14593 lastUpdateTime: String, | 8273 lastUpdateTime: String, |
14594 | |
14595 /** | |
14596 * The list of tabs open for this device. | |
14597 * @type {!Array<!ForeignSessionTab>} | |
14598 */ | |
14599 tabs: { | 8274 tabs: { |
14600 type: Array, | 8275 type: Array, |
14601 value: function() { return []; }, | 8276 value: function() { |
| 8277 return []; |
| 8278 }, |
14602 observer: 'updateIcons_' | 8279 observer: 'updateIcons_' |
14603 }, | 8280 }, |
14604 | |
14605 /** | |
14606 * The indexes where a window separator should be shown. The use of a | |
14607 * separate array here is necessary for window separators to appear | |
14608 * correctly in search. See http://crrev.com/2022003002 for more details. | |
14609 * @type {!Array<number>} | |
14610 */ | |
14611 separatorIndexes: Array, | 8281 separatorIndexes: Array, |
14612 | |
14613 // Whether the card is open. | |
14614 opened: Boolean, | 8282 opened: Boolean, |
14615 | |
14616 searchTerm: String, | 8283 searchTerm: String, |
14617 | 8284 sessionTag: String |
14618 // Internal identifier for the device. | 8285 }, |
14619 sessionTag: String, | |
14620 }, | |
14621 | |
14622 /** | |
14623 * Open a single synced tab. Listens to 'click' rather than 'tap' | |
14624 * to determine what modifier keys were pressed. | |
14625 * @param {DomRepeatClickEvent} e | |
14626 * @private | |
14627 */ | |
14628 openTab_: function(e) { | 8286 openTab_: function(e) { |
14629 var tab = /** @type {ForeignSessionTab} */(e.model.tab); | 8287 var tab = e.model.tab; |
14630 md_history.BrowserService.getInstance().openForeignSessionTab( | 8288 md_history.BrowserService.getInstance().openForeignSessionTab(this.sessionTa
g, tab.windowId, tab.sessionId, e); |
14631 this.sessionTag, tab.windowId, tab.sessionId, e); | |
14632 e.preventDefault(); | 8289 e.preventDefault(); |
14633 }, | 8290 }, |
14634 | |
14635 /** | |
14636 * Toggles the dropdown display of synced tabs for each device card. | |
14637 */ | |
14638 toggleTabCard: function() { | 8291 toggleTabCard: function() { |
14639 this.$.collapse.toggle(); | 8292 this.$.collapse.toggle(); |
14640 this.$['dropdown-indicator'].icon = | 8293 this.$['dropdown-indicator'].icon = this.$.collapse.opened ? 'cr:expand-less
' : 'cr:expand-more'; |
14641 this.$.collapse.opened ? 'cr:expand-less' : 'cr:expand-more'; | 8294 }, |
14642 }, | |
14643 | |
14644 /** | |
14645 * When the synced tab information is set, the icon associated with the tab | |
14646 * website is also set. | |
14647 * @private | |
14648 */ | |
14649 updateIcons_: function() { | 8295 updateIcons_: function() { |
14650 this.async(function() { | 8296 this.async(function() { |
14651 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon'); | 8297 var icons = Polymer.dom(this.root).querySelectorAll('.website-icon'); |
14652 | |
14653 for (var i = 0; i < this.tabs.length; i++) { | 8298 for (var i = 0; i < this.tabs.length; i++) { |
14654 icons[i].style.backgroundImage = | 8299 icons[i].style.backgroundImage = cr.icon.getFaviconImageSet(this.tabs[i]
.url); |
14655 cr.icon.getFaviconImageSet(this.tabs[i].url); | 8300 } |
14656 } | 8301 }); |
14657 }); | 8302 }, |
14658 }, | |
14659 | |
14660 /** @private */ | |
14661 isWindowSeparatorIndex_: function(index, separatorIndexes) { | 8303 isWindowSeparatorIndex_: function(index, separatorIndexes) { |
14662 return this.separatorIndexes.indexOf(index) != -1; | 8304 return this.separatorIndexes.indexOf(index) != -1; |
14663 }, | 8305 }, |
14664 | |
14665 /** | |
14666 * @param {boolean} opened | |
14667 * @return {string} | |
14668 * @private | |
14669 */ | |
14670 getCollapseIcon_: function(opened) { | 8306 getCollapseIcon_: function(opened) { |
14671 return opened ? 'cr:expand-less' : 'cr:expand-more'; | 8307 return opened ? 'cr:expand-less' : 'cr:expand-more'; |
14672 }, | 8308 }, |
14673 | |
14674 /** | |
14675 * @param {boolean} opened | |
14676 * @return {string} | |
14677 * @private | |
14678 */ | |
14679 getCollapseTitle_: function(opened) { | 8309 getCollapseTitle_: function(opened) { |
14680 return opened ? loadTimeData.getString('collapseSessionButton') : | 8310 return opened ? loadTimeData.getString('collapseSessionButton') : loadTimeDa
ta.getString('expandSessionButton'); |
14681 loadTimeData.getString('expandSessionButton'); | 8311 }, |
14682 }, | |
14683 | |
14684 /** | |
14685 * @param {CustomEvent} e | |
14686 * @private | |
14687 */ | |
14688 onMenuButtonTap_: function(e) { | 8312 onMenuButtonTap_: function(e) { |
14689 this.fire('toggle-menu', { | 8313 this.fire('toggle-menu', { |
14690 target: Polymer.dom(e).localTarget, | 8314 target: Polymer.dom(e).localTarget, |
14691 tag: this.sessionTag | 8315 tag: this.sessionTag |
14692 }); | 8316 }); |
14693 e.stopPropagation(); // Prevent iron-collapse. | 8317 e.stopPropagation(); |
14694 }, | 8318 } |
14695 }); | 8319 }); |
| 8320 |
14696 // Copyright 2016 The Chromium Authors. All rights reserved. | 8321 // Copyright 2016 The Chromium Authors. All rights reserved. |
14697 // Use of this source code is governed by a BSD-style license that can be | 8322 // Use of this source code is governed by a BSD-style license that can be |
14698 // found in the LICENSE file. | 8323 // found in the LICENSE file. |
14699 | |
14700 /** | |
14701 * @typedef {{device: string, | |
14702 * lastUpdateTime: string, | |
14703 * opened: boolean, | |
14704 * separatorIndexes: !Array<number>, | |
14705 * timestamp: number, | |
14706 * tabs: !Array<!ForeignSessionTab>, | |
14707 * tag: string}} | |
14708 */ | |
14709 var ForeignDeviceInternal; | 8324 var ForeignDeviceInternal; |
14710 | 8325 |
14711 Polymer({ | 8326 Polymer({ |
14712 is: 'history-synced-device-manager', | 8327 is: 'history-synced-device-manager', |
14713 | |
14714 properties: { | 8328 properties: { |
14715 /** | |
14716 * @type {?Array<!ForeignSession>} | |
14717 */ | |
14718 sessionList: { | 8329 sessionList: { |
14719 type: Array, | 8330 type: Array, |
14720 observer: 'updateSyncedDevices' | 8331 observer: 'updateSyncedDevices' |
14721 }, | 8332 }, |
14722 | |
14723 searchTerm: { | 8333 searchTerm: { |
14724 type: String, | 8334 type: String, |
14725 observer: 'searchTermChanged' | 8335 observer: 'searchTermChanged' |
14726 }, | 8336 }, |
14727 | |
14728 /** | |
14729 * An array of synced devices with synced tab data. | |
14730 * @type {!Array<!ForeignDeviceInternal>} | |
14731 */ | |
14732 syncedDevices_: { | 8337 syncedDevices_: { |
14733 type: Array, | 8338 type: Array, |
14734 value: function() { return []; } | 8339 value: function() { |
14735 }, | 8340 return []; |
14736 | 8341 } |
14737 /** @private */ | 8342 }, |
14738 signInState_: { | 8343 signInState_: { |
14739 type: Boolean, | 8344 type: Boolean, |
14740 value: loadTimeData.getBoolean('isUserSignedIn'), | 8345 value: loadTimeData.getBoolean('isUserSignedIn') |
14741 }, | 8346 }, |
14742 | |
14743 /** @private */ | |
14744 guestSession_: { | 8347 guestSession_: { |
14745 type: Boolean, | 8348 type: Boolean, |
14746 value: loadTimeData.getBoolean('isGuestSession'), | 8349 value: loadTimeData.getBoolean('isGuestSession') |
14747 }, | 8350 }, |
14748 | |
14749 /** @private */ | |
14750 fetchingSyncedTabs_: { | 8351 fetchingSyncedTabs_: { |
14751 type: Boolean, | 8352 type: Boolean, |
14752 value: false, | 8353 value: false |
14753 } | 8354 } |
14754 }, | 8355 }, |
14755 | |
14756 listeners: { | 8356 listeners: { |
14757 'toggle-menu': 'onToggleMenu_', | 8357 'toggle-menu': 'onToggleMenu_', |
14758 'scroll': 'onListScroll_' | 8358 scroll: 'onListScroll_' |
14759 }, | 8359 }, |
14760 | |
14761 /** @override */ | |
14762 attached: function() { | 8360 attached: function() { |
14763 // Update the sign in state. | |
14764 chrome.send('otherDevicesInitialized'); | 8361 chrome.send('otherDevicesInitialized'); |
14765 }, | 8362 }, |
14766 | |
14767 /** | |
14768 * @param {!ForeignSession} session | |
14769 * @return {!ForeignDeviceInternal} | |
14770 */ | |
14771 createInternalDevice_: function(session) { | 8363 createInternalDevice_: function(session) { |
14772 var tabs = []; | 8364 var tabs = []; |
14773 var separatorIndexes = []; | 8365 var separatorIndexes = []; |
14774 for (var i = 0; i < session.windows.length; i++) { | 8366 for (var i = 0; i < session.windows.length; i++) { |
14775 var windowId = session.windows[i].sessionId; | 8367 var windowId = session.windows[i].sessionId; |
14776 var newTabs = session.windows[i].tabs; | 8368 var newTabs = session.windows[i].tabs; |
14777 if (newTabs.length == 0) | 8369 if (newTabs.length == 0) continue; |
14778 continue; | |
14779 | |
14780 newTabs.forEach(function(tab) { | 8370 newTabs.forEach(function(tab) { |
14781 tab.windowId = windowId; | 8371 tab.windowId = windowId; |
14782 }); | 8372 }); |
14783 | |
14784 var windowAdded = false; | 8373 var windowAdded = false; |
14785 if (!this.searchTerm) { | 8374 if (!this.searchTerm) { |
14786 // Add all the tabs if there is no search term. | |
14787 tabs = tabs.concat(newTabs); | 8375 tabs = tabs.concat(newTabs); |
14788 windowAdded = true; | 8376 windowAdded = true; |
14789 } else { | 8377 } else { |
14790 var searchText = this.searchTerm.toLowerCase(); | 8378 var searchText = this.searchTerm.toLowerCase(); |
14791 for (var j = 0; j < newTabs.length; j++) { | 8379 for (var j = 0; j < newTabs.length; j++) { |
14792 var tab = newTabs[j]; | 8380 var tab = newTabs[j]; |
14793 if (tab.title.toLowerCase().indexOf(searchText) != -1) { | 8381 if (tab.title.toLowerCase().indexOf(searchText) != -1) { |
14794 tabs.push(tab); | 8382 tabs.push(tab); |
14795 windowAdded = true; | 8383 windowAdded = true; |
14796 } | 8384 } |
14797 } | 8385 } |
14798 } | 8386 } |
14799 if (windowAdded && i != session.windows.length - 1) | 8387 if (windowAdded && i != session.windows.length - 1) separatorIndexes.push(
tabs.length - 1); |
14800 separatorIndexes.push(tabs.length - 1); | |
14801 } | 8388 } |
14802 return { | 8389 return { |
14803 device: session.name, | 8390 device: session.name, |
14804 lastUpdateTime: '– ' + session.modifiedTime, | 8391 lastUpdateTime: '– ' + session.modifiedTime, |
14805 opened: true, | 8392 opened: true, |
14806 separatorIndexes: separatorIndexes, | 8393 separatorIndexes: separatorIndexes, |
14807 timestamp: session.timestamp, | 8394 timestamp: session.timestamp, |
14808 tabs: tabs, | 8395 tabs: tabs, |
14809 tag: session.tag, | 8396 tag: session.tag |
14810 }; | 8397 }; |
14811 }, | 8398 }, |
14812 | |
14813 onSignInTap_: function() { | 8399 onSignInTap_: function() { |
14814 chrome.send('startSignInFlow'); | 8400 chrome.send('startSignInFlow'); |
14815 }, | 8401 }, |
14816 | |
14817 onListScroll_: function() { | 8402 onListScroll_: function() { |
14818 var menu = this.$.menu.getIfExists(); | 8403 var menu = this.$.menu.getIfExists(); |
14819 if (menu) | 8404 if (menu) menu.closeMenu(); |
14820 menu.closeMenu(); | 8405 }, |
14821 }, | |
14822 | |
14823 onToggleMenu_: function(e) { | 8406 onToggleMenu_: function(e) { |
14824 this.$.menu.get().then(function(menu) { | 8407 this.$.menu.get().then(function(menu) { |
14825 menu.toggleMenu(e.detail.target, e.detail.tag); | 8408 menu.toggleMenu(e.detail.target, e.detail.tag); |
14826 }); | 8409 }); |
14827 }, | 8410 }, |
14828 | |
14829 onOpenAllTap_: function() { | 8411 onOpenAllTap_: function() { |
14830 var menu = assert(this.$.menu.getIfExists()); | 8412 var menu = assert(this.$.menu.getIfExists()); |
14831 md_history.BrowserService.getInstance().openForeignSessionAllTabs( | 8413 md_history.BrowserService.getInstance().openForeignSessionAllTabs(menu.itemD
ata); |
14832 menu.itemData); | |
14833 menu.closeMenu(); | 8414 menu.closeMenu(); |
14834 }, | 8415 }, |
14835 | |
14836 onDeleteSessionTap_: function() { | 8416 onDeleteSessionTap_: function() { |
14837 var menu = assert(this.$.menu.getIfExists()); | 8417 var menu = assert(this.$.menu.getIfExists()); |
14838 md_history.BrowserService.getInstance().deleteForeignSession( | 8418 md_history.BrowserService.getInstance().deleteForeignSession(menu.itemData); |
14839 menu.itemData); | |
14840 menu.closeMenu(); | 8419 menu.closeMenu(); |
14841 }, | 8420 }, |
14842 | |
14843 /** @private */ | |
14844 clearDisplayedSyncedDevices_: function() { | 8421 clearDisplayedSyncedDevices_: function() { |
14845 this.syncedDevices_ = []; | 8422 this.syncedDevices_ = []; |
14846 }, | 8423 }, |
14847 | 8424 showNoSyncedMessage: function(signInState, syncedDevicesLength, guestSession)
{ |
14848 /** | 8425 if (guestSession) return true; |
14849 * Decide whether or not should display no synced tabs message. | |
14850 * @param {boolean} signInState | |
14851 * @param {number} syncedDevicesLength | |
14852 * @param {boolean} guestSession | |
14853 * @return {boolean} | |
14854 */ | |
14855 showNoSyncedMessage: function( | |
14856 signInState, syncedDevicesLength, guestSession) { | |
14857 if (guestSession) | |
14858 return true; | |
14859 | |
14860 return signInState && syncedDevicesLength == 0; | 8426 return signInState && syncedDevicesLength == 0; |
14861 }, | 8427 }, |
14862 | |
14863 /** | |
14864 * Shows the signin guide when the user is not signed in and not in a guest | |
14865 * session. | |
14866 * @param {boolean} signInState | |
14867 * @param {boolean} guestSession | |
14868 * @return {boolean} | |
14869 */ | |
14870 showSignInGuide: function(signInState, guestSession) { | 8428 showSignInGuide: function(signInState, guestSession) { |
14871 var show = !signInState && !guestSession; | 8429 var show = !signInState && !guestSession; |
14872 if (show) { | 8430 if (show) { |
14873 md_history.BrowserService.getInstance().recordAction( | 8431 md_history.BrowserService.getInstance().recordAction('Signin_Impression_Fr
omRecentTabs'); |
14874 'Signin_Impression_FromRecentTabs'); | 8432 } |
14875 } | |
14876 | |
14877 return show; | 8433 return show; |
14878 }, | 8434 }, |
14879 | |
14880 /** | |
14881 * Decide what message should be displayed when user is logged in and there | |
14882 * are no synced tabs. | |
14883 * @param {boolean} fetchingSyncedTabs | |
14884 * @return {string} | |
14885 */ | |
14886 noSyncedTabsMessage: function(fetchingSyncedTabs) { | 8435 noSyncedTabsMessage: function(fetchingSyncedTabs) { |
14887 return loadTimeData.getString( | 8436 return loadTimeData.getString(fetchingSyncedTabs ? 'loading' : 'noSyncedResu
lts'); |
14888 fetchingSyncedTabs ? 'loading' : 'noSyncedResults'); | 8437 }, |
14889 }, | |
14890 | |
14891 /** | |
14892 * Replaces the currently displayed synced tabs with |sessionList|. It is | |
14893 * common for only a single session within the list to have changed, We try to | |
14894 * avoid doing extra work in this case. The logic could be more intelligent | |
14895 * about updating individual tabs rather than replacing whole sessions, but | |
14896 * this approach seems to have acceptable performance. | |
14897 * @param {?Array<!ForeignSession>} sessionList | |
14898 */ | |
14899 updateSyncedDevices: function(sessionList) { | 8438 updateSyncedDevices: function(sessionList) { |
14900 this.fetchingSyncedTabs_ = false; | 8439 this.fetchingSyncedTabs_ = false; |
14901 | 8440 if (!sessionList) return; |
14902 if (!sessionList) | |
14903 return; | |
14904 | |
14905 // First, update any existing devices that have changed. | |
14906 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length); | 8441 var updateCount = Math.min(sessionList.length, this.syncedDevices_.length); |
14907 for (var i = 0; i < updateCount; i++) { | 8442 for (var i = 0; i < updateCount; i++) { |
14908 var oldDevice = this.syncedDevices_[i]; | 8443 var oldDevice = this.syncedDevices_[i]; |
14909 if (oldDevice.tag != sessionList[i].tag || | 8444 if (oldDevice.tag != sessionList[i].tag || oldDevice.timestamp != sessionL
ist[i].timestamp) { |
14910 oldDevice.timestamp != sessionList[i].timestamp) { | 8445 this.splice('syncedDevices_', i, 1, this.createInternalDevice_(sessionLi
st[i])); |
14911 this.splice( | 8446 } |
14912 'syncedDevices_', i, 1, this.createInternalDevice_(sessionList[i])); | 8447 } |
14913 } | |
14914 } | |
14915 | |
14916 if (sessionList.length >= this.syncedDevices_.length) { | 8448 if (sessionList.length >= this.syncedDevices_.length) { |
14917 // The list grew; append new items. | |
14918 for (var i = updateCount; i < sessionList.length; i++) { | 8449 for (var i = updateCount; i < sessionList.length; i++) { |
14919 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i])); | 8450 this.push('syncedDevices_', this.createInternalDevice_(sessionList[i])); |
14920 } | 8451 } |
14921 } else { | 8452 } else { |
14922 // The list shrank; remove deleted items. | 8453 this.splice('syncedDevices_', updateCount, this.syncedDevices_.length - up
dateCount); |
14923 this.splice( | 8454 } |
14924 'syncedDevices_', updateCount, | 8455 }, |
14925 this.syncedDevices_.length - updateCount); | |
14926 } | |
14927 }, | |
14928 | |
14929 /** | |
14930 * End fetching synced tabs when sync is disabled. | |
14931 */ | |
14932 tabSyncDisabled: function() { | 8456 tabSyncDisabled: function() { |
14933 this.fetchingSyncedTabs_ = false; | 8457 this.fetchingSyncedTabs_ = false; |
14934 this.clearDisplayedSyncedDevices_(); | 8458 this.clearDisplayedSyncedDevices_(); |
14935 }, | 8459 }, |
14936 | |
14937 /** | |
14938 * Get called when user's sign in state changes, this will affect UI of synced | |
14939 * tabs page. Sign in promo gets displayed when user is signed out, and | |
14940 * different messages are shown when there are no synced tabs. | |
14941 * @param {boolean} isUserSignedIn | |
14942 */ | |
14943 updateSignInState: function(isUserSignedIn) { | 8460 updateSignInState: function(isUserSignedIn) { |
14944 // If user's sign in state didn't change, then don't change message or | 8461 if (this.signInState_ == isUserSignedIn) return; |
14945 // update UI. | |
14946 if (this.signInState_ == isUserSignedIn) | |
14947 return; | |
14948 | |
14949 this.signInState_ = isUserSignedIn; | 8462 this.signInState_ = isUserSignedIn; |
14950 | |
14951 // User signed out, clear synced device list and show the sign in promo. | |
14952 if (!isUserSignedIn) { | 8463 if (!isUserSignedIn) { |
14953 this.clearDisplayedSyncedDevices_(); | 8464 this.clearDisplayedSyncedDevices_(); |
14954 return; | 8465 return; |
14955 } | 8466 } |
14956 // User signed in, show the loading message when querying for synced | |
14957 // devices. | |
14958 this.fetchingSyncedTabs_ = true; | 8467 this.fetchingSyncedTabs_ = true; |
14959 }, | 8468 }, |
14960 | |
14961 searchTermChanged: function(searchTerm) { | 8469 searchTermChanged: function(searchTerm) { |
14962 this.clearDisplayedSyncedDevices_(); | 8470 this.clearDisplayedSyncedDevices_(); |
14963 this.updateSyncedDevices(this.sessionList); | 8471 this.updateSyncedDevices(this.sessionList); |
14964 } | 8472 } |
14965 }); | 8473 }); |
14966 /** | 8474 |
14967 `iron-selector` is an element which can be used to manage a list of elements | 8475 Polymer({ |
14968 that can be selected. Tapping on the item will make the item selected. The `
selected` indicates | 8476 is: 'iron-selector', |
14969 which item is being selected. The default is to use the index of the item. | 8477 behaviors: [ Polymer.IronMultiSelectableBehavior ] |
14970 | 8478 }); |
14971 Example: | 8479 |
14972 | |
14973 <iron-selector selected="0"> | |
14974 <div>Item 1</div> | |
14975 <div>Item 2</div> | |
14976 <div>Item 3</div> | |
14977 </iron-selector> | |
14978 | |
14979 If you want to use the attribute value of an element for `selected` instead of
the index, | |
14980 set `attrForSelected` to the name of the attribute. For example, if you want
to select item by | |
14981 `name`, set `attrForSelected` to `name`. | |
14982 | |
14983 Example: | |
14984 | |
14985 <iron-selector attr-for-selected="name" selected="foo"> | |
14986 <div name="foo">Foo</div> | |
14987 <div name="bar">Bar</div> | |
14988 <div name="zot">Zot</div> | |
14989 </iron-selector> | |
14990 | |
14991 You can specify a default fallback with `fallbackSelection` in case the `selec
ted` attribute does | |
14992 not match the `attrForSelected` attribute of any elements. | |
14993 | |
14994 Example: | |
14995 | |
14996 <iron-selector attr-for-selected="name" selected="non-existing" | |
14997 fallback-selection="default"> | |
14998 <div name="foo">Foo</div> | |
14999 <div name="bar">Bar</div> | |
15000 <div name="default">Default</div> | |
15001 </iron-selector> | |
15002 | |
15003 Note: When the selector is multi, the selection will set to `fallbackSelection
` iff | |
15004 the number of matching elements is zero. | |
15005 | |
15006 `iron-selector` is not styled. Use the `iron-selected` CSS class to style the
selected element. | |
15007 | |
15008 Example: | |
15009 | |
15010 <style> | |
15011 .iron-selected { | |
15012 background: #eee; | |
15013 } | |
15014 </style> | |
15015 | |
15016 ... | |
15017 | |
15018 <iron-selector selected="0"> | |
15019 <div>Item 1</div> | |
15020 <div>Item 2</div> | |
15021 <div>Item 3</div> | |
15022 </iron-selector> | |
15023 | |
15024 @demo demo/index.html | |
15025 */ | |
15026 | |
15027 Polymer({ | |
15028 | |
15029 is: 'iron-selector', | |
15030 | |
15031 behaviors: [ | |
15032 Polymer.IronMultiSelectableBehavior | |
15033 ] | |
15034 | |
15035 }); | |
15036 // Copyright 2016 The Chromium Authors. All rights reserved. | 8480 // Copyright 2016 The Chromium Authors. All rights reserved. |
15037 // Use of this source code is governed by a BSD-style license that can be | 8481 // Use of this source code is governed by a BSD-style license that can be |
15038 // found in the LICENSE file. | 8482 // found in the LICENSE file. |
15039 | |
15040 Polymer({ | 8483 Polymer({ |
15041 is: 'history-side-bar', | 8484 is: 'history-side-bar', |
15042 | |
15043 properties: { | 8485 properties: { |
15044 selectedPage: { | 8486 selectedPage: { |
15045 type: String, | 8487 type: String, |
15046 notify: true | 8488 notify: true |
15047 }, | 8489 }, |
15048 | |
15049 route: Object, | 8490 route: Object, |
15050 | |
15051 showFooter: Boolean, | 8491 showFooter: Boolean, |
15052 | |
15053 // If true, the sidebar is contained within an app-drawer. | |
15054 drawer: { | 8492 drawer: { |
15055 type: Boolean, | 8493 type: Boolean, |
15056 reflectToAttribute: true | 8494 reflectToAttribute: true |
15057 }, | 8495 } |
15058 }, | 8496 }, |
15059 | |
15060 /** @private */ | |
15061 onSelectorActivate_: function() { | 8497 onSelectorActivate_: function() { |
15062 this.fire('history-close-drawer'); | 8498 this.fire('history-close-drawer'); |
15063 }, | 8499 }, |
15064 | |
15065 /** | |
15066 * Relocates the user to the clear browsing data section of the settings page. | |
15067 * @param {Event} e | |
15068 * @private | |
15069 */ | |
15070 onClearBrowsingDataTap_: function(e) { | 8500 onClearBrowsingDataTap_: function(e) { |
15071 md_history.BrowserService.getInstance().openClearBrowsingData(); | 8501 md_history.BrowserService.getInstance().openClearBrowsingData(); |
15072 e.preventDefault(); | 8502 e.preventDefault(); |
15073 }, | 8503 }, |
15074 | |
15075 /** | |
15076 * @param {Object} route | |
15077 * @private | |
15078 */ | |
15079 getQueryString_: function(route) { | 8504 getQueryString_: function(route) { |
15080 return window.location.search; | 8505 return window.location.search; |
15081 } | 8506 } |
15082 }); | 8507 }); |
| 8508 |
15083 // Copyright 2016 The Chromium Authors. All rights reserved. | 8509 // Copyright 2016 The Chromium Authors. All rights reserved. |
15084 // Use of this source code is governed by a BSD-style license that can be | 8510 // Use of this source code is governed by a BSD-style license that can be |
15085 // found in the LICENSE file. | 8511 // found in the LICENSE file. |
15086 | |
15087 Polymer({ | 8512 Polymer({ |
15088 is: 'history-app', | 8513 is: 'history-app', |
15089 | |
15090 properties: { | 8514 properties: { |
15091 showSidebarFooter: Boolean, | 8515 showSidebarFooter: Boolean, |
15092 | 8516 selectedPage_: { |
15093 // The id of the currently selected page. | 8517 type: String, |
15094 selectedPage_: {type: String, value: 'history', observer: 'unselectAll'}, | 8518 value: 'history', |
15095 | 8519 observer: 'unselectAll' |
15096 // Whether domain-grouped history is enabled. | 8520 }, |
15097 grouped_: {type: Boolean, reflectToAttribute: true}, | 8521 grouped_: { |
15098 | 8522 type: Boolean, |
15099 /** @type {!QueryState} */ | 8523 reflectToAttribute: true |
| 8524 }, |
15100 queryState_: { | 8525 queryState_: { |
15101 type: Object, | 8526 type: Object, |
15102 value: function() { | 8527 value: function() { |
15103 return { | 8528 return { |
15104 // Whether the most recent query was incremental. | |
15105 incremental: false, | 8529 incremental: false, |
15106 // A query is initiated by page load. | |
15107 querying: true, | 8530 querying: true, |
15108 queryingDisabled: false, | 8531 queryingDisabled: false, |
15109 _range: HistoryRange.ALL_TIME, | 8532 _range: HistoryRange.ALL_TIME, |
15110 searchTerm: '', | 8533 searchTerm: '', |
15111 // TODO(calamity): Make history toolbar buttons change the offset | |
15112 groupedOffset: 0, | 8534 groupedOffset: 0, |
15113 | 8535 set range(val) { |
15114 set range(val) { this._range = Number(val); }, | 8536 this._range = Number(val); |
15115 get range() { return this._range; }, | 8537 }, |
| 8538 get range() { |
| 8539 return this._range; |
| 8540 } |
15116 }; | 8541 }; |
15117 } | 8542 } |
15118 }, | 8543 }, |
15119 | |
15120 /** @type {!QueryResult} */ | |
15121 queryResult_: { | 8544 queryResult_: { |
15122 type: Object, | 8545 type: Object, |
15123 value: function() { | 8546 value: function() { |
15124 return { | 8547 return { |
15125 info: null, | 8548 info: null, |
15126 results: null, | 8549 results: null, |
15127 sessionList: null, | 8550 sessionList: null |
15128 }; | 8551 }; |
15129 } | 8552 } |
15130 }, | 8553 }, |
15131 | |
15132 // Route data for the current page. | |
15133 routeData_: Object, | 8554 routeData_: Object, |
15134 | |
15135 // The query params for the page. | |
15136 queryParams_: Object, | 8555 queryParams_: Object, |
15137 | 8556 hasDrawer_: Boolean |
15138 // True if the window is narrow enough for the page to have a drawer. | 8557 }, |
15139 hasDrawer_: Boolean, | 8558 observers: [ 'routeDataChanged_(routeData_.page)', 'selectedPageChanged_(selec
tedPage_)', 'searchTermChanged_(queryState_.searchTerm)', 'searchQueryParamChang
ed_(queryParams_.q)' ], |
15140 }, | |
15141 | |
15142 observers: [ | |
15143 // routeData_.page <=> selectedPage | |
15144 'routeDataChanged_(routeData_.page)', | |
15145 'selectedPageChanged_(selectedPage_)', | |
15146 | |
15147 // queryParams_.q <=> queryState.searchTerm | |
15148 'searchTermChanged_(queryState_.searchTerm)', | |
15149 'searchQueryParamChanged_(queryParams_.q)', | |
15150 | |
15151 ], | |
15152 | |
15153 // TODO(calamity): Replace these event listeners with data bound properties. | |
15154 listeners: { | 8559 listeners: { |
15155 'cr-menu-tap': 'onMenuTap_', | 8560 'cr-menu-tap': 'onMenuTap_', |
15156 'history-checkbox-select': 'checkboxSelected', | 8561 'history-checkbox-select': 'checkboxSelected', |
15157 'unselect-all': 'unselectAll', | 8562 'unselect-all': 'unselectAll', |
15158 'delete-selected': 'deleteSelected', | 8563 'delete-selected': 'deleteSelected', |
15159 'search-domain': 'searchDomain_', | 8564 'search-domain': 'searchDomain_', |
15160 'history-close-drawer': 'closeDrawer_', | 8565 'history-close-drawer': 'closeDrawer_' |
15161 }, | 8566 }, |
15162 | |
15163 /** @override */ | |
15164 ready: function() { | 8567 ready: function() { |
15165 this.grouped_ = loadTimeData.getBoolean('groupByDomain'); | 8568 this.grouped_ = loadTimeData.getBoolean('groupByDomain'); |
15166 | |
15167 cr.ui.decorate('command', cr.ui.Command); | 8569 cr.ui.decorate('command', cr.ui.Command); |
15168 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | 8570 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); |
15169 document.addEventListener('command', this.onCommand_.bind(this)); | 8571 document.addEventListener('command', this.onCommand_.bind(this)); |
15170 | |
15171 // Redirect legacy search URLs to URLs compatible with material history. | |
15172 if (window.location.hash) { | 8572 if (window.location.hash) { |
15173 window.location.href = window.location.href.split('#')[0] + '?' + | 8573 window.location.href = window.location.href.split('#')[0] + '?' + window.l
ocation.hash.substr(1); |
15174 window.location.hash.substr(1); | 8574 } |
15175 } | 8575 }, |
15176 }, | |
15177 | |
15178 /** @private */ | |
15179 onMenuTap_: function() { | 8576 onMenuTap_: function() { |
15180 var drawer = this.$$('#drawer'); | 8577 var drawer = this.$$('#drawer'); |
15181 if (drawer) | 8578 if (drawer) drawer.toggle(); |
15182 drawer.toggle(); | 8579 }, |
15183 }, | |
15184 | |
15185 /** | |
15186 * Listens for history-item being selected or deselected (through checkbox) | |
15187 * and changes the view of the top toolbar. | |
15188 * @param {{detail: {countAddition: number}}} e | |
15189 */ | |
15190 checkboxSelected: function(e) { | 8580 checkboxSelected: function(e) { |
15191 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); | 8581 var toolbar = this.$.toolbar; |
15192 toolbar.count = /** @type {HistoryListContainerElement} */ (this.$.history) | 8582 toolbar.count = this.$.history.getSelectedItemCount(); |
15193 .getSelectedItemCount(); | 8583 }, |
15194 }, | |
15195 | |
15196 /** | |
15197 * Listens for call to cancel selection and loops through all items to set | |
15198 * checkbox to be unselected. | |
15199 * @private | |
15200 */ | |
15201 unselectAll: function() { | 8584 unselectAll: function() { |
15202 var listContainer = | 8585 var listContainer = this.$.history; |
15203 /** @type {HistoryListContainerElement} */ (this.$.history); | 8586 var toolbar = this.$.toolbar; |
15204 var toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar); | |
15205 listContainer.unselectAllItems(toolbar.count); | 8587 listContainer.unselectAllItems(toolbar.count); |
15206 toolbar.count = 0; | 8588 toolbar.count = 0; |
15207 }, | 8589 }, |
15208 | |
15209 deleteSelected: function() { | 8590 deleteSelected: function() { |
15210 this.$.history.deleteSelectedWithPrompt(); | 8591 this.$.history.deleteSelectedWithPrompt(); |
15211 }, | 8592 }, |
15212 | |
15213 /** | |
15214 * @param {HistoryQuery} info An object containing information about the | |
15215 * query. | |
15216 * @param {!Array<HistoryEntry>} results A list of results. | |
15217 */ | |
15218 historyResult: function(info, results) { | 8593 historyResult: function(info, results) { |
15219 this.set('queryState_.querying', false); | 8594 this.set('queryState_.querying', false); |
15220 this.set('queryResult_.info', info); | 8595 this.set('queryResult_.info', info); |
15221 this.set('queryResult_.results', results); | 8596 this.set('queryResult_.results', results); |
15222 var listContainer = | 8597 var listContainer = this.$['history']; |
15223 /** @type {HistoryListContainerElement} */ (this.$['history']); | |
15224 listContainer.historyResult(info, results); | 8598 listContainer.historyResult(info, results); |
15225 }, | 8599 }, |
15226 | 8600 searchDomain_: function(e) { |
15227 /** | 8601 this.$.toolbar.setSearchTerm(e.detail.domain); |
15228 * Fired when the user presses 'More from this site'. | 8602 }, |
15229 * @param {{detail: {domain: string}}} e | |
15230 */ | |
15231 searchDomain_: function(e) { this.$.toolbar.setSearchTerm(e.detail.domain); }, | |
15232 | |
15233 /** | |
15234 * @param {Event} e | |
15235 * @private | |
15236 */ | |
15237 onCanExecute_: function(e) { | 8603 onCanExecute_: function(e) { |
15238 e = /** @type {cr.ui.CanExecuteEvent} */(e); | 8604 e = e; |
15239 switch (e.command.id) { | 8605 switch (e.command.id) { |
15240 case 'find-command': | 8606 case 'find-command': |
15241 e.canExecute = true; | 8607 e.canExecute = true; |
15242 break; | 8608 break; |
15243 case 'slash-command': | 8609 |
15244 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused(); | 8610 case 'slash-command': |
15245 break; | 8611 e.canExecute = !this.$.toolbar.searchBar.isSearchFocused(); |
15246 case 'delete-command': | 8612 break; |
15247 e.canExecute = this.$.toolbar.count > 0; | 8613 |
15248 break; | 8614 case 'delete-command': |
15249 } | 8615 e.canExecute = this.$.toolbar.count > 0; |
15250 }, | 8616 break; |
15251 | 8617 } |
15252 /** | 8618 }, |
15253 * @param {string} searchTerm | |
15254 * @private | |
15255 */ | |
15256 searchTermChanged_: function(searchTerm) { | 8619 searchTermChanged_: function(searchTerm) { |
15257 this.set('queryParams_.q', searchTerm || null); | 8620 this.set('queryParams_.q', searchTerm || null); |
15258 this.$['history'].queryHistory(false); | 8621 this.$['history'].queryHistory(false); |
15259 }, | 8622 }, |
15260 | |
15261 /** | |
15262 * @param {string} searchQuery | |
15263 * @private | |
15264 */ | |
15265 searchQueryParamChanged_: function(searchQuery) { | 8623 searchQueryParamChanged_: function(searchQuery) { |
15266 this.$.toolbar.setSearchTerm(searchQuery || ''); | 8624 this.$.toolbar.setSearchTerm(searchQuery || ''); |
15267 }, | 8625 }, |
15268 | |
15269 /** | |
15270 * @param {Event} e | |
15271 * @private | |
15272 */ | |
15273 onCommand_: function(e) { | 8626 onCommand_: function(e) { |
15274 if (e.command.id == 'find-command' || e.command.id == 'slash-command') | 8627 if (e.command.id == 'find-command' || e.command.id == 'slash-command') this.
$.toolbar.showSearchField(); |
15275 this.$.toolbar.showSearchField(); | 8628 if (e.command.id == 'delete-command') this.deleteSelected(); |
15276 if (e.command.id == 'delete-command') | 8629 }, |
15277 this.deleteSelected(); | |
15278 }, | |
15279 | |
15280 /** | |
15281 * @param {!Array<!ForeignSession>} sessionList Array of objects describing | |
15282 * the sessions from other devices. | |
15283 * @param {boolean} isTabSyncEnabled Is tab sync enabled for this profile? | |
15284 */ | |
15285 setForeignSessions: function(sessionList, isTabSyncEnabled) { | 8630 setForeignSessions: function(sessionList, isTabSyncEnabled) { |
15286 if (!isTabSyncEnabled) { | 8631 if (!isTabSyncEnabled) { |
15287 var syncedDeviceManagerElem = | 8632 var syncedDeviceManagerElem = this.$$('history-synced-device-manager'); |
15288 /** @type {HistorySyncedDeviceManagerElement} */this | 8633 if (syncedDeviceManagerElem) syncedDeviceManagerElem.tabSyncDisabled(); |
15289 .$$('history-synced-device-manager'); | |
15290 if (syncedDeviceManagerElem) | |
15291 syncedDeviceManagerElem.tabSyncDisabled(); | |
15292 return; | 8634 return; |
15293 } | 8635 } |
15294 | |
15295 this.set('queryResult_.sessionList', sessionList); | 8636 this.set('queryResult_.sessionList', sessionList); |
15296 }, | 8637 }, |
15297 | |
15298 /** | |
15299 * Called when browsing data is cleared. | |
15300 */ | |
15301 historyDeleted: function() { | 8638 historyDeleted: function() { |
15302 this.$.history.historyDeleted(); | 8639 this.$.history.historyDeleted(); |
15303 }, | 8640 }, |
15304 | |
15305 /** | |
15306 * Update sign in state of synced device manager after user logs in or out. | |
15307 * @param {boolean} isUserSignedIn | |
15308 */ | |
15309 updateSignInState: function(isUserSignedIn) { | 8641 updateSignInState: function(isUserSignedIn) { |
15310 var syncedDeviceManagerElem = | 8642 var syncedDeviceManagerElem = this.$$('history-synced-device-manager'); |
15311 /** @type {HistorySyncedDeviceManagerElement} */this | 8643 if (syncedDeviceManagerElem) syncedDeviceManagerElem.updateSignInState(isUse
rSignedIn); |
15312 .$$('history-synced-device-manager'); | 8644 }, |
15313 if (syncedDeviceManagerElem) | |
15314 syncedDeviceManagerElem.updateSignInState(isUserSignedIn); | |
15315 }, | |
15316 | |
15317 /** | |
15318 * @param {string} selectedPage | |
15319 * @return {boolean} | |
15320 * @private | |
15321 */ | |
15322 syncedTabsSelected_: function(selectedPage) { | 8645 syncedTabsSelected_: function(selectedPage) { |
15323 return selectedPage == 'syncedTabs'; | 8646 return selectedPage == 'syncedTabs'; |
15324 }, | 8647 }, |
15325 | |
15326 /** | |
15327 * @param {boolean} querying | |
15328 * @param {boolean} incremental | |
15329 * @param {string} searchTerm | |
15330 * @return {boolean} Whether a loading spinner should be shown (implies the | |
15331 * backend is querying a new search term). | |
15332 * @private | |
15333 */ | |
15334 shouldShowSpinner_: function(querying, incremental, searchTerm) { | 8648 shouldShowSpinner_: function(querying, incremental, searchTerm) { |
15335 return querying && !incremental && searchTerm != ''; | 8649 return querying && !incremental && searchTerm != ''; |
15336 }, | 8650 }, |
15337 | |
15338 /** | |
15339 * @param {string} page | |
15340 * @private | |
15341 */ | |
15342 routeDataChanged_: function(page) { | 8651 routeDataChanged_: function(page) { |
15343 this.selectedPage_ = page; | 8652 this.selectedPage_ = page; |
15344 }, | 8653 }, |
15345 | |
15346 /** | |
15347 * @param {string} selectedPage | |
15348 * @private | |
15349 */ | |
15350 selectedPageChanged_: function(selectedPage) { | 8654 selectedPageChanged_: function(selectedPage) { |
15351 this.set('routeData_.page', selectedPage); | 8655 this.set('routeData_.page', selectedPage); |
15352 }, | 8656 }, |
15353 | |
15354 /** | |
15355 * This computed binding is needed to make the iron-pages selector update when | |
15356 * the synced-device-manager is instantiated for the first time. Otherwise the | |
15357 * fallback selection will continue to be used after the corresponding item is | |
15358 * added as a child of iron-pages. | |
15359 * @param {string} selectedPage | |
15360 * @param {Array} items | |
15361 * @return {string} | |
15362 * @private | |
15363 */ | |
15364 getSelectedPage_: function(selectedPage, items) { | 8657 getSelectedPage_: function(selectedPage, items) { |
15365 return selectedPage; | 8658 return selectedPage; |
15366 }, | 8659 }, |
15367 | |
15368 /** @private */ | |
15369 closeDrawer_: function() { | 8660 closeDrawer_: function() { |
15370 var drawer = this.$$('#drawer'); | 8661 var drawer = this.$$('#drawer'); |
15371 if (drawer) | 8662 if (drawer) drawer.close(); |
15372 drawer.close(); | 8663 } |
15373 }, | |
15374 }); | 8664 }); |
OLD | NEW |