layer.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  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. position: relative;
  116. box-sizing: content-box;
  117. width: 5em;
  118. height: 5em;
  119. margin: 2.5em auto 0.6em;
  120. border: 0.25em solid rgba(0, 0, 0, 0);
  121. border-radius: 50%;
  122. font-family: inherit;
  123. line-height: 5em;
  124. cursor: default;
  125. user-select: none;
  126. /* Ring sweep angles (match SweetAlert2 success feel) */
  127. --${PREFIX}ring-start-rotate: -45deg;
  128. /* End is one full turn from start so it "sweeps then settles" */
  129. --${PREFIX}ring-end-rotate: -405deg;
  130. }
  131. /* SVG mark content (kept), sits above the ring */
  132. .${PREFIX}icon svg {
  133. width: 100%;
  134. height: 100%;
  135. display: block;
  136. overflow: visible;
  137. position: relative;
  138. z-index: 3;
  139. }
  140. .${PREFIX}svg-ring,
  141. .${PREFIX}svg-mark {
  142. fill: none;
  143. stroke-linecap: round;
  144. stroke-linejoin: round;
  145. }
  146. .${PREFIX}svg-ring {
  147. stroke-width: 4.5;
  148. opacity: 0.95;
  149. }
  150. .${PREFIX}svg-mark {
  151. stroke-width: 6;
  152. }
  153. .${PREFIX}svg-dot {
  154. transform-box: fill-box;
  155. transform-origin: center;
  156. }
  157. /* =========================================
  158. "success-like" ring (shared by all icons)
  159. - we reuse the success ring pieces/rotation for every icon type
  160. - color is driven by currentColor
  161. ========================================= */
  162. .${PREFIX}icon.success { border-color: #a5dc86; color: #a5dc86; }
  163. .${PREFIX}icon.error { border-color: #f27474; color: #f27474; }
  164. .${PREFIX}icon.warning { border-color: #f8bb86; color: #f8bb86; }
  165. .${PREFIX}icon.info { border-color: #3fc3ee; color: #3fc3ee; }
  166. .${PREFIX}icon.question { border-color: #b18cff; color: #b18cff; }
  167. /* Per-icon ring sweep stop (same timing, different final position) */
  168. /* IMPORTANT: keep a full 360° sweep (end = start - 360) for the same "draw then vanish" feel */
  169. .${PREFIX}icon.warning { --${PREFIX}ring-start-rotate: -90deg; --${PREFIX}ring-end-rotate: -450deg; } /* stop at top */
  170. .${PREFIX}icon.error { --${PREFIX}ring-start-rotate: -135deg; --${PREFIX}ring-end-rotate: -495deg; } /* stop at top-left */
  171. .${PREFIX}icon.info { --${PREFIX}ring-start-rotate: -90deg; --${PREFIX}ring-end-rotate: -450deg; } /* stop at top */
  172. .${PREFIX}icon.question { --${PREFIX}ring-start-rotate: -135deg; --${PREFIX}ring-end-rotate: -495deg; } /* stop at top-left */
  173. .${PREFIX}icon .${PREFIX}success-ring {
  174. position: absolute;
  175. z-index: 2;
  176. top: -0.25em;
  177. left: -0.25em;
  178. box-sizing: content-box;
  179. width: 100%;
  180. height: 100%;
  181. border: 0.25em solid currentColor;
  182. opacity: 0.3;
  183. border-radius: 50%;
  184. }
  185. .${PREFIX}icon .${PREFIX}success-fix {
  186. position: absolute;
  187. z-index: 1;
  188. top: 0.5em;
  189. left: 1.625em;
  190. width: 0.4375em;
  191. height: 5.625em;
  192. transform: rotate(-45deg);
  193. background-color: #fff; /* adjusted at runtime to popup bg */
  194. }
  195. .${PREFIX}icon .${PREFIX}success-circular-line-left,
  196. .${PREFIX}icon .${PREFIX}success-circular-line-right {
  197. position: absolute;
  198. width: 3.75em;
  199. height: 7.5em;
  200. border-radius: 50%;
  201. background-color: #fff; /* adjusted at runtime to popup bg */
  202. }
  203. .${PREFIX}icon .${PREFIX}success-circular-line-left {
  204. top: -0.4375em;
  205. left: -2.0635em;
  206. transform: rotate(-45deg);
  207. transform-origin: 3.75em 3.75em;
  208. border-radius: 7.5em 0 0 7.5em;
  209. }
  210. .${PREFIX}icon .${PREFIX}success-circular-line-right {
  211. top: -0.6875em;
  212. left: 1.875em;
  213. transform: rotate(-45deg);
  214. transform-origin: 0 3.75em;
  215. border-radius: 0 7.5em 7.5em 0;
  216. }
  217. .${PREFIX}icon .${PREFIX}success-line-tip,
  218. .${PREFIX}icon .${PREFIX}success-line-long {
  219. display: block;
  220. position: absolute;
  221. z-index: 2;
  222. height: 0.3125em;
  223. border-radius: 0.125em;
  224. background-color: currentColor;
  225. }
  226. .${PREFIX}icon .${PREFIX}success-line-tip {
  227. top: 2.875em;
  228. left: 0.8125em;
  229. width: 1.5625em;
  230. transform: rotate(45deg);
  231. }
  232. .${PREFIX}icon .${PREFIX}success-line-long {
  233. top: 2.375em;
  234. right: 0.5em;
  235. width: 2.9375em;
  236. transform: rotate(-45deg);
  237. }
  238. /* Triggered when icon has .${PREFIX}icon-show */
  239. .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-tip {
  240. animation: ${PREFIX}animate-success-line-tip 0.75s;
  241. }
  242. .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-long {
  243. animation: ${PREFIX}animate-success-line-long 0.75s;
  244. }
  245. .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-circular-line-right {
  246. animation: ${PREFIX}rotate-icon-circular-line 4.25s ease-in;
  247. }
  248. @keyframes ${PREFIX}animate-success-line-tip {
  249. 0% {
  250. top: 1.1875em;
  251. left: 0.0625em;
  252. width: 0;
  253. }
  254. 54% {
  255. top: 1.0625em;
  256. left: 0.125em;
  257. width: 0;
  258. }
  259. 70% {
  260. top: 2.1875em;
  261. left: -0.375em;
  262. width: 3.125em;
  263. }
  264. 84% {
  265. top: 3em;
  266. left: 1.3125em;
  267. width: 1.0625em;
  268. }
  269. 100% {
  270. top: 2.8125em;
  271. left: 0.8125em;
  272. width: 1.5625em;
  273. }
  274. }
  275. @keyframes ${PREFIX}animate-success-line-long {
  276. 0% {
  277. top: 3.375em;
  278. right: 2.875em;
  279. width: 0;
  280. }
  281. 65% {
  282. top: 3.375em;
  283. right: 2.875em;
  284. width: 0;
  285. }
  286. 84% {
  287. top: 2.1875em;
  288. right: 0;
  289. width: 3.4375em;
  290. }
  291. 100% {
  292. top: 2.375em;
  293. right: 0.5em;
  294. width: 2.9375em;
  295. }
  296. }
  297. @keyframes ${PREFIX}rotate-icon-circular-line {
  298. 0% { transform: rotate(var(--${PREFIX}ring-start-rotate)); }
  299. 5% { transform: rotate(var(--${PREFIX}ring-start-rotate)); }
  300. 12% { transform: rotate(var(--${PREFIX}ring-end-rotate)); }
  301. 100% { transform: rotate(var(--${PREFIX}ring-end-rotate)); } /* match 12% so it "sweeps then stops" */
  302. }
  303. /* (Other icon shapes stay SVG-driven; only the ring animation is unified above) */
  304. `;
  305. // Inject Styles
  306. const injectStyles = () => {
  307. if (document.getElementById(`${PREFIX}styles`)) return;
  308. const styleSheet = document.createElement('style');
  309. styleSheet.id = `${PREFIX}styles`;
  310. styleSheet.textContent = css;
  311. document.head.appendChild(styleSheet);
  312. };
  313. class Layer {
  314. constructor() {
  315. injectStyles();
  316. this.params = {};
  317. this.dom = {};
  318. this.promise = null;
  319. this.resolve = null;
  320. this.reject = null;
  321. this._onKeydown = null;
  322. }
  323. // Constructor helper when called as function: const popup = Layer({...})
  324. static get isProxy() { return true; }
  325. // Static entry point
  326. static fire(options) {
  327. const instance = new Layer();
  328. return instance._fire(options);
  329. }
  330. // Chainable entry point (builder-style)
  331. // Example:
  332. // Layer.$({ title: 'Hi' }).fire().then(...)
  333. // Layer.$().config({ title: 'Hi' }).fire()
  334. static $(options) {
  335. const instance = new Layer();
  336. if (options !== undefined) instance.config(options);
  337. return instance;
  338. }
  339. // Chainable config helper (does not render until `.fire()` is called)
  340. config(options = {}) {
  341. // Support the same shorthand as Layer.fire(title, text, icon)
  342. if (typeof options === 'string') {
  343. options = { title: options };
  344. if (arguments[1]) options.text = arguments[1];
  345. if (arguments[2]) options.icon = arguments[2];
  346. }
  347. this.params = { ...(this.params || {}), ...options };
  348. return this;
  349. }
  350. // Instance entry point (chainable)
  351. fire(options) {
  352. const merged = (options === undefined) ? (this.params || {}) : options;
  353. return this._fire(merged);
  354. }
  355. _fire(options = {}) {
  356. if (typeof options === 'string') {
  357. options = { title: options };
  358. if (arguments[1]) options.text = arguments[1];
  359. if (arguments[2]) options.icon = arguments[2];
  360. }
  361. this.params = {
  362. title: '',
  363. text: '',
  364. icon: null,
  365. iconSize: null, // e.g. '6em' / '72px'
  366. confirmButtonText: 'OK',
  367. cancelButtonText: 'Cancel',
  368. showCancelButton: false,
  369. confirmButtonColor: '#3085d6',
  370. cancelButtonColor: '#aaa',
  371. closeOnClickOutside: true,
  372. closeOnEsc: true,
  373. iconAnimation: true,
  374. popupAnimation: true,
  375. ...options
  376. };
  377. this.promise = new Promise((resolve, reject) => {
  378. this.resolve = resolve;
  379. this.reject = reject;
  380. });
  381. this._render();
  382. return this.promise;
  383. }
  384. static _getXjs() {
  385. // Prefer xjs, fallback to animal (compat)
  386. const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
  387. if (!g) return null;
  388. const x = g.xjs || g.animal;
  389. return (typeof x === 'function') ? x : null;
  390. }
  391. _render() {
  392. // Remove existing if any
  393. const existing = document.querySelector(`.${PREFIX}overlay`);
  394. if (existing) existing.remove();
  395. // Create Overlay
  396. this.dom.overlay = document.createElement('div');
  397. this.dom.overlay.className = `${PREFIX}overlay`;
  398. // Create Popup
  399. this.dom.popup = document.createElement('div');
  400. this.dom.popup.className = `${PREFIX}popup`;
  401. this.dom.overlay.appendChild(this.dom.popup);
  402. // Icon
  403. if (this.params.icon) {
  404. this.dom.icon = this._createIcon(this.params.icon);
  405. this.dom.popup.appendChild(this.dom.icon);
  406. }
  407. // Title
  408. if (this.params.title) {
  409. this.dom.title = document.createElement('h2');
  410. this.dom.title.className = `${PREFIX}title`;
  411. this.dom.title.textContent = this.params.title;
  412. this.dom.popup.appendChild(this.dom.title);
  413. }
  414. // Content (Text / HTML / Element)
  415. if (this.params.text || this.params.html || this.params.content) {
  416. this.dom.content = document.createElement('div');
  417. this.dom.content.className = `${PREFIX}content`;
  418. if (this.params.content) {
  419. // DOM Element or Selector
  420. let el = this.params.content;
  421. if (typeof el === 'string') el = document.querySelector(el);
  422. if (el instanceof Element) this.dom.content.appendChild(el);
  423. } else if (this.params.html) {
  424. this.dom.content.innerHTML = this.params.html;
  425. } else {
  426. this.dom.content.textContent = this.params.text;
  427. }
  428. this.dom.popup.appendChild(this.dom.content);
  429. }
  430. // Actions
  431. this.dom.actions = document.createElement('div');
  432. this.dom.actions.className = `${PREFIX}actions`;
  433. // Cancel Button
  434. if (this.params.showCancelButton) {
  435. this.dom.cancelBtn = document.createElement('button');
  436. this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
  437. this.dom.cancelBtn.textContent = this.params.cancelButtonText;
  438. this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
  439. this.dom.cancelBtn.onclick = () => this._close(false);
  440. this.dom.actions.appendChild(this.dom.cancelBtn);
  441. }
  442. // Confirm Button
  443. this.dom.confirmBtn = document.createElement('button');
  444. this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
  445. this.dom.confirmBtn.textContent = this.params.confirmButtonText;
  446. this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
  447. this.dom.confirmBtn.onclick = () => this._close(true);
  448. this.dom.actions.appendChild(this.dom.confirmBtn);
  449. this.dom.popup.appendChild(this.dom.actions);
  450. // Event Listeners
  451. if (this.params.closeOnClickOutside) {
  452. this.dom.overlay.addEventListener('click', (e) => {
  453. if (e.target === this.dom.overlay) {
  454. this._close(null); // Dismiss
  455. }
  456. });
  457. }
  458. document.body.appendChild(this.dom.overlay);
  459. // Animation
  460. requestAnimationFrame(() => {
  461. this.dom.overlay.classList.add('show');
  462. this._didOpen();
  463. });
  464. }
  465. _createIcon(type) {
  466. const icon = document.createElement('div');
  467. icon.className = `${PREFIX}icon ${type}`;
  468. const applyIconSize = (mode) => {
  469. if (!(this.params && this.params.iconSize)) return;
  470. try {
  471. const raw = this.params.iconSize;
  472. const s = String(raw).trim();
  473. if (!s) return;
  474. // For SweetAlert2-style success icon, scale via font-size so all `em`-based
  475. // parts remain proportional.
  476. if (mode === 'font') {
  477. const m = s.match(/^(-?\d*\.?\d+)\s*(px|em|rem)$/i);
  478. if (m) {
  479. const n = parseFloat(m[1]);
  480. const unit = m[2];
  481. if (Number.isFinite(n) && n > 0) {
  482. icon.style.fontSize = (n / 5) + unit; // icon is 5em wide/tall
  483. return;
  484. }
  485. }
  486. }
  487. // Fallback: directly size the box (works great for SVG icons)
  488. icon.style.width = s;
  489. icon.style.height = s;
  490. } catch {}
  491. };
  492. const svgNs = 'http://www.w3.org/2000/svg';
  493. if (type === 'success') {
  494. // SweetAlert2-compatible DOM structure (circle + tick) for exact style parity
  495. // <div class="...success-circular-line-left"></div>
  496. // <span class="...success-line-tip"></span>
  497. // <span class="...success-line-long"></span>
  498. // <div class="...success-ring"></div>
  499. // <div class="...success-fix"></div>
  500. // <div class="...success-circular-line-right"></div>
  501. const left = document.createElement('div');
  502. left.className = `${PREFIX}success-circular-line-left`;
  503. const tip = document.createElement('span');
  504. tip.className = `${PREFIX}success-line-tip`;
  505. const long = document.createElement('span');
  506. long.className = `${PREFIX}success-line-long`;
  507. const ring = document.createElement('div');
  508. ring.className = `${PREFIX}success-ring`;
  509. const fix = document.createElement('div');
  510. fix.className = `${PREFIX}success-fix`;
  511. const right = document.createElement('div');
  512. right.className = `${PREFIX}success-circular-line-right`;
  513. icon.appendChild(left);
  514. icon.appendChild(tip);
  515. icon.appendChild(long);
  516. icon.appendChild(ring);
  517. icon.appendChild(fix);
  518. icon.appendChild(right);
  519. applyIconSize('font');
  520. return icon;
  521. }
  522. if (type === 'error' || type === 'warning' || type === 'info' || type === 'question') {
  523. // Use the same "success-like" ring parts for every icon
  524. const left = document.createElement('div');
  525. left.className = `${PREFIX}success-circular-line-left`;
  526. const ring = document.createElement('div');
  527. ring.className = `${PREFIX}success-ring`;
  528. const fix = document.createElement('div');
  529. fix.className = `${PREFIX}success-fix`;
  530. const right = document.createElement('div');
  531. right.className = `${PREFIX}success-circular-line-right`;
  532. icon.appendChild(left);
  533. icon.appendChild(ring);
  534. icon.appendChild(fix);
  535. icon.appendChild(right);
  536. applyIconSize('font');
  537. // SVG only draws the inner symbol (no SVG ring)
  538. const svg = document.createElementNS(svgNs, 'svg');
  539. svg.setAttribute('viewBox', '0 0 80 80');
  540. svg.setAttribute('aria-hidden', 'true');
  541. svg.setAttribute('focusable', 'false');
  542. const addPath = (d, extraClass) => {
  543. const p = document.createElementNS(svgNs, 'path');
  544. p.setAttribute('d', d);
  545. p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
  546. p.setAttribute('stroke', 'currentColor');
  547. svg.appendChild(p);
  548. return p;
  549. };
  550. if (type === 'error') {
  551. addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
  552. addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
  553. } else if (type === 'warning') {
  554. addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
  555. const dot = document.createElementNS(svgNs, 'circle');
  556. dot.setAttribute('class', `${PREFIX}svg-dot`);
  557. dot.setAttribute('cx', '40');
  558. dot.setAttribute('cy', '58');
  559. dot.setAttribute('r', '3.2');
  560. dot.setAttribute('fill', 'currentColor');
  561. svg.appendChild(dot);
  562. } else if (type === 'info') {
  563. addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
  564. const dot = document.createElementNS(svgNs, 'circle');
  565. dot.setAttribute('class', `${PREFIX}svg-dot`);
  566. dot.setAttribute('cx', '40');
  567. dot.setAttribute('cy', '25');
  568. dot.setAttribute('r', '3.2');
  569. dot.setAttribute('fill', 'currentColor');
  570. svg.appendChild(dot);
  571. } else if (type === 'question') {
  572. 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`);
  573. const dot = document.createElementNS(svgNs, 'circle');
  574. dot.setAttribute('class', `${PREFIX}svg-dot`);
  575. dot.setAttribute('cx', '42');
  576. dot.setAttribute('cy', '61');
  577. dot.setAttribute('r', '3.2');
  578. dot.setAttribute('fill', 'currentColor');
  579. svg.appendChild(dot);
  580. }
  581. icon.appendChild(svg);
  582. return icon;
  583. }
  584. // Default to SVG icons for other/custom types
  585. applyIconSize('box');
  586. const svg = document.createElementNS(svgNs, 'svg');
  587. svg.setAttribute('viewBox', '0 0 80 80');
  588. svg.setAttribute('aria-hidden', 'true');
  589. svg.setAttribute('focusable', 'false');
  590. const ring = document.createElementNS(svgNs, 'circle');
  591. ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
  592. ring.setAttribute('cx', '40');
  593. ring.setAttribute('cy', '40');
  594. ring.setAttribute('r', '34');
  595. ring.setAttribute('stroke', 'currentColor');
  596. svg.appendChild(ring);
  597. const addPath = (d, extraClass) => {
  598. const p = document.createElementNS(svgNs, 'path');
  599. p.setAttribute('d', d);
  600. p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
  601. p.setAttribute('stroke', 'currentColor');
  602. svg.appendChild(p);
  603. return p;
  604. };
  605. if (type === 'error') {
  606. addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
  607. addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
  608. } else if (type === 'warning') {
  609. addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
  610. const dot = document.createElementNS(svgNs, 'circle');
  611. dot.setAttribute('class', `${PREFIX}svg-dot`);
  612. dot.setAttribute('cx', '40');
  613. dot.setAttribute('cy', '58');
  614. dot.setAttribute('r', '3.2');
  615. dot.setAttribute('fill', 'currentColor');
  616. svg.appendChild(dot);
  617. } else if (type === 'info') {
  618. addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
  619. const dot = document.createElementNS(svgNs, 'circle');
  620. dot.setAttribute('class', `${PREFIX}svg-dot`);
  621. dot.setAttribute('cx', '40');
  622. dot.setAttribute('cy', '25');
  623. dot.setAttribute('r', '3.2');
  624. dot.setAttribute('fill', 'currentColor');
  625. svg.appendChild(dot);
  626. } else if (type === 'question') {
  627. // Question mark (single stroke + dot)
  628. 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`);
  629. const dot = document.createElementNS(svgNs, 'circle');
  630. dot.setAttribute('class', `${PREFIX}svg-dot`);
  631. dot.setAttribute('cx', '42');
  632. dot.setAttribute('cy', '61');
  633. dot.setAttribute('r', '3.2');
  634. dot.setAttribute('fill', 'currentColor');
  635. svg.appendChild(dot);
  636. } else {
  637. // Fallback: ring only
  638. }
  639. icon.appendChild(svg);
  640. return icon;
  641. }
  642. _adjustRingBackgroundColor() {
  643. try {
  644. const icon = this.dom && this.dom.icon;
  645. const popup = this.dom && this.dom.popup;
  646. if (!icon || !popup) return;
  647. const bg = getComputedStyle(popup).backgroundColor;
  648. const parts = icon.querySelectorAll(`.${PREFIX}success-circular-line-left, .${PREFIX}success-circular-line-right, .${PREFIX}success-fix`);
  649. parts.forEach((el) => {
  650. try { el.style.backgroundColor = bg; } catch {}
  651. });
  652. } catch {}
  653. }
  654. _didOpen() {
  655. // Keyboard close (ESC)
  656. if (this.params.closeOnEsc) {
  657. this._onKeydown = (e) => {
  658. if (!e) return;
  659. if (e.key === 'Escape') this._close(null);
  660. };
  661. document.addEventListener('keydown', this._onKeydown);
  662. }
  663. // Keep the "success-like" ring perfectly blended with popup bg
  664. this._adjustRingBackgroundColor();
  665. // Popup animation (optional)
  666. if (this.params.popupAnimation) {
  667. const X = Layer._getXjs();
  668. if (X && this.dom.popup) {
  669. // Override the CSS scale transition with a spring-ish entrance.
  670. try {
  671. this.dom.popup.style.transition = 'none';
  672. this.dom.popup.style.transform = 'scale(0.92)';
  673. X(this.dom.popup).animate({
  674. scale: [0.92, 1],
  675. y: [-6, 0],
  676. duration: 520,
  677. easing: { stiffness: 260, damping: 16 }
  678. });
  679. } catch {}
  680. }
  681. }
  682. // Icon SVG draw animation
  683. if (this.params.iconAnimation) {
  684. this._animateIcon();
  685. }
  686. // User hook (SweetAlert-ish naming)
  687. try {
  688. if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
  689. } catch {}
  690. }
  691. _animateIcon() {
  692. const icon = this.dom.icon;
  693. if (!icon) return;
  694. const type = (this.params && this.params.icon) || '';
  695. const ringTypes = new Set(['success', 'error', 'warning', 'info', 'question']);
  696. // Ring animation (same as success) for all built-in icons
  697. if (ringTypes.has(type)) this._adjustRingBackgroundColor();
  698. try { icon.classList.remove(`${PREFIX}icon-show`); } catch {}
  699. requestAnimationFrame(() => {
  700. try { icon.classList.add(`${PREFIX}icon-show`); } catch {}
  701. });
  702. // Success tick is CSS-driven; others still draw their SVG mark after the ring starts.
  703. if (type === 'success') return;
  704. const X = Layer._getXjs();
  705. if (!X) return;
  706. const svg = icon.querySelector('svg');
  707. if (!svg) return;
  708. const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
  709. const dot = svg.querySelector(`.${PREFIX}svg-dot`);
  710. // If this is a built-in icon, keep the SweetAlert-like order:
  711. // ring sweep first (~0.51s), then draw the inner mark.
  712. const baseDelay = ringTypes.has(type) ? 520 : 0;
  713. if (type === 'error') {
  714. // Draw order: left-top -> right-bottom, then right-top -> left-bottom
  715. // NOTE: A tiny delay (like 70ms) looks simultaneous; make it strictly sequential.
  716. const a = svg.querySelector(`.${PREFIX}svg-error-left`) || marks[0]; // M28 28 L52 52
  717. const b = svg.querySelector(`.${PREFIX}svg-error-right`) || marks[1]; // M52 28 L28 52
  718. const dur = 320;
  719. const gap = 60;
  720. try { if (a) X(a).draw({ duration: dur, easing: 'ease-out', delay: baseDelay }); } catch {}
  721. try { if (b) X(b).draw({ duration: dur, easing: 'ease-out', delay: baseDelay + dur + gap }); } catch {}
  722. } else {
  723. // warning / info / question (single stroke) or custom SVG symbols
  724. marks.forEach((m, i) => {
  725. try { X(m).draw({ duration: 420, easing: 'ease-out', delay: baseDelay + i * 60 }); } catch {}
  726. });
  727. }
  728. if (dot) {
  729. try {
  730. dot.style.opacity = '0';
  731. // Keep dot pop after the ring begins
  732. const d = baseDelay + 140;
  733. if (type === 'info') {
  734. X(dot).animate({ opacity: [0, 1], y: [-8, 0], scale: [0.2, 1], duration: 420, delay: d, easing: { stiffness: 300, damping: 14 } });
  735. } else {
  736. X(dot).animate({ opacity: [0, 1], scale: [0.2, 1], duration: 320, delay: d, easing: { stiffness: 320, damping: 18 } });
  737. }
  738. } catch {}
  739. }
  740. }
  741. _close(isConfirmed) {
  742. this.dom.overlay.classList.remove('show');
  743. setTimeout(() => {
  744. if (this.dom.overlay && this.dom.overlay.parentNode) {
  745. this.dom.overlay.parentNode.removeChild(this.dom.overlay);
  746. }
  747. }, 300);
  748. if (this._onKeydown) {
  749. try { document.removeEventListener('keydown', this._onKeydown); } catch {}
  750. this._onKeydown = null;
  751. }
  752. try {
  753. if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
  754. } catch {}
  755. try {
  756. if (typeof this.params.didClose === 'function') this.params.didClose();
  757. } catch {}
  758. if (isConfirmed === true) {
  759. this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
  760. } else if (isConfirmed === false) {
  761. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'cancel' });
  762. } else {
  763. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'backdrop' });
  764. }
  765. }
  766. }
  767. return Layer;
  768. })));