diff --git a/demo/index.html b/demo/index.html index 5b02898..bccb1af 100644 --- a/demo/index.html +++ b/demo/index.html @@ -43,6 +43,7 @@ @@include('./partials/badges.html') @@include('./partials/alerts.html') @@include('./partials/modals.html') + @@include('./partials/confirm-dialog.html') @@include('./partials/drawer.html') @@include('./partials/toasts.html') @@include('./partials/tabs.html') diff --git a/demo/partials/confirm-dialog.html b/demo/partials/confirm-dialog.html new file mode 100644 index 0000000..9b24298 --- /dev/null +++ b/demo/partials/confirm-dialog.html @@ -0,0 +1,37 @@ +
+

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,