Source: frontend-js-web/src/main/resources/META-INF/resources/liferay/compat/modal/Modal.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import core from 'metal';
import Component from 'metal-component';
import dom from 'metal-dom';
import {EventHandler} from 'metal-events';
import Soy from 'metal-soy';

import templates from './Modal.soy';

const KEY_CODE_ESC = 27;

/**
 * Modal component.
 */

class Modal extends Component {

	/**
	 * @inheritDoc
	 */

	attached() {
		this._autoFocus(this.autoFocus);

		this.addListener('hide', this._defaultHideFn, true);
	}

	/**
	 * @inheritDoc
	 */

	created() {
		this._eventHandler = new EventHandler();
	}

	/**
	 * @inheritDoc
	 */

	detached() {
		super.detached();

		this._eventHandler.removeAllListeners();
	}

	/**
	 * @inheritDoc
	 */

	disposeInternal() {
		dom.exitDocument(this.overlayElement);

		this._unrestrictFocus();

		super.disposeInternal();
	}

	/**
	 * Emits a hide event.
	 */

	hide() {
		this.emit('hide');
	}

	/**
	 * Shows the modal, setting its `visible` state key to true.
	 */

	show() {
		this.visible = true;
	}

	/**
	 * Syncs the component according to the value of the `hideOnEscape` state key.
	 * @param {boolean} hideOnEscape
	 */

	syncHideOnEscape(hideOnEscape) {
		if (hideOnEscape) {
			this._eventHandler.add(
				dom.on(document, 'keyup', this._handleKeyup.bind(this))
			);
		}
		else {
			this._eventHandler.removeAllListeners();
		}
	}

	/**
	 * Syncs the component according to the value of the `overlay` state key.
	 * @param {boolean} overlay
	 */

	syncOverlay(overlay) {
		const willShowOverlay = overlay && this.visible;

		dom[willShowOverlay ? 'enterDocument' : 'exitDocument'](
			this.overlayElement
		);
	}

	/**
	 * Syncs the component according to the value of the `visible` state key.
	 */

	syncVisible() {
		this.syncOverlay(this.overlay);

		if (this.visible) {
			this._lastFocusedElement =
				this._lastFocusedElement || document.activeElement;

			this._autoFocus(this.autoFocus);
			this._restrictFocus();
		}
		else {
			this._unrestrictFocus();
			this._shiftFocusBack();
		}
	}

	/**
	 * Automatically focuses the element specified by the given selector.
	 * @param {boolean|string} autoFocusSelector The selector, or false if no
	 *   element should be automatically focused.
	 * @protected
	 */

	_autoFocus(autoFocusSelector) {
		if (this.inDocument && this.visible && autoFocusSelector) {
			const element = this.element.querySelector(autoFocusSelector);

			if (element) {
				element.focus();
			}
		}
	}

	/**
	 * Run only if no listener calls event.preventDefault().
	 */

	_defaultHideFn() {
		this.visible = false;
	}

	/**
	 * Handles a `focus` event on the document. If the focused element is
	 * outside the modal and an overlay is being used, focuses the modal back.
	 * @param {!Event} event
	 * @protected
	 */

	_handleDocumentFocus(event) {
		if (this.overlay && !this.element.contains(event.target)) {
			this._autoFocus('.modal-dialog');
		}
	}

	/**
	 * Handles document click in order to close the alert.
	 * @param {!Event} event
	 * @protected
	 */

	_handleKeyup(event) {
		if (event.keyCode === KEY_CODE_ESC) {
			this.hide();
		}
	}

	/**
	 * Restricts focus to the modal while it's visible.
	 * @protected
	 */

	_restrictFocus() {
		if (!this._restrictFocusHandle) {
			this._restrictFocusHandle = dom.on(
				document,
				'focus',
				this._handleDocumentFocus.bind(this),
				true
			);
		}
	}

	/**
	 * Shifts the focus back to the last element that had been focused before the
	 * modal was shown.
	 * @protected
	 */

	_shiftFocusBack() {
		if (this._lastFocusedElement) {
			this._lastFocusedElement.focus();
			this._lastFocusedElement = null;
		}
	}

	/**
	 * Removes the handler that restricts focus to elements inside the modal.
	 * @protected
	 */

	_unrestrictFocus() {
		if (this._restrictFocusHandle) {
			this._restrictFocusHandle.removeListener();
			this._restrictFocusHandle = null;
		}
	}

	/**
	 * Defines the default value for the `overlayElement` state key.
	 * @protected
	 * @return {Node}
	 */

	_valueOverlayElementFn() {
		return dom.buildFragment('<div class="modal-backdrop fade show"></div>')
			.firstChild;
	}
}

Modal.STATE = {

	/**
	 * A selector for the element that should be automatically focused when the modal
	 * becomes visible, or `false` if no auto focus should happen. Defaults to the
	 * modal's close button.
	 * @type {boolean|string}
	 */

	autoFocus: {
		validator: (val) => val === false || core.isString(val),
		value: '.close',
	},

	/**
	 * Content to be placed inside modal body. Can be either an html string or
	 * a function that calls incremental-dom to render the body.
	 * @type {string|function()}
	 */

	body: {},

	/**
	 * The id used by the body element.
	 * @type {string}
	 */

	bodyId: {
		valueFn: () => 'modal-body-' + core.getUid(),
	},

	/**
	 * Classes that will be applied to the modal-dialog element.
	 * @type {string}
	 */

	dialogClasses: {
		validator: core.isString,
	},

	/**
	 * Content to be placed inside modal footer. Can be either an html string or
	 * a function that calls incremental-dom to render the footer.
	 * @type {string|function()}
	 */

	footer: {},

	/**
	 * Content to be placed inside modal header. Can be either an html string or
	 * a function that calls incremental-dom to render the header.
	 * @type {string|function()}
	 */

	header: {},

	/**
	 * The id used by the header element.
	 * @type {string}
	 */

	headerId: {
		valueFn: () => 'modal-header-' + core.getUid(),
	},

	/**
	 * Whether modal should hide on esc.
	 * @type {boolean}
	 * @default true
	 */

	hideOnEscape: {
		validator: core.isBoolean,
		value: true,
	},

	/**
	 * Flag indicating if the default "x" button for closing the modal should be
	 * added or not.
	 * @type {boolean}
	 * @default false
	 */

	noCloseButton: {
		value: false,
	},

	/**
	 * Whether overlay should be visible when modal is visible.
	 * @type {boolean}
	 * @default true
	 */

	overlay: {
		validator: core.isBoolean,
		value: true,
	},

	/**
	 * Element to be used as overlay.
	 * @type {Element}
	 */

	overlayElement: {
		valueFn: '_valueOverlayElementFn',
		writeOnce: true,
	},

	/**
	 * The ARIA role to be used for this modal.
	 * @type {string}
	 * @default 'dialog'
	 */

	role: {
		validator: core.isString,
		value: 'dialog',
	},
};

Soy.register(Modal, templates);

export {Modal};
export default Modal;