فهرست منبع

layer动画美化

robert 2 روز پیش
والد
کامیت
78799ea237
5فایلهای تغییر یافته به همراه361 افزوده شده و 59 حذف شده
  1. 1 1
      doc/layer/overview.html
  2. 1 1
      doc/layer/test_confirm_flow.html
  3. 44 5
      doc/layer/test_icons_svg_animation.html
  4. 160 27
      layer.js
  5. 155 25
      xjs.js

+ 1 - 1
doc/layer/overview.html

@@ -162,7 +162,7 @@ xjs.layer({
       xjs.layer({
         title: 'Delete item?',
         text: 'This action cannot be undone.',
-        icon: 'warning',
+        icon: 'question',
         showCancelButton: true,
         confirmButtonText: 'Delete',
         cancelButtonText: 'Cancel'

+ 1 - 1
doc/layer/test_confirm_flow.html

@@ -122,7 +122,7 @@
       xjs.layer({
         title: 'Delete item?',
         text: 'This action cannot be undone.',
-        icon: 'warning',
+        icon: 'question',
         showCancelButton: true,
         confirmButtonText: 'Delete',
         cancelButtonText: 'Cancel',

+ 44 - 5
doc/layer/test_icons_svg_animation.html

@@ -94,10 +94,11 @@
           <label><input id="optEsc" type="checkbox" checked> closeOnEsc</label>
         </div>
         <div class="row">
-          <button class="btn success" onclick="open('success')">Success</button>
-          <button class="btn error" onclick="open('error')">Error</button>
-          <button class="btn warning" onclick="open('warning')">Warning</button>
-          <button class="btn info" onclick="open('info')">Info</button>
+          <button class="btn success" type="button" onclick="open('success')">Success</button>
+          <button class="btn error" type="button" onclick="open('error')">Error</button>
+          <button class="btn warning" type="button" onclick="open('warning')">Warning</button>
+          <button class="btn info" type="button" onclick="open('info')">Info</button>
+          <button class="btn" type="button" onclick="open('question')">Question</button>
         </div>
         <div class="hint">
           Tip: open “Error” to see the two stroke segments staggered.
@@ -122,6 +123,41 @@
   <script src="../../xjs.js"></script>
   <script>
     const CURRENT = 'layer/test_icons_svg_animation.html';
+    // On-page error surface (helps debug inside iframe)
+    (function () {
+      const showErr = (title, detail) => {
+        try {
+          let el = document.getElementById('__err');
+          if (!el) {
+            el = document.createElement('div');
+            el.id = '__err';
+            el.style.position = 'fixed';
+            el.style.left = '16px';
+            el.style.right = '16px';
+            el.style.bottom = '16px';
+            el.style.zIndex = '999999';
+            el.style.background = 'rgba(10,10,10,0.92)';
+            el.style.border = '1px solid rgba(255,75,75,0.35)';
+            el.style.borderRadius = '12px';
+            el.style.padding = '10px 12px';
+            el.style.color = '#ffd1d1';
+            el.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
+            el.style.fontSize = '12px';
+            el.style.whiteSpace = 'pre-wrap';
+            el.style.maxHeight = '40vh';
+            el.style.overflow = 'auto';
+            document.body.appendChild(el);
+          }
+          el.textContent = `[${title}]\n` + String(detail || '');
+        } catch {}
+      };
+      window.addEventListener('error', (e) => {
+        showErr('error', e?.error?.stack || e?.message || e);
+      });
+      window.addEventListener('unhandledrejection', (e) => {
+        showErr('unhandledrejection', e?.reason?.stack || e?.reason || e);
+      });
+    })();
 
     function switchTab(tab) {
       document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -142,7 +178,10 @@
       const iconAnimation = !!document.getElementById('optIcon')?.checked;
       const popupAnimation = !!document.getElementById('optPopup')?.checked;
       const closeOnEsc = !!document.getElementById('optEsc')?.checked;
-      xjs.layer({
+      // Defensive: if Layer failed to load, surface a useful error
+      if (typeof xjs !== 'function') throw new Error('xjs is not loaded');
+      if (!window.Layer) throw new Error('Layer is not loaded');
+      return xjs.layer({
         title: type[0].toUpperCase() + type.slice(1),
         text: 'SVG draw + spring entrance',
         icon: type,

+ 160 - 27
layer.js

@@ -10,8 +10,22 @@
      return new LayerClass();
   }
   
-  // Copy static methods
-  Object.assign(LayerFactory, 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) => {
+        if (k === 'prototype' || k === 'name' || k === 'length') return;
+        const desc = Object.getOwnPropertyDescriptor(from, k);
+        if (!desc) return;
+        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;
   
@@ -52,6 +66,8 @@
       display: flex;
       flex-direction: column;
       align-items: center;
+      position: relative;
+      z-index: 1;
       transform: scale(0.92);
       transition: transform 0.26s ease;
       font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -101,8 +117,8 @@
       background-color: #999;
     }
     .${PREFIX}icon {
-      width: 5em;
-      height: 5em;
+      width: 6em;
+      height: 6em;
       margin: 1.5em auto 1.2em;
       position: relative;
       display: grid;
@@ -138,6 +154,7 @@
     .${PREFIX}icon.error { color: #f27474; }
     .${PREFIX}icon.warning { color: #f8bb86; }
     .${PREFIX}icon.info { color: #3fc3ee; }
+    .${PREFIX}icon.question { color: #b18cff; }
   `;
 
   // Inject Styles
@@ -208,6 +225,7 @@
         title: '',
         text: '',
         icon: null,
+        iconSize: null, // e.g. '6em' / '72px'
         confirmButtonText: 'OK',
         cancelButtonText: 'Cancel',
         showCancelButton: false,
@@ -329,6 +347,17 @@
     _createIcon(type) {
       const icon = document.createElement('div');
       icon.className = `${PREFIX}icon ${type}`;
+
+      // Allow per-popup icon sizing (overrides CSS default)
+      if (this.params && this.params.iconSize) {
+        try {
+          const s = String(this.params.iconSize).trim();
+          if (s) {
+            icon.style.width = s;
+            icon.style.height = s;
+          }
+        } catch {}
+      }
       
       const svgNs = 'http://www.w3.org/2000/svg';
       const svg = document.createElementNS(svgNs, 'svg');
@@ -377,6 +406,16 @@
         dot.setAttribute('r', '3.2');
         dot.setAttribute('fill', 'currentColor');
         svg.appendChild(dot);
+      } else if (type === 'question') {
+        // Question mark (single stroke + dot)
+        addPath('M30 30 C30 23 35 19 42 19 C49 19 54 23 54 30 C54 36 50 39 46 41 C43 42 42 44 42 48 L42 52', `${PREFIX}svg-question`);
+        const dot = document.createElementNS(svgNs, 'circle');
+        dot.setAttribute('class', `${PREFIX}svg-dot`);
+        dot.setAttribute('cx', '42');
+        dot.setAttribute('cy', '61');
+        dot.setAttribute('r', '3.2');
+        dot.setAttribute('fill', 'currentColor');
+        svg.appendChild(dot);
       } else {
         // Fallback to info-like ring only
       }
@@ -431,50 +470,144 @@
       const X = Layer._getXjs();
       if (!X) return;
 
+      const type = (this.params && this.params.icon) || '';
+
       try {
-        X(icon).animate({
-          opacity: [0, 1],
-          scale: [0.68, 1],
-          duration: 520,
-          easing: { stiffness: 240, damping: 14 }
-        });
+        // Entrance varies slightly by type (SweetAlert-like feel)
+        if (type === 'error') {
+          X(icon).animate({
+            opacity: [0, 1],
+            scale: [0.62, 1],
+            rotate: [-10, 0],
+            duration: 520,
+            easing: { stiffness: 240, damping: 14 }
+          });
+        } else if (type === 'warning') {
+          X(icon).animate({
+            opacity: [0, 1],
+            scale: [0.62, 1],
+            y: [-10, 0],
+            duration: 620,
+            easing: { stiffness: 260, damping: 15 }
+          });
+        } else if (type === 'question') {
+          X(icon).animate({
+            opacity: [0, 1],
+            scale: [0.62, 1],
+            rotate: [8, 0],
+            duration: 620,
+            easing: { stiffness: 240, damping: 14 }
+          });
+        } else {
+          X(icon).animate({
+            opacity: [0, 1],
+            scale: [0.66, 1],
+            duration: 520,
+            easing: { stiffness: 240, damping: 14 }
+          });
+        }
       } catch {}
 
       const svg = icon.querySelector('svg');
       if (!svg) return;
 
-      const type = (this.params && this.params.icon) || '';
       const ring = svg.querySelector(`.${PREFIX}svg-ring`);
       const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
       const dot = svg.querySelector(`.${PREFIX}svg-dot`);
 
-      // draw ring first
-      try { if (ring) X(ring).draw({ duration: 520, easing: 'ease-in-out', delay: 60 }); } catch {}
+      // Draw ring first (timing varies by type)
+      try {
+        if (ring) {
+          const ringDur = (type === 'success') ? 520 : (type === 'error') ? 420 : 560;
+          const ringEase = (type === 'warning' || type === 'question') ? 'ease-in-out' : 'ease-out';
+          X(ring).draw({ duration: ringDur, easing: ringEase, delay: 40 });
+        }
+      } catch {}
 
       if (type === 'error') {
-        // two lines staggered
+        // Two lines + shake after draw
         const left = marks[0];
         const right = marks[1];
-        try { if (left) X(left).draw({ duration: 360, easing: 'ease-out', delay: 180 }); } catch {}
-        try { if (right) X(right).draw({ duration: 360, easing: 'ease-out', delay: 250 }); } catch {}
+        try { if (left) X(left).draw({ duration: 320, easing: 'ease-out', delay: 170 }); } catch {}
+        try { if (right) X(right).draw({ duration: 320, easing: 'ease-out', delay: 240 }); } catch {}
+        try {
+          X(icon).animate({
+            rotate: [-10, 10],
+            duration: 70,
+            loop: 4,
+            direction: 'alternate',
+            easing: 'ease-in-out',
+            delay: 360
+          });
+        } catch {}
         return;
       }
 
-      // single mark (success/warning/info)
-      marks.forEach((m, i) => {
-        try { X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 + i * 60 }); } catch {}
-      });
+      // Single-mark types (success / warning / info / question)
+      if (type === 'success') {
+        const m = marks[0];
+        try { if (m) X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 }); } catch {}
+        try {
+          if (ring) X(ring).animate({ strokeWidth: [4.5, 6], duration: 220, delay: 360, easing: { stiffness: 300, damping: 18 } });
+        } catch {}
+      } else if (type === 'warning') {
+        const m = marks[0];
+        try { if (m) X(m).draw({ duration: 380, easing: 'ease-in-out', delay: 190 }); } catch {}
+        try {
+          if (ring) X(ring).animate({ opacity: [1, 0.55], duration: 260, delay: 420, loop: 2, direction: 'alternate', easing: 'ease-in-out' });
+        } catch {}
+      } else if (type === 'info') {
+        const m = marks[0];
+        try { if (m) X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 }); } catch {}
+      } else if (type === 'question') {
+        const m = marks[0];
+        try { if (m) X(m).draw({ duration: 520, easing: 'ease-in-out', delay: 170 }); } catch {}
+        try {
+          X(icon).animate({ rotate: [-6, 6], duration: 90, loop: 3, direction: 'alternate', easing: 'ease-in-out', delay: 420 });
+        } catch {}
+      } else {
+        marks.forEach((m, i) => {
+          try { X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 + i * 60 }); } catch {}
+        });
+      }
 
       if (dot) {
         try {
           dot.style.opacity = '0';
-          X(dot).animate({
-            opacity: [0, 1],
-            scale: [0.2, 1],
-            duration: 320,
-            delay: 280,
-            easing: { stiffness: 320, damping: 18 }
-          });
+          if (type === 'warning') {
+            X(dot).animate({
+              opacity: [0, 1],
+              scale: [0.2, 1],
+              duration: 260,
+              delay: 320,
+              easing: { stiffness: 360, damping: 18 }
+            });
+          } else if (type === 'info') {
+            X(dot).animate({
+              opacity: [0, 1],
+              y: [-8, 0],
+              scale: [0.2, 1],
+              duration: 420,
+              delay: 260,
+              easing: { stiffness: 300, damping: 14 }
+            });
+          } else if (type === 'question') {
+            X(dot).animate({
+              opacity: [0, 1],
+              scale: [0.2, 1],
+              duration: 360,
+              delay: 360,
+              easing: { stiffness: 320, damping: 16 }
+            });
+          } else {
+            X(dot).animate({
+              opacity: [0, 1],
+              scale: [0.2, 1],
+              duration: 320,
+              delay: 280,
+              easing: { stiffness: 320, damping: 18 }
+            });
+          }
         } catch {}
       }
     }

+ 155 - 25
xjs.js

@@ -26,8 +26,22 @@
      return new LayerClass();
   }
   
-  // Copy static methods
-  Object.assign(LayerFactory, 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) => {
+        if (k === 'prototype' || k === 'name' || k === 'length') return;
+        const desc = Object.getOwnPropertyDescriptor(from, k);
+        if (!desc) return;
+        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;
   
@@ -68,6 +82,8 @@
       display: flex;
       flex-direction: column;
       align-items: center;
+      position: relative;
+      z-index: 1;
       transform: scale(0.92);
       transition: transform 0.26s ease;
       font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -117,8 +133,8 @@
       background-color: #999;
     }
     .${PREFIX}icon {
-      width: 5em;
-      height: 5em;
+      width: 6em;
+      height: 6em;
       margin: 1.5em auto 1.2em;
       position: relative;
       display: grid;
@@ -154,6 +170,7 @@
     .${PREFIX}icon.error { color: #f27474; }
     .${PREFIX}icon.warning { color: #f8bb86; }
     .${PREFIX}icon.info { color: #3fc3ee; }
+    .${PREFIX}icon.question { color: #b18cff; }
   `;
 
   // Inject Styles
@@ -224,6 +241,7 @@
         title: '',
         text: '',
         icon: null,
+        iconSize: null, // e.g. '6em' / '72px'
         confirmButtonText: 'OK',
         cancelButtonText: 'Cancel',
         showCancelButton: false,
@@ -346,6 +364,17 @@
       const icon = document.createElement('div');
       icon.className = `${PREFIX}icon ${type}`;
       
+      // Allow per-popup icon sizing (overrides CSS default)
+      if (this.params && this.params.iconSize) {
+        try {
+          const s = String(this.params.iconSize).trim();
+          if (s) {
+            icon.style.width = s;
+            icon.style.height = s;
+          }
+        } catch {}
+      }
+
       const svgNs = 'http://www.w3.org/2000/svg';
       const svg = document.createElementNS(svgNs, 'svg');
       svg.setAttribute('viewBox', '0 0 80 80');
@@ -392,6 +421,15 @@
         dot.setAttribute('r', '3.2');
         dot.setAttribute('fill', 'currentColor');
         svg.appendChild(dot);
+      } else if (type === 'question') {
+        addPath('M30 30 C30 23 35 19 42 19 C49 19 54 23 54 30 C54 36 50 39 46 41 C43 42 42 44 42 48 L42 52', `${PREFIX}svg-question`);
+        const dot = document.createElementNS(svgNs, 'circle');
+        dot.setAttribute('class', `${PREFIX}svg-dot`);
+        dot.setAttribute('cx', '42');
+        dot.setAttribute('cy', '61');
+        dot.setAttribute('r', '3.2');
+        dot.setAttribute('fill', 'currentColor');
+        svg.appendChild(dot);
       } else {
         // ring only
       }
@@ -443,47 +481,139 @@
       const X = Layer._getXjs();
       if (!X) return;
 
+      const type = (this.params && this.params.icon) || '';
+
       try {
-        X(icon).animate({
-          opacity: [0, 1],
-          scale: [0.68, 1],
-          duration: 520,
-          easing: { stiffness: 240, damping: 14 }
-        });
+        if (type === 'error') {
+          X(icon).animate({
+            opacity: [0, 1],
+            scale: [0.62, 1],
+            rotate: [-10, 0],
+            duration: 520,
+            easing: { stiffness: 240, damping: 14 }
+          });
+        } else if (type === 'warning') {
+          X(icon).animate({
+            opacity: [0, 1],
+            scale: [0.62, 1],
+            y: [-10, 0],
+            duration: 620,
+            easing: { stiffness: 260, damping: 15 }
+          });
+        } else if (type === 'question') {
+          X(icon).animate({
+            opacity: [0, 1],
+            scale: [0.62, 1],
+            rotate: [8, 0],
+            duration: 620,
+            easing: { stiffness: 240, damping: 14 }
+          });
+        } else {
+          X(icon).animate({
+            opacity: [0, 1],
+            scale: [0.66, 1],
+            duration: 520,
+            easing: { stiffness: 240, damping: 14 }
+          });
+        }
       } catch {}
 
       const svg = icon.querySelector('svg');
       if (!svg) return;
-
-      const type = (this.params && this.params.icon) || '';
       const ring = svg.querySelector(`.${PREFIX}svg-ring`);
       const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
       const dot = svg.querySelector(`.${PREFIX}svg-dot`);
 
-      try { if (ring) X(ring).draw({ duration: 520, easing: 'ease-in-out', delay: 60 }); } catch {}
+      try {
+        if (ring) {
+          const ringDur = (type === 'success') ? 520 : (type === 'error') ? 420 : 560;
+          const ringEase = (type === 'warning' || type === 'question') ? 'ease-in-out' : 'ease-out';
+          X(ring).draw({ duration: ringDur, easing: ringEase, delay: 40 });
+        }
+      } catch {}
 
       if (type === 'error') {
         const left = marks[0];
         const right = marks[1];
-        try { if (left) X(left).draw({ duration: 360, easing: 'ease-out', delay: 180 }); } catch {}
-        try { if (right) X(right).draw({ duration: 360, easing: 'ease-out', delay: 250 }); } catch {}
+        try { if (left) X(left).draw({ duration: 320, easing: 'ease-out', delay: 170 }); } catch {}
+        try { if (right) X(right).draw({ duration: 320, easing: 'ease-out', delay: 240 }); } catch {}
+        try {
+          X(icon).animate({
+            rotate: [-10, 10],
+            duration: 70,
+            loop: 4,
+            direction: 'alternate',
+            easing: 'ease-in-out',
+            delay: 360
+          });
+        } catch {}
         return;
       }
 
-      marks.forEach((m, i) => {
-        try { X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 + i * 60 }); } catch {}
-      });
+      if (type === 'success') {
+        const m = marks[0];
+        try { if (m) X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 }); } catch {}
+        try {
+          if (ring) X(ring).animate({ strokeWidth: [4.5, 6], duration: 220, delay: 360, easing: { stiffness: 300, damping: 18 } });
+        } catch {}
+      } else if (type === 'warning') {
+        const m = marks[0];
+        try { if (m) X(m).draw({ duration: 380, easing: 'ease-in-out', delay: 190 }); } catch {}
+        try {
+          if (ring) X(ring).animate({ opacity: [1, 0.55], duration: 260, delay: 420, loop: 2, direction: 'alternate', easing: 'ease-in-out' });
+        } catch {}
+      } else if (type === 'info') {
+        const m = marks[0];
+        try { if (m) X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 }); } catch {}
+      } else if (type === 'question') {
+        const m = marks[0];
+        try { if (m) X(m).draw({ duration: 520, easing: 'ease-in-out', delay: 170 }); } catch {}
+        try {
+          X(icon).animate({ rotate: [-6, 6], duration: 90, loop: 3, direction: 'alternate', easing: 'ease-in-out', delay: 420 });
+        } catch {}
+      } else {
+        marks.forEach((m, i) => {
+          try { X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 + i * 60 }); } catch {}
+        });
+      }
 
       if (dot) {
         try {
           dot.style.opacity = '0';
-          X(dot).animate({
-            opacity: [0, 1],
-            scale: [0.2, 1],
-            duration: 320,
-            delay: 280,
-            easing: { stiffness: 320, damping: 18 }
-          });
+          if (type === 'warning') {
+            X(dot).animate({
+              opacity: [0, 1],
+              scale: [0.2, 1],
+              duration: 260,
+              delay: 320,
+              easing: { stiffness: 360, damping: 18 }
+            });
+          } else if (type === 'info') {
+            X(dot).animate({
+              opacity: [0, 1],
+              y: [-8, 0],
+              scale: [0.2, 1],
+              duration: 420,
+              delay: 260,
+              easing: { stiffness: 300, damping: 14 }
+            });
+          } else if (type === 'question') {
+            X(dot).animate({
+              opacity: [0, 1],
+              scale: [0.2, 1],
+              duration: 360,
+              delay: 360,
+              easing: { stiffness: 320, damping: 16 }
+            });
+          } else {
+            X(dot).animate({
+              opacity: [0, 1],
+              scale: [0.2, 1],
+              duration: 320,
+              delay: 280,
+              easing: { stiffness: 320, damping: 18 }
+            });
+          }
         } catch {}
       }
     }