OLD | NEW |
| (Empty) |
1 | |
2 | |
3 /* | |
4 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays | |
5 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety | |
6 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. | |
7 | |
8 ### Closing and canceling | |
9 | |
10 A dialog may be hidden by closing or canceling. The difference between close and
cancel is user | |
11 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, | |
12 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is | |
13 configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click
` properties. | |
14 `close()` should be called explicitly by the implementer when the user interacts
with a control | |
15 in the overlay element. | |
16 | |
17 ### Positioning | |
18 | |
19 By default the element is sized and positioned to fit and centered inside the wi
ndow. You can | |
20 position and size it manually using CSS. See `Polymer.IronFitBehavior`. | |
21 | |
22 ### Backdrop | |
23 | |
24 Set the `with-backdrop` attribute to display a backdrop behind the overlay. The
backdrop is | |
25 appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page
for styling | |
26 options. | |
27 | |
28 ### Limitations | |
29 | |
30 The element is styled to appear on top of other content by setting its `z-index`
property. You | |
31 must ensure no element has a stacking context with a higher `z-index` than its p
arent stacking | |
32 context. You should place this element as a child of `<body>` whenever possible. | |
33 | |
34 @demo demo/index.html | |
35 @polymerBehavior Polymer.IronOverlayBehavior | |
36 */ | |
37 | |
38 Polymer.IronOverlayBehaviorImpl = { | |
39 | |
40 properties: { | |
41 | |
42 /** | |
43 * True if the overlay is currently displayed. | |
44 */ | |
45 opened: { | |
46 observer: '_openedChanged', | |
47 type: Boolean, | |
48 value: false | |
49 }, | |
50 | |
51 /** | |
52 * True if the overlay was canceled when it was last closed. | |
53 */ | |
54 canceled: { | |
55 observer: '_canceledChanged', | |
56 readOnly: true, | |
57 type: Boolean, | |
58 value: false | |
59 }, | |
60 | |
61 /** | |
62 * Set to true to display a backdrop behind the overlay. | |
63 */ | |
64 withBackdrop: { | |
65 type: Boolean, | |
66 value: false | |
67 }, | |
68 | |
69 /** | |
70 * Set to true to disable auto-focusing the overlay or child nodes with | |
71 * the `autofocus` attribute` when the overlay is opened. | |
72 */ | |
73 noAutoFocus: { | |
74 type: Boolean, | |
75 value: false | |
76 }, | |
77 | |
78 /** | |
79 * Set to true to disable canceling the overlay with the ESC key. | |
80 */ | |
81 noCancelOnEscKey: { | |
82 type: Boolean, | |
83 value: false | |
84 }, | |
85 | |
86 /** | |
87 * Set to true to disable canceling the overlay by clicking outside it. | |
88 */ | |
89 noCancelOnOutsideClick: { | |
90 type: Boolean, | |
91 value: false | |
92 }, | |
93 | |
94 /** | |
95 * Returns the reason this dialog was last closed. | |
96 */ | |
97 closingReason: { | |
98 // was a getter before, but needs to be a property so other | |
99 // behaviors can override this. | |
100 type: Object | |
101 }, | |
102 | |
103 _manager: { | |
104 type: Object, | |
105 value: Polymer.IronOverlayManager | |
106 }, | |
107 | |
108 _boundOnCaptureClick: { | |
109 type: Function, | |
110 value: function() { | |
111 return this._onCaptureClick.bind(this); | |
112 } | |
113 }, | |
114 | |
115 _boundOnCaptureKeydown: { | |
116 type: Function, | |
117 value: function() { | |
118 return this._onCaptureKeydown.bind(this); | |
119 } | |
120 } | |
121 | |
122 }, | |
123 | |
124 listeners: { | |
125 'click': '_onClick', | |
126 'iron-resize': '_onIronResize' | |
127 }, | |
128 | |
129 /** | |
130 * The backdrop element. | |
131 * @type Node | |
132 */ | |
133 get backdropElement() { | |
134 return this._backdrop; | |
135 }, | |
136 | |
137 get _focusNode() { | |
138 return Polymer.dom(this).querySelector('[autofocus]') || this; | |
139 }, | |
140 | |
141 registered: function() { | |
142 this._backdrop = document.createElement('iron-overlay-backdrop'); | |
143 }, | |
144 | |
145 ready: function() { | |
146 this._ensureSetup(); | |
147 if (this._callOpenedWhenReady) { | |
148 this._openedChanged(); | |
149 } | |
150 }, | |
151 | |
152 detached: function() { | |
153 this.opened = false; | |
154 this._completeBackdrop(); | |
155 this._manager.removeOverlay(this); | |
156 }, | |
157 | |
158 /** | |
159 * Toggle the opened state of the overlay. | |
160 */ | |
161 toggle: function() { | |
162 this.opened = !this.opened; | |
163 }, | |
164 | |
165 /** | |
166 * Open the overlay. | |
167 */ | |
168 open: function() { | |
169 this.opened = true; | |
170 this.closingReason = {canceled: false}; | |
171 }, | |
172 | |
173 /** | |
174 * Close the overlay. | |
175 */ | |
176 close: function() { | |
177 this.opened = false; | |
178 this._setCanceled(false); | |
179 }, | |
180 | |
181 /** | |
182 * Cancels the overlay. | |
183 */ | |
184 cancel: function() { | |
185 this.opened = false, | |
186 this._setCanceled(true); | |
187 }, | |
188 | |
189 _ensureSetup: function() { | |
190 if (this._overlaySetup) { | |
191 return; | |
192 } | |
193 this._overlaySetup = true; | |
194 this.style.outline = 'none'; | |
195 this.style.display = 'none'; | |
196 }, | |
197 | |
198 _openedChanged: function() { | |
199 if (this.opened) { | |
200 this.removeAttribute('aria-hidden'); | |
201 } else { | |
202 this.setAttribute('aria-hidden', 'true'); | |
203 } | |
204 | |
205 // wait to call after ready only if we're initially open | |
206 if (!this._overlaySetup) { | |
207 this._callOpenedWhenReady = this.opened; | |
208 return; | |
209 } | |
210 if (this._openChangedAsync) { | |
211 this.cancelAsync(this._openChangedAsync); | |
212 } | |
213 | |
214 this._toggleListeners(); | |
215 | |
216 if (this.opened) { | |
217 this._prepareRenderOpened(); | |
218 } | |
219 | |
220 // async here to allow overlay layer to become visible. | |
221 this._openChangedAsync = this.async(function() { | |
222 // overlay becomes visible here | |
223 this.style.display = ''; | |
224 // force layout to ensure transitions will go | |
225 this.offsetWidth; | |
226 if (this.opened) { | |
227 this._renderOpened(); | |
228 } else { | |
229 this._renderClosed(); | |
230 } | |
231 this._openChangedAsync = null; | |
232 }); | |
233 | |
234 }, | |
235 | |
236 _canceledChanged: function() { | |
237 this.closingReason = this.closingReason || {}; | |
238 this.closingReason.canceled = this.canceled; | |
239 }, | |
240 | |
241 _toggleListener: function(enable, node, event, boundListener, capture) { | |
242 if (enable) { | |
243 node.addEventListener(event, boundListener, capture); | |
244 } else { | |
245 node.removeEventListener(event, boundListener, capture); | |
246 } | |
247 }, | |
248 | |
249 _toggleListeners: function() { | |
250 if (this._toggleListenersAsync) { | |
251 this.cancelAsync(this._toggleListenersAsync); | |
252 } | |
253 // async so we don't auto-close immediately via a click. | |
254 this._toggleListenersAsync = this.async(function() { | |
255 this._toggleListener(this.opened, document, 'click', this._boundOnCaptur
eClick, true); | |
256 this._toggleListener(this.opened, document, 'keydown', this._boundOnCapt
ureKeydown, true); | |
257 this._toggleListenersAsync = null; | |
258 }); | |
259 }, | |
260 | |
261 // tasks which must occur before opening; e.g. making the element visible | |
262 _prepareRenderOpened: function() { | |
263 this._manager.addOverlay(this); | |
264 | |
265 if (this.withBackdrop) { | |
266 this.backdropElement.prepare(); | |
267 this._manager.trackBackdrop(this); | |
268 } | |
269 | |
270 this._preparePositioning(); | |
271 this.fit(); | |
272 this._finishPositioning(); | |
273 }, | |
274 | |
275 // tasks which cause the overlay to actually open; typically play an | |
276 // animation | |
277 _renderOpened: function() { | |
278 if (this.withBackdrop) { | |
279 this.backdropElement.open(); | |
280 } | |
281 this._finishRenderOpened(); | |
282 }, | |
283 | |
284 _renderClosed: function() { | |
285 if (this.withBackdrop) { | |
286 this.backdropElement.close(); | |
287 } | |
288 this._finishRenderClosed(); | |
289 }, | |
290 | |
291 _onTransitionend: function(event) { | |
292 // make sure this is our transition event. | |
293 if (event && event.target !== this) { | |
294 return; | |
295 } | |
296 if (this.opened) { | |
297 this._finishRenderOpened(); | |
298 } else { | |
299 this._finishRenderClosed(); | |
300 } | |
301 }, | |
302 | |
303 _finishRenderOpened: function() { | |
304 // focus the child node with [autofocus] | |
305 if (!this.noAutoFocus) { | |
306 this._focusNode.focus(); | |
307 } | |
308 | |
309 this.fire('iron-overlay-opened'); | |
310 | |
311 this._squelchNextResize = true; | |
312 this.async(this.notifyResize); | |
313 }, | |
314 | |
315 _finishRenderClosed: function() { | |
316 // hide the overlay and remove the backdrop | |
317 this.resetFit(); | |
318 this.style.display = 'none'; | |
319 this._completeBackdrop(); | |
320 this._manager.removeOverlay(this); | |
321 | |
322 this._focusNode.blur(); | |
323 // focus the next overlay, if there is one | |
324 this._manager.focusOverlay(); | |
325 | |
326 this.fire('iron-overlay-closed', this.closingReason); | |
327 | |
328 this._squelchNextResize = true; | |
329 this.async(this.notifyResize); | |
330 }, | |
331 | |
332 _completeBackdrop: function() { | |
333 if (this.withBackdrop) { | |
334 this._manager.trackBackdrop(this); | |
335 this.backdropElement.complete(); | |
336 } | |
337 }, | |
338 | |
339 _preparePositioning: function() { | |
340 this.style.transition = this.style.webkitTransition = 'none'; | |
341 this.style.transform = this.style.webkitTransform = 'none'; | |
342 this.style.display = ''; | |
343 }, | |
344 | |
345 _finishPositioning: function(target) { | |
346 this.style.display = 'none'; | |
347 this.style.transform = this.style.webkitTransform = ''; | |
348 // force layout to avoid application of transform | |
349 this.offsetWidth; | |
350 this.style.transition = this.style.webkitTransition = ''; | |
351 }, | |
352 | |
353 _applyFocus: function() { | |
354 if (this.opened) { | |
355 if (!this.noAutoFocus) { | |
356 this._focusNode.focus(); | |
357 } | |
358 } else { | |
359 this._focusNode.blur(); | |
360 this._manager.focusOverlay(); | |
361 } | |
362 }, | |
363 | |
364 _onCaptureClick: function(event) { | |
365 // attempt to close asynchronously and prevent the close of a tap event is
immediately heard | |
366 // on target. This is because in shadow dom due to event retargetting even
t.target is not | |
367 // useful. | |
368 if (!this.noCancelOnOutsideClick && (this._manager.currentOverlay() == thi
s)) { | |
369 this._cancelJob = this.async(function() { | |
370 this.cancel(); | |
371 }, 10); | |
372 } | |
373 }, | |
374 | |
375 _onClick: function(event) { | |
376 if (this._cancelJob) { | |
377 this.cancelAsync(this._cancelJob); | |
378 this._cancelJob = null; | |
379 } | |
380 }, | |
381 | |
382 _onCaptureKeydown: function(event) { | |
383 var ESC = 27; | |
384 if (!this.noCancelOnEscKey && (event.keyCode === ESC)) { | |
385 this.cancel(); | |
386 event.stopPropagation(); | |
387 } | |
388 }, | |
389 | |
390 _onIronResize: function() { | |
391 if (this._squelchNextResize) { | |
392 this._squelchNextResize = false; | |
393 return; | |
394 } | |
395 if (this.opened) { | |
396 this.refit(); | |
397 } | |
398 } | |
399 | |
400 }; | |
401 | |
402 /** @polymerBehavior */ | |
403 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; | |
404 | |
405 /* | |
406 * Fired after the `iron-overlay` opens. | |
407 * @event iron-overlay-opened | |
408 */ | |
409 | |
410 /* | |
411 * Fired after the `iron-overlay` closes. | |
412 * @event iron-overlay-closed {{canceled: boolean}} detail - | |
413 * canceled: True if the overlay was canceled. | |
414 */ | |
415 | |
OLD | NEW |