layer.js 28 KB

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