robert пре 10 часа
родитељ
комит
6b837294b5
2 измењених фајлова са 15 додато и 175 уклоњено
  1. 13 175
      layer.js
  2. 2 0
      xjs.css

+ 13 - 175
layer.js

@@ -1,7 +1,5 @@
 (function (global, factory) {
   const LayerClass = factory();
-  // Allow usage as `Layer({...})` or `new Layer()`
-  // But Layer is a class. We can wrap it in a proxy or factory function.
   
   function LayerFactory(options) {
      if (options && typeof options === 'object') {
@@ -10,8 +8,6 @@
      return new LayerClass();
   }
   
-  // Copy static methods (including non-enumerable class statics like `fire` / `$`)
-  // Class static methods are non-enumerable by default, so Object.assign() would miss them.
   const copyStatic = (to, from) => {
     try {
       Object.getOwnPropertyNames(from).forEach((k) => {
@@ -21,12 +17,10 @@
         Object.defineProperty(to, k, desc);
       });
     } catch (e) {
-      // Best-effort fallback
       try { Object.assign(to, from); } catch {}
     }
   };
   copyStatic(LayerFactory, LayerClass);
-  // Also copy prototype for instanceof checks if needed (though tricky with factory)
   LayerFactory.prototype = LayerClass.prototype;
   
   typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = LayerFactory :
@@ -66,10 +60,7 @@
   
   const svgEl = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag);
   
-  // Ensure library CSS is loaded (xjs.css)
-  // - CSS is centralized in xjs.css (no runtime <style> injection).
-  // - We still auto-load it for convenience/compat, since consumers may forget the <link>.
-  let _xjsCssReady = null; // Promise<void>
+  let _xjsCssReady = null;
   const waitForStylesheet = (link) => new Promise((resolve) => {
     if (!link) return resolve();
     if (link.sheet) return resolve();
@@ -81,7 +72,6 @@
     };
     try { link.addEventListener('load', finish, { once: true }); } catch {}
     try { link.addEventListener('error', finish, { once: true }); } catch {}
-    // Fallback timeout: avoid blocking forever if the load event is missed.
     setTimeout(finish, 200);
   });
   const ensureXjsCss = () => {
@@ -118,12 +108,10 @@
       document.head.appendChild(link);
       return _xjsCssReady;
     } catch {
-      // ignore
       return Promise.resolve();
     }
   };
 
-  // SvgAni (svg.js) lazy loader for "svg:*" icon type
   let _svgAniReady = null;
   let _svgAniPrefetchScheduled = false;
   const isSvgAniIcon = (icon) => {
@@ -243,57 +231,44 @@
       this.resolve = null;
       this.reject = null;
       this._onKeydown = null;
-      this._mounted = null; // { kind, originalEl, placeholder, parent, nextSibling, prevHidden, prevInlineDisplay }
+      this._mounted = null;
       this._isClosing = false;
       this._isReplace = false;
 
-      // Step/flow support
-      this._flowSteps = null; // Array<options>
+      this._flowSteps = null;
       this._flowIndex = 0;
       this._flowValues = [];
-      this._flowBase = null; // base options merged into each step
-      this._flowResolved = null; // resolved merged steps
-      this._flowMountedList = []; // mounted DOM records for move-mode steps
-      this._flowAutoForm = null; // { enabled: true } when using step container shorthand
+      this._flowBase = null;
+      this._flowResolved = null;
+      this._flowMountedList = [];
+      this._flowAutoForm = null;
       this._backgroundSpec = null;
     }
 
-    // Constructor helper when called as function: const popup = Layer({...})
     static get isProxy() { return true; }
     
-    // Static entry point
     static run(options) {
       const instance = new Layer();
       return instance.run(options);
     }
-    // Chainable entry point (builder-style)
-    // Example:
-    //   Layer.$({ title: 'Hi' }).run().then(...)
-    //   Layer.$().config({ title: 'Hi' }).run()
     static $(options) {
       const instance = new Layer();
       if (options !== undefined) instance.config(options);
       return instance;
     }
     
-    // Chainable config helper (does not render until `.run()` is called)
     config(options = {}) {
-      // Support the same shorthand as Layer.run(title, text, icon)
       options = normalizeOptions(options, arguments[1], arguments[2]);
       this.params = { ...(this.params || {}), ...options };
       return this;
     }
 
-    // Add a single step (chainable)
-    // Usage:
-    //   Layer.$().step({ title:'A', dom:'#step1' }).step({ title:'B', dom:'#step2' }).run()
     step(options = {}) {
       if (!this._flowSteps) this._flowSteps = [];
       this._flowSteps.push(normalizeOptions(options, arguments[1], arguments[2]));
       return this;
     }
 
-    // Add multiple steps at once (chainable)
     steps(steps = []) {
       if (!Array.isArray(steps)) return this;
       if (!this._flowSteps) this._flowSteps = [];
@@ -301,18 +276,15 @@
       return this;
     }
 
-    // Convenience static helper: Layer.flow([steps], baseOptions?)
     static flow(steps = [], baseOptions = {}) {
       return Layer.$(baseOptions).steps(steps).run();
     }
     
-    // Instance entry point (chainable)
     run(options) {
       const hasArgs = (options !== undefined && options !== null);
       const normalized = hasArgs ? normalizeOptions(options, arguments[1], arguments[2]) : null;
       const merged = hasArgs ? normalized : (this.params || {});
 
-      // If no explicit steps yet, allow shorthand: { step: '#container', stepItem: '.item' }
       if (!this._flowSteps || !this._flowSteps.length) {
         const didInit = this._initFlowFromStepContainer(merged);
         if (didInit) {
@@ -320,10 +292,8 @@
         }
       }
 
-      // Flow mode: if configured via .step()/.steps() or step container shorthand, ignore per-call options and use steps
       if (this._flowSteps && this._flowSteps.length) {
         if (hasArgs) {
-          // allow providing base options at fire-time
           this.params = { ...(this.params || {}), ...this._stripStepOptions(merged) };
         }
         return this._fireFlow();
@@ -339,8 +309,8 @@
         title: '',
         text: '',
         icon: null,
-        iconSize: null, // e.g. '6em' / '72px'
-        iconLoop: null, // svgAni only: true/false/null (auto)
+        iconSize: null,
+        iconLoop: null,
         confirmButtonText: 'OK',
         cancelButtonText: 'Cancel',
         showCancelButton: false,
@@ -354,19 +324,10 @@
         height: null,
         iwidth: null,
         iheight: null,
-        nextIcon: null, // flow-only: icon for Next button
-        prevIcon: null, // flow-only: icon for Back button
-        // Content:
-        // - text: plain text
-        // - html: innerHTML
-        // - dom: selector / Element / <template> (preferred)
-        // - content: backward compat for selector/Element OR advanced { dom, mode, clone }
+        nextIcon: null,
+        prevIcon: null,
         dom: null,
-        domMode: 'move', // 'move' (default) | 'clone'
-        // Hook for confirming (also used in flow steps)
-        // Return false to prevent close / next.
-        // Return a value to be attached as `value`.
-        // May return Promise.
+        domMode: 'move',
         preConfirm: null,
         ...options
       };
@@ -383,9 +344,6 @@
     _fireFlow() {
       this._flowIndex = 0;
       this._flowValues = [];
-      // In flow mode:
-      // - keep iconAnimation off by default (avoids jitter)
-      // - BUT allow popupAnimation by default so the first step has the same entrance feel
       const base = { ...(this.params || {}) };
       if (!('popupAnimation' in base)) base.popupAnimation = true;
       if (!('iconAnimation' in base)) base.iconAnimation = false;
@@ -404,7 +362,6 @@
     }
 
     static _getXjs() {
-      // Prefer $ (main), fallback to xjs/animal (compat)
       const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
       if (!g) return null;
       const x = g.$ || g.xjs || g.animal;
@@ -520,15 +477,12 @@
     _mountOverlay() {
       const overlay = this.dom && this.dom.overlay;
       if (!overlay) return;
-      // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
-      // then show on a clean frame so opacity transitions are smooth.
       const ready = this._cssReady && typeof this._cssReady.then === 'function' ? this._cssReady : Promise.resolve();
       ready.then(() => {
         try { overlay.style.visibility = ''; } catch {}
         if (!overlay.parentNode) {
           document.body.appendChild(overlay);
         }
-        // Double-rAF gives the browser a chance to apply styles before animating.
         requestAnimationFrame(() => {
           requestAnimationFrame(() => {
             overlay.classList.add('show');
@@ -552,7 +506,6 @@
         return;
       }
 
-      // Cancel Button
       if (this.params.showCancelButton) {
         this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
         this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText);
@@ -560,7 +513,6 @@
         this.dom.actions.appendChild(this.dom.cancelBtn);
       }
 
-      // Confirm Button
       this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
       this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText);
       this.dom.confirmBtn.onclick = () => this._handleConfirm();
@@ -569,7 +521,6 @@
 
     _render(meta = null) {
       this._destroySvgAniIcon();
-      // Remove existing if any (but first, try to restore any mounted DOM from the previous instance)
       const existing = document.querySelector(`.${PREFIX}overlay`);
       const wantReplace = !!(this.params && this.params.replace);
       this._isReplace = false;
@@ -596,7 +547,6 @@
         try {
           existing.style.visibility = '';
           existing.classList.add('show');
-          // Reset any inline fade state from the previous close
           existing.style.transition = 'none';
           existing.style.opacity = '1';
           requestAnimationFrame(() => {
@@ -614,39 +564,32 @@
           } catch {}
           try { existing.remove(); } catch {}
         }
-        // Create Overlay
         this.dom.overlay = el('div', `${PREFIX}overlay`);
         this.dom.overlay._layerInstance = this;
       }
 
-      // Create Popup
       this.dom.popup = el('div', `${PREFIX}popup`);
       this.dom.overlay.appendChild(this.dom.popup);
       this._applyPopupBasics(this.params, { forceBackground: true });
 
-      // Flow mode: pre-mount all steps and switch by hide/show (DOM continuity, less jitter)
       if (meta && meta.flow) {
         this._renderFlowUI();
         return;
       }
 
-      // Icon
       if (this.params.icon) {
         this.dom.icon = this._createIcon(this.params.icon);
         this.dom.popup.appendChild(this.dom.icon);
       }
 
-      // Title
       if (this.params.title) {
         this.dom.title = el('h2', `${PREFIX}title`, this.params.title);
         this.dom.popup.appendChild(this.dom.title);
       }
 
-      // Content (Text / HTML / Element)
       if (this.params.text || this.params.html || this.params.content || this.params.dom) {
         this.dom.content = el('div', `${PREFIX}content`);
 
-        // DOM content (preferred: dom, backward: content)
         const domSpec = this._normalizeDomSpec(this.params);
         if (domSpec) {
           this._mountDomContent(domSpec);
@@ -659,13 +602,11 @@
         this.dom.popup.appendChild(this.dom.content);
       }
 
-      // Actions
       this._buildActions();
 
-      // Event Listeners
       if (this.params.closeOnClickOutside) {
         this._bindOverlayClick((e) => {
-          if (e.target === this.dom.overlay) this._close(null); // Dismiss
+          if (e.target === this.dom.overlay) this._close(null);
         });
       } else {
         this._bindOverlayClick(null);
@@ -676,31 +617,24 @@
 
     _renderFlowUI() {
       this._destroySvgAniIcon();
-      // Icon/title/content/actions are stable; steps are pre-mounted and toggled.
       const popup = this.dom.popup;
       if (!popup) return;
 
-      // Clear popup
       this._clearPopup();
       this._applyPopupBasics(this.params, { forceBackground: true });
 
-      // Icon (in flow: suppressed by default unless question or summary)
       this.dom.icon = null;
       if (this.params.icon) {
         this.dom.icon = this._createIcon(this.params.icon);
         popup.appendChild(this.dom.icon);
       }
 
-      // Title (always present for flow)
       this.dom.title = el('h2', `${PREFIX}title`, this.params.title || '');
       popup.appendChild(this.dom.title);
 
-      // Content container
       this.dom.content = el('div', `${PREFIX}content`);
-      // In flow, stretch content so slide distance is visible.
       this.dom.content.style.alignSelf = 'stretch';
       this.dom.content.style.width = '100%';
-      // Stack container: keep panes absolute so we can cross-fade without layout thrash
       this.dom.stepStack = el('div', `${PREFIX}step-stack`);
       this.dom.stepStack.style.position = 'relative';
       this.dom.stepStack.style.width = '100%';
@@ -714,7 +648,6 @@
         pane.style.boxSizing = 'border-box';
         pane.style.display = (i === this._flowIndex) ? '' : 'none';
 
-        // Fill pane: dom/html/text
         const domSpec = this._normalizeDomSpec(opt);
         if (domSpec) {
           this._mountDomContentInto(pane, domSpec, { collectFlow: true });
@@ -731,10 +664,8 @@
       this.dom.content.appendChild(this.dom.stepStack);
       popup.appendChild(this.dom.content);
 
-      // Actions
       this._buildActions({ flow: true });
 
-      // Event Listeners
       if (this.params.closeOnClickOutside) {
         this._bindOverlayClick((e) => {
           if (e.target === this.dom.overlay) this._close(null, 'backdrop');
@@ -785,7 +716,6 @@
         try {
           const s = String(this.params.iconSize).trim();
           if (!s) return;
-          // For SweetAlert2-style success icon, scale via font-size so all `em`-based parts remain proportional.
           if (mode === 'font') {
             const m = s.match(/^(-?\d*\.?\d+)\s*(px|em|rem)$/i);
             if (m) {
@@ -797,14 +727,12 @@
               }
             }
           }
-          // Fallback: directly size the box (works great for SVG icons)
           icon.style.width = s;
           icon.style.height = s;
         } catch {}
       };
       
       const appendRingParts = () => {
-        // Use the same "success-like" ring parts for every built-in icon
         icon.appendChild(el('div', `${PREFIX}success-circular-line-left`));
         icon.appendChild(el('div', `${PREFIX}success-ring`));
         icon.appendChild(el('div', `${PREFIX}success-fix`));
@@ -856,7 +784,6 @@
       };
       
       if (isSvgAniIcon(rawType)) {
-        // SvgAni animation icon: svg:<name>
         const name = getSvgAniIconName(rawType);
         icon.className = `${PREFIX}icon ${PREFIX}icon-svgAni`;
         icon.dataset.svgani = name;
@@ -870,12 +797,6 @@
       }
 
       if (rawType === 'success') {
-        // SweetAlert2-compatible DOM structure (circle + tick) for exact style parity
-        //   <div class="...success-circular-line-left"></div>
-        //   <div class="...success-mark"><span class="...success-line-tip"></span><span class="...success-line-long"></span></div>
-        //   <div class="...success-ring"></div>
-        //   <div class="...success-fix"></div>
-        //   <div class="...success-circular-line-right"></div>
         icon.appendChild(el('div', `${PREFIX}success-circular-line-left`));
         const mark = el('div', `${PREFIX}success-mark`);
         mark.appendChild(el('span', `${PREFIX}success-line-tip`));
@@ -890,20 +811,17 @@
       }
 
       if (rawType === 'error' || rawType === 'warning' || rawType === 'info' || rawType === 'question') {
-        // Use the same "success-like" ring parts for every icon
         appendRingParts();
 
         applyIconSize('font');
         applyIconBoxSize();
 
-        // SVG only draws the inner symbol (no SVG ring)
         const svg = createInnerMarkSvg();
         appendBuiltInInnerMark(svg);
         icon.appendChild(svg);
         return icon;
       }
 
-      // Default to SVG icons for other/custom types
       applyIconSize('box');
       applyIconBoxSize();
 
@@ -917,8 +835,6 @@
       ring.setAttribute('stroke', 'currentColor');
 
       svg.appendChild(ring);
-      // For custom types, we draw ring only by default (no inner mark).
-
       icon.appendChild(svg);
       
       return icon;
@@ -948,7 +864,6 @@
       }
       if (w) {
         popup.style.width = w;
-        // If width is explicitly set, remove default CSS max-width clamp.
         popup.style.maxWidth = 'none';
       } else {
         popup.style.width = '';
@@ -993,7 +908,6 @@
         if (!css) return null;
         return { type: 'css', css, key: `css:${css}` };
       }
-      // Fallback: treat as raw css string
       return { type: 'css', css: s, key: `css:${s}` };
     }
 
@@ -1193,7 +1107,6 @@
             loop: true,
             autoplay: true,
             animationData: data,
-            // Fill background container even if aspect ratio differs
             rendererSettings: { preserveAspectRatio: 'xMidYMid slice' }
           });
           bgEl._svgAniInstance = anim;
@@ -1219,7 +1132,6 @@
     }
 
     _didOpen() {
-      // Keyboard close (ESC)
       if (this.params.closeOnEsc) {
         this._onKeydown = (e) => {
           if (!e) return;
@@ -1228,30 +1140,22 @@
         document.addEventListener('keydown', this._onKeydown);
       }
 
-      // Keep the "success-like" ring perfectly blended with popup bg
       this._adjustRingBackgroundColor();
 
-      // Popup animation (optional)
       if (this.params.popupAnimation) {
         if (this.dom.popup) {
-          // Use WAAPI directly to guarantee the slide+fade effect is visible,
-          // independent from xjs.animate implementation details.
           try {
             const popup = this.dom.popup;
             try { popup.classList.add(`${PREFIX}popup-anim-slide`); } catch {}
 
             const rect = popup.getBoundingClientRect();
-            // Default entrance: from above center by "more than half" of popup height.
-            // Clamp to viewport so very tall popups don't start far off-screen.
             const vh = (typeof window !== 'undefined' && window && window.innerHeight) ? window.innerHeight : rect.height;
             const baseH = Math.min(rect.height, vh);
             const y0 = -Math.round(Math.max(18, baseH * 0.75));
 
-            // Disable CSS transform transition so it won't fight with WAAPI.
             popup.style.transition = 'none';
             popup.style.willChange = 'transform, opacity';
 
-            // If WAAPI is available, prefer it (most consistent).
             if (popup.animate) {
               const anim = popup.animate(
                 [
@@ -1270,7 +1174,6 @@
                   try { popup.style.willChange = ''; } catch {}
                 });
             } else {
-              // Fallback: try xjs.animate if WAAPI isn't available.
               const X = Layer._getXjs();
               if (X) {
                 popup.style.opacity = '0';
@@ -1290,7 +1193,6 @@
           } catch {}
         }
       }
-      // When popupAnimation is disabled, use a subtle scale-pop fade-in at current position.
       if (!this.params.popupAnimation && !this._isReplace && this.dom.popup) {
         try {
           const popup = this.dom.popup;
@@ -1351,7 +1253,6 @@
           }
         } catch {}
       }
-      // Replace mode: if popupAnimation is off, do a soft fade+scale to avoid a hard cut.
       if (!this.params.popupAnimation && this._isReplace && this.dom.popup) {
         try {
           const popup = this.dom.popup;
@@ -1371,12 +1272,10 @@
         } catch {}
       }
 
-      // Icon SVG draw animation
       if (this.params.iconAnimation) {
         this._animateIcon();
       }
 
-      // User hook (SweetAlert-ish naming)
       try {
         if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
       } catch {}
@@ -1388,14 +1287,12 @@
       const type = (this.params && this.params.icon) || '';
       if (isSvgAniIcon(type)) return;
 
-      // Ring animation (same as success) for all built-in icons
       if (RING_TYPES.has(type)) this._adjustRingBackgroundColor();
       try { icon.classList.remove(`${PREFIX}icon-show`); } catch {}
       requestAnimationFrame(() => {
         try { icon.classList.add(`${PREFIX}icon-show`); } catch {}
       });
 
-      // Success tick is CSS-driven; others still draw their SVG mark after the ring starts.
       if (type === 'success') return;
 
       const X = Layer._getXjs();
@@ -1407,13 +1304,9 @@
       const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
       const dot = svg.querySelector(`.${PREFIX}svg-dot`);
 
-      // If this is a built-in icon, keep the SweetAlert-like order:
-      // ring sweep first (~0.51s), then draw the inner mark.
       const baseDelay = RING_TYPES.has(type) ? 520 : 0;
 
       if (type === 'error') {
-        // Draw order: left-top -> right-bottom, then right-top -> left-bottom
-        // NOTE: A tiny delay (like 70ms) looks simultaneous; make it strictly sequential.
         const a = svg.querySelector(`.${PREFIX}svg-error-left`) || marks[0];  // M28 28 L52 52
         const b = svg.querySelector(`.${PREFIX}svg-error-right`) || marks[1]; // M52 28 L28 52
         const dur = 320;
@@ -1421,7 +1314,6 @@
         try { if (a) X(a).draw({ duration: dur, easing: 'ease-out', delay: baseDelay }); } catch {}
         try { if (b) X(b).draw({ duration: dur, easing: 'ease-out', delay: baseDelay + dur + gap }); } catch {}
       } else {
-        // warning / info / question (single stroke) or custom SVG symbols
         marks.forEach((m, i) => {
           try { X(m).draw({ duration: 420, easing: 'ease-out', delay: baseDelay + i * 60 }); } catch {}
         });
@@ -1430,7 +1322,6 @@
       if (dot) {
         try {
           dot.style.opacity = '0';
-          // Keep dot pop after the ring begins
           const d = baseDelay + 140;
           if (type === 'info') {
             X(dot).animate({ opacity: [0, 1], y: [-8, 0], scale: [0.2, 1], duration: 420, delay: d, easing: { stiffness: 300, damping: 14 } });
@@ -1444,7 +1335,6 @@
     _forceDestroy(reason = 'replace') {
       try { this._destroySvgAniIcon(); } catch {}
       try { this._destroySvgAniBackground(this.dom && this.dom.background); } catch {}
-      // Restore mounted DOM (if any) and cleanup listeners; also resolve the promise so it doesn't hang.
       try {
         if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
         else this._unmountDomContent();
@@ -1477,12 +1367,10 @@
         } catch {}
       };
       if (!shouldDelayUnmount) {
-        // Restore mounted DOM (moved into popup) before removing overlay
         doUnmount();
       }
 
       let customClose = false;
-      // Soft close for non-popupAnimation cases (avoid abrupt cut)
       if (!this.params.popupAnimation) {
         try {
           const popup = this.dom.popup;
@@ -1493,7 +1381,6 @@
           }
           if (this.dom.overlay) {
             const overlay = this.dom.overlay;
-            // Use inline opacity to avoid class-based jumps
             overlay.style.transition = 'opacity 0.22s ease';
             overlay.style.opacity = '1';
             requestAnimationFrame(() => {
@@ -1622,8 +1509,6 @@
     }
 
     _normalizeDomSpec(params) {
-      // Preferred: params.dom (selector/Element/template)
-      // Backward: params.content (selector/Element) OR advanced object { dom, mode, clone }
       const p = params || {};
       let dom = p.dom;
       let mode = p.domMode || 'move';
@@ -1645,16 +1530,13 @@
     }
 
     _mountDomContentInto(target, domSpec, opts = null) {
-      // Like _mountDomContent, but mounts into the provided container.
       const collectFlow = !!(opts && opts.collectFlow);
       const originalContent = this.dom.content;
-      // Temporarily redirect this.dom.content for reuse of internal logic.
       try { this.dom.content = target; } catch {}
       try {
         const before = this._mounted;
         this._mountDomContent(domSpec);
         const rec = this._mounted;
-        // If we mounted in move-mode, _mounted holds record; detach it from single-mode tracking.
         if (collectFlow && rec && rec.kind === 'move') {
           this._flowMountedList.push(rec);
           this._mounted = before; // restore previous single record (usually null)
@@ -1665,13 +1547,11 @@
     }
 
     _unmountFlowMounted() {
-      // Restore all moved DOM nodes for flow steps
       const list = Array.isArray(this._flowMountedList) ? this._flowMountedList : [];
       this._flowMountedList = [];
       list.forEach((m) => {
         try {
           if (!m || m.kind !== 'move') return;
-          // Reuse single unmount logic by swapping _mounted
           const prev = this._mounted;
           this._mounted = m;
           this._unmountDomContent();
@@ -1693,8 +1573,6 @@
           try {
             const cs = (typeof getComputedStyle === 'function') ? getComputedStyle(el) : null;
             const display = cs ? String(cs.display || '') : '';
-            // If element is hidden via CSS (e.g. .hidden-dom{display:none}),
-            // add an inline override so it becomes visible inside the popup.
             if (display === 'none') el.style.display = 'block';
             else if (el.style && el.style.display === 'none') el.style.display = 'block';
           } catch {}
@@ -1705,7 +1583,6 @@
         if (typeof node === 'string') node = document.querySelector(node);
         if (!node) return;
 
-        // <template> support: always clone template content
         if (typeof HTMLTemplateElement !== 'undefined' && node instanceof HTMLTemplateElement) {
           const frag = node.content.cloneNode(true);
           this.dom.content.appendChild(frag);
@@ -1717,7 +1594,6 @@
 
         if (domSpec.mode === 'clone') {
           const clone = node.cloneNode(true);
-          // If original is hidden (via attr or CSS), the clone may inherit; force show it in popup.
           try { clone.hidden = false; } catch {}
           try {
             const cs = (typeof getComputedStyle === 'function') ? getComputedStyle(node) : null;
@@ -1729,12 +1605,10 @@
           return;
         }
 
-        // Default: move into popup but restore on close
         const placeholder = document.createComment('layer-dom-placeholder');
         const parent = node.parentNode;
         const nextSibling = node.nextSibling;
         if (!parent) {
-          // Detached node: moving would lose it when overlay is removed; clone instead.
           const clone = node.cloneNode(true);
           try { clone.hidden = false; } catch {}
           try { if (clone.style && clone.style.display === 'none') clone.style.display = ''; } catch {}
@@ -1750,7 +1624,6 @@
         this.dom.content.appendChild(node);
         this._mounted = { kind: 'move', originalEl: node, placeholder, parent, nextSibling, prevHidden, prevInlineDisplay };
       } catch {
-        // ignore
       }
     }
 
@@ -1763,13 +1636,11 @@
       const node = m.originalEl;
       if (!node) return;
 
-      // Restore hidden/display
       try { node.hidden = !!m.prevHidden; } catch {}
       try {
         if (node.style && typeof m.prevInlineDisplay === 'string') node.style.display = m.prevInlineDisplay;
       } catch {}
 
-      // Move back to original position
       try {
         const ph = m.placeholder;
         if (ph && ph.parentNode) {
@@ -1779,7 +1650,6 @@
         }
       } catch {}
 
-      // Fallback: append to original parent
       try {
         if (m.parent) m.parent.appendChild(node);
       } catch {}
@@ -1833,12 +1703,10 @@
     }
 
     async _handleConfirm() {
-      // Flow next / finalize
       if (this._flowSteps && this._flowSteps.length) {
         return this._flowNext();
       }
 
-      // Single popup: support async preConfirm
       const pre = this.params && this.params.preConfirm;
       if (typeof pre === 'function') {
         try {
@@ -1849,7 +1717,6 @@
             this._setButtonsDisabled(false);
             return;
           }
-          // store value in non-flow mode as single value
           this._flowValues = [v];
         } catch (e) {
           console.error(e);
@@ -1863,7 +1730,6 @@
 
     _handleCancel() {
       if (this._flowSteps && this._flowSteps.length) {
-        // default: if not first step, cancel acts as "back"
         if (this._flowIndex > 0) {
           this._flowPrev();
           return;
@@ -1877,22 +1743,16 @@
       const base = this._flowBase || {};
       const merged = { ...base, ...step };
 
-      // Default button texts for flow
       const isLast = index >= (this._flowSteps.length - 1);
       if (!('confirmButtonText' in step)) merged.confirmButtonText = isLast ? (base.confirmButtonText || 'OK') : 'Next';
 
-      // Show cancel as Back after first step (unless step explicitly overrides)
       if (index > 0) {
         if (!('showCancelButton' in step)) merged.showCancelButton = true;
         if (!('cancelButtonText' in step)) merged.cancelButtonText = 'Back';
       } else {
-        // First step default keeps base settings
         if (!('cancelButtonText' in step) && merged.showCancelButton) merged.cancelButtonText = merged.cancelButtonText || 'Cancel';
       }
 
-      // Icon/animation policy for flow:
-      // - During steps: no icon/animations by default (avoids distraction + layout jitter)
-      // - Allow icon only if step explicitly uses `icon:'question'`, or step is marked as summary.
       const isSummary = !!(step && (step.summary === true || step.isSummary === true));
       const explicitIcon = ('icon' in step) ? step.icon : undefined;
       const baseIcon = ('icon' in base) ? base.icon : undefined;
@@ -1908,7 +1768,6 @@
           merged.iconAnimation = false;
         }
       } else if (!isSummary && isLast) {
-        // last step: still suppress unless summary/question/container icon
         if (allowContainerIcon) {
           merged.icon = chosenIcon;
         } else {
@@ -1916,7 +1775,6 @@
           merged.iconAnimation = false;
         }
       } else {
-        // summary step: allow icon; animation follows explicit config (default true only if provided elsewhere)
         if (!('icon' in step) && chosenIcon === undefined) merged.icon = null;
       }
 
@@ -1934,7 +1792,6 @@
       const total = this._flowSteps.length;
       const isLast = idx >= (total - 1);
 
-      // preConfirm hook for current step
       const pre = this.params && this.params.preConfirm;
       if (typeof pre === 'function') {
         try {
@@ -1994,10 +1851,8 @@
         return;
       }
 
-      // Measure current content height
       const oldH = content.getBoundingClientRect().height;
 
-      // Prepare target pane for measurement without affecting layout
       const prevDisplay = toPane.style.display;
       const prevPos = toPane.style.position;
       const prevVis = toPane.style.visibility;
@@ -2011,13 +1866,11 @@
 
       const newH = toPane.getBoundingClientRect().height;
 
-      // Restore pane styles (keep hidden until animation starts)
       toPane.style.position = prevPos;
       toPane.style.visibility = prevVis;
       toPane.style.pointerEvents = prevPointer;
       toPane.style.display = prevDisplay; // usually 'none'
 
-      // Apply new options (title/buttons/icon policy) before showing the pane
       this._flowIndex = nextIndex;
       this.params = nextOptions;
       this._applyPopupTheme(this.params);
@@ -2027,7 +1880,6 @@
       const exitTo = isNext ? -100 : 100;
       const slideDuration = 320;
 
-      // Update title with directional slide (keep in sync with panes)
       let titleAnim = null;
       let titleCleanup = null;
       try {
@@ -2110,7 +1962,6 @@
       const bgTransition = this._transitionBackground(nextBgSpec, direction);
       this._applyPopupSize(this.params);
 
-      // Icon updates (only if needed)
       try {
         const wantsIcon = !!this.params.icon;
         if (!wantsIcon && this.dom.icon) {
@@ -2125,7 +1976,6 @@
               this.dom.icon.remove();
             }
             this.dom.icon = this._createIcon(this.params.icon);
-            // icon should be on top (before title)
             popup.insertBefore(this.dom.icon, this.dom.title || popup.firstChild);
           }
         }
@@ -2133,7 +1983,6 @@
 
       this._updateFlowActions();
 
-      // Prepare panes for animation
       toPane.style.display = '';
       toPane.style.position = 'absolute';
       toPane.style.left = '0';
@@ -2151,7 +2000,6 @@
       fromPane.style.opacity = '1';
       fromPane.style.pointerEvents = 'none';
 
-      // Lock content height during transition
       content.style.height = oldH + 'px';
       content.style.overflow = 'hidden';
 
@@ -2165,7 +2013,6 @@
           { duration: 220, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
         );
       } catch {}
-      // Ensure the new pane is in layout before starting slide (fixes missing transitions on some browsers).
       try { void toPane.offsetWidth; } catch {}
       await new Promise((resolve) => requestAnimationFrame(resolve));
       try {
@@ -2193,7 +2040,6 @@
             try { fromPane.style.transition = ''; } catch {}
             try { toPane.style.transition = ''; } catch {}
           };
-          // Force a layout so transitions apply reliably.
           void fromPane.offsetWidth;
           requestAnimationFrame(() => {
             try {
@@ -2221,7 +2067,6 @@
       try { if (titleCleanup) titleCleanup(); } catch {}
       try { if (slideCleanup) slideCleanup(); } catch {}
 
-      // Cleanup styles and hide old pane
       fromPane.style.display = 'none';
       fromPane.style.position = '';
       fromPane.style.left = '';
@@ -2242,7 +2087,6 @@
       content.style.height = '';
       content.style.overflow = '';
 
-      // Re-adjust ring background if icon exists (rare in flow)
       try { this._adjustRingBackgroundColor(); } catch {}
     }
 
@@ -2354,27 +2198,23 @@
 
     _rerenderInside() {
       this._destroySvgAniIcon();
-      // Update popup content without recreating overlay (used in flow transitions)
       const popup = this._clearPopup();
       if (!popup) return;
 
       this._applyPopupTheme(this.params);
 
-      // Icon
       this.dom.icon = null;
       if (this.params.icon) {
         this.dom.icon = this._createIcon(this.params.icon);
         popup.appendChild(this.dom.icon);
       }
 
-      // Title
       this.dom.title = null;
       if (this.params.title) {
         this.dom.title = el('h2', `${PREFIX}title`, this.params.title);
         popup.appendChild(this.dom.title);
       }
 
-      // Content
       this.dom.content = null;
       if (this.params.text || this.params.html || this.params.content || this.params.dom) {
         this.dom.content = el('div', `${PREFIX}content`);
@@ -2385,7 +2225,6 @@
         popup.appendChild(this.dom.content);
       }
 
-      // Actions / buttons
       this.dom.actions = el('div', `${PREFIX}actions`);
       this.dom.cancelBtn = null;
       if (this.params.showCancelButton) {
@@ -2402,7 +2241,6 @@
 
       popup.appendChild(this.dom.actions);
 
-      // Re-run open hooks for each step
       try {
         if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
       } catch {}

+ 2 - 0
xjs.css

@@ -73,6 +73,8 @@
 .layer-popup > :not(.layer-bg) {
   position: relative;
   z-index: 1;
+  /* Prevent flex items from collapsing when popup height is fixed (step transitions). */
+  flex-shrink: 0;
 }
 .layer-bg {
   position: absolute;