299 lines
9.4 KiB
JavaScript
299 lines
9.4 KiB
JavaScript
import { dedupeMixin } from '@lion/core';
|
|
import { OverlayController } from './OverlayController.js';
|
|
|
|
/**
|
|
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
|
|
* @typedef {import('../types/OverlayMixinTypes').DefineOverlayConfig} DefineOverlayConfig
|
|
* @typedef {import('../types/OverlayMixinTypes').OverlayHost} OverlayHost
|
|
* @typedef {import('../types/OverlayMixinTypes').OverlayMixin} OverlayMixin
|
|
*/
|
|
|
|
/**
|
|
* @type {OverlayMixin}
|
|
*/
|
|
export const OverlayMixinImplementation = superclass =>
|
|
class OverlayMixin extends superclass {
|
|
static get properties() {
|
|
return {
|
|
opened: {
|
|
type: Boolean,
|
|
reflect: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.opened = false;
|
|
this.__needsSetup = true;
|
|
/** @type {OverlayConfig} */
|
|
this.config = {};
|
|
}
|
|
|
|
get config() {
|
|
return /** @type {OverlayConfig} */ (this.__config);
|
|
}
|
|
|
|
/** @param {OverlayConfig} value */
|
|
set config(value) {
|
|
if (this._overlayCtrl) {
|
|
this._overlayCtrl.updateConfig(value);
|
|
}
|
|
this.__config = value;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {string} name
|
|
* @param {any} oldValue
|
|
*/
|
|
requestUpdateInternal(name, oldValue) {
|
|
super.requestUpdateInternal(name, oldValue);
|
|
if (name === 'opened') {
|
|
this.dispatchEvent(new Event('opened-changed'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @overridable method `_defineOverlay`
|
|
* @desc returns an instance of a (dynamic) overlay controller
|
|
* In case overriding _defineOverlayConfig is not enough
|
|
* @param {DefineOverlayConfig} config
|
|
* @returns {OverlayController}
|
|
*/
|
|
// eslint-disable-next-line
|
|
_defineOverlay({ contentNode, invokerNode, backdropNode, contentWrapperNode }) {
|
|
return new OverlayController({
|
|
contentNode,
|
|
invokerNode,
|
|
backdropNode,
|
|
contentWrapperNode,
|
|
...this._defineOverlayConfig(), // wc provided in the class as defaults
|
|
...this.config, // user provided (e.g. in template)
|
|
popperConfig: {
|
|
...(this._defineOverlayConfig().popperConfig || {}),
|
|
...(this.config.popperConfig || {}),
|
|
modifiers: {
|
|
...((this._defineOverlayConfig().popperConfig &&
|
|
this._defineOverlayConfig()?.popperConfig?.modifiers) ||
|
|
{}),
|
|
...((this.config.popperConfig && this.config.popperConfig.modifiers) || {}),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @overridable method `_defineOverlay`
|
|
* @desc returns an object with default configuration options for your overlay component.
|
|
* This is generally speaking easier to override than _defineOverlay method entirely.
|
|
* @returns {OverlayConfig}
|
|
*/
|
|
// eslint-disable-next-line
|
|
_defineOverlayConfig() {
|
|
return {
|
|
placementMode: 'local',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {{ has: (arg0: string) => any; }} changedProperties
|
|
*/
|
|
updated(changedProperties) {
|
|
super.updated(changedProperties);
|
|
|
|
if (changedProperties.has('opened') && this._overlayCtrl && !this.__blockSyncToOverlayCtrl) {
|
|
this.__syncToOverlayController();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @overridable
|
|
* @desc use this method to setup your open and close event listeners
|
|
* For example, set a click event listener on _overlayInvokerNode to set opened to true
|
|
*/
|
|
// eslint-disable-next-line class-methods-use-this
|
|
_setupOpenCloseListeners() {
|
|
/**
|
|
* @param {{ stopPropagation: () => void; }} ev
|
|
*/
|
|
this.__closeEventInContentNodeHandler = ev => {
|
|
ev.stopPropagation();
|
|
/** @type {OverlayController} */ (this._overlayCtrl).hide();
|
|
};
|
|
if (this._overlayContentNode) {
|
|
this._overlayContentNode.addEventListener(
|
|
'close-overlay',
|
|
this.__closeEventInContentNodeHandler,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @overridable
|
|
* @desc use this method to tear down your event listeners
|
|
*/
|
|
// eslint-disable-next-line class-methods-use-this
|
|
_teardownOpenCloseListeners() {
|
|
if (this._overlayContentNode) {
|
|
this._overlayContentNode.removeEventListener(
|
|
'close-overlay',
|
|
this.__closeEventInContentNodeHandler,
|
|
);
|
|
}
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
// we do a setup after every connectedCallback as firstUpdated will only be called once
|
|
this.__needsSetup = true;
|
|
this.updateComplete.then(() => {
|
|
if (this.__needsSetup) {
|
|
this._setupOverlayCtrl();
|
|
}
|
|
this.__needsSetup = false;
|
|
});
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (super.disconnectedCallback) {
|
|
super.disconnectedCallback();
|
|
}
|
|
if (this._overlayCtrl) {
|
|
this._teardownOverlayCtrl();
|
|
}
|
|
}
|
|
|
|
get _overlayInvokerNode() {
|
|
return Array.from(this.children).find(child => child.slot === 'invoker');
|
|
}
|
|
|
|
get _overlayBackdropNode() {
|
|
return Array.from(this.children).find(child => child.slot === 'backdrop');
|
|
}
|
|
|
|
get _overlayContentNode() {
|
|
if (!this._cachedOverlayContentNode) {
|
|
this._cachedOverlayContentNode = Array.from(this.children).find(
|
|
child => child.slot === 'content',
|
|
);
|
|
}
|
|
return this._cachedOverlayContentNode;
|
|
}
|
|
|
|
get _overlayContentWrapperNode() {
|
|
return this.shadowRoot.querySelector('#overlay-content-node-wrapper');
|
|
}
|
|
|
|
_setupOverlayCtrl() {
|
|
/** @type {OverlayController} */
|
|
this._overlayCtrl = this._defineOverlay({
|
|
contentNode: this._overlayContentNode,
|
|
contentWrapperNode: this._overlayContentWrapperNode,
|
|
invokerNode: this._overlayInvokerNode,
|
|
backdropNode: this._overlayBackdropNode,
|
|
});
|
|
this.__syncToOverlayController();
|
|
this.__setupSyncFromOverlayController();
|
|
this._setupOpenCloseListeners();
|
|
}
|
|
|
|
_teardownOverlayCtrl() {
|
|
this._teardownOpenCloseListeners();
|
|
this.__teardownSyncFromOverlayController();
|
|
/** @type {OverlayController} */
|
|
(this._overlayCtrl).teardown();
|
|
}
|
|
|
|
/**
|
|
* When the opened state is changed by an Application Developer,cthe OverlayController is
|
|
* requested to show/hide. It might happen that this request is not honoured
|
|
* (intercepted in before-hide for instance), so that we need to sync the controller state
|
|
* to this webcomponent again, preventing eternal loops.
|
|
* @param {boolean} newOpened
|
|
*/
|
|
async _setOpenedWithoutPropertyEffects(newOpened) {
|
|
this.__blockSyncToOverlayCtrl = true;
|
|
this.opened = newOpened;
|
|
await this.updateComplete;
|
|
this.__blockSyncToOverlayCtrl = false;
|
|
}
|
|
|
|
__setupSyncFromOverlayController() {
|
|
this.__onOverlayCtrlShow = () => {
|
|
this.opened = true;
|
|
};
|
|
|
|
this.__onOverlayCtrlHide = () => {
|
|
this.opened = false;
|
|
};
|
|
|
|
/**
|
|
* @param {{ preventDefault: () => void; }} beforeShowEvent
|
|
*/
|
|
this.__onBeforeShow = beforeShowEvent => {
|
|
const event = new CustomEvent('before-opened', { cancelable: true });
|
|
this.dispatchEvent(event);
|
|
if (event.defaultPrevented) {
|
|
// Check whether our current `.opened` state is not out of sync with overlayCtrl
|
|
this._setOpenedWithoutPropertyEffects(
|
|
/** @type {OverlayController} */ (this._overlayCtrl).isShown,
|
|
);
|
|
beforeShowEvent.preventDefault();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {{ preventDefault: () => void; }} beforeHideEvent
|
|
*/
|
|
this.__onBeforeHide = beforeHideEvent => {
|
|
const event = new CustomEvent('before-closed', { cancelable: true });
|
|
this.dispatchEvent(event);
|
|
if (event.defaultPrevented) {
|
|
// Check whether our current `.opened` state is not out of sync with overlayCtrl
|
|
this._setOpenedWithoutPropertyEffects(
|
|
/** @type {OverlayController} */
|
|
(this._overlayCtrl).isShown,
|
|
);
|
|
beforeHideEvent.preventDefault();
|
|
}
|
|
};
|
|
|
|
/** @type {OverlayController} */
|
|
(this._overlayCtrl).addEventListener('show', this.__onOverlayCtrlShow);
|
|
/** @type {OverlayController} */
|
|
(this._overlayCtrl).addEventListener('hide', this.__onOverlayCtrlHide);
|
|
/** @type {OverlayController} */
|
|
(this._overlayCtrl).addEventListener('before-show', this.__onBeforeShow);
|
|
/** @type {OverlayController} */
|
|
(this._overlayCtrl).addEventListener('before-hide', this.__onBeforeHide);
|
|
}
|
|
|
|
__teardownSyncFromOverlayController() {
|
|
/** @type {OverlayController} */ (this._overlayCtrl).removeEventListener(
|
|
'show',
|
|
/** @type {EventListener} */ (this.__onOverlayCtrlShow),
|
|
);
|
|
/** @type {OverlayController} */ (this._overlayCtrl).removeEventListener(
|
|
'hide',
|
|
/** @type {EventListener} */ (this.__onOverlayCtrlHide),
|
|
);
|
|
/** @type {OverlayController} */ (this._overlayCtrl).removeEventListener(
|
|
'before-show',
|
|
/** @type {EventListener} */ (this.__onBeforeShow),
|
|
);
|
|
/** @type {OverlayController} */ (this._overlayCtrl).removeEventListener(
|
|
'before-hide',
|
|
/** @type {EventListener} */ (this.__onBeforeHide),
|
|
);
|
|
}
|
|
|
|
__syncToOverlayController() {
|
|
if (this.opened) {
|
|
/** @type {OverlayController} */ (this._overlayCtrl).show();
|
|
} else {
|
|
/** @type {OverlayController} */ (this._overlayCtrl).hide();
|
|
}
|
|
}
|
|
};
|
|
export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);
|