blob: c10c3408cb44e6c9def39fdb75504aff3820f71b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key
const Default = {
backdrop: true,
keyboard: true,
focus: true,
show: true
};
const DefaultType = {
backdrop: "(boolean|string)",
keyboard: "boolean",
focus: "boolean",
show: "boolean"
};
const Event = {
HIDE: "hide.bs.modal",
HIDE_PREVENTED: "hidePrevented.bs.modal",
HIDDEN: "hidden.bs.modal",
SHOW: "show.bs.modal",
SHOWN: "shown.bs.modal",
FOCUSIN: "focusin.bs.modal",
RESIZE: "resize.bs.modal",
CLICK_DISMISS: "click.dismiss.bs.modal",
KEYDOWN_DISMISS: "keydown.dismiss.bs.modal",
MOUSEUP_DISMISS: "mouseup.dismiss.bs.modal",
MOUSEDOWN_DISMISS: "mousedown.dismiss.bs.modal",
CLICK_DATA_API: "click.bs.modal.data-api"
};
const ClassName = {
SCROLLABLE: "modal-dialog-scrollable",
SCROLLBAR_MEASURER: "modal-scrollbar-measure",
BACKDROP: "modal-backdrop",
OPEN: "modal-open",
FADE: "fade",
SHOW: "show",
STATIC: "modal-static"
};
const Selector = {
DIALOG: ".modal-dialog",
MODAL_BODY: ".modal-body",
DATA_TOGGLE: "[data-toggle='modal']",
DATA_DISMISS: "[data-dismiss='modal']",
FIXED_CONTENT: ".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",
STICKY_CONTENT: ".sticky-top"
};
export class Popup extends HTMLElement {
TRANSITION_END: string = "bsTransitionEnd";
_dialog: HTMLElement;
_backdrop;
_isShown;
_isBodyOverflowing;
_ignoreBackdropClick;
// _isTransitioning;
_scrollbarWidth;
_clickDismiss: (event: Event) => void;
constructor() {
super();
this._dialog = this.querySelector(Selector.DIALOG);
this._backdrop = null;
this._isShown = false;
this._isBodyOverflowing = false;
this._ignoreBackdropClick = false;
// this._isTransitioning = false;
this._scrollbarWidth = 0;
}
connectedCallback(): void {
const hidden = Collapse.findHidden(this);
if (hidden.value === "false") {
// XXX hack: this is needed for popups open by AJAX.
// XXX currently the DOM replacement done by Tobago doesn't remove the modal-backdrop
for (const backdrop of document.querySelectorAll(".modal-backdrop")) {
backdrop.parentNode.removeChild(backdrop);
}
this.show(); // inits and opens the popup
} else {
this.hide(); // inits and hides the popup
}
}
// Public
// toggle(relatedTarget) {
// return this._isShown ? this.hide() : this.show(relatedTarget)
// }
show():void {
if (this._isShown /*|| this._isTransitioning*/) {
return;
}
/*
if (this.classList.contains(ClassName.FADE)) {
this._isTransitioning = true;
}
*/
const showEvent = new CustomEvent(Event.SHOW, /*{*/
// detail: relatedTarget // TBD: detail or anything other
/*}*/);
this.dispatchEvent(showEvent);
if (this._isShown || showEvent.defaultPrevented) {
return;
}
this._isShown = true;
// this._checkScrollbar();
// this._setScrollbar();
//
// this._adjustDialog();
//
// this._setEscapeEvent();
// this._setResizeEvent();
this._clickDismiss = (event: Event) => {this.hide();};
// this._clickDismiss = (event: Event) => {this.hide(event)};
if (this.classList.contains(Selector.DATA_DISMISS)) {
this.addEventListener(Event.CLICK_DISMISS, this._clickDismiss);
}
this._dialog.addEventListener(Event.MOUSEDOWN_DISMISS, () => {
// $(this._element).one(Event.MOUSEUP_DISMISS, (event) => { // XXX not implemented yet
// if ($(event.target).is(this._element)) { // XXX not implemented yet
this._ignoreBackdropClick = true;
// }
// })
});
// this._showBackdrop(() => this._showElement(relatedTarget))
this._showElement();
}
hide(/*event*/):void {
// if (event) {
// event.preventDefault()
// }
if (!this._isShown/* || this._isTransitioning*/) {
return;
}
const hideEvent = new CustomEvent(Event.HIDE);
this.dispatchEvent(hideEvent);
if (!this._isShown || hideEvent.defaultPrevented) {
return;
}
this._isShown = false;
// const transition = this.classList.contains(ClassName.FADE);
// if (transition) {
// this._isTransitioning = true
// }
// this._setEscapeEvent();
// this._setResizeEvent();
$(document).off(Event.FOCUSIN);
this.classList.remove(ClassName.SHOW);
this.removeEventListener(Event.CLICK_DISMISS, this._clickDismiss);
$(this._dialog).off(Event.MOUSEDOWN_DISMISS);
// if (transition) {
// const transitionDuration = this.getTransitionDuration();
//
// this.addEventListener(Popup.TRANSITION_END, (event:Event) => this._hideModal(event));
// this.emulateTransitionEnd(transitionDuration)
// } else {
this._hideModal();
// }
}
// dispose() {
// [window, this._element, this._dialog]
// .forEach((htmlElement) => $(htmlElement).off(`.bs.modal`));
/**
* `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`
* Do not move `document` in `htmlElements` array
* It will remove `Event.CLICK_DATA_API` event that should remain
*/
// $(document).off(Event.FOCUSIN);
//
// $.removeData(this._element, 'bs.modal');
//
// this._dialog = null;
// this._backdrop = null;
// this._isShown = null;
// this._isBodyOverflowing = null;
// this._ignoreBackdropClick = null;
// this._isTransitioning = null;
// this._scrollbarWidth = null;
// }
// handleUpdate() {
// this._adjustDialog();
// }
// Private
/*
_triggerBackdropTransition() {
if (this._config.backdrop === 'static') {
const hideEventPrevented = $.Event(Event.HIDE_PREVENTED);
$(this._element).trigger(hideEventPrevented);
if (hideEventPrevented.defaultPrevented) {
return;
}
this._element.classList.add(ClassName.STATIC);
const modalTransitionDuration = Util.getTransitionDurationFromElement(this._element)
$(this._element).one(Util.TRANSITION_END, () => {
this._element.classList.remove(ClassName.STATIC)
})
.emulateTransitionEnd(modalTransitionDuration);
this._element.focus();
} else {
this.hide();
}
}
*/
_showElement(/*relatedTarget*/):void {
// const transition = $(this._element).hasClass(ClassName.FADE)
const modalBody = this._dialog ? this._dialog.querySelector(Selector.MODAL_BODY) : null;
if (!this.parentNode ||
this.parentNode.nodeType !== Node.ELEMENT_NODE) {
// Don't move modal's DOM position
document.body.appendChild(this);
}
this.style.display = "block";
this.removeAttribute("aria-hidden");
this.setAttribute("aria-modal", "true");
if ($(this._dialog).hasClass(ClassName.SCROLLABLE) && modalBody) {
modalBody.scrollTop = 0;
} else {
this.scrollTop = 0;
}
// if (transition) {
// Util.reflow(this._element)
// }
this.classList.add(ClassName.SHOW);
// if (this._config.focus) {
// this._enforceFocus()
// }
// const shownEvent = $.Event(Event.SHOWN, {
// relatedTarget
// });
// const transitionComplete = () => {
// if (this._config.focus) {
// this._element.focus()
// }
// this._isTransitioning = false
// $(this._element).trigger(shownEvent)
// };
// if (transition) {
// const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)
//
// $(this._dialog)
// .one(Util.TRANSITION_END, transitionComplete)
// .emulateTransitionEnd(transitionDuration)
// } else {
// transitionComplete()
// }
}
/*
_enforceFocus() {
$(document)
.off(Event.FOCUSIN) // Guard against infinite focus loop
.on(Event.FOCUSIN, (event) => {
if (document !== event.target &&
this._element !== event.target &&
$(this._element).has(event.target).length === 0) {
this._element.focus()
}
})
}
_setEscapeEvent() {
if (this._isShown && this._config.keyboard) {
$(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
if (event.which === ESCAPE_KEYCODE) {
this._triggerBackdropTransition()
}
})
} else if (!this._isShown) {
$(this._element).off(Event.KEYDOWN_DISMISS)
}
}
_setResizeEvent() {
if (this._isShown) {
$(window).on(Event.RESIZE, (event) => this.handleUpdate(event))
} else {
$(window).off(Event.RESIZE)
}
}
*/
_hideModal():void {
this.style.display = "none";
this.setAttribute("aria-hidden", "true");
this.removeAttribute("aria-modal");
// this._isTransitioning = false;
/*
this._showBackdrop(() => {
$(document.body).removeClass(ClassName.OPEN);
this._resetAdjustments();
this._resetScrollbar();
$(this._element).trigger(Event.HIDDEN)
})
*/
}
/*
_removeBackdrop() {
if (this._backdrop) {
$(this._backdrop).remove();
this._backdrop = null;
}
}
_showBackdrop(callback) {
const animate = $(this._element).hasClass(ClassName.FADE)
? ClassName.FADE : ''
if (this._isShown && this._config.backdrop) {
this._backdrop = document.createElement('div')
this._backdrop.className = ClassName.BACKDROP
if (animate) {
this._backdrop.classList.add(animate)
}
$(this._backdrop).appendTo(document.body)
$(this._element).on(Event.CLICK_DISMISS, (event) => {
if (this._ignoreBackdropClick) {
this._ignoreBackdropClick = false
return
}
if (event.target !== event.currentTarget) {
return
}
this._triggerBackdropTransition()
})
if (animate) {
Util.reflow(this._backdrop)
}
$(this._backdrop).addClass(ClassName.SHOW)
if (!callback) {
return
}
if (!animate) {
callback()
return
}
const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
$(this._backdrop)
.one(Util.TRANSITION_END, callback)
.emulateTransitionEnd(backdropTransitionDuration)
} else if (!this._isShown && this._backdrop) {
$(this._backdrop).removeClass(ClassName.SHOW)
const callbackRemove = () => {
this._removeBackdrop()
if (callback) {
callback()
}
}
if ($(this._element).hasClass(ClassName.FADE)) {
const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
$(this._backdrop)
.one(Util.TRANSITION_END, callbackRemove)
.emulateTransitionEnd(backdropTransitionDuration)
} else {
callbackRemove()
}
} else if (callback) {
callback()
}
}
// ----------------------------------------------------------------------
// the following methods are used to handle overflowing modals
// todo (fat): these should probably be refactored out of modal.js
// ----------------------------------------------------------------------
_adjustDialog() {
const isModalOverflowing =
this._element.scrollHeight > document.documentElement.clientHeight
if (!this._isBodyOverflowing && isModalOverflowing) {
this._element.style.paddingLeft = `${this._scrollbarWidth}px`
}
if (this._isBodyOverflowing && !isModalOverflowing) {
this._element.style.paddingRight = `${this._scrollbarWidth}px`
}
}
_resetAdjustments() {
this._element.style.paddingLeft = ''
this._element.style.paddingRight = ''
}
_checkScrollbar() {
const rect = document.body.getBoundingClientRect()
this._isBodyOverflowing = rect.left + rect.right < window.innerWidth
this._scrollbarWidth = this._getScrollbarWidth()
}
_setScrollbar() {
if (this._isBodyOverflowing) {
// Note: DOMNode.style.paddingRight returns the actual value or '' if not set
// while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))
// Adjust fixed content padding
$(fixedContent).each((index, element) => {
const actualPadding = element.style.paddingRight
const calculatedPadding = $(element).css('padding-right')
$(element)
.data('padding-right', actualPadding)
.css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
})
// Adjust sticky content margin
$(stickyContent).each((index, element) => {
const actualMargin = element.style.marginRight
const calculatedMargin = $(element).css('margin-right')
$(element)
.data('margin-right', actualMargin)
.css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
})
// Adjust body padding
const actualPadding = document.body.style.paddingRight
const calculatedPadding = $(document.body).css('padding-right')
$(document.body)
.data('padding-right', actualPadding)
.css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
}
$(document.body).addClass(ClassName.OPEN)
}
_resetScrollbar() {
// Restore fixed content padding
const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
$(fixedContent).each((index, element) => {
const padding = $(element).data('padding-right')
$(element).removeData('padding-right')
element.style.paddingRight = padding ? padding : ''
})
// Restore sticky content
const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))
$(elements).each((index, element) => {
const margin = $(element).data('margin-right')
if (typeof margin !== 'undefined') {
$(element).css('margin-right', margin).removeData('margin-right')
}
})
// Restore body padding
const padding = $(document.body).data('padding-right')
$(document.body).removeData('padding-right')
document.body.style.paddingRight = padding ? padding : ''
}
_getScrollbarWidth() { // thx d.walsh
const scrollDiv = document.createElement('div')
scrollDiv.className = ClassName.SCROLLBAR_MEASURER
document.body.appendChild(scrollDiv)
const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
document.body.removeChild(scrollDiv)
return scrollbarWidth
}
// Static
static _jQueryInterface(config, relatedTarget) {
return this.each(function () {
let data = $(this).data('bs.modal')
const _config = {
...Default,
...$(this).data(),
...typeof config === 'object' && config ? config : {}
}
if (!data) {
data = new Modal(this, _config)
$(this).data('bs.modal', data)
}
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config](relatedTarget)
} else if (_config.show) {
data.show(relatedTarget)
}
})
}
getTransitionDuration(): number {
// Get transition-duration of the element
let transitionDuration = this.style.transitionDuration;
let transitionDelay = this.style.transitionDelay;
const floatTransitionDuration = parseFloat(transitionDuration);
const floatTransitionDelay = parseFloat(transitionDelay);
// Return 0 if element or transition duration is not found
if (!floatTransitionDuration && !floatTransitionDelay) {
return 0;
}
// If multiple durations are defined, take the first
transitionDuration = transitionDuration.split(',')[0];
transitionDelay = transitionDelay.split(',')[0];
return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * 1000;
}
*/
}
document.addEventListener("DOMContentLoaded", function (event: Event): void {
window.customElements.define("tobago-popup", Popup);
});
export class Collapse {
static findHidden(element: HTMLElement): HTMLInputElement {
return document.getElementById(element.id + "::collapse") as HTMLInputElement;
}
static execute = function (action: string, target: HTMLElement): void {
const hidden = Collapse.findHidden(target);
let newCollapsed;
switch (action) {
case "hide":
newCollapsed = true;
break;
case "show":
newCollapsed = false;
break;
default:
console.error("unknown action: '" + action + "'");
}
if (newCollapsed) {
if (target instanceof Popup) {
target.hide();
} else {
target.classList.add("tobago-collapsed");
}
} else {
if (target instanceof Popup) {
target.show();
} else {
target.classList.remove("tobago-collapsed");
}
}
hidden.value = newCollapsed;
};
}