robert 11 stundas atpakaļ
vecāks
revīzija
c04c75bbe4

+ 11 - 10
doc/layer/test_confirm_flow.html

@@ -5,6 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Layer Confirm Flow - Animal.js</title>
   <link rel="stylesheet" href="../demo.css">
+  <link rel="stylesheet" href="../../xjs.css">
   <style>
     .row {
       display: flex;
@@ -43,6 +44,16 @@
     </p>
 
     <div class="box-container">
+      <div class="demo-visual">
+        <div class="row">
+          <button class="btn danger" onclick="openConfirm()">Open confirm</button>
+          <button class="btn" onclick="openError()">Open error</button>
+        </div>
+        <div class="hint">
+          “Open error” shows the staggered X mark animation.
+        </div>
+      </div>
+
       <div class="box-header">
         <div class="box-title">Example</div>
         <div class="tabs">
@@ -73,16 +84,6 @@
       <div class="feature-desc">
         <strong>功能说明:</strong>验证 Layer 的 Promise 结果结构,以及多弹窗串联时的体验(warning → success/info)。
       </div>
-
-      <div class="demo-visual">
-        <div class="row">
-          <button class="btn danger" onclick="openConfirm()">Open confirm</button>
-          <button class="btn" onclick="openError()">Open error</button>
-        </div>
-        <div class="hint">
-          “Open error” shows the staggered X mark animation.
-        </div>
-      </div>
     </div>
 
     <div class="doc-nav" aria-label="Previous and next navigation">

+ 8 - 2
doc/layer/test_dom_steps.html

@@ -5,6 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Layer DOM Content + Steps</title>
   <link rel="stylesheet" href="../demo.css">
+  <link rel="stylesheet" href="../../xjs.css">
   <style>
     .row {
       display: flex;
@@ -114,6 +115,7 @@ xjs.layer({
 xjs.Layer.$({
   closeOnClickOutside: false,
   closeOnEsc: false,
+  popupAnimation: false,
   confirmButtonColor: '#3085d6',
   cancelButtonColor: '#444'
 })
@@ -150,6 +152,8 @@ xjs.Layer.$({
     xjs.layer({
       title: 'Done',
       icon: 'success',
+      popupAnimation: false,
+      replace: true,
       html: '&lt;pre&gt;' + JSON.stringify(res.value, null, 2) + '&lt;/pre&gt;'
     });
   }
@@ -275,7 +279,7 @@ then restore it back (and restore display/hidden state) on close. */</pre>
       xjs.Layer.$({
         closeOnClickOutside: false,
         closeOnEsc: false,
-        popupAnimation: true,
+        popupAnimation: false,
         confirmButtonColor: '#3085d6',
         cancelButtonColor: '#444'
       })
@@ -312,10 +316,12 @@ then restore it back (and restore display/hidden state) on close. */</pre>
           xjs.layer({
             title: 'Done',
             icon: 'success',
+            popupAnimation: false,
+            replace: true,
             html: '<pre>' + JSON.stringify(res.value, null, 2) + '</pre>'
           });
         } else {
-          xjs.layer({ title: 'Dismissed', icon: 'info', text: 'Flow cancelled' });
+          xjs.layer({ title: 'Dismissed', icon: 'info', popupAnimation: false, replace: true, text: 'Flow cancelled' });
         }
       });
     }

+ 19 - 19
doc/layer/test_icons_svg_animation.html

@@ -5,7 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Layer SVG Icon Animation - Animal.js</title>
   <link rel="stylesheet" href="../demo.css">
-  <link rel="stylesheet" href="../../xjs.css?v=2">
+  <link rel="stylesheet" href="../../xjs.css">
   <style>
     /* Keep Layer above the doc UI */
     .layer-overlay { z-index: 99999; }
@@ -66,6 +66,24 @@
     </p>
 
     <div class="box-container">
+      <div class="demo-visual">
+        <div class="controls" style="margin-bottom: 14px;">
+          <label><input id="optIcon" type="checkbox" checked> iconAnimation</label>
+          <label><input id="optPopup" type="checkbox" checked> popupAnimation</label>
+          <label><input id="optEsc" type="checkbox" checked> closeOnEsc</label>
+        </div>
+        <div class="row">
+          <button class="btn success" type="button" onclick="openDemo('success')">Success</button>
+          <button class="btn error" type="button" onclick="openDemo('error')">Error</button>
+          <button class="btn warning" type="button" onclick="openDemo('warning')">Warning</button>
+          <button class="btn info" type="button" onclick="openDemo('info')">Info</button>
+          <button class="btn" type="button" onclick="openDemo('question')">Question</button>
+        </div>
+        <div class="hint">
+          Tip: open “Error” to see the two stroke segments staggered.
+        </div>
+      </div>
+
       <div class="box-header">
         <div class="box-title">Controls</div>
         <div class="tabs">
@@ -99,24 +117,6 @@
       <div class="feature-desc">
         <strong>功能说明:</strong>本页专门验证 Layer 的 icon 动画:描边(draw)+ 弹性缩放(spring)。你可以关闭其中任意一项来对比体验。
       </div>
-
-      <div class="demo-visual">
-        <div class="controls" style="margin-bottom: 14px;">
-          <label><input id="optIcon" type="checkbox" checked> iconAnimation</label>
-          <label><input id="optPopup" type="checkbox" checked> popupAnimation</label>
-          <label><input id="optEsc" type="checkbox" checked> closeOnEsc</label>
-        </div>
-        <div class="row">
-          <button class="btn success" type="button" onclick="openDemo('success')">Success</button>
-          <button class="btn error" type="button" onclick="openDemo('error')">Error</button>
-          <button class="btn warning" type="button" onclick="openDemo('warning')">Warning</button>
-          <button class="btn info" type="button" onclick="openDemo('info')">Info</button>
-          <button class="btn" type="button" onclick="openDemo('question')">Question</button>
-        </div>
-        <div class="hint">
-          Tip: open “Error” to see the two stroke segments staggered.
-        </div>
-      </div>
     </div>
 
     <div class="doc-nav" aria-label="Previous and next navigation">

+ 18 - 17
doc/layer/test_selection_layer_dataset.html

@@ -5,6 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Selection.layer() + dataset - Animal.js</title>
   <link rel="stylesheet" href="../demo.css">
+  <link rel="stylesheet" href="../../xjs.css">
   <style>
     .row {
       display: flex;
@@ -58,6 +59,23 @@
     </p>
 
     <div class="box-container">
+      <div class="demo-visual">
+        <div class="row">
+          <button class="btn danger btn-delete">Delete (bind .layer)</button>
+          <button
+            class="btn btn-data"
+            data-layer-title="Data-driven popup"
+            data-layer-text="Configured via data-layer-* attributes"
+            data-layer-icon="success"
+            data-layer-cancel="true"
+          >Open (data-layer-*)</button>
+        </div>
+        <div class="hint">
+          Re-binding calls should replace old handlers automatically.
+          <span class="pill">xjs('.btn').layer(options)</span>
+        </div>
+      </div>
+
       <div class="box-header">
         <div class="box-title">Binding example</div>
         <div class="tabs">
@@ -94,23 +112,6 @@ xjs('.btn-data').layer();</pre>
         <strong>功能说明:</strong>验证 <code class="inline">Selection.layer()</code> 的去重绑定与 dataset 解析(<code class="inline">data-layer-title/text/icon/cancel</code>)。
       </div>
 
-      <div class="demo-visual">
-        <div class="row">
-          <button class="btn danger btn-delete">Delete (bind .layer)</button>
-          <button
-            class="btn btn-data"
-            data-layer-title="Data-driven popup"
-            data-layer-text="Configured via data-layer-* attributes"
-            data-layer-icon="success"
-            data-layer-cancel="true"
-          >Open (data-layer-*)</button>
-        </div>
-        <div class="hint">
-          Re-binding calls should replace old handlers automatically.
-          <span class="pill">xjs('.btn').layer(options)</span>
-        </div>
-      </div>
-
       <div class="action-bar">
         <button class="play-btn" onclick="bindAll()">RE-BIND</button>
       </div>

+ 11 - 10
doc/layer/test_static_fire.html

@@ -5,6 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Layer Static API - Animal.js</title>
   <link rel="stylesheet" href="../demo.css">
+  <link rel="stylesheet" href="../../xjs.css">
   <style>
     .row {
       display: flex;
@@ -44,6 +45,16 @@
     </p>
 
     <div class="box-container">
+      <div class="demo-visual">
+        <div class="row">
+          <button class="btn primary" onclick="openFire()">Layer.fire()</button>
+          <button class="btn" onclick="openBuilder()">Builder fire()</button>
+        </div>
+        <div class="hint">
+          Promise result is printed to <span class="mono">console</span>.
+        </div>
+      </div>
+
       <div class="box-header">
         <div class="box-title">Examples</div>
         <div class="tabs">
@@ -70,16 +81,6 @@ Layer.$()
       <div class="feature-desc">
         <strong>功能说明:</strong>验证 Layer 自身的静态入口(不依赖 Selection.layer 绑定),以及 builder 模式的链式配置。
       </div>
-
-      <div class="demo-visual">
-        <div class="row">
-          <button class="btn primary" onclick="openFire()">Layer.fire()</button>
-          <button class="btn" onclick="openBuilder()">Builder fire()</button>
-        </div>
-        <div class="hint">
-          Promise result is printed to <span class="mono">console</span>.
-        </div>
-      </div>
     </div>
 
     <div class="doc-nav" aria-label="Previous and next navigation">

+ 155 - 21
layer.js

@@ -124,6 +124,7 @@
       this._onKeydown = null;
       this._mounted = null; // { kind, originalEl, placeholder, parent, nextSibling, prevHidden, prevInlineDisplay }
       this._isClosing = false;
+      this._isReplace = false;
 
       // Step/flow support
       this._flowSteps = null; // Array<options>
@@ -273,18 +274,54 @@
     _render(meta = null) {
       // Remove existing if any (but first, try to restore any mounted DOM from the previous instance)
       const existing = document.querySelector(`.${PREFIX}overlay`);
-      if (existing) {
+      const wantReplace = !!(this.params && this.params.replace);
+      this._isReplace = false;
+      if (existing && wantReplace) {
         try {
           const prev = existing._layerInstance;
           if (prev && typeof prev._forceDestroy === 'function') prev._forceDestroy('replace');
         } catch {}
-        try { existing.remove(); } catch {}
+        try {
+          if (existing._layerCloseTimer) {
+            clearTimeout(existing._layerCloseTimer);
+            existing._layerCloseTimer = null;
+          }
+        } catch {}
+        try {
+          if (existing._layerOnClick) {
+            existing.removeEventListener('click', existing._layerOnClick);
+            existing._layerOnClick = null;
+          }
+        } catch {}
+        try {
+          while (existing.firstChild) existing.removeChild(existing.firstChild);
+        } catch {}
+        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(() => {
+            try { existing.style.transition = ''; } catch {}
+          });
+        } catch {}
+        this.dom.overlay = existing;
+        this.dom.overlay._layerInstance = this;
+        this._isReplace = true;
+      } else {
+        if (existing) {
+          try {
+            const prev = existing._layerInstance;
+            if (prev && typeof prev._forceDestroy === 'function') prev._forceDestroy('replace');
+          } catch {}
+          try { existing.remove(); } catch {}
+        }
+        // Create Overlay
+        this.dom.overlay = el('div', `${PREFIX}overlay`);
+        this.dom.overlay._layerInstance = this;
       }
 
-      // 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);
@@ -345,11 +382,25 @@
 
       // Event Listeners
       if (this.params.closeOnClickOutside) {
-        this.dom.overlay.addEventListener('click', (e) => {
+        const onClick = (e) => {
           if (e.target === this.dom.overlay) {
             this._close(null); // Dismiss
           }
-        });
+        };
+        try {
+          if (this.dom.overlay._layerOnClick) {
+            this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
+          }
+        } catch {}
+        this.dom.overlay._layerOnClick = onClick;
+        this.dom.overlay.addEventListener('click', onClick);
+      } else {
+        try {
+          if (this.dom.overlay._layerOnClick) {
+            this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
+            this.dom.overlay._layerOnClick = null;
+          }
+        } catch {}
       }
 
       // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
@@ -429,11 +480,25 @@
 
       // Event Listeners
       if (this.params.closeOnClickOutside) {
-        this.dom.overlay.addEventListener('click', (e) => {
+        const onClick = (e) => {
           if (e.target === this.dom.overlay) {
             this._close(null, 'backdrop');
           }
-        });
+        };
+        try {
+          if (this.dom.overlay._layerOnClick) {
+            this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
+          }
+        } catch {}
+        this.dom.overlay._layerOnClick = onClick;
+        this.dom.overlay.addEventListener('click', onClick);
+      } else {
+        try {
+          if (this.dom.overlay._layerOnClick) {
+            this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
+            this.dom.overlay._layerOnClick = null;
+          }
+        } catch {}
       }
 
       // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
@@ -687,6 +752,25 @@
           } 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;
+          popup.style.transition = 'none';
+          popup.style.opacity = '0';
+          popup.style.transform = 'scale(0.98)';
+          requestAnimationFrame(() => {
+            popup.style.transition = 'opacity 0.22s ease, transform 0.22s ease';
+            popup.style.opacity = '1';
+            popup.style.transform = 'scale(1)';
+            setTimeout(() => {
+              try { popup.style.transition = ''; } catch {}
+              try { popup.style.opacity = ''; } catch {}
+              try { popup.style.transform = ''; } catch {}
+            }, 260);
+          });
+        } catch {}
+      }
 
       // Icon SVG draw animation
       if (this.params.iconAnimation) {
@@ -759,7 +843,10 @@
 
     _forceDestroy(reason = 'replace') {
       // Restore mounted DOM (if any) and cleanup listeners; also resolve the promise so it doesn't hang.
-      try { this._unmountDomContent(); } catch {}
+      try {
+        if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
+        else this._unmountDomContent();
+      } catch {}
       if (this._onKeydown) {
         try { document.removeEventListener('keydown', this._onKeydown); } catch {}
         this._onKeydown = null;
@@ -775,18 +862,65 @@
       if (this._isClosing) return;
       this._isClosing = true;
 
-      // Restore mounted DOM (moved into popup) before removing overlay
-      try {
-        if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
-        else this._unmountDomContent();
-      } catch {}
+      const shouldDelayUnmount = !!(
+        (this._mounted && this._mounted.kind === 'move') ||
+        (this._flowSteps && this._flowSteps.length && this._flowMountedList && this._flowMountedList.length)
+      ) || !this.params.popupAnimation;
+      const doUnmount = () => {
+        try {
+          if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
+          else this._unmountDomContent();
+        } catch {}
+      };
+      if (!shouldDelayUnmount) {
+        // Restore mounted DOM (moved into popup) before removing overlay
+        doUnmount();
+      }
 
-      this.dom.overlay.classList.remove('show');
-      setTimeout(() => {
-        if (this.dom.overlay && this.dom.overlay.parentNode) {
-          this.dom.overlay.parentNode.removeChild(this.dom.overlay);
+      let customClose = false;
+      // Soft close for non-popupAnimation cases (avoid abrupt cut)
+      if (!this.params.popupAnimation) {
+        try {
+          const popup = this.dom.popup;
+          if (popup) {
+            popup.style.transition = 'opacity 0.22s ease';
+            popup.style.opacity = '0';
+            popup.style.transform = '';
+          }
+          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(() => {
+              try {
+                if (overlay._layerInstance !== this) return;
+                overlay.style.opacity = '0';
+              } catch {}
+            });
+          }
+          customClose = true;
+        } catch {}
+      }
+
+      if (!customClose) {
+        this.dom.overlay.classList.remove('show');
+      }
+      try {
+        if (this.dom.overlay) {
+          const overlay = this.dom.overlay;
+          const delay = customClose ? 240 : 300;
+          overlay._layerCloseTimer = setTimeout(() => {
+            if (shouldDelayUnmount) {
+              doUnmount();
+            }
+            if (overlay._layerInstance !== this) return;
+            if (overlay.parentNode) {
+              overlay.parentNode.removeChild(overlay);
+            }
+          }, delay);
         }
-      }, 300);
+      } catch {}
 
       if (this._onKeydown) {
         try { document.removeEventListener('keydown', this._onKeydown); } catch {}