layer.js 28 KB

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