238 lines
7.6 KiB
JavaScript
238 lines
7.6 KiB
JavaScript
import { render, dedupeMixin } from '@lion/core';
|
|
import { OverlayController } from './OverlayController.js';
|
|
|
|
/**
|
|
* @type {Function()}
|
|
* @polymerMixin
|
|
* @mixinFunction
|
|
*/
|
|
export const OverlayMixin = dedupeMixin(
|
|
superclass =>
|
|
// eslint-disable-next-line no-shadow
|
|
class OverlayMixin extends superclass {
|
|
static get properties() {
|
|
return {
|
|
opened: {
|
|
type: Boolean,
|
|
reflect: true,
|
|
},
|
|
config: {
|
|
type: Object,
|
|
},
|
|
closeEventName: {
|
|
type: String,
|
|
},
|
|
};
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.config = {};
|
|
this.closeEventName = 'overlay-close';
|
|
}
|
|
|
|
get opened() {
|
|
return this._overlayCtrl.isShown;
|
|
}
|
|
|
|
set opened(show) {
|
|
if (show) {
|
|
this.dispatchEvent(new Event('before-show'));
|
|
}
|
|
this._opened = show; // mainly captured for sync on connectedCallback
|
|
if (this._overlayCtrl) {
|
|
this.__syncOpened();
|
|
}
|
|
}
|
|
|
|
get config() {
|
|
return this._config;
|
|
}
|
|
|
|
set config(value) {
|
|
if (this._overlayCtrl) {
|
|
this._overlayCtrl.updateConfig(value);
|
|
}
|
|
this._config = value;
|
|
}
|
|
|
|
/**
|
|
* @overridable method `_overlayTemplate`
|
|
* Be aware that the overlay will be placed in a different shadow root.
|
|
* Therefore, style encapsulation should be provided by the contents of
|
|
* _overlayTemplate
|
|
* @return {TemplateResult}
|
|
*/
|
|
|
|
/**
|
|
* @overridable method `_defineOverlay`
|
|
* @desc returns an instance of a (dynamic) overlay controller
|
|
* In case overriding _defineOverlayConfig is not enough
|
|
* @returns {OverlayController}
|
|
*/
|
|
// eslint-disable-next-line
|
|
_defineOverlay({ contentNode, invokerNode }) {
|
|
return new OverlayController({
|
|
contentNode,
|
|
invokerNode,
|
|
...this._defineOverlayConfig(),
|
|
...this.config,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @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 {OverlayController}
|
|
*/
|
|
// eslint-disable-next-line
|
|
_defineOverlayConfig() {
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* @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() {}
|
|
|
|
/**
|
|
* @overridable
|
|
* @desc use this method to tear down your event listeners
|
|
*/
|
|
// eslint-disable-next-line class-methods-use-this
|
|
_teardownOpenCloseListeners() {}
|
|
|
|
connectedCallback() {
|
|
if (super.connectedCallback) {
|
|
super.connectedCallback();
|
|
}
|
|
this._createOverlay();
|
|
|
|
// Default close event catcher on the contentNode which is useful if people want to close
|
|
// their overlay but the content is not in the global root node (nowhere near the overlay component)
|
|
this.__close = () => {
|
|
this.opened = false;
|
|
};
|
|
this._overlayCtrl.contentNode.addEventListener(this.closeEventName, this.__close);
|
|
|
|
this._setupOpenCloseListeners();
|
|
this.__syncOpened();
|
|
this.__syncPopper();
|
|
}
|
|
|
|
firstUpdated(c) {
|
|
super.firstUpdated(c);
|
|
this._createOutletForLocalOverlay();
|
|
}
|
|
|
|
updated(c) {
|
|
super.updated(c);
|
|
if (this.__managesOverlayViaTemplate) {
|
|
this._renderOverlayContent();
|
|
}
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (super.disconnectedCallback) {
|
|
super.disconnectedCallback();
|
|
}
|
|
this._overlayCtrl.contentNode.removeEventListener(this.closeEventName, this.__close);
|
|
this._teardownOpenCloseListeners();
|
|
this._overlayCtrl.teardown();
|
|
}
|
|
|
|
get _overlayInvokerNode() {
|
|
return Array.from(this.children).find(child => child.slot === 'invoker');
|
|
}
|
|
|
|
get _overlayContentNode() {
|
|
let contentNode;
|
|
|
|
// FIXME: This should shadow outlet in between the host and the content slot, is a problem
|
|
// Should simply be Array.from(this.children).find(child => child.slot === 'content')
|
|
// Issue: https://github.com/ing-bank/lion/issues/382
|
|
const shadowOutlet = Array.from(this.children).find(
|
|
child => child.slot === '_overlay-shadow-outlet',
|
|
);
|
|
if (shadowOutlet) {
|
|
contentNode = Array.from(shadowOutlet.children).find(child => child.slot === 'content');
|
|
} else {
|
|
contentNode = Array.from(this.children).find(child => child.slot === 'content');
|
|
}
|
|
|
|
if (contentNode) {
|
|
this._cachedOverlayContentNode = contentNode;
|
|
}
|
|
return contentNode || this._cachedOverlayContentNode;
|
|
}
|
|
|
|
_renderOverlayContent() {
|
|
render(this._overlayTemplate(), this.__contentParent, {
|
|
scopeName: this.localName,
|
|
eventContext: this,
|
|
});
|
|
}
|
|
|
|
_createOverlay() {
|
|
let contentNode;
|
|
if (this.__managesOverlayViaTemplate) {
|
|
this.__contentParent = document.createElement('div');
|
|
this._renderOverlayContent();
|
|
contentNode = this.__contentParent.firstElementChild;
|
|
} else {
|
|
contentNode = this._overlayContentNode;
|
|
}
|
|
|
|
// Why no template support for invokerNode?
|
|
// -> Because this node will always be managed by the Subclasser and should
|
|
// reside in the dom of the sub class. A reference to a rendered node suffices.
|
|
const invokerNode = this._overlayInvokerNode;
|
|
this._overlayCtrl = this._defineOverlay({ contentNode, invokerNode });
|
|
}
|
|
|
|
// FIXME: We add an overlay slot to the wrapper, but the content node already has a slot="content"
|
|
// This is a big problem, because slots should be direct children of its host element.
|
|
// Putting the shadow outlet slot in between breaks that. https://github.com/ing-bank/lion/issues/382
|
|
/**
|
|
* @desc Should be called by Subclasser for local overlay support in shadow roots
|
|
* Create an outlet slot in shadow dom that our local overlay can pass through
|
|
*/
|
|
_createOutletForLocalOverlay() {
|
|
const outlet = document.createElement('slot');
|
|
outlet.name = '_overlay-shadow-outlet';
|
|
this.shadowRoot.appendChild(outlet);
|
|
this._overlayCtrl._contentNodeWrapper.slot = '_overlay-shadow-outlet';
|
|
}
|
|
|
|
/**
|
|
* @desc Two options for a Subclasser:
|
|
* - 1: Define a template in `._overlayTemplate`. In this case the overlay content is
|
|
* predefined and thus belongs to the web component. Examples: datepicker.
|
|
* - 2: Define a getter `_overlayContentNode` that returns a node reference to a (content
|
|
* projected) node. Used when Application Developer is in charge of the content. Examples:
|
|
* popover, dialog, bottom sheet, dropdown, tooltip, select, combobox etc.
|
|
*/
|
|
get __managesOverlayViaTemplate() {
|
|
return Boolean(this._overlayTemplate);
|
|
}
|
|
|
|
__syncOpened() {
|
|
if (this._opened) {
|
|
this._overlayCtrl.show();
|
|
} else {
|
|
this._overlayCtrl.hide();
|
|
}
|
|
}
|
|
|
|
__syncPopper() {
|
|
if (this._overlayCtrl) {
|
|
// TODO: Use updateConfig directly.. But maybe we can remove this entirely.
|
|
this._overlayCtrl.updatePopperConfig(this.config.popperConfig);
|
|
}
|
|
}
|
|
},
|
|
);
|