|
|
@@ -68,8 +68,8 @@
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
- transform: scale(0.9);
|
|
|
- transition: transform 0.3s;
|
|
|
+ transform: scale(0.92);
|
|
|
+ transition: transform 0.26s ease;
|
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
}
|
|
|
.${PREFIX}overlay.show .${PREFIX}popup {
|
|
|
@@ -119,98 +119,41 @@
|
|
|
.${PREFIX}icon {
|
|
|
width: 5em;
|
|
|
height: 5em;
|
|
|
- border: 0.25em solid transparent;
|
|
|
- border-radius: 50%;
|
|
|
margin: 1.5em auto 1.2em;
|
|
|
- box-sizing: content-box;
|
|
|
position: relative;
|
|
|
- }
|
|
|
-
|
|
|
- /* Success Icon */
|
|
|
- .${PREFIX}icon.success {
|
|
|
- border-color: #a5dc86;
|
|
|
- color: #a5dc86;
|
|
|
- }
|
|
|
- .${PREFIX}icon.success::before, .${PREFIX}icon.success::after {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- background: #fff;
|
|
|
- border-radius: 50%;
|
|
|
- }
|
|
|
- .${PREFIX}success-line-tip {
|
|
|
- width: 1.5em;
|
|
|
- height: 0.3em;
|
|
|
- background-color: #a5dc86;
|
|
|
- display: block;
|
|
|
- position: absolute;
|
|
|
- top: 2.85em;
|
|
|
- left: 0.85em;
|
|
|
- transform: rotate(45deg);
|
|
|
- border-radius: 0.15em;
|
|
|
- }
|
|
|
- .${PREFIX}success-line-long {
|
|
|
- width: 2.9em;
|
|
|
- height: 0.3em;
|
|
|
- background-color: #a5dc86;
|
|
|
- display: block;
|
|
|
- position: absolute;
|
|
|
- top: 2.4em;
|
|
|
- right: 0.5em;
|
|
|
- transform: rotate(-45deg);
|
|
|
- border-radius: 0.15em;
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
}
|
|
|
|
|
|
- /* Error Icon */
|
|
|
- .${PREFIX}icon.error {
|
|
|
- border-color: #f27474;
|
|
|
- color: #f27474;
|
|
|
- }
|
|
|
- .${PREFIX}error-x-mark {
|
|
|
- position: relative;
|
|
|
+ /* SVG Icon (SweetAlert-like, animated via xjs.draw + spring) */
|
|
|
+ .${PREFIX}icon svg {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
display: block;
|
|
|
+ overflow: visible;
|
|
|
}
|
|
|
- .${PREFIX}error-line {
|
|
|
- position: absolute;
|
|
|
- height: 0.3em;
|
|
|
- width: 3em;
|
|
|
- background-color: #f27474;
|
|
|
- display: block;
|
|
|
- top: 2.3em;
|
|
|
- left: 1em;
|
|
|
- border-radius: 0.15em;
|
|
|
+ .${PREFIX}svg-ring,
|
|
|
+ .${PREFIX}svg-mark {
|
|
|
+ fill: none;
|
|
|
+ stroke-linecap: round;
|
|
|
+ stroke-linejoin: round;
|
|
|
}
|
|
|
- .${PREFIX}error-line.left {
|
|
|
- transform: rotate(45deg);
|
|
|
+ .${PREFIX}svg-ring {
|
|
|
+ stroke-width: 4.5;
|
|
|
+ opacity: 0.95;
|
|
|
}
|
|
|
- .${PREFIX}error-line.right {
|
|
|
- transform: rotate(-45deg);
|
|
|
+ .${PREFIX}svg-mark {
|
|
|
+ stroke-width: 6;
|
|
|
}
|
|
|
-
|
|
|
- /* Warning Icon */
|
|
|
- .${PREFIX}icon.warning {
|
|
|
- border-color: #facea8;
|
|
|
- color: #f8bb86;
|
|
|
- }
|
|
|
- .${PREFIX}warning-body {
|
|
|
- position: absolute;
|
|
|
- width: 0.3em;
|
|
|
- height: 2.9em;
|
|
|
- background-color: #f8bb86;
|
|
|
- left: 50%;
|
|
|
- top: 0.6em;
|
|
|
- margin-left: -0.15em;
|
|
|
- border-radius: 0.15em;
|
|
|
- }
|
|
|
- .${PREFIX}warning-dot {
|
|
|
- position: absolute;
|
|
|
- width: 0.3em;
|
|
|
- height: 0.3em;
|
|
|
- background-color: #f8bb86;
|
|
|
- left: 50%;
|
|
|
- bottom: 0.6em;
|
|
|
- margin-left: -0.15em;
|
|
|
- border-radius: 50%;
|
|
|
+ .${PREFIX}svg-dot {
|
|
|
+ transform-box: fill-box;
|
|
|
+ transform-origin: center;
|
|
|
}
|
|
|
+
|
|
|
+ .${PREFIX}icon.success { color: #a5dc86; }
|
|
|
+ .${PREFIX}icon.error { color: #f27474; }
|
|
|
+ .${PREFIX}icon.warning { color: #f8bb86; }
|
|
|
+ .${PREFIX}icon.info { color: #3fc3ee; }
|
|
|
`;
|
|
|
|
|
|
// Inject Styles
|
|
|
@@ -230,6 +173,7 @@
|
|
|
this.promise = null;
|
|
|
this.resolve = null;
|
|
|
this.reject = null;
|
|
|
+ this._onKeydown = null;
|
|
|
}
|
|
|
|
|
|
// Constructor helper when called as function: const popup = Layer({...})
|
|
|
@@ -286,6 +230,9 @@
|
|
|
confirmButtonColor: '#3085d6',
|
|
|
cancelButtonColor: '#aaa',
|
|
|
closeOnClickOutside: true,
|
|
|
+ closeOnEsc: true,
|
|
|
+ iconAnimation: true,
|
|
|
+ popupAnimation: true,
|
|
|
...options
|
|
|
};
|
|
|
|
|
|
@@ -298,6 +245,14 @@
|
|
|
return this.promise;
|
|
|
}
|
|
|
|
|
|
+ static _getXjs() {
|
|
|
+ // Prefer xjs, fallback to animal (compat)
|
|
|
+ const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
|
|
|
+ if (!g) return null;
|
|
|
+ const x = g.xjs || g.animal;
|
|
|
+ return (typeof x === 'function') ? x : null;
|
|
|
+ }
|
|
|
+
|
|
|
_render() {
|
|
|
// Remove existing if any
|
|
|
const existing = document.querySelector(`.${PREFIX}overlay`);
|
|
|
@@ -383,6 +338,7 @@
|
|
|
// Animation
|
|
|
requestAnimationFrame(() => {
|
|
|
this.dom.overlay.classList.add('show');
|
|
|
+ this._didOpen();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -390,35 +346,148 @@
|
|
|
const icon = document.createElement('div');
|
|
|
icon.className = `${PREFIX}icon ${type}`;
|
|
|
|
|
|
+ const svgNs = 'http://www.w3.org/2000/svg';
|
|
|
+ const svg = document.createElementNS(svgNs, 'svg');
|
|
|
+ svg.setAttribute('viewBox', '0 0 80 80');
|
|
|
+ svg.setAttribute('aria-hidden', 'true');
|
|
|
+ svg.setAttribute('focusable', 'false');
|
|
|
+
|
|
|
+ const ring = document.createElementNS(svgNs, 'circle');
|
|
|
+ ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
|
|
|
+ ring.setAttribute('cx', '40');
|
|
|
+ ring.setAttribute('cy', '40');
|
|
|
+ ring.setAttribute('r', '34');
|
|
|
+ ring.setAttribute('stroke', 'currentColor');
|
|
|
+ svg.appendChild(ring);
|
|
|
+
|
|
|
+ const addPath = (d, extraClass) => {
|
|
|
+ const p = document.createElementNS(svgNs, 'path');
|
|
|
+ p.setAttribute('d', d);
|
|
|
+ p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
|
|
|
+ p.setAttribute('stroke', 'currentColor');
|
|
|
+ svg.appendChild(p);
|
|
|
+ return p;
|
|
|
+ };
|
|
|
+
|
|
|
if (type === 'success') {
|
|
|
- const tip = document.createElement('div');
|
|
|
- tip.className = `${PREFIX}success-line-tip`;
|
|
|
- const long = document.createElement('div');
|
|
|
- long.className = `${PREFIX}success-line-long`;
|
|
|
- icon.appendChild(tip);
|
|
|
- icon.appendChild(long);
|
|
|
+ addPath('M23 41 L34.5 53 L58 29', `${PREFIX}svg-success`);
|
|
|
} else if (type === 'error') {
|
|
|
- const xMark = document.createElement('span');
|
|
|
- xMark.className = `${PREFIX}error-x-mark`;
|
|
|
- const left = document.createElement('span');
|
|
|
- left.className = `${PREFIX}error-line left`;
|
|
|
- const right = document.createElement('span');
|
|
|
- right.className = `${PREFIX}error-line right`;
|
|
|
- xMark.appendChild(left);
|
|
|
- xMark.appendChild(right);
|
|
|
- icon.appendChild(xMark);
|
|
|
+ addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
|
|
|
+ addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
|
|
|
} else if (type === 'warning') {
|
|
|
- const body = document.createElement('div');
|
|
|
- body.className = `${PREFIX}warning-body`;
|
|
|
- const dot = document.createElement('div');
|
|
|
- dot.className = `${PREFIX}warning-dot`;
|
|
|
- icon.appendChild(body);
|
|
|
- icon.appendChild(dot);
|
|
|
+ addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
|
|
|
+ const dot = document.createElementNS(svgNs, 'circle');
|
|
|
+ dot.setAttribute('class', `${PREFIX}svg-dot`);
|
|
|
+ dot.setAttribute('cx', '40');
|
|
|
+ dot.setAttribute('cy', '58');
|
|
|
+ dot.setAttribute('r', '3.2');
|
|
|
+ dot.setAttribute('fill', 'currentColor');
|
|
|
+ svg.appendChild(dot);
|
|
|
+ } else if (type === 'info') {
|
|
|
+ addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
|
|
|
+ const dot = document.createElementNS(svgNs, 'circle');
|
|
|
+ dot.setAttribute('class', `${PREFIX}svg-dot`);
|
|
|
+ dot.setAttribute('cx', '40');
|
|
|
+ dot.setAttribute('cy', '25');
|
|
|
+ dot.setAttribute('r', '3.2');
|
|
|
+ dot.setAttribute('fill', 'currentColor');
|
|
|
+ svg.appendChild(dot);
|
|
|
+ } else {
|
|
|
+ // ring only
|
|
|
}
|
|
|
+
|
|
|
+ icon.appendChild(svg);
|
|
|
|
|
|
return icon;
|
|
|
}
|
|
|
|
|
|
+ _didOpen() {
|
|
|
+ // Keyboard close (ESC)
|
|
|
+ if (this.params.closeOnEsc) {
|
|
|
+ this._onKeydown = (e) => {
|
|
|
+ if (!e) return;
|
|
|
+ if (e.key === 'Escape') this._close(null);
|
|
|
+ };
|
|
|
+ document.addEventListener('keydown', this._onKeydown);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Popup animation (optional)
|
|
|
+ if (this.params.popupAnimation) {
|
|
|
+ const X = Layer._getXjs();
|
|
|
+ if (X && this.dom.popup) {
|
|
|
+ try {
|
|
|
+ this.dom.popup.style.transition = 'none';
|
|
|
+ this.dom.popup.style.transform = 'scale(0.92)';
|
|
|
+ X(this.dom.popup).animate({
|
|
|
+ scale: [0.92, 1],
|
|
|
+ y: [-6, 0],
|
|
|
+ duration: 520,
|
|
|
+ easing: { stiffness: 260, damping: 16 }
|
|
|
+ });
|
|
|
+ } catch {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.params.iconAnimation) {
|
|
|
+ this._animateIcon();
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
|
|
|
+ } catch {}
|
|
|
+ }
|
|
|
+
|
|
|
+ _animateIcon() {
|
|
|
+ const icon = this.dom.icon;
|
|
|
+ if (!icon) return;
|
|
|
+ const X = Layer._getXjs();
|
|
|
+ if (!X) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ X(icon).animate({
|
|
|
+ opacity: [0, 1],
|
|
|
+ scale: [0.68, 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 {}
|
|
|
+
|
|
|
+ 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 {}
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 }
|
|
|
+ });
|
|
|
+ } catch {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
_close(isConfirmed) {
|
|
|
this.dom.overlay.classList.remove('show');
|
|
|
setTimeout(() => {
|
|
|
@@ -427,6 +496,18 @@
|
|
|
}
|
|
|
}, 300);
|
|
|
|
|
|
+ if (this._onKeydown) {
|
|
|
+ try { document.removeEventListener('keydown', this._onKeydown); } catch {}
|
|
|
+ this._onKeydown = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
|
|
|
+ } catch {}
|
|
|
+ try {
|
|
|
+ if (typeof this.params.didClose === 'function') this.params.didClose();
|
|
|
+ } catch {}
|
|
|
+
|
|
|
if (isConfirmed === true) {
|
|
|
this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
|
|
|
} else if (isConfirmed === false) {
|
|
|
@@ -993,8 +1074,25 @@
|
|
|
if (k in t.style) return t.style[k];
|
|
|
}
|
|
|
|
|
|
- // SVG attribute
|
|
|
+ // SVG
|
|
|
+ // For many SVG presentation attributes (e.g. strokeDashoffset), style overrides attribute.
|
|
|
+ // Prefer computed style / inline style when available, and fallback to attributes.
|
|
|
if (isSVG(t)) {
|
|
|
+ // Geometry attrs must remain attrs (not style)
|
|
|
+ if (k === 'd' || k === 'points') {
|
|
|
+ const attr = toKebab(k);
|
|
|
+ if (t.hasAttribute(attr)) return t.getAttribute(attr);
|
|
|
+ if (t.hasAttribute(k)) return t.getAttribute(k);
|
|
|
+ return t.getAttribute(k);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const cs = getComputedStyle(t);
|
|
|
+ const v = cs.getPropertyValue(toKebab(k));
|
|
|
+ if (v && v.trim()) return v.trim();
|
|
|
+ } catch (_) {}
|
|
|
+ try {
|
|
|
+ if (k in t.style) return t.style[k];
|
|
|
+ } catch (_) {}
|
|
|
const attr = toKebab(k);
|
|
|
if (t.hasAttribute(attr)) return t.getAttribute(attr);
|
|
|
if (t.hasAttribute(k)) return t.getAttribute(k);
|
|
|
@@ -1012,21 +1110,6 @@
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // SVG attribute
|
|
|
- if (isSVG(t)) {
|
|
|
- const attr = toKebab(k);
|
|
|
- t.setAttribute(attr, v);
|
|
|
- // Verify write
|
|
|
- // if (k === 'd' && Math.random() < 0.01) console.log('Readback:', t.getAttribute('d')?.slice(0, 10));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Fallback
|
|
|
- if ((k === 'd' || k === 'points') && isEl(t)) {
|
|
|
- t.setAttribute(k, v);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
// scroll
|
|
|
if (k === 'scrollTop' || k === 'scrollLeft') {
|
|
|
t[k] = v;
|
|
|
@@ -1045,8 +1128,19 @@
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // SVG attribute
|
|
|
+ // SVG
|
|
|
+ // Prefer style for presentation attrs so it can animate when svgDraw initialized styles.
|
|
|
if (isSVG(t)) {
|
|
|
+ if (k === 'd' || k === 'points') {
|
|
|
+ t.setAttribute(k, v);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ if (k in t.style) {
|
|
|
+ t.style[k] = v;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (_) {}
|
|
|
const attr = toKebab(k);
|
|
|
t.setAttribute(attr, v);
|
|
|
return;
|