From 49bd26d5f385a64db9166a705e4c4c22a94099fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E4=BB=81?= Date: Fri, 14 Aug 2026 12:13:19 +0800 Subject: [PATCH 1/5] fix: stack toasts and non-modal dialogs above page chrome Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ README.md | 28 ++++++++++++++++++++++++++++ build.mjs | 9 ++++++++- dist/dialogify.cjs | 2 +- dist/dialogify.cjs.map | 4 ++-- dist/dialogify.css | 2 +- dist/dialogify.min.js | 2 +- dist/dialogify.min.js.map | 4 ++-- dist/dialogify.mjs | 2 +- dist/dialogify.mjs.map | 4 ++-- docs/css/dialogify.css | 2 +- docs/js/dialogify.min.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/css/dialogify.css | 2 +- src/scss/dialogify.scss | 18 ++++++++++++++++-- test/features.test.js | 20 ++++++++++++++++++++ 17 files changed, 93 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cad2120..f94b1b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.2.2 + fix: toasts and non-modal dialogs no longer hide behind site chrome such as a sticky nav; the z-index defaults are now 1010 and 1000 + feat: --dialogify-z-index and --dialogify-toast-z-index custom properties to fit another layering scheme + chore: postcss no longer emits a static fallback next to every var(), which is dead weight for the browsers this build targets + ## 2.2.1 chore: build target raised from es2018 to es2022, matching the browsers that ship a native chore: replaced the deprecated jQuery `.click()` shorthand with `.on('click')`/`.trigger('click')`, so the library is clean under jQuery 4 diff --git a/README.md b/README.md index eb515f7..53b8880 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,34 @@ its own height at the same time, so the toasts stacked below it slide up instead of jumping when it is removed. All of them are disabled under `prefers-reduced-motion: reduce`. +### Stacking (`z-index`) + +`showModal()` promotes a dialog to the browser's top layer, which always paints +above the page, so `z-index` never applies to modal dialogs. Everything else — +`show()`, `showAt()` popovers and toasts — stays in the normal stacking context +and has to out-stack the rest of the page. + +The defaults are deliberately high so a toast is not hidden behind a sticky +header: + +| Custom property | Default | Applies to | +| --------------------------- | ------- | ------------------------------ | +| `--dialogify-z-index` | `1000` | Non-modal dialogs and popovers | +| `--dialogify-toast-z-index` | `1010` | The toast container | + +Override them anywhere in your own CSS to fit a different layering scheme: + +```css +:root { + --dialogify-z-index: 500; + --dialogify-toast-z-index: 510; +} +``` + +The bundled stylesheet only reads these properties (they are `var()` fallbacks, +never declared by the library), so your declaration wins regardless of source +order — even though the browser build injects its stylesheet last. + ## Accessibility - `title()` links the heading to the dialog with `aria-labelledby`. diff --git a/build.mjs b/build.mjs index 9b1e8c8..d333d57 100644 --- a/build.mjs +++ b/build.mjs @@ -25,7 +25,14 @@ async function buildCss() { async transform(source) { const { css } = await postcss([ autoprefixer, - postcssPresetEnv({ stage: 0 }) + // Custom properties are the public theming hook. The + // transform would prepend a static fallback to every + // var(), which is dead weight for the browsers this + // build targets. + postcssPresetEnv({ + stage: 0, + features: { 'custom-properties': false } + }) ]).process(source, { from: undefined }); return css; } diff --git a/dist/dialogify.cjs b/dist/dialogify.cjs index b2c2292..5683d94 100644 --- a/dist/dialogify.cjs +++ b/dist/dialogify.cjs @@ -1,3 +1,3 @@ -var W=Object.create;var M=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var $=Object.getPrototypeOf,V=Object.prototype.hasOwnProperty;var U=(n,i)=>{for(var t in i)M(n,t,{get:i[t],enumerable:!0})},P=(n,i,t,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let e of Y(i))!V.call(n,e)&&e!==t&&M(n,e,{get:()=>i[e],enumerable:!(o=G(i,e))||o.enumerable});return n};var L=(n,i,t)=>(t=n!=null?W($(n)):{},P(i||!n||!n.__esModule?M(t,"default",{value:n,enumerable:!0}):t,n)),F=n=>P(M({},"__esModule",{value:!0}),n);var pi={};U(pi,{default:()=>ui});module.exports=F(pi);var a=L(require("jquery"),1);function z(){let n=window.dialogifyConfig;return(n==null||typeof n!="object")&&(n={}),n.locale=n.locale||"zh_TW",n}function S(n,i,t){if(!Object.prototype.hasOwnProperty.call(n,i)){let o=window.HTMLDialogElement&&window.HTMLDialogElement.prototype;if(o&&typeof o[i]=="function")return o[i].apply(n,t||[])}return n[i].apply(n,t||[])}function m(n,i,t){if(typeof window.CustomEvent!="function")return!0;let o=new window.CustomEvent(i,{bubbles:!1,cancelable:!0,detail:t});return n.dispatchEvent(o),!o.defaultPrevented}function I(){let n=z();return s.LOCALE[n.locale]||s.LOCALE.zh_TW}var Q=["left","right","top","bottom"],X=["top-left","top-center","top-right","bottom-left","bottom-center","bottom-right"],k="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMzZweCcgaGVpZ2h0PSczNnB4JyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgY2xhc3M9InVpbC1kZWZhdWx0Ij48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0ibm9uZSIgY2xhc3M9ImJrIj48L3JlY3Q+PHJlY3QgIHg9JzQ2LjUnIHk9JzQwJyB3aWR0aD0nNycgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjMDA5NDk5JyB0cmFuc2Zvcm09J3JvdGF0ZSgwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMHMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0Ni41JyB5PSc0MCcgd2lkdGg9JzcnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nIzAwOTQ5OScgdHJhbnNmb3JtPSdyb3RhdGUoMzAgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMXMnIGJlZ2luPScwLjA4MzMzMzMzMzMzMzMzMzMzcycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PHJlY3QgIHg9JzQ2LjUnIHk9JzQwJyB3aWR0aD0nNycgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjMDA5NDk5JyB0cmFuc2Zvcm09J3JvdGF0ZSg2MCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScxcycgYmVnaW49JzAuMTY2NjY2NjY2NjY2NjY2NjZzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYuNScgeT0nNDAnIHdpZHRoPSc3JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyMwMDk0OTknIHRyYW5zZm9ybT0ncm90YXRlKDkwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMC4yNXMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0Ni41JyB5PSc0MCcgd2lkdGg9JzcnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nIzAwOTQ5OScgdHJhbnNmb3JtPSdyb3RhdGUoMTIwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMC4zMzMzMzMzMzMzMzMzMzMzcycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PHJlY3QgIHg9JzQ2LjUnIHk9JzQwJyB3aWR0aD0nNycgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjMDA5NDk5JyB0cmFuc2Zvcm09J3JvdGF0ZSgxNTAgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMXMnIGJlZ2luPScwLjQxNjY2NjY2NjY2NjY2NjdzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYuNScgeT0nNDAnIHdpZHRoPSc3JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyMwMDk0OTknIHRyYW5zZm9ybT0ncm90YXRlKDE4MCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScxcycgYmVnaW49JzAuNXMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0Ni41JyB5PSc0MCcgd2lkdGg9JzcnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nIzAwOTQ5OScgdHJhbnNmb3JtPSdyb3RhdGUoMjEwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMC41ODMzMzMzMzMzMzMzMzM0cycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PHJlY3QgIHg9JzQ2LjUnIHk9JzQwJyB3aWR0aD0nNycgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjMDA5NDk5JyB0cmFuc2Zvcm09J3JvdGF0ZSgyNDAgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMXMnIGJlZ2luPScwLjY2NjY2NjY2NjY2NjY2NjZzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYuNScgeT0nNDAnIHdpZHRoPSc3JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyMwMDk0OTknIHRyYW5zZm9ybT0ncm90YXRlKDI3MCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScxcycgYmVnaW49JzAuNzVzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYuNScgeT0nNDAnIHdpZHRoPSc3JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyMwMDk0OTknIHRyYW5zZm9ybT0ncm90YXRlKDMwMCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScxcycgYmVnaW49JzAuODMzMzMzMzMzMzMzMzMzNHMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0Ni41JyB5PSc0MCcgd2lkdGg9JzcnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nIzAwOTQ5OScgdHJhbnNmb3JtPSdyb3RhdGUoMzMwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMC45MTY2NjY2NjY2NjY2NjY2cycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PC9zdmc+";function K(n){return String(n??"").replace(/[&<>"']/g,function(i){return{"&":"&","<":"<",">":">",'"':""","'":"'"}[i]})}function B(n,i,t){return Math.min(Math.max(n,i),t)}function A(n,i,t,o){return n=="center"?i+t/2-o/2:n=="end"?i+t-o:i}var q=600;function ii(n,i){let t=typeof n.getAnimations=="function"?n.getAnimations():[];if(!t.length){i();return}let o=!1,e=function(){o||(o=!0,i())};Promise.all(t.map(function(d){return d.finished.catch(function(){})})).then(e),window.setTimeout(e,q)}function ti(n,i){let t=n.getBoundingClientRect();return!t.width&&!t.height?!0:i.clientXt.right||i.clientYt.bottom}var s=class n{constructor(i,t){this._setup(window.document.createElement("dialog"),i,t,{append:!0})}_setup(i,t,o,e){let d=z();(e==null||typeof e!="object")&&(e={}),window.dialogPolyfill&&typeof window.dialogPolyfill.registerDialog=="function"&&window.dialogPolyfill.registerDialog(i),(o==null||typeof o!="object")&&(o={}),this.options=o;let r=o.ajaxPrefix===void 0?"/ajax/":o.ajaxPrefix,l=this,c,g=!1,u=e.content||null;u?c=null:t.charAt(0)=="#"?c=(0,a.default)(t).html():t.indexOf(r)==0?(c=`
${K(I().loading)}
`,g=!0):c=t,c=c==null?null:`
${c}
`;let H=o.size||"dialogify__fixedwidth",w="dialogify";o.fixed!==!1&&(w+=" fixed"),Q.indexOf(o.position)>=0&&(w+=` dialogify--drawer dialogify--${o.position}`),o.variant&&(w+=` dialogify--${o.variant}`);let j=`
`;o.useDialogForm!==!1&&(j=`
${j}
`),n.counter++,this.id=i.id||`dialogify_${n.counter}`;let v=(0,a.default)(i).attr("id",this.id).addClass(w).append(j).on("close",function(){l._teardownAnchor(),(0,a.default)(l).triggerHandler("close"),o.backgroundScroll===!1&&(0,a.default)("body").css({overflow:"","padding-right":""}),(0,a.default)(i).addClass("dialogify--closing"),l._onClosing&&l._onClosing(),ii(i,function(){(0,a.default)(i).removeClass("dialogify--closing"),o.autoRemove!==!1&&(l._resizeObserver&&l._resizeObserver.disconnect(),(0,a.default)(i).remove()),l._onExit&&l._onExit()}),l._resolveClosePromise()}).on("cancel",function(f){(0,a.default)(l).triggerHandler("cancel"),o.closable===!1||!l._fireBeforeClose()?f.preventDefault():l._markClosing()}).on("click",function(f){o.closable!==!1&&f.target==i&&ti(i,f)&&m(i,"cancel")&&l.close()});if(o.useDialogForm!==!1&&v.find('form[method="dialog"]').on("submit",function(f){l._fireBeforeClose(this.returnValue)?l._markClosing():f.preventDefault()}),e.append!==!1&&v.appendTo("body"),o.closable!==!1){let f="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDE2IDE2OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3Qwe2ZpbGw6IzA1NTY2Njt9PC9zdHlsZT48cmVjdCB4PSIzLjEiIHk9IjEiIGNsYXNzPSJzdDAiIHdpZHRoPSIxLjciIGhlaWdodD0iMSIvPjxyZWN0IHg9IjIuMSIgeT0iMi4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMSIgeT0iMy4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxLjciLz48cmVjdCB4PSIxIiB5PSIxMS4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxLjciLz48cmVjdCB4PSIyLjEiIHk9IjEyLjkiIGNsYXNzPSJzdDAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIzLjEiIHk9IjEzLjkiIGNsYXNzPSJzdDAiIHdpZHRoPSIxLjciIGhlaWdodD0iMSIvPjxnPjxyZWN0IHg9IjQuOSIgY2xhc3M9InN0MCIgd2lkdGg9IjYuMyIgaGVpZ2h0PSIxIi8+PC9nPjxyZWN0IHg9IjQuOSIgeT0iMTUiIGNsYXNzPSJzdDAiIHdpZHRoPSI2LjMiIGhlaWdodD0iMSIvPjxyZWN0IHk9IjQuOSIgY2xhc3M9InN0MCIgd2lkdGg9IjEiIGhlaWdodD0iNi4zIi8+PHJlY3QgeD0iMTEuMSIgeT0iMSIgY2xhc3M9InN0MCIgd2lkdGg9IjEuNyIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTIuOSIgeT0iMi4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTMuOSIgeT0iMy4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxLjciLz48cmVjdCB4PSIxMy45IiB5PSIxMS4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxLjciLz48cmVjdCB4PSIxMi45IiB5PSIxMi45IiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTEuMSIgeT0iMTMuOSIgY2xhc3M9InN0MCIgd2lkdGg9IjEuNyIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTUiIHk9IjQuOSIgY2xhc3M9InN0MCIgd2lkdGg9IjEiIGhlaWdodD0iNi4zIi8+PHBvbHlnb24gY2xhc3M9InN0MCIgcG9pbnRzPSI5LjcsMy44IDkuNyw0LjkgOC43LDQuOSA4LjcsNS45IDcuMyw1LjkgNy4zLDQuOSA2LjMsNC45IDYuMywzLjggMy44LDMuOCAzLjgsNS4yIDQuOSw1LjIgNC45LDYuMyA1LjksNi4zIDUuOSw3LjMgNyw3LjMgNyw4LjMgNS45LDguMyA1LjksOS40IDQuOSw5LjQgNC45LDEwLjQgMy44LDEwLjQgMy44LDEwLjggMy44LDExLjggNi4zLDExLjggNi4zLDEwLjggNy4zLDEwLjggNy4zLDkuNyA4LjcsOS43IDguNywxMC44IDkuNywxMC44IDkuNywxMS44IDEyLjIsMTEuOCAxMi4yLDEwLjQgMTEuMSwxMC40IDExLjEsOS40IDEwLjEsOS40IDEwLjEsOC4zIDksOC4zIDksNy4zIDEwLjEsNy4zIDEwLjEsNi4zIDExLjEsNi4zIDExLjEsNS4yIDEyLjIsNS4yIDEyLjIsNC45IDEyLjIsMy44ICIvPjwvc3ZnPg==",x="",h={},b=o.closeButton||d.closeButton;b!=null&&typeof b=="object"&&(f=b.image||f,x=b.className||x,h=b.style||h);let R=(0,a.default)("").attr({src:f,alt:""}),J=(0,a.default)("").addClass("dialogify__close").attr({role:"button",tabindex:0,"aria-label":I().close}).css(h).append(R).on("click",function(){m(i,"cancel")&&l.close()}).on("keydown",function(_){(_.key=="Enter"||_.key==" "||_.key=="Spacebar")&&(_.preventDefault(),(0,a.default)(this).trigger("click"))});x&&J.addClass(x),(0,a.default)(i).append(J)}let p=o.dialog||d.dialog;if(p!=null&&typeof p=="object"){(0,a.default)(i).css(p.style||{}),p.className&&(0,a.default)(i).addClass(p.className);let f=(0,a.default)(i).find(".dialogify__content");f.css(p.contentStyle||{}),p.contentClassName&&f.addClass(p.contentClassName)}if(this.$content=(0,a.default)(i).find(".dialogify__content > div"),u?this.$content.append((0,a.default)("
").addClass("dialogify__body").append(u)):this.$content.append(c),this.$body=this.$content.find(".dialogify__body"),this.$form=(0,a.default)(i).find('form[method="dialog"]'),g){let f=t,x=o.ajaxData||{};try{let h=new URL(t,window.location.href);Object.keys(x).forEach(function(b){h.searchParams.append(b,x[b])}),f=h.toString()}catch{}window.fetch(f,{headers:{"X-Requested-With":"XMLHttpRequest"}}).then(function(h){return h.text()}).then(function(h){o.ajaxComplete&&o.ajaxComplete.call(l),l.$content.find(".dialogify-ajax-content").html(h)})}this.dialog=i,window.ResizeObserver&&!(0,a.default)(i).is(".dialogify--drawer, .dialogify--toast")&&(this._resizeObserver=new window.ResizeObserver(function(){(0,a.default)(i).is(".dialogify--popover")||ai(i)}),this._resizeObserver.observe(i))}_fireBeforeClose(i){let t=a.default.Event("beforeclose");return t.returnValue=i,(0,a.default)(this).triggerHandler(t),m(this.dialog,"beforeclose",{returnValue:i})&&!t.isDefaultPrevented()}_resolveClosePromise(){if(this._closeResolver){let i=this._closeResolver;this._closeResolver=null,this._closePromise=null,i(this.dialog.returnValue)}}_openPromise(){let i=this;return(0,a.default)(this.dialog).removeClass("dialogify--closing"),this._closePromise||(this._closePromise=new Promise(function(t){i._closeResolver=t})),this._closePromise}showModal(){this.options.backgroundScroll===!1&&E();let i=this._openPromise();return S(this.dialog,"showModal"),m(this.dialog,"show"),(0,a.default)(this).triggerHandler("show"),i}show(){this.options.backgroundScroll===!1&&E();let i=this._openPromise();return S(this.dialog,"show"),m(this.dialog,"show"),(0,a.default)(this).triggerHandler("show"),i}showAt(i,t){let o=(0,a.default)(i);if(!o.length)return this.show();(t==null||typeof t!="object")&&(t={}),this._anchor=o[0],this._anchorOptions=t,(0,a.default)(this.dialog).addClass("dialogify--popover").removeClass("fixed");let e=this.show();this.reposition();let d=this;return this._anchorListener=function(){d.reposition()},window.addEventListener("scroll",this._anchorListener,!0),window.addEventListener("resize",this._anchorListener),this.options.closable!==!1&&(this._outsideListener=function(r){if(d.dialog.contains(r.target))return;let l=t.toggle!==!1&&d._anchor.contains(r.target);m(d.dialog,"cancel")&&(l&&(r.preventDefault(),r.stopImmediatePropagation()),d.close())},this._outsideTimer=window.setTimeout(function(){d._outsideListener&&window.document.addEventListener("click",d._outsideListener,!0)},0)),e}reposition(){if(!this._anchor||!this.dialog.open)return this;let i=this._anchorOptions||{},t=i.offset===void 0?8:i.offset,o=i.align||"start",e=this._anchor.getBoundingClientRect(),d=this.dialog.getBoundingClientRect(),r=window.innerWidth||0,l=window.innerHeight||0,c=i.placement||"bottom";c=="bottom"&&e.bottom+t+d.height>l?c="top":c=="top"&&e.top-t-d.height<0?c="bottom":c=="right"&&e.right+t+d.width>r?c="left":c=="left"&&e.left-t-d.width<0&&(c="right");let g,u;return c=="top"||c=="bottom"?(g=c=="bottom"?e.bottom+t:e.top-t-d.height,u=A(o,e.left,e.width,d.width)):(u=c=="right"?e.right+t:e.left-t-d.width,g=A(o,e.top,e.height,d.height)),u=B(u,0,Math.max(0,r-d.width)),g=B(g,0,Math.max(0,l-d.height)),(0,a.default)(this.dialog).attr("data-placement",c).css({position:"fixed",top:`${Math.round(g)}px`,left:`${Math.round(u)}px`,right:"auto",bottom:"auto",transform:"none",margin:0}),this}_teardownAnchor(){this._anchorListener&&(window.removeEventListener("scroll",this._anchorListener,!0),window.removeEventListener("resize",this._anchorListener),this._anchorListener=null),this._outsideTimer&&(window.clearTimeout(this._outsideTimer),this._outsideTimer=null),this._outsideListener&&(window.document.removeEventListener("click",this._outsideListener,!0),this._outsideListener=null),this._anchor=null}_markClosing(){this.dialog.open&&(0,a.default)(this.dialog).addClass("dialogify--closing")}close(i){return this._fireBeforeClose(i)?(this._markClosing(),S(this.dialog,"close",i===void 0?[]:[i]),this):this}isOpen(){return this.dialog.open}on(i,t){return(0,a.default)(this).on(i,t),this}off(i,t){return(0,a.default)(this).off(i,t),this}setContent(i){return i instanceof window.Node||i instanceof a.default?this.$body.empty().append(i):this.$body.html(i??""),this}getContent(){return this.$body.html()}load(i,t){let o=this,e=i;try{let d=new URL(i,window.location.href),r=t||{};Object.keys(r).forEach(function(l){d.searchParams.append(l,r[l])}),e=d.toString()}catch{}return this.setLoading(!0),window.fetch(e,{headers:{"X-Requested-With":"XMLHttpRequest"}}).then(function(d){return d.text()}).then(function(d){return o.setContent(d),o.setLoading(!1),o.options.ajaxComplete&&o.options.ajaxComplete.call(o),o}).catch(function(d){throw o.setLoading(!1),d})}setLoading(i){if(i===!1)return this.$content.find(".dialogify__loading").remove(),(0,a.default)(this.dialog).removeClass("is-loading"),this;if(!this.$content.find(".dialogify__loading").length){let t=typeof i=="string"?i:I().loading;this.$content.append((0,a.default)("
").addClass("dialogify__loading").attr({role:"status","aria-live":"polite"}).append((0,a.default)("").attr({src:k,alt:t}))),(0,a.default)(this.dialog).addClass("is-loading")}return this}isLoading(){return this.$content.find(".dialogify__loading").length>0}validate(){let i=this.$form[0];return!i||typeof i.reportValidity!="function"?!0:i.reportValidity()}formData(){let i=this.$form[0];return!i||typeof window.FormData!="function"?null:new window.FormData(i)}formValues(){let i=this.formData();if(!i)return{};let t={};return i.forEach(function(o,e){Object.prototype.hasOwnProperty.call(t,e)?(Array.isArray(t[e])||(t[e]=[t[e]]),t[e].push(o)):t[e]=o}),t}title(i){let t=`${this.id}_title`,o=(0,a.default)("
").addClass("dialogify_title").attr("id",t).append('').append(i);return this.$content.find(".dialogify_title").length?this.$content.find(".dialogify_title").replaceWith(o):this.$content.prepend(o),(0,a.default)(this.dialog).attr("aria-labelledby",t),this}_createButton(i,t){let o=this;if(typeof i=="string"){let d=(0,a.default)(i);return{id:d.attr("id")||t,$button:d}}(i==null||typeof i!="object")&&(i={});let e=(0,a.default)(' + + + Dialogify + 2.3.0 + + +
+ + Star + + + + + + + +
-
- Dialogify -
- - - - - - - - - - - - - - - + + + + + + + + + diff --git a/docs/js/docs.js b/docs/js/docs.js new file mode 100644 index 0000000..8525885 --- /dev/null +++ b/docs/js/docs.js @@ -0,0 +1,892 @@ +/* Dialogify documentation site behaviour: theme, language, navigation and the + live demos. Written as a plain script so the page needs no build step. */ +(function () { + 'use strict'; + + const THEME_KEY = 'dialogify-docs-theme'; + const LANG_KEY = 'dialogify-docs-lang'; + const THEMES = ['auto', 'light', 'dark']; + + /* --- theme --------------------------------------------------------- */ + + const root = document.documentElement; + + function currentTheme() { + const value = root.dataset.theme; + return THEMES.indexOf(value) >= 0 ? value : 'auto'; + } + + function applyTheme(theme) { + root.dataset.theme = theme; + try { + if (theme === 'auto') { + localStorage.removeItem(THEME_KEY); + } else { + localStorage.setItem(THEME_KEY, theme); + } + } catch { + /* storage can be blocked */ + } + + document.querySelectorAll('[data-theme-icon]').forEach(function (icon) { + icon.hidden = icon.dataset.themeIcon !== theme; + }); + + const dark = theme === 'dark' || (theme === 'auto' && prefersDark.matches); + + // highlight.js ships one stylesheet per theme, so they are swapped + // rather than re-declared. + document.querySelectorAll('[data-hljs]').forEach(function (sheet) { + sheet.disabled = (sheet.dataset.hljs === 'dark') !== dark; + }); + + // The star button is an iframe: its colour scheme is baked in when it + // renders, so it has to be rebuilt on every change. + renderGitHubButtons(dark ? 'dark' : 'light'); + } + + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + prefersDark.addEventListener('change', function () { + if (currentTheme() === 'auto') { + applyTheme('auto'); + } + }); + + document.querySelector('[data-theme-toggle]').addEventListener('click', function () { + applyTheme(THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length]); + }); + + function renderGitHubButtons(scheme) { + const host = document.querySelector('[data-gh-buttons]'); + if (!host) { + return; + } + + if (!host.dataset.template) { + host.dataset.template = host.innerHTML; + } + + if (!window.GitHubButton || !window.GitHubButton.render) { + // buttons.js is async: try again once it has landed. + clearTimeout(renderGitHubButtons.retry); + renderGitHubButtons.retry = setTimeout(function () { + renderGitHubButtons(scheme); + }, 100); + return; + } + + host.innerHTML = host.dataset.template; + host.querySelectorAll('.github-button').forEach(function (anchor) { + // buttons.js expects a full scheme map, not a bare keyword. + anchor.setAttribute( + 'data-color-scheme', + 'no-preference: ' + scheme + '; light: ' + scheme + '; dark: ' + scheme + ';' + ); + window.GitHubButton.render(anchor); + }); + } + + // buttons.js renders on load; re-render once it is available so the very + // first paint also matches the current theme. + window.addEventListener('load', function () { + applyTheme(currentTheme()); + }); + + /* --- language ------------------------------------------------------ */ + + const dictionary = window.DIALOGIFY_DOCS_EN || {}; + const originals = new Map(); + let lang = 'zh-TW'; + + function captureOriginals() { + document.querySelectorAll('[data-i18n]').forEach(function (el) { + originals.set(el, el.innerHTML); + }); + document.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) { + originals.set(el.dataset.i18nPlaceholder, el.placeholder); + }); + } + + function applyLang(next) { + lang = next === 'en' ? 'en' : 'zh-TW'; + const english = lang === 'en'; + + document.querySelectorAll('[data-i18n]').forEach(function (el) { + const key = el.dataset.i18n; + if (!english) { + el.innerHTML = originals.get(el); + return; + } + + if (Object.prototype.hasOwnProperty.call(dictionary, key)) { + el.innerHTML = dictionary[key]; + } else { + // Leaving the Chinese in place is better than blanking the page. + console.warn('[docs] missing English string:', key); + } + }); + + document.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) { + const key = el.dataset.i18nPlaceholder; + el.placeholder = english && dictionary[key] ? dictionary[key] : originals.get(key); + }); + + document.documentElement.lang = english ? 'en' : 'zh-Hant-TW'; + document.title = english ? dictionary['page.title'] : originals.get('page.title'); + + // Replacing innerHTML wipes the highlighting, so code blocks are redone. + document.querySelectorAll('code[data-i18n]').forEach(function (code) { + if (window.hljs) { + delete code.dataset.highlighted; + code.classList.remove('hljs'); + window.hljs.highlightElement(code); + } + }); + + document.querySelectorAll('[data-lang]').forEach(function (button) { + button.setAttribute('aria-pressed', String(button.dataset.lang === lang)); + }); + + try { + localStorage.setItem(LANG_KEY, lang); + } catch { + /* storage can be blocked */ + } + + buildNav(); + } + + document.querySelectorAll('[data-lang]').forEach(function (button) { + button.addEventListener('click', function () { + applyLang(button.dataset.lang); + }); + }); + + function initialLang() { + const fromQuery = new URLSearchParams(location.search).get('lang'); + if (fromQuery === 'en' || fromQuery === 'zh-TW') { + return fromQuery; + } + try { + return localStorage.getItem(LANG_KEY) === 'en' ? 'en' : 'zh-TW'; + } catch { + return 'zh-TW'; + } + } + + /* --- navigation ---------------------------------------------------- */ + + const navList = document.querySelector('[data-nav]'); + const tocList = document.querySelector('[data-toc]'); + + function buildNav() { + navList.innerHTML = ''; + document.querySelectorAll('.content section[id]').forEach(function (section) { + const heading = section.querySelector('h2'); + if (!heading) { + return; + } + + const item = document.createElement('li'); + item.appendChild(link('#' + section.id, heading.textContent.trim())); + + const subs = section.querySelectorAll('h3[id]'); + if (subs.length) { + const sublist = document.createElement('ul'); + sublist.className = 'sidebar__sub'; + subs.forEach(function (sub) { + const subItem = document.createElement('li'); + subItem.appendChild(link('#' + sub.id, sub.textContent.trim())); + sublist.appendChild(subItem); + }); + item.appendChild(sublist); + } + + navList.appendChild(item); + }); + + buildToc(); + } + + function link(href, text) { + const anchor = document.createElement('a'); + anchor.href = href; + anchor.textContent = text; + return anchor; + } + + function buildToc() { + if (!tocList) { + return; + } + + tocList.innerHTML = ''; + const active = document.querySelector('.sidebar a[aria-current]'); + const sectionId = active + ? active.getAttribute('href').slice(1) + : (document.querySelector('.content section[id]') || {}).id; + const section = sectionId && document.getElementById(sectionId); + const target = section && section.closest ? section.closest('section[id]') : null; + if (!target) { + return; + } + + target.querySelectorAll('h3[id]').forEach(function (heading) { + const item = document.createElement('li'); + item.appendChild(link('#' + heading.id, heading.textContent.trim())); + tocList.appendChild(item); + }); + } + + // Highlight whichever heading is closest to the top of the viewport. + const headings = function () { + return Array.prototype.slice.call( + document.querySelectorAll('.content section[id], .content h3[id]') + ); + }; + + let ticking = false; + function updateCurrent() { + ticking = false; + const offset = 120; + let current = null; + + headings().forEach(function (element) { + if (element.getBoundingClientRect().top <= offset) { + current = element; + } + }); + + const id = current ? current.id : null; + let changed = false; + + document.querySelectorAll('.sidebar a, .toc a').forEach(function (anchor) { + const match = anchor.getAttribute('href') === '#' + id; + if (match !== anchor.hasAttribute('aria-current')) { + changed = true; + } + if (match) { + anchor.setAttribute('aria-current', 'true'); + } else { + anchor.removeAttribute('aria-current'); + } + }); + + if (changed) { + buildToc(); + updateTocCurrent(); + } + } + + function updateTocCurrent() { + const offset = 120; + let current = null; + document.querySelectorAll('.content h3[id]').forEach(function (heading) { + if (heading.getBoundingClientRect().top <= offset) { + current = heading; + } + }); + + document.querySelectorAll('.toc a').forEach(function (anchor) { + if (current && anchor.getAttribute('href') === '#' + current.id) { + anchor.setAttribute('aria-current', 'true'); + } else { + anchor.removeAttribute('aria-current'); + } + }); + } + + window.addEventListener( + 'scroll', + function () { + if (!ticking) { + ticking = true; + window.requestAnimationFrame(function () { + updateCurrent(); + updateTocCurrent(); + }); + } + }, + { passive: true } + ); + + /* --- sidebar filter and mobile drawer ------------------------------ */ + + const filter = document.querySelector('[data-filter]'); + const filterEmpty = document.querySelector('[data-filter-empty]'); + + filter.addEventListener('input', function () { + const term = filter.value.trim().toLowerCase(); + let visible = 0; + + navList.querySelectorAll(':scope > li').forEach(function (item) { + const own = item.querySelector('a').textContent.toLowerCase(); + let show = !term || own.indexOf(term) >= 0; + + item.querySelectorAll('.sidebar__sub li').forEach(function (sub) { + const match = !term || sub.textContent.toLowerCase().indexOf(term) >= 0 || show; + sub.hidden = !match; + if (match && term) { + show = true; + } + }); + + item.hidden = !show; + if (show) { + visible++; + } + }); + + filterEmpty.hidden = visible > 0; + }); + + const sidebar = document.getElementById('sidebar'); + const menuButton = document.querySelector('[data-toggle-sidebar]'); + + menuButton.addEventListener('click', function () { + const open = sidebar.toggleAttribute('data-open'); + menuButton.setAttribute('aria-expanded', String(open)); + }); + + sidebar.addEventListener('click', function (event) { + if (event.target.tagName === 'A') { + sidebar.removeAttribute('data-open'); + menuButton.setAttribute('aria-expanded', 'false'); + } + }); + + /* --- code blocks --------------------------------------------------- */ + + function decorateCode() { + document.querySelectorAll('pre > code').forEach(function (code) { + if (window.hljs) { + window.hljs.highlightElement(code); + } + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'copy-button'; + button.textContent = 'Copy'; + button.addEventListener('click', function () { + navigator.clipboard.writeText(code.textContent).then(function () { + button.textContent = 'Copied'; + button.dataset.copied = ''; + setTimeout(function () { + button.textContent = 'Copy'; + delete button.dataset.copied; + }, 1400); + }); + }); + code.parentNode.appendChild(button); + }); + } + + /* --- theme token table --------------------------------------------- */ + + const TOKENS = [ + ['--dialogify-surface', '#fff', 'token.surface'], + ['--dialogify-text', '#464646', 'token.text'], + ['--dialogify-heading', '#00555f', 'token.heading'], + ['--dialogify-muted', '#a6a6a6', 'token.muted'], + ['--dialogify-divider', '#e0e6e8', 'token.divider'], + ['--dialogify-link', '#117e96', 'token.link'], + ['--dialogify-link-hover', '#126e7d', 'token.linkHover'], + ['--dialogify-focus-ring', '#117e96', 'token.focus'], + ['--dialogify-icon-filter', 'none', 'token.iconFilter'], + ['--dialogify-shadow', '—', 'token.shadow'], + ['--dialogify-loading-overlay', 'rgba(255,255,255,.75)', 'token.loading'], + ['--dialogify-button-bg', '#e5e5e5', 'token.buttonBg'], + ['--dialogify-button-hover-bg', '#dcdcdc', 'token.buttonHover'], + ['--dialogify-button-text', '#a6a6a6', 'token.buttonText'], + ['--dialogify-primary', '#117e96', 'token.primary'], + ['--dialogify-primary-hover', '#126e7d', 'token.primaryHover'], + ['--dialogify-on-primary', '#fff', 'token.onPrimary'], + ['--dialogify-danger', '#f44336', 'token.danger'], + ['--dialogify-danger-hover', '#de2427', 'token.dangerHover'], + ['--dialogify-on-danger', '#fff', 'token.onDanger'], + ['--dialogify-danger-shadow', '—', 'token.dangerShadow'], + ['--dialogify-field-bg', '#fff', 'token.fieldBg'], + ['--dialogify-field-border', '#d9d9d9', 'token.fieldBorder'], + ['--dialogify-field-text', '#00555f', 'token.fieldText'], + ['--dialogify-field-focus-border', '#117e96', 'token.fieldFocus'], + ['--dialogify-field-placeholder', '#b3b3b3', 'token.placeholder'], + ['--dialogify-error', '#de2427', 'token.error'], + ['--dialogify-toast-info', '#117e96', 'token.toastInfo'], + ['--dialogify-toast-success', '#2f9e44', 'token.toastSuccess'], + ['--dialogify-toast-warning', '#e8a33d', 'token.toastWarning'], + ['--dialogify-toast-error', '#de2427', 'token.toastError'], + ['--dialogify-toast-close', '#8d8d8d', 'token.toastClose'], + ['--dialogify-toast-close-hover', '#464646', 'token.toastCloseHover'], + ['--dialogify-backdrop', '#000', 'token.backdrop'], + ['--dialogify-backdrop-opacity', '0.7', 'token.backdropOpacity'], + ['--dialogify-z-index', '1000', 'token.z'], + ['--dialogify-toast-z-index', '1010', 'token.toastZ'] + ]; + + const TOKEN_LABELS_ZH = { + 'token.surface': '燈箱背景', + 'token.text': '內文文字', + 'token.heading': '標題與關閉鈕', + 'token.muted': '次要文字', + 'token.divider': '分隔線 <hr>', + 'token.link': '內容中的連結', + 'token.linkHover': '連結 hover', + 'token.focus': ':focus-visible 外框', + 'token.iconFilter': '內建圖示的濾鏡', + 'token.shadow': '燈箱陰影', + 'token.loading': 'setLoading() 遮罩', + 'token.buttonBg': '一般按鈕', + 'token.buttonHover': '一般按鈕 hover', + 'token.buttonText': '一般按鈕文字', + 'token.primary': 'BUTTON_PRIMARY', + 'token.primaryHover': 'BUTTON_PRIMARY hover', + 'token.onPrimary': 'BUTTON_PRIMARY 文字', + 'token.danger': 'BUTTON_DANGER', + 'token.dangerHover': 'BUTTON_DANGER hover', + 'token.onDanger': 'BUTTON_DANGER 文字', + 'token.dangerShadow': '.btn-danger-shadow', + 'token.fieldBg': '.text-field 背景', + 'token.fieldBorder': '.text-field 邊框', + 'token.fieldText': '.text-field 文字', + 'token.fieldFocus': '.text-field 聚焦邊框', + 'token.placeholder': '.text-field 提示文字', + 'token.error': '.is-error 欄位', + 'token.toastInfo': '吐司狀態條 info', + 'token.toastSuccess': '吐司狀態條 success', + 'token.toastWarning': '吐司狀態條 warning', + 'token.toastError': '吐司狀態條 error', + 'token.toastClose': '吐司關閉鈕', + 'token.toastCloseHover': '吐司關閉鈕 hover', + 'token.backdrop': 'modal 背景遮罩', + 'token.backdropOpacity': 'modal 背景遮罩透明度', + 'token.z': '非 modal 燈箱與浮層', + 'token.toastZ': '吐司容器' + }; + + function buildTokenTable() { + const body = document.querySelector('[data-tokens]'); + if (!body) { + return; + } + + TOKENS.forEach(function (entry) { + const row = document.createElement('tr'); + + const name = document.createElement('td'); + name.innerHTML = '' + entry[0] + ''; + + const value = document.createElement('td'); + const isColour = /^#|^rgba?\(/.test(entry[1]); + value.innerHTML = + (isColour + ? '' + : '') + + '' + + entry[1] + + ''; + + const description = document.createElement('td'); + description.dataset.i18n = entry[2]; + description.innerHTML = TOKEN_LABELS_ZH[entry[2]] || entry[2]; + + row.append(name, value, description); + body.appendChild(row); + }); + } + + /* --- demos --------------------------------------------------------- */ + + const t = function (zh, en) { + return lang === 'en' ? en : zh; + }; + + let toastCorner = 0; + + const demos = { + hero: function () { + new Dialogify( + t( + '

這就是 Dialogify。內容可以是任意 HTML,' + + '底下的按鈕、標題與表單都由 API 控制。

', + '

This is Dialogify. The content is arbitrary HTML, and the ' + + 'buttons, title and form below are all driven by the API.

' + ) + ) + .title(t('哈囉!', 'Hello!')) + .buttons([{ type: Dialogify.BUTTON_PRIMARY }]) + .showModal(); + }, + + hello: function () { + new Dialogify('dialog content') + .title('dialog title') + .buttons([{ type: Dialogify.BUTTON_PRIMARY }]) + .showModal(); + }, + + modal: function () { + new Dialogify(t('我是 modal,會鎖住頁面。', 'I am modal and block the page.')) + .title('showModal()') + .showModal(); + }, + + modeless: function () { + new Dialogify( + t( + '我是 non-modal,頁面照常可以捲動與操作。', + 'I am non-modal — the page still scrolls and responds.' + ) + ) + .title('show()') + .show(); + }, + + await: async function () { + const dialog = new Dialogify(t('確定要送出嗎?', 'Ready to submit?')).buttons([ + { + text: 'Yes', + type: Dialogify.BUTTON_PRIMARY, + click: function () { + dialog.close('yes'); + } + }, + { + text: 'No', + click: function () { + dialog.close('no'); + } + } + ]); + + const answer = await dialog.showModal(); + Dialogify.toast( + t('您選了:', 'You picked: ') + (answer || t('(直接關閉)', '(dismissed)')), + { type: Dialogify.TOAST_INFO } + ); + }, + + setContent: function () { + const dialog = new Dialogify(t('第一步', 'Step 1')) + .title(t('設定精靈', 'Setup wizard')) + .buttons([ + { + text: t('下一步', 'Next'), + type: Dialogify.BUTTON_PRIMARY, + click: function () { + dialog.setContent( + '

' + + t( + '第二步:內容換掉了,標題與按鈕都還在。', + 'Step 2: the body was replaced, the title and buttons stayed.' + ) + + '

' + ); + } + } + ]); + dialog.showModal(); + }, + + load: function () { + const dialog = new Dialogify(t('準備載入…', 'About to load…')).title('load()').show(); + + // The docs site is static, so the request is simulated rather than real. + dialog.setLoading(true); + setTimeout(function () { + dialog.setLoading(false); + dialog.setContent( + '

' + + t( + '這裡會是 /ajax/… 回傳的內容。', + 'This would be the response from /ajax/….' + ) + + '

' + ); + }, 1200); + }, + + loading: function () { + const dialog = new Dialogify( + t('按下確定後會顯示載入狀態。', 'Press OK to show the loading state.') + ) + .title('setLoading()') + .buttons([ + { + text: 'OK', + type: Dialogify.BUTTON_PRIMARY, + click: function () { + dialog.setLoading(true); + setTimeout(function () { + dialog.setLoading(false); + }, 1500); + } + } + ]); + dialog.showModal(); + }, + + form: function () { + const dialog = new Dialogify( + '' + ) + .title(t('新增項目', 'New item')) + .buttons([ + { + id: 'save', + text: t('儲存', 'Save'), + type: Dialogify.BUTTON_PRIMARY, + loadingText: t('儲存中…', 'Saving…'), + click: function () { + if (!dialog.validate()) { + return; + } + + dialog.updateButton('save', { loading: true }); + setTimeout(function () { + const values = dialog.formValues(); + dialog.close('saved'); + Dialogify.toast(t('已儲存:', 'Saved: ') + values.title, { + type: Dialogify.TOAST_SUCCESS + }); + }, 900); + } + } + ]); + dialog.showModal(); + }, + + buttons: function () { + const dialog = new Dialogify(t('要刪除這筆資料嗎?', 'Delete this record?')).buttons( + [ + { + text: t('取消', 'Cancel'), + click: function () { + dialog.close(); + } + }, + { + id: 'del', + text: t('刪除', 'Delete'), + type: Dialogify.BUTTON_DANGER, + loadingText: t('刪除中…', 'Deleting…'), + click: function () { + dialog.updateButton('del', { loading: true }); + setTimeout(function () { + dialog.close(); + Dialogify.toast(t('已刪除', 'Deleted'), { + type: Dialogify.TOAST_SUCCESS + }); + }, 900); + } + } + ], + { position: Dialogify.BUTTON_CENTER } + ); + dialog.showModal(); + }, + + buttonState: function () { + const dialog = new Dialogify( + t('用底下的按鈕操作按鈕列。', 'Use the buttons below to drive the button row.') + ).buttons([ + { + id: 'target', + text: t('目標按鈕', 'Target'), + type: Dialogify.BUTTON_PRIMARY, + click: function () {} + }, + { + text: t('停用它', 'Disable it'), + click: function () { + dialog.updateButton('target', { disabled: true }); + } + }, + { + text: t('忙碌中', 'Busy'), + click: function () { + dialog.updateButton('target', { loading: true }); + setTimeout(function () { + dialog.updateButton('target', { loading: false }); + }, 1200); + } + } + ]); + dialog.showModal(); + }, + + beforeclose: function () { + let dirty = true; + const dialog = new Dialogify( + t( + '假設這裡有尚未儲存的變更,試著用 ESC 或關閉鈕關掉它。', + 'Pretend there are unsaved changes here, then try ESC or the close button.' + ) + ).title('beforeclose'); + + dialog.on('beforeclose', async function (event) { + if (!dirty) { + return; + } + + event.preventDefault(); + if ( + await Dialogify.confirm( + t('要放棄尚未儲存的變更嗎?', 'Discard your unsaved changes?') + ) + ) { + dirty = false; + dialog.close(); + } + }); + + dialog.showModal(); + }, + + drawer: function (button) { + new Dialogify( + '

' + + t('篩選條件', 'Filters') + + '

' + + t('抽屜會從指定的邊緣滑入。', 'A drawer slides in from the edge you pick.') + + '

', + { position: button.dataset.position } + ).showModal(); + }, + + toast: function (button) { + const type = button.dataset.type; + Dialogify.toast( + t('這是一則 ' + type + ' 通知', 'This is a ' + type + ' notification'), + { type: type, closable: true } + ); + }, + + toastPosition: function () { + const corners = [ + Dialogify.TOAST_TOP_LEFT, + Dialogify.TOAST_TOP_CENTER, + Dialogify.TOAST_BOTTOM_LEFT, + Dialogify.TOAST_BOTTOM_CENTER, + Dialogify.TOAST_BOTTOM_RIGHT, + Dialogify.TOAST_TOP_RIGHT + ]; + const position = corners[toastCorner++ % corners.length]; + Dialogify.toast(position, { position: position }); + }, + + popover: function (button) { + // The library swallows a click on the anchor of an open popover, so + // reaching this handler always means "open". Clicking a different + // trigger closes the previous one through the same outside-click + // path before this runs. + new Dialogify( + '' + + t('錨定浮層', 'Anchored popover') + + '

' + + t( + '它會跟著錨點捲動,空間不足時自動翻面。', + 'It follows the anchor on scroll and flips when space runs out.' + ) + + '

', + { useDialogForm: false } + ).showAt(button, { placement: 'bottom', align: 'start', offset: 8 }); + }, + + alert: function () { + Dialogify.alert(t('這是 alert。', 'This is an alert.')); + }, + + confirm: async function () { + const ok = await Dialogify.confirm(t('要繼續嗎?', 'Shall we continue?')); + Dialogify.toast(String(ok), { + type: ok ? Dialogify.TOAST_SUCCESS : Dialogify.TOAST_WARNING + }); + }, + + prompt: async function () { + const answer = await Dialogify.prompt(t('您的暱稱?', 'What is your name?'), { + placeholder: t('請輸入', 'Type here') + }); + Dialogify.toast(answer === null ? 'null' : answer); + }, + + closeAll: function () { + new Dialogify(t('一號燈箱', 'Dialog one')).show(); + new Dialogify(t('二號燈箱', 'Dialog two')).show(); + setTimeout(function () { + Dialogify.closeAll(); + Dialogify.toast(t('全部關閉了', 'All closed')); + }, 1200); + }, + + declarative: function () { + let dialog = document.getElementById('demo-declarative'); + if (!dialog) { + const host = document.createElement('div'); + host.innerHTML = + '' + + t('這個燈箱寫在 HTML 裡 ', 'This dialog is written in HTML ') + + 'bold text'; + dialog = host.firstElementChild; + document.body.appendChild(dialog); + } + + if (typeof dialog.showModal !== 'function' || !dialog.dialogify) { + Dialogify.alert( + t( + '這個瀏覽器不支援客製化內建元素,請載入選用的 custom-elements 套件。', + 'This browser has no customized built-in elements; load the optional custom-elements bundle.' + ) + ); + return; + } + + dialog.showModal(); + }, + + darkDialog: function () { + const previous = root.dataset.theme; + root.dataset.theme = 'dark'; + applyTheme('dark'); + + new Dialogify( + t( + '

這是 data-theme="dark" 下的燈箱。關閉後會還原原本的設定。

', + '

This is the dialog under data-theme="dark". Closing it restores your setting.

' + ) + ) + .title(t('暗色主題', 'Dark theme')) + .buttons([{ type: Dialogify.BUTTON_PRIMARY }]) + .showModal() + .then(function () { + applyTheme(THEMES.indexOf(previous) >= 0 ? previous : 'auto'); + }); + } + }; + + document.addEventListener('click', function (event) { + const button = event.target.closest('[data-demo]'); + if (!button) { + return; + } + + event.preventDefault(); + const demo = demos[button.dataset.demo]; + if (demo) { + demo(button); + } + }); + + /* --- boot ---------------------------------------------------------- */ + + originals.set('page.title', document.title); + buildTokenTable(); + captureOriginals(); + applyTheme(currentTheme()); + applyLang(initialLang()); + decorateCode(); + updateCurrent(); + updateTocCurrent(); +})(); diff --git a/docs/js/i18n.en.js b/docs/js/i18n.en.js new file mode 100644 index 0000000..841bcb3 --- /dev/null +++ b/docs/js/i18n.en.js @@ -0,0 +1,476 @@ +/* English strings for the documentation site. + The page ships with Traditional Chinese in the markup; this file only holds + the English replacements, keyed by the data-i18n attribute. */ +window.DIALOGIFY_DOCS_EN = { + 'page.title': 'Dialogify — a dialog toolkit built on <dialog>', + skip: 'Skip to content', + try: 'Try it', + 'toc.title': 'On this page', + 'nav.toggle': 'Toggle navigation', + 'nav.theme': 'Switch theme', + 'nav.filter': 'Filter sections…', + 'nav.noMatch': 'No matching section', + 'footer.design': 'Designed by', + 'footer.license': 'Licensed', + + 'hero.lead': + 'A dialog toolkit built on the native <dialog> element. It covers modal dialogs, drawers, toasts, anchored popovers and forms, and can also be written declaratively in HTML.', + 'hero.try': 'Try it now', + 'hero.start': 'Get started', + 'hero.api': 'API reference', + + 'start.h': 'Getting started', + 'start.install.h': 'Installation', + 'start.install.p': + 'jQuery is a peer dependency — install it alongside dialogify, or load it globally before the browser bundle.', + 'start.browser.h': 'Browser (global build)', + 'start.browser.p': 'The browser build injects its own CSS, so no extra stylesheet is required.', + 'start.module.h': 'Module (ESM / CommonJS)', + 'start.hello.h': 'Your first dialog', + + 'concepts.h': 'Core concepts', + 'concepts.modal.h': 'Modal and non-modal', + 'concepts.modal.p': + 'showModal() promotes the dialog to the browser top layer, where it always paints above the page, and handles focus trapping and ESC for you. show() leaves it in the normal stacking context, so the page stays usable.', + 'concepts.modal.try': 'Open a modal', + 'concepts.modeless.try': 'Open a non-modal', + 'concepts.chain.h': 'Chaining and promises', + 'concepts.chain.p': + 'Configuration methods return the instance so they can be chained; show() and showModal() return a promise that settles with returnValue once the dialog closes.', + 'concepts.life.h': 'Lifecycle', + 'concepts.life.p': + 'A dialog built with new Dialogify() is removed from the DOM once it closes (keep it with autoRemove: false); a declarative dialog written in HTML is never removed, so it can be shown again.', + + 'create.h': 'Creating a dialog', + 'create.ctor.h': 'Constructor', + 'create.ctor.p': + 'content is an HTML string. When it starts with ajaxPrefix (/ajax/ by default), the content is fetched from that URL instead.', + 'create.options.h': 'Options', + 'create.config.h': 'Global configuration', + 'create.config.p': + 'window.dialogifyConfig is read on every call, so changes take effect immediately.', + + 'opt.size': 'Set to Dialogify.SIZE_LARGE to grow with the content', + 'opt.closable': 'Show the close button in the corner', + 'opt.closeButton': + 'Custom close button image, className and style', + 'opt.fixed': 'Pin to the viewport with position: fixed', + 'opt.bgScroll': 'Allow the page behind the dialog to scroll', + 'opt.autoRemove': 'Remove the dialog from the DOM once it closes', + 'opt.useForm': 'Wrap the content in <form method="dialog">', + 'opt.position': + 'Turns the dialog into a drawer; left / right / top / bottom', + 'opt.ajaxPrefix': 'Prefix that marks the content as an ajax URL', + 'opt.ajaxData': 'Parameters sent with the ajax request', + 'opt.ajaxComplete': 'Called once the ajax content is in; this is the instance', + + 'content.h': 'Working with content', + 'content.basic.h': 'Title and body', + 'content.ajax.h': 'Loading over ajax', + 'content.loading.h': 'Loading state', + 'content.xss': + 'Security: the content is rendered as HTML — that is a deliberate feature, so markup and components can be dropped straight in. Always escape or sanitize untrusted or user-supplied input first to avoid XSS.', + + 'forms.h': 'Forms', + 'forms.p': + 'The content is wrapped in <form method="dialog"> by default, so native validation and FormData work out of the box.', + 'forms.validate': + 'Runs native validation, returns whether it passed and reports the first invalid field', + 'forms.values': 'Returns a plain object; repeated fields become arrays', + 'forms.data': + 'Returns a FormData, or null when useDialogForm is false', + + 'buttons.h': 'Buttons', + 'buttons.define.h': 'Defining buttons', + 'buttons.position': + 'The second argument takes a position of Dialogify.BUTTON_LEFT or Dialogify.BUTTON_CENTER; the default is right-aligned.', + 'buttons.manage.h': 'Managing them at runtime', + 'btn.text': 'Button label; falls back to the localized default', + 'btn.type': + 'Dialogify.BUTTON_PRIMARY or Dialogify.BUTTON_DANGER; omit for the default style', + 'btn.id': 'Used to address the button from updateButton() and friends', + 'btn.click': 'Click handler; without one the button closes the dialog', + 'btn.loadingText': 'Label shown while the button is busy', + + 'events.h': 'Events', + 'ev.show': 'After the dialog is shown', + 'ev.beforeclose': 'Fires before every kind of close; cancel it to keep the dialog open', + 'ev.close': 'After the dialog closes (native event)', + 'ev.cancel': 'When it is closed with ESC (native event)', + 'events.before.h': 'Intercepting a close', + 'events.before.p': + 'beforeclose covers buttons, the close button, ESC, a backdrop click, a <form method="dialog"> submit and close() itself.', + + 'variants.h': 'Drawers, toasts and popovers', + 'variants.drawer.h': 'Drawer', + 'variants.drawer.p': + 'With position set, the dialog slides in from that edge instead of being centred.', + 'variants.toast.h': 'Toast', + 'variants.toast.p': + 'A lightweight, auto-dismissing notification. Toasts are non-modal and stack in a fixed corner container, so they never block the page. Hovering one pauses its timer.', + 'variants.toast.pos': 'Move it around', + 'variants.toast.ret': + 'Dialogify.toast() returns the dialog instance, so close() and the usual events are available.', + 'variants.popover.h': 'Anchored popover', + 'variants.popover.p': + 'showAt() positions a non-modal dialog next to another element, for dropdowns and popover cards. It flips to the opposite side when the preferred placement would overflow, and follows the anchor on scroll and resize.', + 'variants.popover.other': 'A different anchor', + 'variants.popover.dismiss': + 'A popover is dismissed by a click anywhere outside it. Clicking the anchor that opened it toggles it shut, and that click is swallowed so it is not immediately reopened. Clicking a different trigger is left alone, so it closes this popover and opens its own.', + 'variants.anim.h': 'Animation', + 'variants.anim.p': + 'Drawers slide in and out from their edge, toasts fade and scale, and popovers fade in place. The exit animation runs after the dialog has closed, so a dialog with autoRemove stays in the DOM until it finishes. A closing toast collapses its own height at the same time, so the toasts below slide up instead of jumping. All of it is disabled under prefers-reduced-motion: reduce.', + + 'shortcuts.h': 'Shortcuts', + 'shortcuts.p': + 'Drop-in replacements for the browser alert, confirm and prompt, all promise-based.', + 'shortcuts.opts': + 'All three take a second argument: ok, cancel and close are per-button callbacks, and dialogOptions is handed straight to the dialog constructor (for example { dialogOptions: { size: Dialogify.SIZE_LARGE } }). prompt() also accepts placeholder and value. confirm() resolves to true / false; prompt() resolves to the entered text or null.', + + 'decl.h': 'Declarative usage', + 'decl.p': + 'Besides the programmatic API, a dialog can be written directly in HTML as a customized built-in element. It is registered as soon as dialogify loads.', + 'decl.name': + 'The element name is prefixed because the spec requires a hyphen in a custom element name — is="dialogify" is not valid. Note also that a declarative dialog is not removed from the DOM when it closes, so it can be shown again.', + 'decl.attr.h': 'Options as attributes', + 'attr.title': 'Dialog title (title is the native tooltip attribute)', + 'attr.size': + 'large maps to SIZE_LARGE; any other value is used as-is', + 'attr.position': 'Drawer edge', + 'attr.closable': 'Show the close button', + 'attr.fixed': 'Use position: fixed', + 'attr.bgScroll': 'Allow the page behind to scroll', + 'attr.useForm': 'Wrap the content in a form', + 'attr.src': 'Load the content from this URL instead of the inline markup', + 'attr.ajaxPrefix': 'Prefix used to resolve src', + 'attr.options': 'Any option at once, as JSON', + 'attr.buttonsPos': 'left / center / right (default)', + 'attr.anchor': 'Selector for the anchor element used by showAt()', + 'attr.place': 'Placement, alignment and gap for an anchored popover', + 'decl.bool': + 'Careful: attributes are strings, so these booleans are value-based, not presence-based: write closable="false" to disable. A bare attribute, "true", or any other value means true. This differs from standard HTML boolean attributes.', + 'decl.objOpts': + 'Options that cannot be expressed as attributes (callbacks, style objects) can be assigned before the dialog is first shown:', + 'decl.btn.h': 'Buttons', + 'decl.btn.p': + 'Add a <button> with one of the marker attributes inside the content. They are collected into a button box that matches the programmatic buttons() output. Without text, a localized default is used.', + 'decl.btn.cancel': '✅ (also fires cancel)', + 'decl.btn.selfclose': + 'Write <button ok></button>, not <button ok />. Self-closing syntax is not valid for HTML elements and makes the parser nest the following siblings.', + 'decl.btn.js': + 'Declarative buttons join the button list too, so they can be driven from JavaScript. Give them an id to address them by name (otherwise they are keyed by index), and a loading-text for their busy state.', + 'decl.ev.h': 'Events', + 'decl.ev.p': + 'close and cancel are native <dialog> events; show and beforeclose are dispatched by dialogify. They can all be bound in the usual ways.', + 'decl.ev.inline': + 'They can also be bound in markup. onclose and oncancel are native inline handlers and behave exactly as the browser defines. In addition, dialogify resolves a function name on onshow, onclose, oncancel and onbeforeclose:', + 'decl.ev.lookup': + 'The name is looked up in Dialogify.handlers first, then on window (dotted paths are supported), and it is resolved when the event fires — so handlers may be defined after the markup. Values that are not plain identifiers are ignored by dialogify and left to the browser native inline handler support, so no code is ever evaluated by dialogify itself.', + 'decl.safari.h': 'Safari support', + 'decl.safari.p': + 'Customized built-in elements are not supported in Safari/WebKit, so <dialog is="bahamut-dialogify"> is never upgraded there. An optional bundle of @ungap/custom-elements ships with dialogify; load it before dialogify to enable the declarative syntax. It is a no-op in browsers that already support the feature, and the programmatic API works everywhere without it.', + + 'theme.h': 'Theming', + 'theme.p': + 'Every colour in the stylesheet is read through a CSS custom property, so you can retheme the dialog without fighting selectors.', + 'theme.fallback': + 'The library never declares these properties itself — the light values are only var() fallbacks — so your declaration wins regardless of source order, even though the browser build injects its stylesheet last.', + 'theme.dark.h': 'Dark theme', + 'theme.dark.p': + 'A dark palette ships with the stylesheet, behind data-theme. The attribute can sit on any ancestor of the dialog (usually <html>) and can be flipped at runtime.', + 'theme.dark.try': 'Open a dark dialog', + 'theme.absent': 'absent', + 'theme.dark.absent': 'Light (the default, unchanged from earlier releases)', + 'theme.light': 'Light', + 'theme.darkv': 'Dark', + 'theme.auto': 'Follows prefers-color-scheme', + 'theme.optin': + 'Dark mode is opt-in on purpose: without the attribute the dialog stays light even when the visitor system is set to dark, so an existing site never changes appearance on upgrade. Use data-theme="auto" to follow the system.', + 'theme.override': + 'The dark rules only set custom properties, never a real declaration, so if you already ship your own overrides for .dialogify they keep winning by source order. Be aware that anything your stylesheet leaves alone will pick up the dark palette, which can mix the two; declare --dialogify-* properties instead of rules if you want full control.', + 'theme.icons': + 'The built-in icons are single-colour SVGs with the fill baked in, so they are re-tinted with --dialogify-icon-filter rather than swapped. This also applies to a custom closeButton.image; set it to none to opt out.', + 'theme.z.h': 'Stacking (z-index)', + 'theme.z.p': + 'showModal() promotes a dialog to the browser top layer, where z-index does not apply. Everything else — show(), showAt() popovers and toasts — stays in the normal stacking context and has to out-stack the rest of the page. The defaults are deliberately high so a toast is not hidden behind a sticky header.', + 'z.dialog': 'Non-modal dialogs and popovers', + 'z.toast': 'The toast container', + 'theme.tokens.h': 'All theme properties', + 'theme.tokens.sum': 'Show the full list', + + 'a11y.h': 'Accessibility', + 'a11y.title': + 'title() links the heading to the dialog with aria-labelledby.', + 'a11y.close': + 'The close button is exposed as a button, is keyboard focusable, activates with Enter / Space, and is labelled from the locale.', + 'a11y.icons': + 'Decorative icons are hidden from assistive technology; the loading indicator and toasts announce themselves with role="status".', + 'a11y.busy': 'Buttons in their loading state are marked aria-busy.', + 'a11y.motion': 'Animations are disabled under prefers-reduced-motion.', + 'a11y.focus': + 'Focus trapping and focus restoration for modal dialogs come from the native <dialog> element itself.', + + 'locale.h': 'Locale', + 'locale.p': + 'zh_TW (the default), zh_CN and en_US ship with the library. The locale drives the default button labels, the close button label and the loading text.', + + 'browser.h': 'Browser support', + 'browser.p': + 'Every browser with a native <dialog> element: Chrome 94+, Firefox 98+, Safari 15.4+. The bundles are built for es2022.', + 'browser.polyfill': + 'Older browsers can load the optional dialog-polyfill bundle. Load it before dialogify; it exposes window.dialogPolyfill, which dialogify detects at runtime.', + 'browser.deps': + 'The only dependency is jQuery (a peer dependency, >=3.0.0, jQuery 4 included).', + + 'api.h': 'API reference', + 'api.instance.h': 'Instance methods', + 'api.show': 'Show as non-modal; resolves when it closes', + 'api.showModal': 'Show as a modal; resolves when it closes', + 'api.showAt': 'Show anchored to another element', + 'api.reposition': 'Recompute the anchored position', + 'api.close': 'Close the dialog (fires beforeclose)', + 'api.isOpen': 'Whether the dialog is open', + 'api.title': 'Set the dialog title', + 'api.setContent': 'Replace the body, keeping title and buttons', + 'api.getContent': 'The current body HTML', + 'api.load': 'Load the body over ajax, with a loading state', + 'api.loading': 'Toggle or query the loading overlay', + 'api.validate': 'Run native form validation', + 'api.form': 'Read the dialog form', + 'api.buttons': 'Set the buttons', + 'api.addButton': 'Append (or prepend) one button', + 'api.updateButton': 'Update a button in place', + 'api.button': 'Remove or fetch a button', + 'api.on': 'Bind or unbind an event', + 'api.static.h': 'Static members', + 'api.shortcuts': 'Promise-based shortcuts', + 'api.toast': 'Show a toast; returns the dialog instance', + 'api.closeAll': 'Close every open dialog', + 'api.handlers': 'Lookup table for declarative inline handlers', + 'api.locale': 'Locale strings; extend as needed', + 'api.const1': 'Size and button style constants', + 'api.const2': 'Button box alignment constants', + 'api.const3': 'Drawer edge constants', + 'api.const4': 'Toast style constants', + 'api.const5': 'The six toast position constants', + + 'th.option': 'Option', + 'th.type': 'Type', + 'th.default': 'Default', + 'th.desc': 'Description', + 'th.method': 'Method', + 'th.field': 'Field', + 'th.event': 'Event', + 'th.cancelable': 'Cancelable', + 'th.attr': 'Attribute', + 'th.style': 'Style', + 'th.closes': 'Closes the dialog', + 'th.prop': 'Custom property', + 'th.applies': 'Applies to', + 'th.result': 'Result', + 'th.member': 'Member', + 'style.primary': 'Primary', + 'style.default': 'Default', + 'style.danger': 'Danger', + + /* code samples */ + 'code.module': + "import Dialogify from '@oneup_network/dialogify';\n" + + '\n' + + '// when you want the standalone stylesheet:\n' + + "// import '@oneup_network/dialogify/css';", + + 'code.modal': + "new Dialogify('I am modal and block the page').showModal();\n" + + "new Dialogify('I am non-modal, the page still works').show();", + + 'code.await': + "const dialog = new Dialogify('Ready to submit?').buttons([\n" + + " { text: 'Yes', type: Dialogify.BUTTON_PRIMARY, click: () => dialog.close('yes') },\n" + + " { text: 'No', click: () => dialog.close('no') }\n" + + ']);\n' + + '\n' + + "const answer = await dialog.showModal(); // 'yes' or 'no'", + + 'code.config': + 'window.dialogifyConfig = {\n' + + " locale: 'zh_TW', // zh_TW (default) / zh_CN / en_US\n" + + ' closeButton: { // shared by every dialog\n' + + " className: 'my-close'\n" + + ' }\n' + + '};', + + 'code.setContent': + "const dialog = new Dialogify('Step 1').title('Setup wizard').show();\n" + + '\n' + + "dialog.setContent('<p>Step 2</p>'); // body only, title and buttons stay\n" + + 'dialog.getContent(); // the current body HTML', + + 'code.ajax': + '// loaded on construction\n' + + "new Dialogify('/ajax/detail', { ajaxData: { id: 7 } }).showModal();\n" + + '\n' + + '// loaded later, with a loading state while it runs\n' + + "await dialog.load('/ajax/step2', { id: 7 });", + + 'code.loading': + 'dialog.setLoading(true); // dims the body and spins\n' + + 'dialog.setLoading(false);\n' + + 'dialog.isLoading();', + + 'code.form': + 'const dialog = new Dialogify(\'<input class="text-field" name="title" required>\').buttons([\n' + + ' {\n' + + " id: 'save',\n" + + " text: 'Save',\n" + + ' type: Dialogify.BUTTON_PRIMARY,\n' + + " loadingText: 'Saving...',\n" + + ' click: async () => {\n' + + ' if (!dialog.validate()) {\n' + + ' return; // reports the first invalid field\n' + + ' }\n' + + '\n' + + " dialog.updateButton('save', { loading: true });\n" + + " await save(dialog.formValues()); // { title: '...' }\n" + + " dialog.close('saved');\n" + + ' }\n' + + ' }\n' + + ']);', + + 'code.buttons': + "new Dialogify('Delete this record?')\n" + + ' .buttons(\n' + + ' [\n' + + " { text: 'Cancel', click: () => dialog.close() },\n" + + " { id: 'del', text: 'Delete', type: Dialogify.BUTTON_DANGER, loadingText: 'Deleting…' }\n" + + ' ],\n' + + ' { position: Dialogify.BUTTON_CENTER }\n' + + ' )\n' + + ' .showModal();', + + 'code.buttonManage': + "dialog.addButton({ id: 'more', text: 'More' });\n" + + "dialog.addButton({ id: 'first', text: 'First' }, { prepend: true });\n" + + "dialog.updateButton('more', { text: 'Fewer', type: Dialogify.BUTTON_DANGER, disabled: true });\n" + + "dialog.updateButton('save', { loading: true }); // spins, disables, swaps in loadingText\n" + + "dialog.removeButton('more');\n" + + "dialog.getButton('save'); // the jQuery object for that button", + + 'code.beforeclose': + "dialog.on('beforeclose', async (event) => {\n" + + ' if (isDirty) {\n' + + ' event.preventDefault();\n' + + " if (await Dialogify.confirm('Discard your unsaved changes?')) {\n" + + ' isDirty = false;\n' + + ' dialog.close();\n' + + ' }\n' + + ' }\n' + + '});', + + 'code.drawer': + "new Dialogify('<h4>Filters</h4>', { position: Dialogify.POSITION_RIGHT }).showModal();", + + 'code.toast': + "Dialogify.toast('Saved');\n" + + '\n' + + "Dialogify.toast('Could not save', {\n" + + ' type: Dialogify.TOAST_ERROR, // TOAST_INFO (default) / TOAST_SUCCESS / TOAST_WARNING\n' + + " position: 'bottom-right', // or Dialogify.TOAST_BOTTOM_RIGHT, default top-right\n" + + ' duration: 5000, // 0 keeps it open\n' + + " title: 'Error',\n" + + ' closable: true\n' + + '});', + + 'code.popover': + "new Dialogify('<ul>…</ul>', { useDialogForm: false }).showAt('#menu-button', {\n" + + " placement: 'bottom', // top | bottom | left | right\n" + + " align: 'start', // start | center | end\n" + + ' offset: 8,\n' + + ' toggle: true // clicking the anchor again closes it, default true\n' + + '});\n' + + '\n' + + '// Note: closable: false drops the close button in the corner and, with it,\n' + + '// the click-outside dismissal — only close() can shut the popover then.', + + 'code.shortcuts': + "await Dialogify.alert('Alert!!');\n" + + '\n' + + "if (await Dialogify.confirm('Shall we continue?')) {\n" + + ' // ...\n' + + '}\n' + + '\n' + + "const answer = await Dialogify.prompt('What is your name?', { placeholder: 'Type here', value: '' });\n" + + '\n' + + 'Dialogify.closeAll(); // close every dialog that is open', + + 'code.declButtons': + '<dialog is="bahamut-dialogify">\n' + + ' Delete this record?\n' + + ' <button cancel></button>\n' + + ' <button danger onclick="doDelete()">Delete</button>\n' + + '</dialog>', + + 'code.declForm': + '<dialog is="bahamut-dialogify">\n' + + ' <input class="text-field" name="title" required />\n' + + ' <button ok id="save" loading-text="Saving…">Save</button>\n' + + '</dialog>', + + 'code.declEvents': + "dialog.addEventListener('show', () => {});\n" + + 'dialog.onshow = () => {};\n' + + 'dialog.onclose = () => {};\n' + + '\n' + + "dialog.addEventListener('beforeclose', (e) => e.preventDefault()); // keeps it open\n" + + 'dialog.onbeforeclose = () => false; // same thing', + + 'code.handlers': + 'Dialogify.handlers.myHandler = function () {\n' + + ' // this is the dialog element\n' + + '};', + + 'code.locale': + "window.dialogifyConfig = { locale: 'en_US' };\n" + + '\n' + + '// extend or override as needed\n' + + "Dialogify.LOCALE.ja_JP = { ok: 'OK', cancel: 'キャンセル', close: '閉じる', loading: '読み込み中' };\n" + + "window.dialogifyConfig = { locale: 'ja_JP' };", + + 'token.surface': 'Dialog background', + 'token.text': 'Body text', + 'token.heading': 'Title and close button', + 'token.muted': 'Secondary text', + 'token.divider': 'The <hr> rule', + 'token.link': 'Links in the body', + 'token.linkHover': 'Links on hover', + 'token.focus': ':focus-visible outlines', + 'token.iconFilter': 'Filter applied to the built-in icons', + 'token.shadow': 'Dialog shadow', + 'token.loading': 'The setLoading() overlay', + 'token.buttonBg': 'Default button', + 'token.buttonHover': 'Default button on hover', + 'token.buttonText': 'Default button label', + 'token.primary': 'BUTTON_PRIMARY', + 'token.primaryHover': 'BUTTON_PRIMARY on hover', + 'token.onPrimary': 'BUTTON_PRIMARY label', + 'token.danger': 'BUTTON_DANGER', + 'token.dangerHover': 'BUTTON_DANGER on hover', + 'token.onDanger': 'BUTTON_DANGER label', + 'token.dangerShadow': '.btn-danger-shadow', + 'token.fieldBg': '.text-field background', + 'token.fieldBorder': '.text-field border', + 'token.fieldText': '.text-field text', + 'token.fieldFocus': '.text-field border on focus', + 'token.placeholder': '.text-field placeholder', + 'token.error': '.is-error fields', + 'token.toastInfo': 'Toast status bar, info', + 'token.toastSuccess': 'Toast status bar, success', + 'token.toastWarning': 'Toast status bar, warning', + 'token.toastError': 'Toast status bar, error', + 'token.toastClose': 'Toast close button', + 'token.toastCloseHover': 'Toast close button on hover', + 'token.backdrop': 'Modal backdrop', + 'token.backdropOpacity': 'Modal backdrop opacity', + 'token.z': 'Non-modal dialogs and popovers', + 'token.toastZ': 'The toast container' +}; diff --git a/eslint.config.js b/eslint.config.js index 28598cf..85c5dd7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,7 +4,15 @@ import prettier from 'eslint-config-prettier'; export default [ { - ignores: ['dist/**', 'docs/**', 'src/css/**', 'src/js/dialog-polyfill.esm.js'] + ignores: [ + 'dist/**', + 'docs/css/**', + 'docs/js/dialogify.min.js', + 'docs/js/custom-elements.min.js', + 'docs/js/dialog-polyfill.min.js', + 'src/css/**', + 'src/js/dialog-polyfill.esm.js' + ] }, js.configs.recommended, { @@ -40,5 +48,20 @@ export default [ } } }, + { + files: ['docs/js/*.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'script', + globals: { + ...globals.browser, + Dialogify: 'readonly' + } + }, + rules: { + 'no-unused-vars': ['error', { args: 'none' }], + 'no-empty': ['error', { allowEmptyCatch: true }] + } + }, prettier ]; From 4486eab8f3fe1df045f466489f82c865030c15a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E4=BB=81?= Date: Fri, 14 Aug 2026 17:06:07 +0800 Subject: [PATCH 4/5] docs: address review feedback on the new site - keep site-only work out of the changelog - drop the favicon: the close glyph does not represent the package - render the star button through a re-run of buttons.js so it picks up the colour scheme, and switch it to the large size - drive the theme icons from CSS: `hidden` is an HTMLElement property and the UA rule is namespaced to HTML, so it never applied to the SVGs and all three showed at once; toggling now also raises a toast - leave drawer, toast and popover untranslated - call preventDefault() in the demos that must stay open, since a primary button submits the dialog form, and document that behaviour - follow the reading position in the sidebar and stop the filter focus ring being clipped by the scroll container - hide the menu button when the sidebar is already open; the .icon-button rule was overriding it on order Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 -- docs/css/docs.css | 34 +++++++++-- docs/index.html | 64 ++++++++++++-------- docs/js/docs.js | 143 +++++++++++++++++++++++++++++++-------------- docs/js/i18n.en.js | 2 + 5 files changed, 170 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83061ac..4785b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,5 @@ # Changelog -## Unreleased - docs: rewrote the documentation site — every option, method, event and theme property is now listed with a runnable example, replacing the unmaintained Material Design Lite layout - docs: light/dark theme switch on the site, wired to the same data-theme attribute the dialog itself reads - docs: Traditional Chinese and English, switchable at runtime and remembered; Chinese ships in the markup so the page reads without JavaScript - ## 2.3.0 feat: a dark palette behind data-theme="dark", with data-theme="auto" following prefers-color-scheme; opt-in, so an existing site is unchanged on upgrade feat: every colour in the stylesheet is now a --dialogify-* custom property, so a page can retheme the dialog without overriding rules diff --git a/docs/css/docs.css b/docs/css/docs.css index 7cee0bc..64ee3df 100644 --- a/docs/css/docs.css +++ b/docs/css/docs.css @@ -193,10 +193,6 @@ a:hover { margin-left: auto; } -.topbar__menu { - display: none; -} - .gh-buttons { display: flex; align-items: center; @@ -222,6 +218,12 @@ a:hover { background: var(--bg-soft); } +/* The sidebar is permanently open at this width, so the button would do + nothing. Declared after .icon-button, which would otherwise win on order. */ +.topbar__menu { + display: none; +} + .icon-button svg { width: 18px; height: 18px; @@ -231,6 +233,19 @@ a:hover { stroke-linecap: round; } +/* Only the icon for the mode in force is shown. This is done in CSS rather + than with the hidden attribute, which does nothing on an SVG element: the + [hidden] rule lives in the HTML UA stylesheet and is namespaced to HTML. */ +[data-theme-icon] { + display: none; +} + +:root[data-theme='light'] [data-theme-icon='light'], +:root[data-theme='dark'] [data-theme-icon='dark'], +:root[data-theme='auto'] [data-theme-icon='auto'] { + display: block; +} + .segmented { display: inline-flex; border: 1px solid var(--border); @@ -277,10 +292,19 @@ a:hover { align-self: start; max-height: calc(100vh - var(--topbar-height)); overflow-y: auto; - padding: 24px 0 48px; + /* overflow-y alone makes overflow-x compute to auto, which would clip the + focus ring of the filter box against the sides. */ + padding: 24px 3px 48px; font-size: 14px; } +.sidebar__filter:focus-visible, +.sidebar__filter:focus { + outline: 2px solid var(--accent); + outline-offset: 0; + border-color: var(--accent); +} + .sidebar__filter { width: 100%; margin-bottom: 12px; diff --git a/docs/index.html b/docs/index.html index 6e4209e..b77b85d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,9 +6,8 @@ Dialogify — 以 <dialog> 打造的燈箱工具 - Star -

Dialogify

以原生 <dialog> 元素實作的燈箱工具。提供 modal - 對話框、抽屜、吐司通知、錨定浮層與表單處理,也支援直接寫在 HTML + 對話框、drawer、toast 通知、popover 與表單處理,也支援直接寫在 HTML 裡的宣告式用法。

@@ -297,7 +297,7 @@

選項

string — - 設定後改以抽屜形式呈現,可用 left / + 設定後改以 drawer 形式呈現,可用 left / right / top / bottom @@ -514,6 +514,16 @@

定義按鈕

Dialogify.BUTTON_LEFT、Dialogify.BUTTON_CENTER,預設靠右。

+
+ 注意:BUTTON_PRIMARY 會被渲染成 + type="submit",按下時會送出燈箱內建的 + <form method="dialog"> 並關閉燈箱,即使有自訂 + click + 也一樣。若要在按下後繼續留在燈箱裡(例如顯示載入狀態或更換內容),請在 + click 中呼叫 + e.preventDefault(),或改用一般按鈕、或建立燈箱時設定 + useDialogForm: false。 +

動態管理

@@ -595,9 +605,9 @@

攔截關閉

-

抽屜、吐司與錨定浮層

+

drawer、toast 與 popover

-

抽屜 Drawer

+

Drawer

指定 position 後,燈箱會改從該側邊緣滑入,而不是置中顯示。

@@ -611,9 +621,9 @@

抽屜 Drawer

new Dialogify('<h4>篩選條件</h4>', { position: Dialogify.POSITION_RIGHT }).showModal();
-

吐司 Toast

+

Toast

- 輕量、會自動消失的通知。吐司是非 modal + 輕量、會自動消失的通知。toast 是非 modal 的,會堆疊在畫面角落的容器中,不會阻擋頁面。滑鼠停留其上時會暫停倒數。

@@ -643,7 +653,7 @@

吐司 Toast

close() 與各種事件都可以照常使用。

-

錨定浮層 Popover

+

Popover

showAt() 會把非 modal 燈箱定位在指定元素旁,適合做下拉選單與說明卡片。空間不足時會自動翻到反向,並在捲動與縮放時跟隨錨點。 @@ -671,15 +681,18 @@

錨定浮層 Popover

// 注意:closable: false 除了拿掉右上角關閉鈕, // 也會一併關掉「點擊外部關閉」,此時只能自行呼叫 close()。

- 點擊浮層以外的任何地方都會關閉它。點擊開啟它的那個錨點會收合浮層,且該次點擊會被吞掉,因此不會馬上又被重新開啟;點擊 + 點擊 popover 以外的任何地方都會關閉它。點擊開啟它的那個錨點會收合 + popover,且該次點擊會被吞掉,因此不會馬上又被重新開啟;點擊 另一個 觸發元素則不受影響,會關掉舊的並開啟新的。

動畫

- 抽屜由邊緣滑入滑出、吐司淡入並帶縮放、浮層則原地淡入。離場動畫在燈箱關閉之後才播放,所以帶 + drawer 由邊緣滑入滑出、toast 淡入並帶縮放、popover + 則原地淡入。離場動畫在燈箱關閉之後才播放,所以帶 autoRemove - 的燈箱會等動畫結束才移除。關閉中的吐司會同步收合自己的高度,讓下方的吐司平順上移。以上動畫在 + 的燈箱會等動畫結束才移除。關閉中的 toast 會同步收合自己的高度,讓下方的 + toast 平順上移。以上動畫在 prefers-reduced-motion: reduce 下全部停用。

@@ -782,7 +795,7 @@

以屬性設定選項

position position - 抽屜邊緣 + drawer 邊緣 closable @@ -844,7 +857,7 @@

以屬性設定選項

offset — - 錨定浮層的方位、對齊與間距 + popover 的方位、對齊與間距 @@ -1066,7 +1079,9 @@

堆疊順序(z-index)

showModal() 會把燈箱推進瀏覽器的 top layer,那裡不受 z-index 影響;其餘的 show()、showAt() - 浮層與吐司都留在一般堆疊脈絡,必須蓋過頁面上的其他元件。預設值刻意設得很高,避免吐司被固定頁首擋住。 + popover 與 toast + 都留在一般堆疊脈絡,必須蓋過頁面上的其他元件。預設值刻意設得很高,避免 toast + 被固定頁首擋住。

@@ -1081,12 +1096,12 @@

堆疊順序(z-index)

- + - +
--dialogify-z-index 1000非 modal 燈箱與浮層非 modal 燈箱與 popover
--dialogify-toast-z-index 1010吐司容器toast 容器
@@ -1123,7 +1138,7 @@

無障礙

Space 觸發,標籤取自語系設定。
  • - 裝飾性圖示會對輔助科技隱藏;載入指示與吐司則以 + 裝飾性圖示會對輔助科技隱藏;載入指示與 toast 則以 role="status" 主動播報。
  • 忙碌狀態的按鈕會標記 aria-busy。
  • @@ -1291,7 +1306,7 @@

    靜態成員

    Dialogify.toast(message, options?) - 顯示吐司通知,回傳燈箱實例 + 顯示 toast 通知,回傳燈箱實例 Dialogify.closeAll() @@ -1324,21 +1339,21 @@

    靜態成員

    POSITION_LEFT / POSITION_RIGHT / POSITION_TOP / POSITION_BOTTOM - 抽屜方位常數 + drawer 方位常數 TOAST_INFO / TOAST_SUCCESS / TOAST_WARNING / TOAST_ERROR - 吐司樣式常數 + toast 樣式常數 TOAST_TOP_LEFT … TOAST_BOTTOM_RIGHT - 吐司位置常數,共六種 + toast 位置常數,共六種 @@ -1364,7 +1379,6 @@

    靜態成員

    - diff --git a/docs/js/docs.js b/docs/js/docs.js index 8525885..f4f85c6 100644 --- a/docs/js/docs.js +++ b/docs/js/docs.js @@ -6,6 +6,15 @@ const THEME_KEY = 'dialogify-docs-theme'; const LANG_KEY = 'dialogify-docs-lang'; const THEMES = ['auto', 'light', 'dark']; + const GH_BUTTONS_SRC = 'https://buttons.github.io/buttons.js'; + + let lang = 'zh-TW'; + + // Picks the string for the language in force. Used for the handful of + // strings that live in this file rather than in the markup. + function t(zh, en) { + return lang === 'en' ? en : zh; + } /* --- theme --------------------------------------------------------- */ @@ -16,7 +25,13 @@ return THEMES.indexOf(value) >= 0 ? value : 'auto'; } + function isDark(theme) { + return theme === 'dark' || (theme === 'auto' && prefersDark.matches); + } + function applyTheme(theme) { + // Which icon shows is driven by CSS off this attribute: the hidden + // attribute has no effect on SVG, which has no HTML UA stylesheet. root.dataset.theme = theme; try { if (theme === 'auto') { @@ -28,11 +43,7 @@ /* storage can be blocked */ } - document.querySelectorAll('[data-theme-icon]').forEach(function (icon) { - icon.hidden = icon.dataset.themeIcon !== theme; - }); - - const dark = theme === 'dark' || (theme === 'auto' && prefersDark.matches); + const dark = isDark(theme); // highlight.js ships one stylesheet per theme, so they are swapped // rather than re-declared. @@ -40,8 +51,6 @@ sheet.disabled = (sheet.dataset.hljs === 'dark') !== dark; }); - // The star button is an iframe: its colour scheme is baked in when it - // renders, so it has to be rebuilt on every change. renderGitHubButtons(dark ? 'dark' : 'light'); } @@ -52,51 +61,69 @@ } }); + const THEME_LABELS = { + auto: function () { + return t( + '跟隨系統(目前為' + (prefersDark.matches ? '暗色' : '亮色') + ')', + 'Follow the system (currently ' + (prefersDark.matches ? 'dark' : 'light') + ')' + ); + }, + light: function () { + return t('亮色主題', 'Light theme'); + }, + dark: function () { + return t('暗色主題', 'Dark theme'); + } + }; + document.querySelector('[data-theme-toggle]').addEventListener('click', function () { - applyTheme(THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length]); + const next = THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length]; + applyTheme(next); + Dialogify.toast(THEME_LABELS[next](), { position: Dialogify.TOAST_TOP_CENTER }); }); + // github-buttons exposes no API for re-rendering, so the anchors are put + // back and the script is run again. It renders every a.github-button it + // finds when it executes, which is exactly what is needed here. function renderGitHubButtons(scheme) { const host = document.querySelector('[data-gh-buttons]'); if (!host) { return; } - if (!host.dataset.template) { + if (host.dataset.template == null) { host.dataset.template = host.innerHTML; } - if (!window.GitHubButton || !window.GitHubButton.render) { - // buttons.js is async: try again once it has landed. - clearTimeout(renderGitHubButtons.retry); - renderGitHubButtons.retry = setTimeout(function () { - renderGitHubButtons(scheme); - }, 100); + if (host.dataset.scheme === scheme) { return; } + host.dataset.scheme = scheme; host.innerHTML = host.dataset.template; - host.querySelectorAll('.github-button').forEach(function (anchor) { - // buttons.js expects a full scheme map, not a bare keyword. + host.querySelectorAll('a.github-button').forEach(function (anchor) { anchor.setAttribute( 'data-color-scheme', 'no-preference: ' + scheme + '; light: ' + scheme + '; dark: ' + scheme + ';' ); - window.GitHubButton.render(anchor); }); - } - // buttons.js renders on load; re-render once it is available so the very - // first paint also matches the current theme. - window.addEventListener('load', function () { - applyTheme(currentTheme()); - }); + const previous = document.querySelector('script[data-gh-script]'); + if (previous) { + previous.remove(); + } + + const script = document.createElement('script'); + script.src = GH_BUTTONS_SRC; + script.async = true; + script.dataset.ghScript = ''; + document.head.appendChild(script); + } /* --- language ------------------------------------------------------ */ const dictionary = window.DIALOGIFY_DOCS_EN || {}; const originals = new Map(); - let lang = 'zh-TW'; function captureOriginals() { document.querySelectorAll('[data-i18n]').forEach(function (el) { @@ -275,9 +302,34 @@ if (changed) { buildToc(); updateTocCurrent(); + revealCurrentInSidebar(); } } + // The section list is taller than its scroll container, so following the + // page means scrolling the sidebar too. Only done when the entry has left + // the visible part, otherwise every section change would nudge the list. + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + + function revealCurrentInSidebar() { + const anchor = navList.querySelector('a[aria-current]'); + if (!anchor || sidebar.scrollHeight <= sidebar.clientHeight) { + return; + } + + const entry = anchor.getBoundingClientRect(); + const view = sidebar.getBoundingClientRect(); + const margin = 24; + if (entry.top >= view.top + margin && entry.bottom <= view.bottom - margin) { + return; + } + + sidebar.scrollTo({ + top: sidebar.scrollTop + entry.top - view.top - (view.height - entry.height) / 2, + behavior: reducedMotion.matches ? 'auto' : 'smooth' + }); + } + function updateTocCurrent() { const offset = 120; let current = null; @@ -451,16 +503,16 @@ 'token.fieldFocus': '.text-field 聚焦邊框', 'token.placeholder': '.text-field 提示文字', 'token.error': '.is-error 欄位', - 'token.toastInfo': '吐司狀態條 info', - 'token.toastSuccess': '吐司狀態條 success', - 'token.toastWarning': '吐司狀態條 warning', - 'token.toastError': '吐司狀態條 error', - 'token.toastClose': '吐司關閉鈕', - 'token.toastCloseHover': '吐司關閉鈕 hover', + 'token.toastInfo': 'toast 狀態條 info', + 'token.toastSuccess': 'toast 狀態條 success', + 'token.toastWarning': 'toast 狀態條 warning', + 'token.toastError': 'toast 狀態條 error', + 'token.toastClose': 'toast 關閉鈕', + 'token.toastCloseHover': 'toast 關閉鈕 hover', 'token.backdrop': 'modal 背景遮罩', 'token.backdropOpacity': 'modal 背景遮罩透明度', - 'token.z': '非 modal 燈箱與浮層', - 'token.toastZ': '吐司容器' + 'token.z': '非 modal 燈箱與 popover', + 'token.toastZ': 'toast 容器' }; function buildTokenTable() { @@ -496,10 +548,6 @@ /* --- demos --------------------------------------------------------- */ - const t = function (zh, en) { - return lang === 'en' ? en : zh; - }; - let toastCorner = 0; const demos = { @@ -572,7 +620,10 @@ { text: t('下一步', 'Next'), type: Dialogify.BUTTON_PRIMARY, - click: function () { + click: function (e) { + // A primary button submits the dialog form, which + // would close it before the new content is seen. + e.preventDefault(); dialog.setContent( '

    ' + t( @@ -614,7 +665,8 @@ { text: 'OK', type: Dialogify.BUTTON_PRIMARY, - click: function () { + click: function (e) { + e.preventDefault(); dialog.setLoading(true); setTimeout(function () { dialog.setLoading(false); @@ -638,7 +690,10 @@ text: t('儲存', 'Save'), type: Dialogify.BUTTON_PRIMARY, loadingText: t('儲存中…', 'Saving…'), - click: function () { + click: function (e) { + // Keep the dialog open while the save is running; + // the primary button would otherwise submit it. + e.preventDefault(); if (!dialog.validate()) { return; } @@ -695,7 +750,9 @@ id: 'target', text: t('目標按鈕', 'Target'), type: Dialogify.BUTTON_PRIMARY, - click: function () {} + click: function (e) { + e.preventDefault(); + } }, { text: t('停用它', 'Disable it'), @@ -749,7 +806,7 @@ '

    ' + t('篩選條件', 'Filters') + '

    ' + - t('抽屜會從指定的邊緣滑入。', 'A drawer slides in from the edge you pick.') + + t('drawer 會從指定的邊緣滑入。', 'A drawer slides in from the edge you pick.') + '

    ', { position: button.dataset.position } ).showModal(); @@ -783,7 +840,7 @@ // path before this runs. new Dialogify( '' + - t('錨定浮層', 'Anchored popover') + + t('popover', 'Anchored popover') + '

    ' + t( '它會跟著錨點捲動,空間不足時自動翻面。', diff --git a/docs/js/i18n.en.js b/docs/js/i18n.en.js index 841bcb3..35fbd2a 100644 --- a/docs/js/i18n.en.js +++ b/docs/js/i18n.en.js @@ -84,6 +84,8 @@ window.DIALOGIFY_DOCS_EN = { 'buttons.define.h': 'Defining buttons', 'buttons.position': 'The second argument takes a position of Dialogify.BUTTON_LEFT or Dialogify.BUTTON_CENTER; the default is right-aligned.', + 'buttons.submit': + 'Note: BUTTON_PRIMARY renders as type="submit", so pressing it submits the built-in <form method="dialog"> and closes the dialog — even when a custom click handler is given. To stay open (to show a loading state or swap the content, say), call e.preventDefault() inside click, use a plain button, or create the dialog with useDialogForm: false.', 'buttons.manage.h': 'Managing them at runtime', 'btn.text': 'Button label; falls back to the localized default', 'btn.type': From 9770d44641a4ae793966ed7d026c2cac1e6f4661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E4=BB=81?= Date: Fri, 14 Aug 2026 17:21:41 +0800 Subject: [PATCH 5/5] docs: drop the accessibility section and highlight each block once applyLang() re-highlighted every translated code block on boot, before decorateCode() had run, so the second pass logged "Element previously highlighted." for each of them. It now only redoes blocks that were already highlighted, and decorateCode() skips those. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/index.html | 27 --------------------------- docs/js/docs.js | 8 +++++--- docs/js/i18n.en.js | 12 ------------ 3 files changed, 5 insertions(+), 42 deletions(-) diff --git a/docs/index.html b/docs/index.html index b77b85d..6f767b1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1125,33 +1125,6 @@

    所有主題屬性

    - -
    -

    無障礙

    -
      -
    • - title() 會以 - aria-labelledby 把標題與燈箱關聯起來。 -
    • -
    • - 關閉鈕以按鈕語意呈現,可用鍵盤聚焦,並支援 Enter / - Space 觸發,標籤取自語系設定。 -
    • -
    • - 裝飾性圖示會對輔助科技隱藏;載入指示與 toast 則以 - role="status" 主動播報。 -
    • -
    • 忙碌狀態的按鈕會標記 aria-busy。
    • -
    • - 動畫在 prefers-reduced-motion 下全部停用。 -
    • -
    • - modal 燈箱的焦點鎖定與焦點還原,直接由原生 - <dialog> 元素提供。 -
    • -
    -
    -

    語系

    diff --git a/docs/js/docs.js b/docs/js/docs.js index f4f85c6..0357574 100644 --- a/docs/js/docs.js +++ b/docs/js/docs.js @@ -161,9 +161,11 @@ document.documentElement.lang = english ? 'en' : 'zh-Hant-TW'; document.title = english ? dictionary['page.title'] : originals.get('page.title'); - // Replacing innerHTML wipes the highlighting, so code blocks are redone. + // Replacing innerHTML wipes the highlighting, so blocks that were + // already highlighted are redone. Untouched ones are left to + // decorateCode(), which runs once after this on boot. document.querySelectorAll('code[data-i18n]').forEach(function (code) { - if (window.hljs) { + if (window.hljs && code.dataset.highlighted) { delete code.dataset.highlighted; code.classList.remove('hljs'); window.hljs.highlightElement(code); @@ -411,7 +413,7 @@ function decorateCode() { document.querySelectorAll('pre > code').forEach(function (code) { - if (window.hljs) { + if (window.hljs && !code.dataset.highlighted) { window.hljs.highlightElement(code); } diff --git a/docs/js/i18n.en.js b/docs/js/i18n.en.js index 35fbd2a..7507155 100644 --- a/docs/js/i18n.en.js +++ b/docs/js/i18n.en.js @@ -200,18 +200,6 @@ window.DIALOGIFY_DOCS_EN = { 'theme.tokens.h': 'All theme properties', 'theme.tokens.sum': 'Show the full list', - 'a11y.h': 'Accessibility', - 'a11y.title': - 'title() links the heading to the dialog with aria-labelledby.', - 'a11y.close': - 'The close button is exposed as a button, is keyboard focusable, activates with Enter / Space, and is labelled from the locale.', - 'a11y.icons': - 'Decorative icons are hidden from assistive technology; the loading indicator and toasts announce themselves with role="status".', - 'a11y.busy': 'Buttons in their loading state are marked aria-busy.', - 'a11y.motion': 'Animations are disabled under prefers-reduced-motion.', - 'a11y.focus': - 'Focus trapping and focus restoration for modal dialogs come from the native <dialog> element itself.', - 'locale.h': 'Locale', 'locale.p': 'zh_TW (the default), zh_CN and en_US ship with the library. The locale drives the default button labels, the close button label and the loading text.',