+ Confirm Dialog
+
+
+
+
+
+
+
+
+
+
diff --git a/src/js/components/confirm-popup.js b/src/js/components/confirm-popup.js
new file mode 100644
index 0000000..f722235
--- /dev/null
+++ b/src/js/components/confirm-popup.js
@@ -0,0 +1,44 @@
+/**
+ * confirmPopup browser helper.
+ *
+ * Mirrors gnexus-ui-kit confirmPopup public surface.
+ */
+
+import Modals from "./modals.js";
+
+export default function confirmPopup(text, confirmedCb, canceledCb) {
+ const modal = Modals.create("confirm-popup", {
+ title: "Requires confirmation",
+ body: () => {
+ const paragraph = document.createElement("p");
+ paragraph.textContent = text ?? "";
+ return paragraph;
+ },
+ actions: modalInstance => {
+ const buttonNo = document.createElement("button");
+ buttonNo.className = "btn btn-primary";
+ buttonNo.type = "button";
+ buttonNo.textContent = "NO";
+
+ const buttonYes = document.createElement("button");
+ buttonYes.className = "btn btn-warning";
+ buttonYes.type = "button";
+ buttonYes.textContent = "YES";
+
+ buttonNo.addEventListener("click", () => {
+ modalInstance.close();
+ canceledCb?.();
+ });
+
+ buttonYes.addEventListener("click", () => {
+ modalInstance.close();
+ confirmedCb?.();
+ });
+
+ return [buttonNo, buttonYes];
+ }
+ });
+
+ modal.show();
+ return modal;
+}
diff --git a/src/js/components/modals.js b/src/js/components/modals.js
new file mode 100644
index 0000000..d5bae46
--- /dev/null
+++ b/src/js/components/modals.js
@@ -0,0 +1,143 @@
+/**
+ * Modals browser helper.
+ *
+ * Mirrors gnexus-ui-kit Modals public surface:
+ * create(id, props)
+ */
+
+function appendContent(container, content, mode = "html") {
+ if(content instanceof Node) {
+ container.append(content);
+ return;
+ }
+
+ if(typeof content !== "undefined" && content !== null) {
+ if(mode === "text") {
+ container.textContent = content;
+ } else {
+ container.innerHTML = content;
+ }
+ }
+}
+
+function template(id, title, footer) {
+ const modal = document.createElement("div");
+ modal.className = "modal";
+ modal.setAttribute("aria-hidden", "true");
+ modal.id = id;
+
+ const backdrop = document.createElement("div");
+ backdrop.className = "modal-backdrop";
+
+ const dialog = document.createElement("div");
+ dialog.className = "modal-dialog";
+ dialog.setAttribute("role", "dialog");
+ dialog.setAttribute("aria-modal", "true");
+ dialog.setAttribute("aria-labelledby", `${id}-title`);
+
+ const panel = document.createElement("div");
+ panel.className = "modal-panel";
+
+ const header = document.createElement("header");
+ header.className = "modal-header";
+
+ const modalTitle = document.createElement("h4");
+ modalTitle.className = "modal-title";
+ modalTitle.id = `${id}-title`;
+ modalTitle.textContent = title;
+
+ const close = document.createElement("button");
+ close.className = "btn-icon modal-close";
+ close.type = "button";
+ close.setAttribute("aria-label", "Close");
+ close.textContent = "✕";
+
+ const body = document.createElement("div");
+ body.className = "modal-body";
+
+ const modalFooter = document.createElement("footer");
+ modalFooter.className = "modal-footer";
+ appendContent(modalFooter, footer);
+
+ header.append(modalTitle, close);
+ panel.append(body, modalFooter);
+ dialog.append(header, panel);
+ modal.append(backdrop, dialog);
+
+ return modal;
+}
+
+function init(modal, onready) {
+ modal.show = function() {
+ document.querySelector("body").append(modal);
+ setTimeout(() => {
+ modal.classList.add("a-show");
+ }, 10);
+ };
+
+ modal.close = function() {
+ modal.classList.add("a-hide");
+ setTimeout(() => {
+ modal.remove();
+ }, 300);
+ };
+
+ modal.querySelector(".modal-close").addEventListener("click", () => {
+ modal.close();
+ });
+
+ modal.querySelector(".modal-backdrop")?.addEventListener("click", () => {
+ modal.close();
+ });
+
+ if(typeof onready === "function") {
+ onready(modal);
+ }
+
+ return modal;
+}
+
+/**
+ * Create a new modal window.
+ * @param {string} id - Unique id
+ * @param {object} props - { title, footer, body, bodyText, bodyHtml, actions, onready }
+ * @returns {HTMLElement} Modal DOM element with show()/close()
+ */
+function create(id, props = {}) {
+ const title = props.title || "";
+ const footer = props.footer || "";
+
+ const modal = template(id, title, footer);
+ const modalBody = modal.querySelector(".modal-body");
+ const modalFooter = modal.querySelector(".modal-footer");
+
+ if(typeof props.actions === "function") {
+ const actionsResult = props.actions(modal);
+
+ if(Array.isArray(actionsResult) && actionsResult[0] instanceof Node) {
+ const actions = document.createElement("div");
+ actions.classList.add("actions");
+ for(const actionElement of actionsResult) {
+ actions.append(actionElement);
+ }
+ modalFooter.append(actions);
+ } else if(actionsResult instanceof Node) {
+ modalFooter.append(actionsResult);
+ }
+ }
+
+ if(typeof props.body === "function") {
+ const bodyResult = props.body(modal);
+ appendContent(modalBody, bodyResult, props.bodyMode ?? "html");
+ } else if(typeof props.bodyText !== "undefined") {
+ appendContent(modalBody, props.bodyText, "text");
+ } else if(typeof props.bodyHtml !== "undefined") {
+ appendContent(modalBody, props.bodyHtml, "html");
+ }
+
+ return init(modal, props?.onready);
+}
+
+export default {
+ create
+};
diff --git a/src/js/index.js b/src/js/index.js
index 2a5ed93..f20a3be 100644
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -5,6 +5,8 @@
import * as Icons from "./components/icon-helper.js";
import Toasts from "./components/toasts.js";
+import Modals from "./components/modals.js";
+import confirmPopup from "./components/confirm-popup.js";
import Tabs from "./components/tabs.js";
import Progress from "./components/progress.js";
import Tables from "./components/tables.js";
@@ -22,6 +24,8 @@
const api = {
Helper: Icons,
Toasts,
+ Modals,
+ confirmPopup,
Tabs,
Progress,
Tables,
@@ -54,5 +58,5 @@
});
}
-export { Icons as Helper, Toasts, Tabs, Progress, Tables, Lists, Skeleton, Tooltips, Dropdowns, NavigationShell, Background, Breadcrumbs, Accordion, Popover, Loader };
+export { Icons as Helper, Toasts, Modals, confirmPopup, Tabs, Progress, Tables, Lists, Skeleton, Tooltips, Dropdowns, NavigationShell, Background, Breadcrumbs, Accordion, Popover, Loader };
export default api;
diff --git a/src/vue/components/GnConfirmDialog.js b/src/vue/components/GnConfirmDialog.js
new file mode 100644
index 0000000..10c0f44
--- /dev/null
+++ b/src/vue/components/GnConfirmDialog.js
@@ -0,0 +1,58 @@
+/**
+ * GnConfirmDialog — confirmation dialog.
+ *
+ * API-compatible with gnexus-ui-kit GnConfirmDialog.
+ *
+ * @typedef {Object} GnConfirmDialogProps
+ * @property {boolean} [open=false]
+ * @property {string} [title='Requires confirmation']
+ * @property {string} [message='']
+ * @property {string} [confirmText='YES']
+ * @property {string} [cancelText='NO']
+ * @property {string} [confirmVariant='warning']
+ *
+ * @slots default - Custom body content (overrides message)
+ * @emits update:open
+ * @emits confirm
+ * @emits cancel
+ */
+
+import { defineComponent, h } from "vue";
+import GnButton from "./GnButton.js";
+import GnModal from "./GnModal.js";
+
+export default defineComponent({
+ name: "GnConfirmDialog",
+ props: {
+ open: { type: Boolean, default: false },
+ title: { type: String, default: "Requires confirmation" },
+ message: { type: String, default: "" },
+ confirmText: { type: String, default: "YES" },
+ cancelText: { type: String, default: "NO" },
+ confirmVariant: { type: String, default: "warning" }
+ },
+ emits: ["update:open", "confirm", "cancel"],
+ setup(props, { emit, slots }) {
+ const close = () => emit("update:open", false);
+ const cancel = () => {
+ emit("cancel");
+ close();
+ };
+ const confirm = () => {
+ emit("confirm");
+ close();
+ };
+
+ return () => h(GnModal, {
+ open: props.open,
+ title: props.title,
+ "onUpdate:open": value => emit("update:open", value)
+ }, {
+ default: () => slots.default?.() || (props.message ? h("p", {}, props.message) : null),
+ actions: () => [
+ h(GnButton, { variant: "primary", onClick: cancel }, () => props.cancelText),
+ h(GnButton, { variant: props.confirmVariant, onClick: confirm }, () => props.confirmText)
+ ]
+ });
+ }
+});
diff --git a/src/vue/index.js b/src/vue/index.js
index b7caf80..3c826fa 100644
--- a/src/vue/index.js
+++ b/src/vue/index.js
@@ -18,6 +18,7 @@
export { default as GnTextarea } from "./components/GnTextarea.js";
export { default as GnCard } from "./components/GnCard.js";
export { default as GnModal } from "./components/GnModal.js";
+export { default as GnConfirmDialog } from "./components/GnConfirmDialog.js";
export { default as GnDrawer } from "./components/GnDrawer.js";
export { default as GnToastProvider } from "./components/GnToastProvider.js";
export { default as GnTabs } from "./components/GnTabs.js";
diff --git a/src/vue/plugin.js b/src/vue/plugin.js
index 2b4ea4b..068bb85 100644
--- a/src/vue/plugin.js
+++ b/src/vue/plugin.js
@@ -18,6 +18,7 @@
import GnTextarea from "./components/GnTextarea.js";
import GnCard from "./components/GnCard.js";
import GnModal from "./components/GnModal.js";
+import GnConfirmDialog from "./components/GnConfirmDialog.js";
import GnDrawer from "./components/GnDrawer.js";
import GnToastProvider from "./components/GnToastProvider.js";
import GnTabs from "./components/GnTabs.js";
@@ -68,6 +69,7 @@
GnTextarea,
GnCard,
GnModal,
+ GnConfirmDialog,
GnDrawer,
GnToastProvider,
GnTabs,