layer.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. (function (global, factory) {
  2. const LayerClass = factory();
  3. // Allow usage as `Layer({...})` or `new Layer()`
  4. // But Layer is a class. We can wrap it in a proxy or factory function.
  5. function LayerFactory(options) {
  6. if (options && typeof options === 'object') {
  7. return LayerClass.$(options);
  8. }
  9. return new LayerClass();
  10. }
  11. // Copy static methods (including non-enumerable class statics like `fire` / `$`)
  12. // Class static methods are non-enumerable by default, so Object.assign() would miss them.
  13. const copyStatic = (to, from) => {
  14. try {
  15. Object.getOwnPropertyNames(from).forEach((k) => {
  16. if (k === 'prototype' || k === 'name' || k === 'length') return;
  17. const desc = Object.getOwnPropertyDescriptor(from, k);
  18. if (!desc) return;
  19. Object.defineProperty(to, k, desc);
  20. });
  21. } catch (e) {
  22. // Best-effort fallback
  23. try { Object.assign(to, from); } catch {}
  24. }
  25. };
  26. copyStatic(LayerFactory, LayerClass);
  27. // Also copy prototype for instanceof checks if needed (though tricky with factory)
  28. LayerFactory.prototype = LayerClass.prototype;
  29. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = LayerFactory :
  30. typeof define === 'function' && define.amd ? define(() => LayerFactory) :
  31. (global.Layer = LayerFactory);
  32. }(this, (function () {
  33. 'use strict';
  34. const PREFIX = 'layer-';
  35. // Theme & Styles
  36. const css = `
  37. .${PREFIX}overlay {
  38. position: fixed;
  39. top: 0;
  40. left: 0;
  41. width: 100%;
  42. height: 100%;
  43. background: rgba(0, 0, 0, 0.4);
  44. display: flex;
  45. justify-content: center;
  46. align-items: center;
  47. z-index: 1000;
  48. opacity: 0;
  49. transition: opacity 0.3s;
  50. }
  51. .${PREFIX}overlay.show {
  52. opacity: 1;
  53. }
  54. .${PREFIX}popup {
  55. background: #fff;
  56. border-radius: 8px;
  57. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  58. width: 32em;
  59. max-width: 90%;
  60. padding: 1.5em;
  61. display: flex;
  62. flex-direction: column;
  63. align-items: center;
  64. position: relative;
  65. z-index: 1;
  66. transform: scale(0.92);
  67. transition: transform 0.26s ease;
  68. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  69. }
  70. .${PREFIX}overlay.show .${PREFIX}popup {
  71. transform: scale(1);
  72. }
  73. .${PREFIX}title {
  74. font-size: 1.8em;
  75. font-weight: 600;
  76. color: #333;
  77. margin: 0 0 0.5em;
  78. text-align: center;
  79. }
  80. .${PREFIX}content {
  81. font-size: 1.125em;
  82. color: #545454;
  83. margin-bottom: 1.5em;
  84. text-align: center;
  85. line-height: 1.5;
  86. }
  87. .${PREFIX}actions {
  88. display: flex;
  89. justify-content: center;
  90. gap: 1em;
  91. width: 100%;
  92. }
  93. .${PREFIX}button {
  94. border: none;
  95. border-radius: 4px;
  96. padding: 0.6em 1.2em;
  97. font-size: 1em;
  98. cursor: pointer;
  99. transition: background-color 0.2s, box-shadow 0.2s;
  100. color: #fff;
  101. }
  102. .${PREFIX}confirm {
  103. background-color: #3085d6;
  104. }
  105. .${PREFIX}confirm:hover {
  106. background-color: #2b77c0;
  107. }
  108. .${PREFIX}cancel {
  109. background-color: #aaa;
  110. }
  111. .${PREFIX}cancel:hover {
  112. background-color: #999;
  113. }
  114. .${PREFIX}icon {
  115. width: 6em;
  116. height: 6em;
  117. margin: 1.5em auto 1.2em;
  118. position: relative;
  119. display: grid;
  120. place-items: center;
  121. }
  122. /* SVG Icon (SweetAlert-like, animated via xjs.draw + spring) */
  123. .${PREFIX}icon svg {
  124. width: 100%;
  125. height: 100%;
  126. display: block;
  127. overflow: visible;
  128. }
  129. .${PREFIX}svg-ring,
  130. .${PREFIX}svg-mark {
  131. fill: none;
  132. stroke-linecap: round;
  133. stroke-linejoin: round;
  134. }
  135. .${PREFIX}svg-ring {
  136. stroke-width: 4.5;
  137. opacity: 0.95;
  138. }
  139. .${PREFIX}svg-mark {
  140. stroke-width: 6;
  141. }
  142. .${PREFIX}svg-dot {
  143. transform-box: fill-box;
  144. transform-origin: center;
  145. }
  146. /* =========================================
  147. SweetAlert2-compatible SUCCESS icon (DOM)
  148. (matches sweetalert2 v11.26.17)
  149. ========================================= */
  150. .${PREFIX}icon.success {
  151. /* Override the generic SVG icon box */
  152. display: block;
  153. place-items: initial;
  154. box-sizing: content-box;
  155. width: 5em;
  156. height: 5em;
  157. margin: 2.5em auto 0.6em;
  158. border: 0.25em solid rgba(0, 0, 0, 0);
  159. border-radius: 50%;
  160. border-color: #a5dc86;
  161. color: #a5dc86;
  162. font-family: inherit;
  163. line-height: 5em;
  164. cursor: default;
  165. user-select: none;
  166. }
  167. .${PREFIX}icon.success .${PREFIX}success-ring {
  168. position: absolute;
  169. z-index: 2;
  170. top: -0.25em;
  171. left: -0.25em;
  172. box-sizing: content-box;
  173. width: 100%;
  174. height: 100%;
  175. border: 0.25em solid rgba(165, 220, 134, 0.3);
  176. border-radius: 50%;
  177. }
  178. .${PREFIX}icon.success .${PREFIX}success-fix {
  179. position: absolute;
  180. z-index: 1;
  181. top: 0.5em;
  182. left: 1.625em;
  183. width: 0.4375em;
  184. height: 5.625em;
  185. transform: rotate(-45deg);
  186. background-color: #fff; /* adjusted at runtime to popup bg */
  187. }
  188. .${PREFIX}icon.success .${PREFIX}success-circular-line-left,
  189. .${PREFIX}icon.success .${PREFIX}success-circular-line-right {
  190. position: absolute;
  191. width: 3.75em;
  192. height: 7.5em;
  193. border-radius: 50%;
  194. background-color: #fff; /* adjusted at runtime to popup bg */
  195. }
  196. .${PREFIX}icon.success .${PREFIX}success-circular-line-left {
  197. top: -0.4375em;
  198. left: -2.0635em;
  199. transform: rotate(-45deg);
  200. transform-origin: 3.75em 3.75em;
  201. border-radius: 7.5em 0 0 7.5em;
  202. }
  203. .${PREFIX}icon.success .${PREFIX}success-circular-line-right {
  204. top: -0.6875em;
  205. left: 1.875em;
  206. transform: rotate(-45deg);
  207. transform-origin: 0 3.75em;
  208. border-radius: 0 7.5em 7.5em 0;
  209. }
  210. .${PREFIX}icon.success .${PREFIX}success-line-tip,
  211. .${PREFIX}icon.success .${PREFIX}success-line-long {
  212. display: block;
  213. position: absolute;
  214. z-index: 2;
  215. height: 0.3125em;
  216. border-radius: 0.125em;
  217. background-color: #a5dc86;
  218. }
  219. .${PREFIX}icon.success .${PREFIX}success-line-tip {
  220. top: 2.875em;
  221. left: 0.8125em;
  222. width: 1.5625em;
  223. transform: rotate(45deg);
  224. }
  225. .${PREFIX}icon.success .${PREFIX}success-line-long {
  226. top: 2.375em;
  227. right: 0.5em;
  228. width: 2.9375em;
  229. transform: rotate(-45deg);
  230. }
  231. /* Triggered when icon has .${PREFIX}icon-show */
  232. .${PREFIX}icon.success.${PREFIX}icon-show .${PREFIX}success-line-tip {
  233. animation: ${PREFIX}animate-success-line-tip 0.75s;
  234. }
  235. .${PREFIX}icon.success.${PREFIX}icon-show .${PREFIX}success-line-long {
  236. animation: ${PREFIX}animate-success-line-long 0.75s;
  237. }
  238. .${PREFIX}icon.success.${PREFIX}icon-show .${PREFIX}success-circular-line-right {
  239. animation: ${PREFIX}rotate-success-circular-line 4.25s ease-in;
  240. }
  241. @keyframes ${PREFIX}animate-success-line-tip {
  242. 0% {
  243. top: 1.1875em;
  244. left: 0.0625em;
  245. width: 0;
  246. }
  247. 54% {
  248. top: 1.0625em;
  249. left: 0.125em;
  250. width: 0;
  251. }
  252. 70% {
  253. top: 2.1875em;
  254. left: -0.375em;
  255. width: 3.125em;
  256. }
  257. 84% {
  258. top: 3em;
  259. left: 1.3125em;
  260. width: 1.0625em;
  261. }
  262. 100% {
  263. top: 2.8125em;
  264. left: 0.8125em;
  265. width: 1.5625em;
  266. }
  267. }
  268. @keyframes ${PREFIX}animate-success-line-long {
  269. 0% {
  270. top: 3.375em;
  271. right: 2.875em;
  272. width: 0;
  273. }
  274. 65% {
  275. top: 3.375em;
  276. right: 2.875em;
  277. width: 0;
  278. }
  279. 84% {
  280. top: 2.1875em;
  281. right: 0;
  282. width: 3.4375em;
  283. }
  284. 100% {
  285. top: 2.375em;
  286. right: 0.5em;
  287. width: 2.9375em;
  288. }
  289. }
  290. @keyframes ${PREFIX}rotate-success-circular-line {
  291. 0% { transform: rotate(-45deg); }
  292. 5% { transform: rotate(-45deg); }
  293. 12% { transform: rotate(-405deg); }
  294. 100% { transform: rotate(-405deg); }
  295. }
  296. .${PREFIX}icon.error { color: #f27474; }
  297. .${PREFIX}icon.warning { color: #f8bb86; }
  298. .${PREFIX}icon.info { color: #3fc3ee; }
  299. .${PREFIX}icon.question { color: #b18cff; }
  300. `;
  301. // Inject Styles
  302. const injectStyles = () => {
  303. if (document.getElementById(`${PREFIX}styles`)) return;
  304. const styleSheet = document.createElement('style');
  305. styleSheet.id = `${PREFIX}styles`;
  306. styleSheet.textContent = css;
  307. document.head.appendChild(styleSheet);
  308. };
  309. class Layer {
  310. constructor() {
  311. injectStyles();
  312. this.params = {};
  313. this.dom = {};
  314. this.promise = null;
  315. this.resolve = null;
  316. this.reject = null;
  317. this._onKeydown = null;
  318. }
  319. // Constructor helper when called as function: const popup = Layer({...})
  320. static get isProxy() { return true; }
  321. // Static entry point
  322. static fire(options) {
  323. const instance = new Layer();
  324. return instance._fire(options);
  325. }
  326. // Chainable entry point (builder-style)
  327. // Example:
  328. // Layer.$({ title: 'Hi' }).fire().then(...)
  329. // Layer.$().config({ title: 'Hi' }).fire()
  330. static $(options) {
  331. const instance = new Layer();
  332. if (options !== undefined) instance.config(options);
  333. return instance;
  334. }
  335. // Chainable config helper (does not render until `.fire()` is called)
  336. config(options = {}) {
  337. // Support the same shorthand as Layer.fire(title, text, icon)
  338. if (typeof options === 'string') {
  339. options = { title: options };
  340. if (arguments[1]) options.text = arguments[1];
  341. if (arguments[2]) options.icon = arguments[2];
  342. }
  343. this.params = { ...(this.params || {}), ...options };
  344. return this;
  345. }
  346. // Instance entry point (chainable)
  347. fire(options) {
  348. const merged = (options === undefined) ? (this.params || {}) : options;
  349. return this._fire(merged);
  350. }
  351. _fire(options = {}) {
  352. if (typeof options === 'string') {
  353. options = { title: options };
  354. if (arguments[1]) options.text = arguments[1];
  355. if (arguments[2]) options.icon = arguments[2];
  356. }
  357. this.params = {
  358. title: '',
  359. text: '',
  360. icon: null,
  361. iconSize: null, // e.g. '6em' / '72px'
  362. confirmButtonText: 'OK',
  363. cancelButtonText: 'Cancel',
  364. showCancelButton: false,
  365. confirmButtonColor: '#3085d6',
  366. cancelButtonColor: '#aaa',
  367. closeOnClickOutside: true,
  368. closeOnEsc: true,
  369. iconAnimation: true,
  370. popupAnimation: true,
  371. ...options
  372. };
  373. this.promise = new Promise((resolve, reject) => {
  374. this.resolve = resolve;
  375. this.reject = reject;
  376. });
  377. this._render();
  378. return this.promise;
  379. }
  380. static _getXjs() {
  381. // Prefer xjs, fallback to animal (compat)
  382. const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
  383. if (!g) return null;
  384. const x = g.xjs || g.animal;
  385. return (typeof x === 'function') ? x : null;
  386. }
  387. _render() {
  388. // Remove existing if any
  389. const existing = document.querySelector(`.${PREFIX}overlay`);
  390. if (existing) existing.remove();
  391. // Create Overlay
  392. this.dom.overlay = document.createElement('div');
  393. this.dom.overlay.className = `${PREFIX}overlay`;
  394. // Create Popup
  395. this.dom.popup = document.createElement('div');
  396. this.dom.popup.className = `${PREFIX}popup`;
  397. this.dom.overlay.appendChild(this.dom.popup);
  398. // Icon
  399. if (this.params.icon) {
  400. this.dom.icon = this._createIcon(this.params.icon);
  401. this.dom.popup.appendChild(this.dom.icon);
  402. }
  403. // Title
  404. if (this.params.title) {
  405. this.dom.title = document.createElement('h2');
  406. this.dom.title.className = `${PREFIX}title`;
  407. this.dom.title.textContent = this.params.title;
  408. this.dom.popup.appendChild(this.dom.title);
  409. }
  410. // Content (Text / HTML / Element)
  411. if (this.params.text || this.params.html || this.params.content) {
  412. this.dom.content = document.createElement('div');
  413. this.dom.content.className = `${PREFIX}content`;
  414. if (this.params.content) {
  415. // DOM Element or Selector
  416. let el = this.params.content;
  417. if (typeof el === 'string') el = document.querySelector(el);
  418. if (el instanceof Element) this.dom.content.appendChild(el);
  419. } else if (this.params.html) {
  420. this.dom.content.innerHTML = this.params.html;
  421. } else {
  422. this.dom.content.textContent = this.params.text;
  423. }
  424. this.dom.popup.appendChild(this.dom.content);
  425. }
  426. // Actions
  427. this.dom.actions = document.createElement('div');
  428. this.dom.actions.className = `${PREFIX}actions`;
  429. // Cancel Button
  430. if (this.params.showCancelButton) {
  431. this.dom.cancelBtn = document.createElement('button');
  432. this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
  433. this.dom.cancelBtn.textContent = this.params.cancelButtonText;
  434. this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
  435. this.dom.cancelBtn.onclick = () => this._close(false);
  436. this.dom.actions.appendChild(this.dom.cancelBtn);
  437. }
  438. // Confirm Button
  439. this.dom.confirmBtn = document.createElement('button');
  440. this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
  441. this.dom.confirmBtn.textContent = this.params.confirmButtonText;
  442. this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
  443. this.dom.confirmBtn.onclick = () => this._close(true);
  444. this.dom.actions.appendChild(this.dom.confirmBtn);
  445. this.dom.popup.appendChild(this.dom.actions);
  446. // Event Listeners
  447. if (this.params.closeOnClickOutside) {
  448. this.dom.overlay.addEventListener('click', (e) => {
  449. if (e.target === this.dom.overlay) {
  450. this._close(null); // Dismiss
  451. }
  452. });
  453. }
  454. document.body.appendChild(this.dom.overlay);
  455. // Animation
  456. requestAnimationFrame(() => {
  457. this.dom.overlay.classList.add('show');
  458. this._didOpen();
  459. });
  460. }
  461. _createIcon(type) {
  462. const icon = document.createElement('div');
  463. icon.className = `${PREFIX}icon ${type}`;
  464. const applyIconSize = (mode) => {
  465. if (!(this.params && this.params.iconSize)) return;
  466. try {
  467. const raw = this.params.iconSize;
  468. const s = String(raw).trim();
  469. if (!s) return;
  470. // For SweetAlert2-style success icon, scale via font-size so all `em`-based
  471. // parts remain proportional.
  472. if (mode === 'font') {
  473. const m = s.match(/^(-?\d*\.?\d+)\s*(px|em|rem)$/i);
  474. if (m) {
  475. const n = parseFloat(m[1]);
  476. const unit = m[2];
  477. if (Number.isFinite(n) && n > 0) {
  478. icon.style.fontSize = (n / 5) + unit; // icon is 5em wide/tall
  479. return;
  480. }
  481. }
  482. }
  483. // Fallback: directly size the box (works great for SVG icons)
  484. icon.style.width = s;
  485. icon.style.height = s;
  486. } catch {}
  487. };
  488. const svgNs = 'http://www.w3.org/2000/svg';
  489. if (type === 'success') {
  490. // SweetAlert2-compatible DOM structure (circle + tick) for exact style parity
  491. // <div class="...success-circular-line-left"></div>
  492. // <span class="...success-line-tip"></span>
  493. // <span class="...success-line-long"></span>
  494. // <div class="...success-ring"></div>
  495. // <div class="...success-fix"></div>
  496. // <div class="...success-circular-line-right"></div>
  497. const left = document.createElement('div');
  498. left.className = `${PREFIX}success-circular-line-left`;
  499. const tip = document.createElement('span');
  500. tip.className = `${PREFIX}success-line-tip`;
  501. const long = document.createElement('span');
  502. long.className = `${PREFIX}success-line-long`;
  503. const ring = document.createElement('div');
  504. ring.className = `${PREFIX}success-ring`;
  505. const fix = document.createElement('div');
  506. fix.className = `${PREFIX}success-fix`;
  507. const right = document.createElement('div');
  508. right.className = `${PREFIX}success-circular-line-right`;
  509. icon.appendChild(left);
  510. icon.appendChild(tip);
  511. icon.appendChild(long);
  512. icon.appendChild(ring);
  513. icon.appendChild(fix);
  514. icon.appendChild(right);
  515. applyIconSize('font');
  516. return icon;
  517. }
  518. // Default to SVG icons for other types
  519. applyIconSize('box');
  520. const svg = document.createElementNS(svgNs, 'svg');
  521. svg.setAttribute('viewBox', '0 0 80 80');
  522. svg.setAttribute('aria-hidden', 'true');
  523. svg.setAttribute('focusable', 'false');
  524. const ring = document.createElementNS(svgNs, 'circle');
  525. ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
  526. ring.setAttribute('cx', '40');
  527. ring.setAttribute('cy', '40');
  528. ring.setAttribute('r', '34');
  529. ring.setAttribute('stroke', 'currentColor');
  530. svg.appendChild(ring);
  531. const addPath = (d, extraClass) => {
  532. const p = document.createElementNS(svgNs, 'path');
  533. p.setAttribute('d', d);
  534. p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
  535. p.setAttribute('stroke', 'currentColor');
  536. svg.appendChild(p);
  537. return p;
  538. };
  539. if (type === 'error') {
  540. addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
  541. addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
  542. } else if (type === 'warning') {
  543. addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
  544. const dot = document.createElementNS(svgNs, 'circle');
  545. dot.setAttribute('class', `${PREFIX}svg-dot`);
  546. dot.setAttribute('cx', '40');
  547. dot.setAttribute('cy', '58');
  548. dot.setAttribute('r', '3.2');
  549. dot.setAttribute('fill', 'currentColor');
  550. svg.appendChild(dot);
  551. } else if (type === 'info') {
  552. addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
  553. const dot = document.createElementNS(svgNs, 'circle');
  554. dot.setAttribute('class', `${PREFIX}svg-dot`);
  555. dot.setAttribute('cx', '40');
  556. dot.setAttribute('cy', '25');
  557. dot.setAttribute('r', '3.2');
  558. dot.setAttribute('fill', 'currentColor');
  559. svg.appendChild(dot);
  560. } else if (type === 'question') {
  561. // Question mark (single stroke + dot)
  562. 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`);
  563. const dot = document.createElementNS(svgNs, 'circle');
  564. dot.setAttribute('class', `${PREFIX}svg-dot`);
  565. dot.setAttribute('cx', '42');
  566. dot.setAttribute('cy', '61');
  567. dot.setAttribute('r', '3.2');
  568. dot.setAttribute('fill', 'currentColor');
  569. svg.appendChild(dot);
  570. } else {
  571. // Fallback: ring only
  572. }
  573. icon.appendChild(svg);
  574. return icon;
  575. }
  576. _adjustSuccessIconBackgroundColor() {
  577. try {
  578. const icon = this.dom && this.dom.icon;
  579. const popup = this.dom && this.dom.popup;
  580. if (!icon || !popup) return;
  581. if (!(icon.classList && icon.classList.contains('success'))) return;
  582. const bg = getComputedStyle(popup).backgroundColor;
  583. const parts = icon.querySelectorAll(`.${PREFIX}success-circular-line-left, .${PREFIX}success-circular-line-right, .${PREFIX}success-fix`);
  584. parts.forEach((el) => {
  585. try { el.style.backgroundColor = bg; } catch {}
  586. });
  587. } catch {}
  588. }
  589. _didOpen() {
  590. // Keyboard close (ESC)
  591. if (this.params.closeOnEsc) {
  592. this._onKeydown = (e) => {
  593. if (!e) return;
  594. if (e.key === 'Escape') this._close(null);
  595. };
  596. document.addEventListener('keydown', this._onKeydown);
  597. }
  598. // Keep SweetAlert2 success icon perfectly blended with popup bg
  599. this._adjustSuccessIconBackgroundColor();
  600. // Popup animation (optional)
  601. if (this.params.popupAnimation) {
  602. const X = Layer._getXjs();
  603. if (X && this.dom.popup) {
  604. // Override the CSS scale transition with a spring-ish entrance.
  605. try {
  606. this.dom.popup.style.transition = 'none';
  607. this.dom.popup.style.transform = 'scale(0.92)';
  608. X(this.dom.popup).animate({
  609. scale: [0.92, 1],
  610. y: [-6, 0],
  611. duration: 520,
  612. easing: { stiffness: 260, damping: 16 }
  613. });
  614. } catch {}
  615. }
  616. }
  617. // Icon SVG draw animation
  618. if (this.params.iconAnimation) {
  619. this._animateIcon();
  620. }
  621. // User hook (SweetAlert-ish naming)
  622. try {
  623. if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
  624. } catch {}
  625. }
  626. _animateIcon() {
  627. const icon = this.dom.icon;
  628. if (!icon) return;
  629. const X = Layer._getXjs();
  630. const type = (this.params && this.params.icon) || '';
  631. // SweetAlert2-style success icon uses pure CSS keyframes; keep the look identical.
  632. if (type === 'success') {
  633. this._adjustSuccessIconBackgroundColor();
  634. // Force restart by removing/adding the class
  635. try { icon.classList.remove(`${PREFIX}icon-show`); } catch {}
  636. requestAnimationFrame(() => {
  637. try { icon.classList.add(`${PREFIX}icon-show`); } catch {}
  638. });
  639. return;
  640. }
  641. if (!X) return;
  642. try {
  643. // Entrance varies slightly by type (SweetAlert-like feel)
  644. if (type === 'error') {
  645. X(icon).animate({
  646. opacity: [0, 1],
  647. scale: [0.62, 1],
  648. rotate: [-10, 0],
  649. duration: 520,
  650. easing: { stiffness: 240, damping: 14 }
  651. });
  652. } else if (type === 'warning') {
  653. X(icon).animate({
  654. opacity: [0, 1],
  655. scale: [0.62, 1],
  656. y: [-10, 0],
  657. duration: 620,
  658. easing: { stiffness: 260, damping: 15 }
  659. });
  660. } else if (type === 'question') {
  661. X(icon).animate({
  662. opacity: [0, 1],
  663. scale: [0.62, 1],
  664. rotate: [8, 0],
  665. duration: 620,
  666. easing: { stiffness: 240, damping: 14 }
  667. });
  668. } else {
  669. X(icon).animate({
  670. opacity: [0, 1],
  671. scale: [0.66, 1],
  672. duration: 520,
  673. easing: { stiffness: 240, damping: 14 }
  674. });
  675. }
  676. } catch {}
  677. const svg = icon.querySelector('svg');
  678. if (!svg) return;
  679. const ring = svg.querySelector(`.${PREFIX}svg-ring`);
  680. const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
  681. const dot = svg.querySelector(`.${PREFIX}svg-dot`);
  682. // Draw ring first (timing varies by type)
  683. try {
  684. if (ring) {
  685. const ringDur = (type === 'success') ? 520 : (type === 'error') ? 420 : 560;
  686. const ringEase = (type === 'warning' || type === 'question') ? 'ease-in-out' : 'ease-out';
  687. X(ring).draw({ duration: ringDur, easing: ringEase, delay: 40 });
  688. }
  689. } catch {}
  690. if (type === 'error') {
  691. // Two lines + shake after draw
  692. const left = marks[0];
  693. const right = marks[1];
  694. try { if (left) X(left).draw({ duration: 320, easing: 'ease-out', delay: 170 }); } catch {}
  695. try { if (right) X(right).draw({ duration: 320, easing: 'ease-out', delay: 240 }); } catch {}
  696. try {
  697. X(icon).animate({
  698. rotate: [-10, 10],
  699. duration: 70,
  700. loop: 4,
  701. direction: 'alternate',
  702. easing: 'ease-in-out',
  703. delay: 360
  704. });
  705. } catch {}
  706. return;
  707. }
  708. // Single-mark types (success / warning / info / question)
  709. if (type === 'success') {
  710. const m = marks[0];
  711. try { if (m) X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 }); } catch {}
  712. try {
  713. if (ring) X(ring).animate({ strokeWidth: [4.5, 6], duration: 220, delay: 360, easing: { stiffness: 300, damping: 18 } });
  714. } catch {}
  715. } else if (type === 'warning') {
  716. const m = marks[0];
  717. try { if (m) X(m).draw({ duration: 380, easing: 'ease-in-out', delay: 190 }); } catch {}
  718. try {
  719. if (ring) X(ring).animate({ opacity: [1, 0.55], duration: 260, delay: 420, loop: 2, direction: 'alternate', easing: 'ease-in-out' });
  720. } catch {}
  721. } else if (type === 'info') {
  722. const m = marks[0];
  723. try { if (m) X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 }); } catch {}
  724. } else if (type === 'question') {
  725. const m = marks[0];
  726. try { if (m) X(m).draw({ duration: 520, easing: 'ease-in-out', delay: 170 }); } catch {}
  727. try {
  728. X(icon).animate({ rotate: [-6, 6], duration: 90, loop: 3, direction: 'alternate', easing: 'ease-in-out', delay: 420 });
  729. } catch {}
  730. } else {
  731. marks.forEach((m, i) => {
  732. try { X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 + i * 60 }); } catch {}
  733. });
  734. }
  735. if (dot) {
  736. try {
  737. dot.style.opacity = '0';
  738. if (type === 'warning') {
  739. X(dot).animate({
  740. opacity: [0, 1],
  741. scale: [0.2, 1],
  742. duration: 260,
  743. delay: 320,
  744. easing: { stiffness: 360, damping: 18 }
  745. });
  746. } else if (type === 'info') {
  747. X(dot).animate({
  748. opacity: [0, 1],
  749. y: [-8, 0],
  750. scale: [0.2, 1],
  751. duration: 420,
  752. delay: 260,
  753. easing: { stiffness: 300, damping: 14 }
  754. });
  755. } else if (type === 'question') {
  756. X(dot).animate({
  757. opacity: [0, 1],
  758. scale: [0.2, 1],
  759. duration: 360,
  760. delay: 360,
  761. easing: { stiffness: 320, damping: 16 }
  762. });
  763. } else {
  764. X(dot).animate({
  765. opacity: [0, 1],
  766. scale: [0.2, 1],
  767. duration: 320,
  768. delay: 280,
  769. easing: { stiffness: 320, damping: 18 }
  770. });
  771. }
  772. } catch {}
  773. }
  774. }
  775. _close(isConfirmed) {
  776. this.dom.overlay.classList.remove('show');
  777. setTimeout(() => {
  778. if (this.dom.overlay && this.dom.overlay.parentNode) {
  779. this.dom.overlay.parentNode.removeChild(this.dom.overlay);
  780. }
  781. }, 300);
  782. if (this._onKeydown) {
  783. try { document.removeEventListener('keydown', this._onKeydown); } catch {}
  784. this._onKeydown = null;
  785. }
  786. try {
  787. if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
  788. } catch {}
  789. try {
  790. if (typeof this.params.didClose === 'function') this.params.didClose();
  791. } catch {}
  792. if (isConfirmed === true) {
  793. this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
  794. } else if (isConfirmed === false) {
  795. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'cancel' });
  796. } else {
  797. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'backdrop' });
  798. }
  799. }
  800. }
  801. return Layer;
  802. })));