mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-18 03:18:19 -07:00
373 lines
11 KiB
HTML
373 lines
11 KiB
HTML
<!--
|
|
@license
|
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
Code distributed by Google as part of the polymer project is also
|
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
-->
|
|
|
|
<link rel="import" href="../polymer/polymer.html">
|
|
<link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
|
|
|
|
<script>
|
|
|
|
/**
|
|
* @struct
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
Polymer.IronOverlayManagerClass = function() {
|
|
/**
|
|
* Used to keep track of the opened overlays.
|
|
* @private {Array<Element>}
|
|
*/
|
|
this._overlays = [];
|
|
|
|
/**
|
|
* iframes have a default z-index of 100,
|
|
* so this default should be at least that.
|
|
* @private {number}
|
|
*/
|
|
this._minimumZ = 101;
|
|
|
|
/**
|
|
* Memoized backdrop element.
|
|
* @private {Element|null}
|
|
*/
|
|
this._backdropElement = null;
|
|
|
|
// Listen to mousedown or touchstart to be sure to be the first to capture
|
|
// clicks outside the overlay.
|
|
var clickEvent = ('ontouchstart' in window) ? 'touchstart' : 'mousedown';
|
|
document.addEventListener(clickEvent, this._onCaptureClick.bind(this), true);
|
|
document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
|
|
document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
|
|
};
|
|
|
|
Polymer.IronOverlayManagerClass.prototype = {
|
|
|
|
constructor: Polymer.IronOverlayManagerClass,
|
|
|
|
/**
|
|
* The shared backdrop element.
|
|
* @type {Element} backdropElement
|
|
*/
|
|
get backdropElement() {
|
|
if (!this._backdropElement) {
|
|
this._backdropElement = document.createElement('iron-overlay-backdrop');
|
|
}
|
|
return this._backdropElement;
|
|
},
|
|
|
|
/**
|
|
* The deepest active element.
|
|
* @type {Element} activeElement the active element
|
|
*/
|
|
get deepActiveElement() {
|
|
// document.activeElement can be null
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
|
|
// In case of null, default it to document.body.
|
|
var active = document.activeElement || document.body;
|
|
while (active.root && Polymer.dom(active.root).activeElement) {
|
|
active = Polymer.dom(active.root).activeElement;
|
|
}
|
|
return active;
|
|
},
|
|
|
|
/**
|
|
* Brings the overlay at the specified index to the front.
|
|
* @param {number} i
|
|
* @private
|
|
*/
|
|
_bringOverlayAtIndexToFront: function(i) {
|
|
var overlay = this._overlays[i];
|
|
var lastI = this._overlays.length - 1;
|
|
// Ensure always-on-top overlay stays on top.
|
|
if (!overlay.alwaysOnTop && this._overlays[lastI].alwaysOnTop) {
|
|
lastI--;
|
|
}
|
|
// If already the top element, return.
|
|
if (!overlay || i >= lastI) {
|
|
return;
|
|
}
|
|
// Update z-index to be on top.
|
|
var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
|
|
if (this._getZ(overlay) <= minimumZ) {
|
|
this._applyOverlayZ(overlay, minimumZ);
|
|
}
|
|
|
|
// Shift other overlays behind the new on top.
|
|
while (i < lastI) {
|
|
this._overlays[i] = this._overlays[i + 1];
|
|
i++;
|
|
}
|
|
this._overlays[lastI] = overlay;
|
|
},
|
|
|
|
/**
|
|
* Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
|
|
* Also updates the backdrop z-index.
|
|
* @param {Element} overlay
|
|
*/
|
|
addOrRemoveOverlay: function(overlay) {
|
|
if (overlay.opened) {
|
|
this.addOverlay(overlay);
|
|
} else {
|
|
this.removeOverlay(overlay);
|
|
}
|
|
this.trackBackdrop();
|
|
},
|
|
|
|
/**
|
|
* Tracks overlays for z-index and focus management.
|
|
* Ensures the last added overlay with always-on-top remains on top.
|
|
* @param {Element} overlay
|
|
*/
|
|
addOverlay: function(overlay) {
|
|
var i = this._overlays.indexOf(overlay);
|
|
if (i >= 0) {
|
|
this._bringOverlayAtIndexToFront(i);
|
|
return;
|
|
}
|
|
var insertionIndex = this._overlays.length;
|
|
var currentOverlay = this._overlays[insertionIndex - 1];
|
|
var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
|
|
var newZ = this._getZ(overlay);
|
|
|
|
// Ensure always-on-top overlay stays on top.
|
|
if (currentOverlay && currentOverlay.alwaysOnTop && !overlay.alwaysOnTop) {
|
|
// This bumps the z-index of +2.
|
|
this._applyOverlayZ(currentOverlay, minimumZ);
|
|
insertionIndex--;
|
|
// Update minimumZ to match previous overlay's z-index.
|
|
var previousOverlay = this._overlays[insertionIndex - 1];
|
|
minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
|
|
}
|
|
|
|
// Update z-index and insert overlay.
|
|
if (newZ <= minimumZ) {
|
|
this._applyOverlayZ(overlay, minimumZ);
|
|
}
|
|
this._overlays.splice(insertionIndex, 0, overlay);
|
|
|
|
// Get focused node.
|
|
var element = this.deepActiveElement;
|
|
overlay.restoreFocusNode = this._overlayParent(element) ? null : element;
|
|
},
|
|
|
|
/**
|
|
* @param {Element} overlay
|
|
*/
|
|
removeOverlay: function(overlay) {
|
|
var i = this._overlays.indexOf(overlay);
|
|
if (i === -1) {
|
|
return;
|
|
}
|
|
this._overlays.splice(i, 1);
|
|
|
|
var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null;
|
|
overlay.restoreFocusNode = null;
|
|
// Focus back only if still contained in document.body
|
|
if (node && Polymer.dom(document.body).deepContains(node)) {
|
|
node.focus();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the current overlay.
|
|
* @return {Element|undefined}
|
|
*/
|
|
currentOverlay: function() {
|
|
var i = this._overlays.length - 1;
|
|
return this._overlays[i];
|
|
},
|
|
|
|
/**
|
|
* Returns the current overlay z-index.
|
|
* @return {number}
|
|
*/
|
|
currentOverlayZ: function() {
|
|
return this._getZ(this.currentOverlay());
|
|
},
|
|
|
|
/**
|
|
* Ensures that the minimum z-index of new overlays is at least `minimumZ`.
|
|
* This does not effect the z-index of any existing overlays.
|
|
* @param {number} minimumZ
|
|
*/
|
|
ensureMinimumZ: function(minimumZ) {
|
|
this._minimumZ = Math.max(this._minimumZ, minimumZ);
|
|
},
|
|
|
|
focusOverlay: function() {
|
|
var current = /** @type {?} */ (this.currentOverlay());
|
|
// We have to be careful to focus the next overlay _after_ any current
|
|
// transitions are complete (due to the state being toggled prior to the
|
|
// transition). Otherwise, we risk infinite recursion when a transitioning
|
|
// (closed) overlay becomes the current overlay.
|
|
//
|
|
// NOTE: We make the assumption that any overlay that completes a transition
|
|
// will call into focusOverlay to kick the process back off. Currently:
|
|
// transitionend -> _applyFocus -> focusOverlay.
|
|
if (current && !current.transitioning) {
|
|
current._applyFocus();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Updates the backdrop z-index.
|
|
*/
|
|
trackBackdrop: function() {
|
|
this.backdropElement.style.zIndex = this.backdropZ();
|
|
},
|
|
|
|
/**
|
|
* @return {Array<Element>}
|
|
*/
|
|
getBackdrops: function() {
|
|
var backdrops = [];
|
|
for (var i = 0; i < this._overlays.length; i++) {
|
|
if (this._overlays[i].withBackdrop) {
|
|
backdrops.push(this._overlays[i]);
|
|
}
|
|
}
|
|
return backdrops;
|
|
},
|
|
|
|
/**
|
|
* Returns the z-index for the backdrop.
|
|
* @return {number}
|
|
*/
|
|
backdropZ: function() {
|
|
return this._getZ(this._overlayWithBackdrop()) - 1;
|
|
},
|
|
|
|
/**
|
|
* Returns the first opened overlay that has a backdrop.
|
|
* @return {Element|undefined}
|
|
* @private
|
|
*/
|
|
_overlayWithBackdrop: function() {
|
|
for (var i = 0; i < this._overlays.length; i++) {
|
|
if (this._overlays[i].withBackdrop) {
|
|
return this._overlays[i];
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Calculates the minimum z-index for the overlay.
|
|
* @param {Element=} overlay
|
|
* @private
|
|
*/
|
|
_getZ: function(overlay) {
|
|
var z = this._minimumZ;
|
|
if (overlay) {
|
|
var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
|
|
// Check if is a number
|
|
// Number.isNaN not supported in IE 10+
|
|
if (z1 === z1) {
|
|
z = z1;
|
|
}
|
|
}
|
|
return z;
|
|
},
|
|
|
|
/**
|
|
* @param {Element} element
|
|
* @param {number|string} z
|
|
* @private
|
|
*/
|
|
_setZ: function(element, z) {
|
|
element.style.zIndex = z;
|
|
},
|
|
|
|
/**
|
|
* @param {Element} overlay
|
|
* @param {number} aboveZ
|
|
* @private
|
|
*/
|
|
_applyOverlayZ: function(overlay, aboveZ) {
|
|
this._setZ(overlay, aboveZ + 2);
|
|
},
|
|
|
|
/**
|
|
* Returns the overlay containing the provided node. If the node is an overlay,
|
|
* it returns the node.
|
|
* @param {Element=} node
|
|
* @return {Element|undefined}
|
|
* @private
|
|
*/
|
|
_overlayParent: function(node) {
|
|
while (node && node !== document.body) {
|
|
// Check if it is an overlay.
|
|
if (node._manager === this) {
|
|
return node;
|
|
}
|
|
// Use logical parentNode, or native ShadowRoot host.
|
|
node = Polymer.dom(node).parentNode || node.host;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the deepest overlay in the path.
|
|
* @param {Array<Element>=} path
|
|
* @return {Element|undefined}
|
|
* @private
|
|
*/
|
|
_overlayInPath: function(path) {
|
|
path = path || [];
|
|
for (var i = 0; i < path.length; i++) {
|
|
if (path[i]._manager === this) {
|
|
return path[i];
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ensures the click event is delegated to the right overlay.
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
_onCaptureClick: function(event) {
|
|
var overlay = /** @type {?} */ (this.currentOverlay());
|
|
// Check if clicked outside of top overlay.
|
|
if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
|
|
overlay._onCaptureClick(event);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ensures the focus event is delegated to the right overlay.
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
_onCaptureFocus: function(event) {
|
|
var overlay = /** @type {?} */ (this.currentOverlay());
|
|
if (overlay) {
|
|
overlay._onCaptureFocus(event);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ensures TAB and ESC keyboard events are delegated to the right overlay.
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
_onCaptureKeyDown: function(event) {
|
|
var overlay = /** @type {?} */ (this.currentOverlay());
|
|
if (overlay) {
|
|
if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
|
|
overlay._onCaptureEsc(event);
|
|
} else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
|
|
overlay._onCaptureTab(event);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
|
|
</script>
|