@sandlada/vue-mdc
Version:

317 lines (316 loc) • 10.6 kB
JavaScript
/**
* @license
* Copyright 2025 Sandlada & Kai Orion
* SPDX-License-Identifier: MIT
*/
import { defineComponent, createVNode } from "vue";
import { isServer } from "../../utils/is-server.js";
import { redispatchEvent } from "../../internals/events/redispatch-event.js";
import "../../internals/navigation/render-navigation-destination.js";
import { componentNamePrefix } from "../../internals/component-name-prefix/component-name-prefix.js";
import { Divider } from "../divider/divider.js";
import "../divider/divider.definition.js";
import { DialogDefaultCloseAnimation, DialogDefaultOpenAnimation } from "./animations.js";
import { SDialogController } from "./dialog-controller.js";
import { props } from "./dialog.definition.js";
import css from "./styles/dialog.module.scss.js";
const Dialog = /* @__PURE__ */ defineComponent({
name: `${componentNamePrefix}-dialog`,
props,
slots: {},
emits: ["open", "opend", "cancel", "close", "closed", "submit", "update:modelValue"],
created() {
if (this.modelValue !== null) {
this.$emit("update:modelValue", this.defaultOpen);
}
},
mounted() {
this.isOpen = this.defaultOpen;
this.$el[SDialogController] = {
show: () => {
this.isOpen = true;
},
close: () => {
this.isOpen = true;
}
};
if (isServer()) {
return;
}
this.intersectionObserver = new IntersectionObserver((entries) => {
for (const entry of entries) {
this.handleAnchorIntersection(entry);
}
}, {
root: this.getScrollerElement()
});
this.intersectionObserver.observe(this.getTopAnchorElement());
this.intersectionObserver.observe(this.getBottomAnchorElement());
this.getContainerElement().addEventListener("click", this.handleContentClick);
this.$el.addEventListener("submit", this.handleSubmit);
this.getDialogElement().addEventListener("click", this.handleDialogClick);
this.getDialogElement().addEventListener("cancel", this.handleCancel);
this.getDialogElement().addEventListener("close", this.handleClose);
this.getDialogElement().addEventListener("keydown", this.handleKeydown);
this.getDialogElement().returnValue = this.returnValue;
},
updated() {
if (this.modelValue !== null) {
this.isOpen = this.modelValue;
}
this.getDialogElement().returnValue = this.returnValue;
},
data: () => ({
animationController: null,
open: false,
opening: false,
returnValue: "",
nextClickIsFromContent: false,
escapePressedWithoutCancel: false,
isAtScrollTop: false,
isAtScrollBottom: false,
intersectionObserver: null
}),
computed: {
isOpen: {
get() {
return this.open;
},
set(value) {
if (this.open === value) {
return;
}
this.open = value;
if (this.isOpen) {
this.show();
} else {
this.close();
}
}
}
},
methods: {
async show() {
this.opening = true;
if (this.getDialogElement().open || !this.opening) {
this.opening = false;
return;
}
const openEvent = new Event("open", {
bubbles: false,
cancelable: true,
composed: false
});
this.$emit("open", openEvent);
const preventOpen = !this.$el.dispatchEvent(openEvent);
if (preventOpen) {
this.isOpen = false;
this.opening = false;
return;
}
this.isOpen = true;
this.getDialogElement().showModal();
await this.animateDialog(DialogDefaultOpenAnimation);
this.opening = false;
const opendEvent = new Event("opend", {
bubbles: false,
cancelable: false,
composed: false
});
this.$emit("opend", opendEvent);
this.$el.dispatchEvent(opendEvent);
},
async close(returnValue) {
this.opening = false;
if (!this.getDialogElement().open || this.opening) {
this.isOpen = false;
return;
}
const preReturnValue = this.returnValue;
this.returnValue = returnValue ? returnValue : this.returnValue;
const closeEvent = new Event("close", {
bubbles: false,
cancelable: true,
composed: false
});
this.$emit("close", closeEvent);
const preventClose = !this.$el.dispatchEvent(closeEvent);
if (preventClose) {
this.returnValue = preReturnValue;
return;
}
await this.animateDialog(DialogDefaultCloseAnimation);
this.getDialogElement().close(returnValue ? returnValue : this.returnValue);
this.isOpen = false;
const closedEvent = new Event("closed");
this.$emit("closed", closedEvent);
this.$el.dispatchEvent(closeEvent);
},
getDialogElement() {
return this.$el.querySelector("&>dialog");
},
getScrimElement() {
return this.$el.querySelector(`&>.${css.scrim}`);
},
getContentElement() {
return this.$el.querySelector(`& .${css.content}`);
},
getContainerElement() {
return this.$el.querySelector(`& .${css.container}`);
},
getScrollerElement() {
return this.$el.querySelector(`& .${css.scroller}`);
},
getHeadlineElement() {
return this.$el.querySelector(`& .${css.headline}`);
},
getActionsElement() {
return this.$el.querySelector(`& .${css.actions}`);
},
getTopAnchorElement() {
return this.$el.querySelector(`& .${css.top}.${css.anchor}`);
},
getBottomAnchorElement() {
return this.$el.querySelector(`& .${css.bottom}.${css.anchor}`);
},
async animateDialog(animation) {
this.animationController?.abort();
this.animationController = new AbortController();
const {
container: containerAnimate,
dialog: dialogAnimate,
scrim: scrimAnimate,
headline: headlineAnimate,
content: contentAnimate,
actions: actionsAnimate
} = animation;
const elementAndAnimation = [[this.getDialogElement(), dialogAnimate ?? []], [this.getScrimElement(), scrimAnimate ?? []], [this.getContainerElement(), containerAnimate ?? []], [this.getHeadlineElement(), headlineAnimate ?? []], [this.getContentElement(), contentAnimate ?? []], [this.getActionsElement(), actionsAnimate ?? []]];
const animations = [];
for (const [element, animation2] of elementAndAnimation) {
for (const animateArgs of animation2) {
const animation3 = element.animate(...animateArgs);
this.animationController.signal.addEventListener("abort", () => {
animation3.cancel();
});
animations.push(animation3);
}
}
await Promise.all(animations.map((animation2) => animation2.finished.catch(() => {
})));
},
handleCancel(e) {
if (e.target !== this.getDialogElement()) {
return;
}
this.escapePressedWithoutCancel = false;
this.$emit("cancel", e);
const preventCancel = !redispatchEvent(this.$el, e);
e.preventDefault();
if (preventCancel) {
return;
}
this.close();
},
handleClose() {
if (!this.escapePressedWithoutCancel) {
return;
}
this.escapePressedWithoutCancel = false;
this.getDialogElement().dispatchEvent(new Event("cancel", {
cancelable: true
}));
},
handleSubmit(e) {
const form = e.target;
const {
submitter
} = e;
if (form.method !== "dialog" || !submitter) {
return;
}
this.close(submitter.getAttribute("value") ?? this.returnValue);
},
handleContentClick() {
this.nextClickIsFromContent = true;
},
handleDialogClick(e) {
if (this.nextClickIsFromContent) {
this.nextClickIsFromContent = false;
return;
}
const cancelEvent = new Event("cancel");
this.$emit("cancel", cancelEvent);
const preventCancel = !this.$el.dispatchEvent(cancelEvent);
if (preventCancel) {
return;
}
this.close();
},
handleKeydown(event) {
if (event.key !== "Escape") {
return;
}
this.escapePressedWithoutCancel = true;
setTimeout(() => {
this.escapePressedWithoutCancel = false;
});
},
handleAnchorIntersection(entry) {
const {
target,
isIntersecting
} = entry;
if (target === this.getTopAnchorElement()) {
this.isAtScrollTop = isIntersecting;
}
if (target === this.getBottomAnchorElement()) {
this.isAtScrollBottom = isIntersecting;
}
}
},
render() {
const scrollable = this.isOpen && !(this.isAtScrollTop && this.isAtScrollBottom);
const classes = [this.$slots.headline && css["has-headline"], this.$slots.actions && css["has-actions"], this.$slots.icon && css["has-icon"], scrollable && css.scrollable, scrollable && !this.isAtScrollTop && css["show-top-divider"], scrollable && !this.isAtScrollBottom && css["show-bottom-divider"]];
const renderHeadline = createVNode("div", {
"class": css["headline-wrapper"]
}, [createVNode("div", {
"class": css["icon-wrapper"],
"aria-hidden": "true"
}, [this.$slots.icon && createVNode("span", {
"class": css.icon
}, [this.$slots.icon()])]), createVNode("h2", {
"class": css.headline
}, [this.$slots.headline && this.$slots.headline()]), createVNode(Divider, null, null)]);
const renderScroller = createVNode("div", {
"class": css.scroller
}, [createVNode("div", {
"class": css["content-wrapper"]
}, [createVNode("div", {
"class": [css.top, css.anchor]
}, null), this.$slots.default && createVNode("span", {
"class": css.content
}, [this.$slots.default()]), createVNode("div", {
"class": [css.bottom, css.anchor]
}, null)])]);
const renderActions = createVNode("div", {
"class": css["actions-wrapper"]
}, [createVNode(Divider, null, null), this.$slots.actions && createVNode("span", {
"class": css.actions
}, [this.$slots.actions()])]);
return createVNode("div", {
"data-component": "dialog",
"class": [css["dialog-container"], ...classes]
}, [createVNode("div", {
"class": css.scrim
}, null), createVNode("dialog", {
"role": "alertdialog"
}, [createVNode("div", {
"class": css.container
}, [renderHeadline, renderScroller, renderActions])])]);
},
inheritAttrs: true
});
export {
Dialog
};
//# sourceMappingURL=dialog.js.map