layer.js 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860
  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. const RING_TYPES = new Set(['success', 'error', 'warning', 'info', 'question']);
  36. const RING_BG_PARTS_SELECTOR = `.${PREFIX}success-circular-line-left, .${PREFIX}success-circular-line-right, .${PREFIX}success-fix`;
  37. const LOTTIE_PREFIX = 'svg:';
  38. const normalizeOptions = (options, text, icon) => {
  39. if (typeof options !== 'string') return options || {};
  40. const o = { title: options };
  41. if (text) o.text = text;
  42. if (icon) o.icon = icon;
  43. return o;
  44. };
  45. const el = (tag, className, text) => {
  46. const node = document.createElement(tag);
  47. if (className) node.className = className;
  48. if (text !== undefined) node.textContent = text;
  49. return node;
  50. };
  51. const svgEl = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag);
  52. // Ensure library CSS is loaded (xjs.css)
  53. // - CSS is centralized in xjs.css (no runtime <style> injection).
  54. // - We still auto-load it for convenience/compat, since consumers may forget the <link>.
  55. let _xjsCssReady = null; // Promise<void>
  56. const waitForStylesheet = (link) => new Promise((resolve) => {
  57. if (!link) return resolve();
  58. if (link.sheet) return resolve();
  59. let done = false;
  60. const finish = () => {
  61. if (done) return;
  62. done = true;
  63. resolve();
  64. };
  65. try { link.addEventListener('load', finish, { once: true }); } catch {}
  66. try { link.addEventListener('error', finish, { once: true }); } catch {}
  67. // Fallback timeout: avoid blocking forever if the load event is missed.
  68. setTimeout(finish, 200);
  69. });
  70. const ensureXjsCss = () => {
  71. try {
  72. if (_xjsCssReady) return _xjsCssReady;
  73. if (typeof document === 'undefined') return Promise.resolve();
  74. if (!document.head) return Promise.resolve();
  75. const existingLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
  76. .find((l) => /(^|\/)xjs\.css(\?|#|$)/.test((l.getAttribute('href') || '').trim()));
  77. if (existingLink) return (_xjsCssReady = waitForStylesheet(existingLink));
  78. const id = 'xjs-css';
  79. const existing = document.getElementById(id);
  80. if (existing) return (_xjsCssReady = waitForStylesheet(existing));
  81. const scripts = Array.from(document.getElementsByTagName('script'));
  82. const scriptSrc = scripts
  83. .map((s) => s && s.src)
  84. .find((src) => /(^|\/)(xjs|layer)\.js(\?|#|$)/.test(String(src || '')));
  85. let href = 'xjs.css';
  86. if (scriptSrc) {
  87. href = String(scriptSrc)
  88. .replace(/(^|\/)xjs\.js(\?|#|$)/, '$1xjs.css$2')
  89. .replace(/(^|\/)layer\.js(\?|#|$)/, '$1xjs.css$2');
  90. }
  91. const link = document.createElement('link');
  92. link.id = id;
  93. link.rel = 'stylesheet';
  94. link.href = href;
  95. _xjsCssReady = waitForStylesheet(link);
  96. document.head.appendChild(link);
  97. return _xjsCssReady;
  98. } catch {
  99. // ignore
  100. return Promise.resolve();
  101. }
  102. };
  103. // Lottie (svg.js) lazy loader for "svg:*" icon type
  104. let _lottieReady = null;
  105. let _lottiePrefetchScheduled = false;
  106. const isLottieIcon = (icon) => {
  107. if (typeof icon !== 'string') return false;
  108. const s = icon.trim();
  109. return s.startsWith(LOTTIE_PREFIX);
  110. };
  111. const getLottieIconName = (icon) => {
  112. if (!isLottieIcon(icon)) return '';
  113. let name = String(icon || '').trim().slice(LOTTIE_PREFIX.length).trim();
  114. if (name === 'loadding') name = 'loading';
  115. return name;
  116. };
  117. const getLayerScriptBase = () => {
  118. try {
  119. if (typeof document === 'undefined') return '';
  120. const scripts = Array.from(document.getElementsByTagName('script'));
  121. const src = scripts
  122. .map((s) => s && s.src)
  123. .find((s) => /(^|\/)(xjs|layer)\.js(\?|#|$)/.test(String(s || '')));
  124. if (!src) return '';
  125. return String(src)
  126. .replace(/[#?].*$/, '')
  127. .replace(/\/[^\/]*$/, '/');
  128. } catch {
  129. return '';
  130. }
  131. };
  132. const resolveLottieScriptUrl = () => {
  133. const base = getLayerScriptBase();
  134. return base ? (base + 'svg/svg.js') : 'svg/svg.js';
  135. };
  136. const resolveLottieJsonUrl = (name) => {
  137. const raw = String(name || '').trim();
  138. if (!raw) return '';
  139. if (/^(https?:)?\/\//.test(raw)) return raw;
  140. if (raw.startsWith('/') || raw.startsWith('./') || raw.startsWith('../')) return raw;
  141. const base = getLayerScriptBase();
  142. const file = raw.endsWith('.json') ? raw : (raw + '.json');
  143. return (base ? base : '') + 'svg/' + file;
  144. };
  145. const ensureLottieLib = () => {
  146. try {
  147. if (typeof window === 'undefined') return Promise.resolve(null);
  148. if (window.lottie) return Promise.resolve(window.lottie);
  149. if (typeof document === 'undefined' || !document.head) return Promise.resolve(null);
  150. if (_lottieReady) return _lottieReady;
  151. const existing = document.getElementById('layer-lottie-lib');
  152. _lottieReady = new Promise((resolve) => {
  153. const finish = () => resolve(window.lottie || null);
  154. if (existing) {
  155. try { existing.addEventListener('load', finish, { once: true }); } catch {}
  156. try { existing.addEventListener('error', finish, { once: true }); } catch {}
  157. setTimeout(finish, 1200);
  158. return;
  159. }
  160. const script = document.createElement('script');
  161. script.id = 'layer-lottie-lib';
  162. script.async = true;
  163. script.src = resolveLottieScriptUrl();
  164. try { script.addEventListener('load', finish, { once: true }); } catch {}
  165. try { script.addEventListener('error', finish, { once: true }); } catch {}
  166. setTimeout(finish, 1800);
  167. document.head.appendChild(script);
  168. });
  169. return _lottieReady;
  170. } catch {
  171. return Promise.resolve(null);
  172. }
  173. };
  174. const scheduleLottiePrefetch = () => {
  175. if (_lottiePrefetchScheduled) return;
  176. _lottiePrefetchScheduled = true;
  177. try {
  178. if (typeof window === 'undefined') return;
  179. const start = () => {
  180. setTimeout(() => {
  181. try { ensureLottieLib(); } catch {}
  182. }, 0);
  183. };
  184. if (document.readyState === 'complete') start();
  185. else window.addEventListener('load', start, { once: true });
  186. } catch {}
  187. };
  188. class Layer {
  189. constructor() {
  190. this._cssReady = ensureXjsCss();
  191. this.params = {};
  192. this.dom = {};
  193. this.promise = null;
  194. this.resolve = null;
  195. this.reject = null;
  196. this._onKeydown = null;
  197. this._mounted = null; // { kind, originalEl, placeholder, parent, nextSibling, prevHidden, prevInlineDisplay }
  198. this._isClosing = false;
  199. this._isReplace = false;
  200. // Step/flow support
  201. this._flowSteps = null; // Array<options>
  202. this._flowIndex = 0;
  203. this._flowValues = [];
  204. this._flowBase = null; // base options merged into each step
  205. this._flowResolved = null; // resolved merged steps
  206. this._flowMountedList = []; // mounted DOM records for move-mode steps
  207. this._flowAutoForm = null; // { enabled: true } when using step container shorthand
  208. }
  209. // Constructor helper when called as function: const popup = Layer({...})
  210. static get isProxy() { return true; }
  211. // Static entry point
  212. static run(options) {
  213. const instance = new Layer();
  214. return instance._fire(options);
  215. }
  216. // Backward-compatible alias
  217. static fire(options) { return Layer.run(options); }
  218. // Chainable entry point (builder-style)
  219. // Example:
  220. // Layer.$({ title: 'Hi' }).run().then(...)
  221. // Layer.$().config({ title: 'Hi' }).run()
  222. static $(options) {
  223. const instance = new Layer();
  224. if (options !== undefined) instance.config(options);
  225. return instance;
  226. }
  227. // Chainable config helper (does not render until `.run()` is called)
  228. config(options = {}) {
  229. // Support the same shorthand as Layer.fire(title, text, icon)
  230. options = normalizeOptions(options, arguments[1], arguments[2]);
  231. this.params = { ...(this.params || {}), ...options };
  232. return this;
  233. }
  234. // Add a single step (chainable)
  235. // Usage:
  236. // Layer.$().step({ title:'A', dom:'#step1' }).step({ title:'B', dom:'#step2' }).run()
  237. step(options = {}) {
  238. if (!this._flowSteps) this._flowSteps = [];
  239. this._flowSteps.push(normalizeOptions(options, arguments[1], arguments[2]));
  240. return this;
  241. }
  242. // Add multiple steps at once (chainable)
  243. steps(steps = []) {
  244. if (!Array.isArray(steps)) return this;
  245. if (!this._flowSteps) this._flowSteps = [];
  246. steps.forEach((s) => this.step(s));
  247. return this;
  248. }
  249. // Convenience static helper: Layer.flow([steps], baseOptions?)
  250. static flow(steps = [], baseOptions = {}) {
  251. return Layer.$(baseOptions).steps(steps).run();
  252. }
  253. // Instance entry point (chainable)
  254. run(options) {
  255. const hasArgs = (options !== undefined && options !== null);
  256. const normalized = hasArgs ? normalizeOptions(options, arguments[1], arguments[2]) : null;
  257. const merged = hasArgs ? normalized : (this.params || {});
  258. // If no explicit steps yet, allow shorthand: { step: '#container', stepItem: '.item' }
  259. if (!this._flowSteps || !this._flowSteps.length) {
  260. const didInit = this._initFlowFromStepContainer(merged);
  261. if (didInit) {
  262. this.params = { ...(this.params || {}), ...this._stripStepOptions(merged) };
  263. }
  264. }
  265. // Flow mode: if configured via .step()/.steps() or step container shorthand, ignore per-call options and use steps
  266. if (this._flowSteps && this._flowSteps.length) {
  267. if (hasArgs) {
  268. // allow providing base options at fire-time
  269. this.params = { ...(this.params || {}), ...this._stripStepOptions(merged) };
  270. }
  271. return this._fireFlow();
  272. }
  273. return this._fire(merged);
  274. }
  275. // Backward-compatible alias
  276. fire(options) { return this.run(options); }
  277. _fire(options = {}) {
  278. options = normalizeOptions(options, arguments[1], arguments[2]);
  279. this.params = {
  280. title: '',
  281. text: '',
  282. icon: null,
  283. iconSize: null, // e.g. '6em' / '72px'
  284. iconLoop: null, // lottie only: true/false/null (auto)
  285. confirmButtonText: 'OK',
  286. cancelButtonText: 'Cancel',
  287. showCancelButton: false,
  288. closeOnClickOutside: true,
  289. closeOnEsc: true,
  290. iconAnimation: true,
  291. popupAnimation: true,
  292. nextIcon: null, // flow-only: icon for Next button
  293. prevIcon: null, // flow-only: icon for Back button
  294. // Content:
  295. // - text: plain text
  296. // - html: innerHTML
  297. // - dom: selector / Element / <template> (preferred)
  298. // - content: backward compat for selector/Element OR advanced { dom, mode, clone }
  299. dom: null,
  300. domMode: 'move', // 'move' (default) | 'clone'
  301. // Hook for confirming (also used in flow steps)
  302. // Return false to prevent close / next.
  303. // Return a value to be attached as `value`.
  304. // May return Promise.
  305. preConfirm: null,
  306. ...options
  307. };
  308. this.promise = new Promise((resolve, reject) => {
  309. this.resolve = resolve;
  310. this.reject = reject;
  311. });
  312. this._render();
  313. return this.promise;
  314. }
  315. _fireFlow() {
  316. this._flowIndex = 0;
  317. this._flowValues = [];
  318. // In flow mode:
  319. // - keep iconAnimation off by default (avoids jitter)
  320. // - BUT allow popupAnimation by default so the first step has the same entrance feel
  321. const base = { ...(this.params || {}) };
  322. if (!('popupAnimation' in base)) base.popupAnimation = true;
  323. if (!('iconAnimation' in base)) base.iconAnimation = false;
  324. this._flowBase = base;
  325. this.promise = new Promise((resolve, reject) => {
  326. this.resolve = resolve;
  327. this.reject = reject;
  328. });
  329. this._flowResolved = (this._flowSteps || []).map((_, i) => this._getFlowStepOptions(i));
  330. const first = this._flowResolved[0] || this._getFlowStepOptions(0);
  331. this.params = first;
  332. this._render({ flow: true });
  333. return this.promise;
  334. }
  335. static _getXjs() {
  336. // Prefer $ (main), fallback to xjs/animal (compat)
  337. const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
  338. if (!g) return null;
  339. const x = g.$ || g.xjs || g.animal;
  340. return (typeof x === 'function') ? x : null;
  341. }
  342. _stripStepOptions(options) {
  343. const out = { ...(options || {}) };
  344. try { delete out.step; } catch {}
  345. try { delete out.stepItem; } catch {}
  346. return out;
  347. }
  348. _normalizeStepContainer(options) {
  349. const opts = options || {};
  350. const raw = opts.step;
  351. if (!raw || typeof document === 'undefined') return null;
  352. const container = (typeof raw === 'string') ? document.querySelector(raw) : raw;
  353. if (!container || (typeof Element !== 'undefined' && !(container instanceof Element))) return null;
  354. const itemSelector = opts.stepItem;
  355. let items = [];
  356. if (typeof itemSelector === 'string' && itemSelector.trim()) {
  357. try { items = Array.from(container.querySelectorAll(itemSelector)); } catch {}
  358. } else {
  359. try { items = Array.from(container.children || []); } catch {}
  360. }
  361. return { container, items };
  362. }
  363. _initFlowFromStepContainer(options) {
  364. const spec = this._normalizeStepContainer(options);
  365. if (!spec) return false;
  366. const { items } = spec;
  367. if (!items || !items.length) {
  368. try { console.warn('Layer step container has no items.'); } catch {}
  369. return false;
  370. }
  371. this._flowSteps = items.map((item) => {
  372. const step = { dom: item };
  373. try {
  374. const title =
  375. (item.getAttribute('data-step-title') || item.getAttribute('data-layer-title') || item.getAttribute('title') || '').trim();
  376. if (title) step.title = title;
  377. } catch {}
  378. return step;
  379. });
  380. this._flowAutoForm = { enabled: true };
  381. return true;
  382. }
  383. _render(meta = null) {
  384. this._destroyLottieIcon();
  385. // Remove existing if any (but first, try to restore any mounted DOM from the previous instance)
  386. const existing = document.querySelector(`.${PREFIX}overlay`);
  387. const wantReplace = !!(this.params && this.params.replace);
  388. this._isReplace = false;
  389. if (existing && wantReplace) {
  390. try {
  391. const prev = existing._layerInstance;
  392. if (prev && typeof prev._forceDestroy === 'function') prev._forceDestroy('replace');
  393. } catch {}
  394. try {
  395. if (existing._layerCloseTimer) {
  396. clearTimeout(existing._layerCloseTimer);
  397. existing._layerCloseTimer = null;
  398. }
  399. } catch {}
  400. try {
  401. if (existing._layerOnClick) {
  402. existing.removeEventListener('click', existing._layerOnClick);
  403. existing._layerOnClick = null;
  404. }
  405. } catch {}
  406. try {
  407. while (existing.firstChild) existing.removeChild(existing.firstChild);
  408. } catch {}
  409. try {
  410. existing.style.visibility = '';
  411. existing.classList.add('show');
  412. // Reset any inline fade state from the previous close
  413. existing.style.transition = 'none';
  414. existing.style.opacity = '1';
  415. requestAnimationFrame(() => {
  416. try { existing.style.transition = ''; } catch {}
  417. });
  418. } catch {}
  419. this.dom.overlay = existing;
  420. this.dom.overlay._layerInstance = this;
  421. this._isReplace = true;
  422. } else {
  423. if (existing) {
  424. try {
  425. const prev = existing._layerInstance;
  426. if (prev && typeof prev._forceDestroy === 'function') prev._forceDestroy('replace');
  427. } catch {}
  428. try { existing.remove(); } catch {}
  429. }
  430. // Create Overlay
  431. this.dom.overlay = el('div', `${PREFIX}overlay`);
  432. this.dom.overlay._layerInstance = this;
  433. }
  434. // Create Popup
  435. this.dom.popup = el('div', `${PREFIX}popup`);
  436. this.dom.overlay.appendChild(this.dom.popup);
  437. // Flow mode: pre-mount all steps and switch by hide/show (DOM continuity, less jitter)
  438. if (meta && meta.flow) {
  439. this._renderFlowUI();
  440. return;
  441. }
  442. // Icon
  443. if (this.params.icon) {
  444. this.dom.icon = this._createIcon(this.params.icon);
  445. this.dom.popup.appendChild(this.dom.icon);
  446. }
  447. // Title
  448. if (this.params.title) {
  449. this.dom.title = el('h2', `${PREFIX}title`, this.params.title);
  450. this.dom.popup.appendChild(this.dom.title);
  451. }
  452. // Content (Text / HTML / Element)
  453. if (this.params.text || this.params.html || this.params.content || this.params.dom) {
  454. this.dom.content = el('div', `${PREFIX}content`);
  455. // DOM content (preferred: dom, backward: content)
  456. const domSpec = this._normalizeDomSpec(this.params);
  457. if (domSpec) {
  458. this._mountDomContent(domSpec);
  459. } else if (this.params.html) {
  460. this.dom.content.innerHTML = this.params.html;
  461. } else {
  462. this.dom.content.textContent = this.params.text;
  463. }
  464. this.dom.popup.appendChild(this.dom.content);
  465. }
  466. // Actions
  467. this.dom.actions = el('div', `${PREFIX}actions`);
  468. // Cancel Button
  469. if (this.params.showCancelButton) {
  470. this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
  471. this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText);
  472. this.dom.cancelBtn.onclick = () => this._handleCancel();
  473. this.dom.actions.appendChild(this.dom.cancelBtn);
  474. }
  475. // Confirm Button
  476. this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
  477. this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText);
  478. this.dom.confirmBtn.onclick = () => this._handleConfirm();
  479. this.dom.actions.appendChild(this.dom.confirmBtn);
  480. this.dom.popup.appendChild(this.dom.actions);
  481. // Event Listeners
  482. if (this.params.closeOnClickOutside) {
  483. const onClick = (e) => {
  484. if (e.target === this.dom.overlay) {
  485. this._close(null); // Dismiss
  486. }
  487. };
  488. try {
  489. if (this.dom.overlay._layerOnClick) {
  490. this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
  491. }
  492. } catch {}
  493. this.dom.overlay._layerOnClick = onClick;
  494. this.dom.overlay.addEventListener('click', onClick);
  495. } else {
  496. try {
  497. if (this.dom.overlay._layerOnClick) {
  498. this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
  499. this.dom.overlay._layerOnClick = null;
  500. }
  501. } catch {}
  502. }
  503. // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
  504. // then show on a clean frame so opacity transitions are smooth.
  505. const ready = this._cssReady && typeof this._cssReady.then === 'function' ? this._cssReady : Promise.resolve();
  506. ready.then(() => {
  507. try { this.dom.overlay.style.visibility = ''; } catch {}
  508. if (!this.dom.overlay.parentNode) {
  509. document.body.appendChild(this.dom.overlay);
  510. }
  511. // Double-rAF gives the browser a chance to apply styles before animating.
  512. requestAnimationFrame(() => {
  513. requestAnimationFrame(() => {
  514. this.dom.overlay.classList.add('show');
  515. this._didOpen();
  516. });
  517. });
  518. });
  519. }
  520. _renderFlowUI() {
  521. this._destroyLottieIcon();
  522. // Icon/title/content/actions are stable; steps are pre-mounted and toggled.
  523. const popup = this.dom.popup;
  524. if (!popup) return;
  525. // Clear popup
  526. while (popup.firstChild) popup.removeChild(popup.firstChild);
  527. // Icon (in flow: suppressed by default unless question or summary)
  528. this.dom.icon = null;
  529. if (this.params.icon) {
  530. this.dom.icon = this._createIcon(this.params.icon);
  531. popup.appendChild(this.dom.icon);
  532. }
  533. // Title (always present for flow)
  534. this.dom.title = el('h2', `${PREFIX}title`, this.params.title || '');
  535. popup.appendChild(this.dom.title);
  536. // Content container
  537. this.dom.content = el('div', `${PREFIX}content`);
  538. // Stack container: keep panes absolute so we can cross-fade without layout thrash
  539. this.dom.stepStack = el('div', `${PREFIX}step-stack`);
  540. this.dom.stepStack.style.position = 'relative';
  541. this.dom.stepStack.style.width = '100%';
  542. this.dom.stepPanes = [];
  543. this._flowMountedList = [];
  544. const steps = this._flowResolved || (this._flowSteps || []).map((_, i) => this._getFlowStepOptions(i));
  545. steps.forEach((opt, i) => {
  546. const pane = el('div', `${PREFIX}step-pane`);
  547. pane.style.width = '100%';
  548. pane.style.boxSizing = 'border-box';
  549. pane.style.display = (i === this._flowIndex) ? '' : 'none';
  550. // Fill pane: dom/html/text
  551. const domSpec = this._normalizeDomSpec(opt);
  552. if (domSpec) {
  553. this._mountDomContentInto(pane, domSpec, { collectFlow: true });
  554. } else if (opt.html) {
  555. pane.innerHTML = opt.html;
  556. } else if (opt.text) {
  557. pane.textContent = opt.text;
  558. }
  559. this.dom.stepStack.appendChild(pane);
  560. this.dom.stepPanes.push(pane);
  561. });
  562. this.dom.content.appendChild(this.dom.stepStack);
  563. popup.appendChild(this.dom.content);
  564. // Actions
  565. this.dom.actions = el('div', `${PREFIX}actions`);
  566. popup.appendChild(this.dom.actions);
  567. this._updateFlowActions();
  568. // Event Listeners
  569. if (this.params.closeOnClickOutside) {
  570. const onClick = (e) => {
  571. if (e.target === this.dom.overlay) {
  572. this._close(null, 'backdrop');
  573. }
  574. };
  575. try {
  576. if (this.dom.overlay._layerOnClick) {
  577. this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
  578. }
  579. } catch {}
  580. this.dom.overlay._layerOnClick = onClick;
  581. this.dom.overlay.addEventListener('click', onClick);
  582. } else {
  583. try {
  584. if (this.dom.overlay._layerOnClick) {
  585. this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
  586. this.dom.overlay._layerOnClick = null;
  587. }
  588. } catch {}
  589. }
  590. // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
  591. // then show on a clean frame so opacity transitions are smooth.
  592. const ready = this._cssReady && typeof this._cssReady.then === 'function' ? this._cssReady : Promise.resolve();
  593. ready.then(() => {
  594. try { this.dom.overlay.style.visibility = ''; } catch {}
  595. if (!this.dom.overlay.parentNode) {
  596. document.body.appendChild(this.dom.overlay);
  597. }
  598. requestAnimationFrame(() => {
  599. requestAnimationFrame(() => {
  600. this.dom.overlay.classList.add('show');
  601. this._didOpen();
  602. });
  603. });
  604. });
  605. }
  606. _updateFlowActions() {
  607. const actions = this.dom && this.dom.actions;
  608. if (!actions) return;
  609. while (actions.firstChild) actions.removeChild(actions.firstChild);
  610. this.dom.cancelBtn = null;
  611. const total = (this._flowSteps && this._flowSteps.length) ? this._flowSteps.length : 0;
  612. const isLast = total > 0 ? (this._flowIndex >= (total - 1)) : true;
  613. if (this.params.showCancelButton) {
  614. const prevIcon = (this._flowIndex > 0) ? this.params.prevIcon : null;
  615. this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
  616. this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText, prevIcon, 'start');
  617. this.dom.cancelBtn.onclick = () => this._handleCancel();
  618. actions.appendChild(this.dom.cancelBtn);
  619. }
  620. const nextIcon = (!isLast) ? this.params.nextIcon : null;
  621. this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
  622. this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText, nextIcon, 'end');
  623. this.dom.confirmBtn.onclick = () => this._handleConfirm();
  624. actions.appendChild(this.dom.confirmBtn);
  625. }
  626. _createIcon(type) {
  627. const rawType = String(type || '').trim();
  628. const icon = el('div', `${PREFIX}icon ${rawType}`);
  629. const applyIconSize = (mode) => {
  630. if (!(this.params && this.params.iconSize)) return;
  631. try {
  632. const s = String(this.params.iconSize).trim();
  633. if (!s) return;
  634. // For SweetAlert2-style success icon, scale via font-size so all `em`-based parts remain proportional.
  635. if (mode === 'font') {
  636. const m = s.match(/^(-?\d*\.?\d+)\s*(px|em|rem)$/i);
  637. if (m) {
  638. const n = parseFloat(m[1]);
  639. const unit = m[2];
  640. if (Number.isFinite(n) && n > 0) {
  641. icon.style.fontSize = (n / 5) + unit; // icon is 5em wide/tall
  642. return;
  643. }
  644. }
  645. }
  646. // Fallback: directly size the box (works great for SVG icons)
  647. icon.style.width = s;
  648. icon.style.height = s;
  649. } catch {}
  650. };
  651. const appendRingParts = () => {
  652. // Use the same "success-like" ring parts for every built-in icon
  653. icon.appendChild(el('div', `${PREFIX}success-circular-line-left`));
  654. icon.appendChild(el('div', `${PREFIX}success-ring`));
  655. icon.appendChild(el('div', `${PREFIX}success-fix`));
  656. icon.appendChild(el('div', `${PREFIX}success-circular-line-right`));
  657. };
  658. const createInnerMarkSvg = () => {
  659. const svg = svgEl('svg');
  660. svg.setAttribute('viewBox', '0 0 80 80');
  661. svg.setAttribute('aria-hidden', 'true');
  662. svg.setAttribute('focusable', 'false');
  663. return svg;
  664. };
  665. const addMarkPath = (svg, d, extraClass) => {
  666. const p = svgEl('path');
  667. p.setAttribute('d', d);
  668. p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
  669. p.setAttribute('stroke', 'currentColor');
  670. svg.appendChild(p);
  671. return p;
  672. };
  673. const addDot = (svg, cx, cy) => {
  674. const dot = svgEl('circle');
  675. dot.setAttribute('class', `${PREFIX}svg-dot`);
  676. dot.setAttribute('cx', String(cx));
  677. dot.setAttribute('cy', String(cy));
  678. dot.setAttribute('r', '3.2');
  679. dot.setAttribute('fill', 'currentColor');
  680. svg.appendChild(dot);
  681. return dot;
  682. };
  683. const appendBuiltInInnerMark = (svg) => {
  684. if (rawType === 'error') {
  685. addMarkPath(svg, 'M28 28 L52 52', `${PREFIX}svg-error-left`);
  686. addMarkPath(svg, 'M52 28 L28 52', `${PREFIX}svg-error-right`);
  687. } else if (rawType === 'warning') {
  688. addMarkPath(svg, 'M40 20 L40 46', `${PREFIX}svg-warning-line`);
  689. addDot(svg, 40, 58);
  690. } else if (rawType === 'info') {
  691. addMarkPath(svg, 'M40 34 L40 56', `${PREFIX}svg-info-line`);
  692. addDot(svg, 40, 25);
  693. } else if (rawType === 'question') {
  694. addMarkPath(svg, '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`);
  695. addDot(svg, 42, 61);
  696. }
  697. };
  698. if (isLottieIcon(rawType)) {
  699. // Lottie animation icon: svg:<name>
  700. const name = getLottieIconName(rawType);
  701. icon.className = `${PREFIX}icon ${PREFIX}icon-lottie`;
  702. icon.dataset.lottie = name;
  703. const container = el('div', `${PREFIX}lottie`);
  704. icon.appendChild(container);
  705. applyIconSize('box');
  706. scheduleLottiePrefetch();
  707. this._initLottieIcon(icon, name);
  708. return icon;
  709. }
  710. if (rawType === 'success') {
  711. // SweetAlert2-compatible DOM structure (circle + tick) for exact style parity
  712. // <div class="...success-circular-line-left"></div>
  713. // <div class="...success-mark"><span class="...success-line-tip"></span><span class="...success-line-long"></span></div>
  714. // <div class="...success-ring"></div>
  715. // <div class="...success-fix"></div>
  716. // <div class="...success-circular-line-right"></div>
  717. icon.appendChild(el('div', `${PREFIX}success-circular-line-left`));
  718. const mark = el('div', `${PREFIX}success-mark`);
  719. mark.appendChild(el('span', `${PREFIX}success-line-tip`));
  720. mark.appendChild(el('span', `${PREFIX}success-line-long`));
  721. icon.appendChild(mark);
  722. icon.appendChild(el('div', `${PREFIX}success-ring`));
  723. icon.appendChild(el('div', `${PREFIX}success-fix`));
  724. icon.appendChild(el('div', `${PREFIX}success-circular-line-right`));
  725. applyIconSize('font');
  726. return icon;
  727. }
  728. if (rawType === 'error' || rawType === 'warning' || rawType === 'info' || rawType === 'question') {
  729. // Use the same "success-like" ring parts for every icon
  730. appendRingParts();
  731. applyIconSize('font');
  732. // SVG only draws the inner symbol (no SVG ring)
  733. const svg = createInnerMarkSvg();
  734. appendBuiltInInnerMark(svg);
  735. icon.appendChild(svg);
  736. return icon;
  737. }
  738. // Default to SVG icons for other/custom types
  739. applyIconSize('box');
  740. const svg = createInnerMarkSvg();
  741. const ring = svgEl('circle');
  742. ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
  743. ring.setAttribute('cx', '40');
  744. ring.setAttribute('cy', '40');
  745. ring.setAttribute('r', '34');
  746. ring.setAttribute('stroke', 'currentColor');
  747. svg.appendChild(ring);
  748. // For custom types, we draw ring only by default (no inner mark).
  749. icon.appendChild(svg);
  750. return icon;
  751. }
  752. _adjustRingBackgroundColor() {
  753. try {
  754. const icon = this.dom && this.dom.icon;
  755. const popup = this.dom && this.dom.popup;
  756. if (!icon || !popup) return;
  757. const bg = getComputedStyle(popup).backgroundColor;
  758. const parts = icon.querySelectorAll(RING_BG_PARTS_SELECTOR);
  759. parts.forEach((el) => {
  760. try { el.style.backgroundColor = bg; } catch {}
  761. });
  762. } catch {}
  763. }
  764. _didOpen() {
  765. // Keyboard close (ESC)
  766. if (this.params.closeOnEsc) {
  767. this._onKeydown = (e) => {
  768. if (!e) return;
  769. if (e.key === 'Escape') this._close(null, 'esc');
  770. };
  771. document.addEventListener('keydown', this._onKeydown);
  772. }
  773. // Keep the "success-like" ring perfectly blended with popup bg
  774. this._adjustRingBackgroundColor();
  775. // Popup animation (optional)
  776. if (this.params.popupAnimation) {
  777. if (this.dom.popup) {
  778. // Use WAAPI directly to guarantee the slide+fade effect is visible,
  779. // independent from xjs.animate implementation details.
  780. try {
  781. const popup = this.dom.popup;
  782. try { popup.classList.add(`${PREFIX}popup-anim-slide`); } catch {}
  783. const rect = popup.getBoundingClientRect();
  784. // Default entrance: from above center by "more than half" of popup height.
  785. // Clamp to viewport so very tall popups don't start far off-screen.
  786. const vh = (typeof window !== 'undefined' && window && window.innerHeight) ? window.innerHeight : rect.height;
  787. const baseH = Math.min(rect.height, vh);
  788. const y0 = -Math.round(Math.max(18, baseH * 0.75));
  789. // Disable CSS transform transition so it won't fight with WAAPI.
  790. popup.style.transition = 'none';
  791. popup.style.willChange = 'transform, opacity';
  792. // If WAAPI is available, prefer it (most consistent).
  793. if (popup.animate) {
  794. const anim = popup.animate(
  795. [
  796. { transform: `translateY(${y0}px) scale(0.92)`, opacity: 0 },
  797. { transform: 'translateY(0px) scale(1)', opacity: 1 }
  798. ],
  799. {
  800. duration: 520,
  801. easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)',
  802. fill: 'forwards'
  803. }
  804. );
  805. anim.finished
  806. .catch(() => {})
  807. .finally(() => {
  808. try { popup.style.willChange = ''; } catch {}
  809. });
  810. } else {
  811. // Fallback: try xjs.animate if WAAPI isn't available.
  812. const X = Layer._getXjs();
  813. if (X) {
  814. popup.style.opacity = '0';
  815. popup.style.transform = `translateY(${y0}px) scale(0.92)`;
  816. X(popup).animate({
  817. y: [y0, 0],
  818. scale: [0.92, 1],
  819. opacity: [0, 1],
  820. duration: 520,
  821. easing: 'ease-out'
  822. });
  823. }
  824. setTimeout(() => {
  825. try { popup.style.willChange = ''; } catch {}
  826. }, 560);
  827. }
  828. } catch {}
  829. }
  830. }
  831. // Replace mode: if popupAnimation is off, do a soft fade+scale to avoid a hard cut.
  832. if (!this.params.popupAnimation && this._isReplace && this.dom.popup) {
  833. try {
  834. const popup = this.dom.popup;
  835. popup.style.transition = 'none';
  836. popup.style.opacity = '0';
  837. popup.style.transform = 'scale(0.98)';
  838. requestAnimationFrame(() => {
  839. popup.style.transition = 'opacity 0.22s ease, transform 0.22s ease';
  840. popup.style.opacity = '1';
  841. popup.style.transform = 'scale(1)';
  842. setTimeout(() => {
  843. try { popup.style.transition = ''; } catch {}
  844. try { popup.style.opacity = ''; } catch {}
  845. try { popup.style.transform = ''; } catch {}
  846. }, 260);
  847. });
  848. } catch {}
  849. }
  850. // Icon SVG draw animation
  851. if (this.params.iconAnimation) {
  852. this._animateIcon();
  853. }
  854. // User hook (SweetAlert-ish naming)
  855. try {
  856. if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
  857. } catch {}
  858. }
  859. _animateIcon() {
  860. const icon = this.dom.icon;
  861. if (!icon) return;
  862. const type = (this.params && this.params.icon) || '';
  863. if (isLottieIcon(type)) return;
  864. // Ring animation (same as success) for all built-in icons
  865. if (RING_TYPES.has(type)) this._adjustRingBackgroundColor();
  866. try { icon.classList.remove(`${PREFIX}icon-show`); } catch {}
  867. requestAnimationFrame(() => {
  868. try { icon.classList.add(`${PREFIX}icon-show`); } catch {}
  869. });
  870. // Success tick is CSS-driven; others still draw their SVG mark after the ring starts.
  871. if (type === 'success') return;
  872. const X = Layer._getXjs();
  873. if (!X) return;
  874. const svg = icon.querySelector('svg');
  875. if (!svg) return;
  876. const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
  877. const dot = svg.querySelector(`.${PREFIX}svg-dot`);
  878. // If this is a built-in icon, keep the SweetAlert-like order:
  879. // ring sweep first (~0.51s), then draw the inner mark.
  880. const baseDelay = RING_TYPES.has(type) ? 520 : 0;
  881. if (type === 'error') {
  882. // Draw order: left-top -> right-bottom, then right-top -> left-bottom
  883. // NOTE: A tiny delay (like 70ms) looks simultaneous; make it strictly sequential.
  884. const a = svg.querySelector(`.${PREFIX}svg-error-left`) || marks[0]; // M28 28 L52 52
  885. const b = svg.querySelector(`.${PREFIX}svg-error-right`) || marks[1]; // M52 28 L28 52
  886. const dur = 320;
  887. const gap = 60;
  888. try { if (a) X(a).draw({ duration: dur, easing: 'ease-out', delay: baseDelay }); } catch {}
  889. try { if (b) X(b).draw({ duration: dur, easing: 'ease-out', delay: baseDelay + dur + gap }); } catch {}
  890. } else {
  891. // warning / info / question (single stroke) or custom SVG symbols
  892. marks.forEach((m, i) => {
  893. try { X(m).draw({ duration: 420, easing: 'ease-out', delay: baseDelay + i * 60 }); } catch {}
  894. });
  895. }
  896. if (dot) {
  897. try {
  898. dot.style.opacity = '0';
  899. // Keep dot pop after the ring begins
  900. const d = baseDelay + 140;
  901. if (type === 'info') {
  902. X(dot).animate({ opacity: [0, 1], y: [-8, 0], scale: [0.2, 1], duration: 420, delay: d, easing: { stiffness: 300, damping: 14 } });
  903. } else {
  904. X(dot).animate({ opacity: [0, 1], scale: [0.2, 1], duration: 320, delay: d, easing: { stiffness: 320, damping: 18 } });
  905. }
  906. } catch {}
  907. }
  908. }
  909. _forceDestroy(reason = 'replace') {
  910. try { this._destroyLottieIcon(); } catch {}
  911. // Restore mounted DOM (if any) and cleanup listeners; also resolve the promise so it doesn't hang.
  912. try {
  913. if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
  914. else this._unmountDomContent();
  915. } catch {}
  916. if (this._onKeydown) {
  917. try { document.removeEventListener('keydown', this._onKeydown); } catch {}
  918. this._onKeydown = null;
  919. }
  920. try {
  921. if (this.resolve) {
  922. this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason });
  923. }
  924. } catch {}
  925. }
  926. _close(isConfirmed, reason) {
  927. if (this._isClosing) return;
  928. this._isClosing = true;
  929. try { this._destroyLottieIcon(); } catch {}
  930. const shouldDelayUnmount = !!(
  931. (this._mounted && this._mounted.kind === 'move') ||
  932. (this._flowSteps && this._flowSteps.length && this._flowMountedList && this._flowMountedList.length)
  933. ) || !this.params.popupAnimation;
  934. const doUnmount = () => {
  935. try {
  936. if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
  937. else this._unmountDomContent();
  938. } catch {}
  939. };
  940. if (!shouldDelayUnmount) {
  941. // Restore mounted DOM (moved into popup) before removing overlay
  942. doUnmount();
  943. }
  944. let customClose = false;
  945. // Soft close for non-popupAnimation cases (avoid abrupt cut)
  946. if (!this.params.popupAnimation) {
  947. try {
  948. const popup = this.dom.popup;
  949. if (popup) {
  950. popup.style.transition = 'opacity 0.22s ease';
  951. popup.style.opacity = '0';
  952. popup.style.transform = '';
  953. }
  954. if (this.dom.overlay) {
  955. const overlay = this.dom.overlay;
  956. // Use inline opacity to avoid class-based jumps
  957. overlay.style.transition = 'opacity 0.22s ease';
  958. overlay.style.opacity = '1';
  959. requestAnimationFrame(() => {
  960. try {
  961. if (overlay._layerInstance !== this) return;
  962. overlay.style.opacity = '0';
  963. } catch {}
  964. });
  965. }
  966. customClose = true;
  967. } catch {}
  968. }
  969. if (!customClose) {
  970. this.dom.overlay.classList.remove('show');
  971. }
  972. try {
  973. if (this.dom.overlay) {
  974. const overlay = this.dom.overlay;
  975. const delay = customClose ? 240 : 300;
  976. overlay._layerCloseTimer = setTimeout(() => {
  977. if (shouldDelayUnmount) {
  978. doUnmount();
  979. }
  980. if (overlay._layerInstance !== this) return;
  981. if (overlay.parentNode) {
  982. overlay.parentNode.removeChild(overlay);
  983. }
  984. }, delay);
  985. }
  986. } catch {}
  987. if (this._onKeydown) {
  988. try { document.removeEventListener('keydown', this._onKeydown); } catch {}
  989. this._onKeydown = null;
  990. }
  991. try {
  992. if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
  993. } catch {}
  994. try {
  995. if (typeof this.params.didClose === 'function') this.params.didClose();
  996. } catch {}
  997. const value = (this._flowSteps && this._flowSteps.length) ? (this._flowValues || []) : undefined;
  998. let data;
  999. if (this._flowSteps && this._flowSteps.length && this._flowAutoForm && this._flowAutoForm.enabled) {
  1000. const root = (this.dom && (this.dom.stepStack || this.dom.content || this.dom.popup)) || null;
  1001. data = this._collectFormData(root);
  1002. }
  1003. if (isConfirmed === true) {
  1004. const payload = { isConfirmed: true, isDenied: false, isDismissed: false, value };
  1005. if (data !== undefined) payload.data = data;
  1006. this.resolve(payload);
  1007. } else if (isConfirmed === false) {
  1008. const payload = { isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason || 'cancel', value };
  1009. if (data !== undefined) payload.data = data;
  1010. this.resolve(payload);
  1011. } else {
  1012. const payload = { isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason || 'backdrop', value };
  1013. if (data !== undefined) payload.data = data;
  1014. this.resolve(payload);
  1015. }
  1016. }
  1017. _assignFormValue(data, name, value, forceArray) {
  1018. if (!data || !name) return;
  1019. let key = name;
  1020. let asArray = !!forceArray;
  1021. if (key.endsWith('[]')) {
  1022. key = key.slice(0, -2);
  1023. asArray = true;
  1024. }
  1025. if (!(key in data)) {
  1026. data[key] = asArray ? [value] : value;
  1027. return;
  1028. }
  1029. if (Array.isArray(data[key])) {
  1030. data[key].push(value);
  1031. return;
  1032. }
  1033. data[key] = [data[key], value];
  1034. }
  1035. _collectFormData(root) {
  1036. const data = {};
  1037. if (!root || !root.querySelectorAll) return data;
  1038. const fields = root.querySelectorAll('input, select, textarea');
  1039. fields.forEach((el) => {
  1040. try {
  1041. if (!el || el.disabled) return;
  1042. const name = (el.getAttribute('name') || '').trim();
  1043. if (!name) return;
  1044. const tag = (el.tagName || '').toLowerCase();
  1045. if (tag === 'select') {
  1046. if (el.multiple) {
  1047. const values = Array.from(el.options || []).filter((o) => o.selected).map((o) => o.value);
  1048. values.forEach((v) => this._assignFormValue(data, name, v, true));
  1049. } else {
  1050. this._assignFormValue(data, name, el.value, false);
  1051. }
  1052. return;
  1053. }
  1054. if (tag === 'textarea') {
  1055. this._assignFormValue(data, name, el.value, false);
  1056. return;
  1057. }
  1058. const type = (el.getAttribute('type') || 'text').toLowerCase();
  1059. if (type === 'radio') {
  1060. if (el.checked) this._assignFormValue(data, name, el.value, false);
  1061. return;
  1062. }
  1063. if (type === 'checkbox') {
  1064. if (el.checked) this._assignFormValue(data, name, el.value, true);
  1065. return;
  1066. }
  1067. if (type === 'file') {
  1068. const files = el.files ? Array.from(el.files) : [];
  1069. this._assignFormValue(data, name, files, true);
  1070. return;
  1071. }
  1072. this._assignFormValue(data, name, el.value, false);
  1073. } catch {}
  1074. });
  1075. return data;
  1076. }
  1077. _normalizeDomSpec(params) {
  1078. // Preferred: params.dom (selector/Element/template)
  1079. // Backward: params.content (selector/Element) OR advanced object { dom, mode, clone }
  1080. const p = params || {};
  1081. let dom = p.dom;
  1082. let mode = p.domMode || 'move';
  1083. if (dom == null && p.content != null) {
  1084. if (typeof p.content === 'object' && !(p.content instanceof Element)) {
  1085. if (p.content && (p.content.dom != null || p.content.selector != null)) {
  1086. dom = (p.content.dom != null) ? p.content.dom : p.content.selector;
  1087. if (p.content.mode) mode = p.content.mode;
  1088. if (p.content.clone === true) mode = 'clone';
  1089. }
  1090. } else {
  1091. dom = p.content;
  1092. }
  1093. }
  1094. if (dom == null) return null;
  1095. return { dom, mode };
  1096. }
  1097. _mountDomContentInto(target, domSpec, opts = null) {
  1098. // Like _mountDomContent, but mounts into the provided container.
  1099. const collectFlow = !!(opts && opts.collectFlow);
  1100. const originalContent = this.dom.content;
  1101. // Temporarily redirect this.dom.content for reuse of internal logic.
  1102. try { this.dom.content = target; } catch {}
  1103. try {
  1104. const before = this._mounted;
  1105. this._mountDomContent(domSpec);
  1106. const rec = this._mounted;
  1107. // If we mounted in move-mode, _mounted holds record; detach it from single-mode tracking.
  1108. if (collectFlow && rec && rec.kind === 'move') {
  1109. this._flowMountedList.push(rec);
  1110. this._mounted = before; // restore previous single record (usually null)
  1111. }
  1112. } finally {
  1113. try { this.dom.content = originalContent; } catch {}
  1114. }
  1115. }
  1116. _unmountFlowMounted() {
  1117. // Restore all moved DOM nodes for flow steps
  1118. const list = Array.isArray(this._flowMountedList) ? this._flowMountedList : [];
  1119. this._flowMountedList = [];
  1120. list.forEach((m) => {
  1121. try {
  1122. if (!m || m.kind !== 'move') return;
  1123. // Reuse single unmount logic by swapping _mounted
  1124. const prev = this._mounted;
  1125. this._mounted = m;
  1126. this._unmountDomContent();
  1127. this._mounted = prev;
  1128. } catch {}
  1129. });
  1130. }
  1131. _mountDomContent(domSpec) {
  1132. try {
  1133. if (!this.dom.content) return;
  1134. this._unmountDomContent(); // ensure only one mount at a time
  1135. const forceVisible = (el) => {
  1136. if (!el) return { prevHidden: false, prevInlineDisplay: '' };
  1137. const prevHidden = !!el.hidden;
  1138. const prevInlineDisplay = (el.style && typeof el.style.display === 'string') ? el.style.display : '';
  1139. try { el.hidden = false; } catch {}
  1140. try {
  1141. const cs = (typeof getComputedStyle === 'function') ? getComputedStyle(el) : null;
  1142. const display = cs ? String(cs.display || '') : '';
  1143. // If element is hidden via CSS (e.g. .hidden-dom{display:none}),
  1144. // add an inline override so it becomes visible inside the popup.
  1145. if (display === 'none') el.style.display = 'block';
  1146. else if (el.style && el.style.display === 'none') el.style.display = 'block';
  1147. } catch {}
  1148. return { prevHidden, prevInlineDisplay };
  1149. };
  1150. let node = domSpec.dom;
  1151. if (typeof node === 'string') node = document.querySelector(node);
  1152. if (!node) return;
  1153. // <template> support: always clone template content
  1154. if (typeof HTMLTemplateElement !== 'undefined' && node instanceof HTMLTemplateElement) {
  1155. const frag = node.content.cloneNode(true);
  1156. this.dom.content.appendChild(frag);
  1157. this._mounted = { kind: 'template' };
  1158. return;
  1159. }
  1160. if (!(node instanceof Element)) return;
  1161. if (domSpec.mode === 'clone') {
  1162. const clone = node.cloneNode(true);
  1163. // If original is hidden (via attr or CSS), the clone may inherit; force show it in popup.
  1164. try { clone.hidden = false; } catch {}
  1165. try {
  1166. const cs = (typeof getComputedStyle === 'function') ? getComputedStyle(node) : null;
  1167. const display = cs ? String(cs.display || '') : '';
  1168. if (display === 'none') clone.style.display = 'block';
  1169. } catch {}
  1170. this.dom.content.appendChild(clone);
  1171. this._mounted = { kind: 'clone' };
  1172. return;
  1173. }
  1174. // Default: move into popup but restore on close
  1175. const placeholder = document.createComment('layer-dom-placeholder');
  1176. const parent = node.parentNode;
  1177. const nextSibling = node.nextSibling;
  1178. if (!parent) {
  1179. // Detached node: moving would lose it when overlay is removed; clone instead.
  1180. const clone = node.cloneNode(true);
  1181. try { clone.hidden = false; } catch {}
  1182. try { if (clone.style && clone.style.display === 'none') clone.style.display = ''; } catch {}
  1183. this.dom.content.appendChild(clone);
  1184. this._mounted = { kind: 'clone' };
  1185. return;
  1186. }
  1187. try { parent.insertBefore(placeholder, nextSibling); } catch {}
  1188. const { prevHidden, prevInlineDisplay } = forceVisible(node);
  1189. this.dom.content.appendChild(node);
  1190. this._mounted = { kind: 'move', originalEl: node, placeholder, parent, nextSibling, prevHidden, prevInlineDisplay };
  1191. } catch {
  1192. // ignore
  1193. }
  1194. }
  1195. _unmountDomContent() {
  1196. const m = this._mounted;
  1197. if (!m) return;
  1198. this._mounted = null;
  1199. if (m.kind !== 'move') return;
  1200. const node = m.originalEl;
  1201. if (!node) return;
  1202. // Restore hidden/display
  1203. try { node.hidden = !!m.prevHidden; } catch {}
  1204. try {
  1205. if (node.style && typeof m.prevInlineDisplay === 'string') node.style.display = m.prevInlineDisplay;
  1206. } catch {}
  1207. // Move back to original position
  1208. try {
  1209. const ph = m.placeholder;
  1210. if (ph && ph.parentNode) {
  1211. ph.parentNode.insertBefore(node, ph);
  1212. ph.parentNode.removeChild(ph);
  1213. return;
  1214. }
  1215. } catch {}
  1216. // Fallback: append to original parent
  1217. try {
  1218. if (m.parent) m.parent.appendChild(node);
  1219. } catch {}
  1220. }
  1221. _setButtonsDisabled(disabled) {
  1222. try {
  1223. if (this.dom && this.dom.confirmBtn) this.dom.confirmBtn.disabled = !!disabled;
  1224. if (this.dom && this.dom.cancelBtn) this.dom.cancelBtn.disabled = !!disabled;
  1225. } catch {}
  1226. }
  1227. _normalizeButtonIcon(icon) {
  1228. if (!icon) return null;
  1229. try {
  1230. if (icon && icon.nodeType) return icon.cloneNode(true);
  1231. } catch {}
  1232. if (typeof icon === 'string') {
  1233. const s = icon.trim();
  1234. if (!s) return null;
  1235. if (s.indexOf('<') !== -1 && s.indexOf('>') !== -1) {
  1236. const wrap = document.createElement('span');
  1237. wrap.innerHTML = s;
  1238. return wrap;
  1239. }
  1240. return document.createTextNode(s);
  1241. }
  1242. return null;
  1243. }
  1244. _setButtonContent(btn, text, icon, iconPosition) {
  1245. if (!btn) return;
  1246. const labelText = (text === undefined || text === null) ? '' : String(text);
  1247. if (!icon) {
  1248. try { btn.textContent = labelText; } catch {}
  1249. return;
  1250. }
  1251. while (btn.firstChild) btn.removeChild(btn.firstChild);
  1252. const iconNode = this._normalizeButtonIcon(icon);
  1253. const iconWrap = el('span', `${PREFIX}btn-icon`);
  1254. if (iconNode) iconWrap.appendChild(iconNode);
  1255. const label = el('span', `${PREFIX}btn-label`, labelText);
  1256. const atEnd = iconPosition === 'end';
  1257. if (atEnd) {
  1258. btn.appendChild(label);
  1259. btn.appendChild(iconWrap);
  1260. } else {
  1261. btn.appendChild(iconWrap);
  1262. btn.appendChild(label);
  1263. }
  1264. }
  1265. async _handleConfirm() {
  1266. // Flow next / finalize
  1267. if (this._flowSteps && this._flowSteps.length) {
  1268. return this._flowNext();
  1269. }
  1270. // Single popup: support async preConfirm
  1271. const pre = this.params && this.params.preConfirm;
  1272. if (typeof pre === 'function') {
  1273. try {
  1274. this._setButtonsDisabled(true);
  1275. const r = pre(this.dom && this.dom.popup);
  1276. const v = (r && typeof r.then === 'function') ? await r : r;
  1277. if (v === false) {
  1278. this._setButtonsDisabled(false);
  1279. return;
  1280. }
  1281. // store value in non-flow mode as single value
  1282. this._flowValues = [v];
  1283. } catch (e) {
  1284. console.error(e);
  1285. this._setButtonsDisabled(false);
  1286. return;
  1287. }
  1288. }
  1289. this._close(true, 'confirm');
  1290. }
  1291. _handleCancel() {
  1292. if (this._flowSteps && this._flowSteps.length) {
  1293. // default: if not first step, cancel acts as "back"
  1294. if (this._flowIndex > 0) {
  1295. this._flowPrev();
  1296. return;
  1297. }
  1298. }
  1299. this._close(false, 'cancel');
  1300. }
  1301. _getFlowStepOptions(index) {
  1302. const step = (this._flowSteps && this._flowSteps[index]) ? this._flowSteps[index] : {};
  1303. const base = this._flowBase || {};
  1304. const merged = { ...base, ...step };
  1305. // Default button texts for flow
  1306. const isLast = index >= (this._flowSteps.length - 1);
  1307. if (!('confirmButtonText' in step)) merged.confirmButtonText = isLast ? (base.confirmButtonText || 'OK') : 'Next';
  1308. // Show cancel as Back after first step (unless step explicitly overrides)
  1309. if (index > 0) {
  1310. if (!('showCancelButton' in step)) merged.showCancelButton = true;
  1311. if (!('cancelButtonText' in step)) merged.cancelButtonText = 'Back';
  1312. } else {
  1313. // First step default keeps base settings
  1314. if (!('cancelButtonText' in step) && merged.showCancelButton) merged.cancelButtonText = merged.cancelButtonText || 'Cancel';
  1315. }
  1316. // Icon/animation policy for flow:
  1317. // - During steps: no icon/animations by default (avoids distraction + layout jitter)
  1318. // - Allow icon only if step explicitly uses `icon:'question'`, or step is marked as summary.
  1319. const isSummary = !!(step && (step.summary === true || step.isSummary === true));
  1320. const explicitIcon = ('icon' in step) ? step.icon : undefined;
  1321. const baseIcon = ('icon' in base) ? base.icon : undefined;
  1322. const chosenIcon = (explicitIcon !== undefined) ? explicitIcon : baseIcon;
  1323. if (!isSummary && !isLast) {
  1324. merged.icon = (chosenIcon === 'question') ? 'question' : null;
  1325. merged.iconAnimation = false;
  1326. } else if (!isSummary && isLast) {
  1327. // last step: still suppress unless summary or question
  1328. merged.icon = (chosenIcon === 'question') ? 'question' : null;
  1329. merged.iconAnimation = false;
  1330. } else {
  1331. // summary step: allow icon; animation follows explicit config (default true only if provided elsewhere)
  1332. if (!('icon' in step) && chosenIcon === undefined) merged.icon = null;
  1333. }
  1334. return merged;
  1335. }
  1336. async _flowNext() {
  1337. const idx = this._flowIndex;
  1338. const total = this._flowSteps.length;
  1339. const isLast = idx >= (total - 1);
  1340. // preConfirm hook for current step
  1341. const pre = this.params && this.params.preConfirm;
  1342. if (typeof pre === 'function') {
  1343. try {
  1344. this._setButtonsDisabled(true);
  1345. const r = pre(this.dom && this.dom.popup, idx);
  1346. const v = (r && typeof r.then === 'function') ? await r : r;
  1347. if (v === false) {
  1348. this._setButtonsDisabled(false);
  1349. return;
  1350. }
  1351. this._flowValues[idx] = v;
  1352. } catch (e) {
  1353. console.error(e);
  1354. this._setButtonsDisabled(false);
  1355. return;
  1356. }
  1357. }
  1358. if (isLast) {
  1359. this._close(true, 'confirm');
  1360. return;
  1361. }
  1362. this._setButtonsDisabled(false);
  1363. await this._flowGo(idx + 1, 'next');
  1364. }
  1365. async _flowPrev() {
  1366. const idx = this._flowIndex;
  1367. if (idx <= 0) return;
  1368. await this._flowGo(idx - 1, 'prev');
  1369. }
  1370. async _flowGo(index, direction) {
  1371. const next = (this._flowResolved && this._flowResolved[index]) ? this._flowResolved[index] : this._getFlowStepOptions(index);
  1372. await this._transitionToFlow(index, next, direction);
  1373. }
  1374. async _transitionToFlow(nextIndex, nextOptions, direction) {
  1375. const popup = this.dom && this.dom.popup;
  1376. const content = this.dom && this.dom.content;
  1377. const panes = this.dom && this.dom.stepPanes;
  1378. if (!popup || !content || !panes || !panes.length) {
  1379. this._flowIndex = nextIndex;
  1380. this.params = nextOptions;
  1381. this._render({ flow: true });
  1382. return;
  1383. }
  1384. const fromIndex = this._flowIndex;
  1385. const fromPane = panes[fromIndex];
  1386. const toPane = panes[nextIndex];
  1387. if (!fromPane || !toPane) {
  1388. this._flowIndex = nextIndex;
  1389. this.params = nextOptions;
  1390. this._render({ flow: true });
  1391. return;
  1392. }
  1393. // Measure current content height
  1394. const oldH = content.getBoundingClientRect().height;
  1395. // Prepare target pane for measurement without affecting layout
  1396. const prevDisplay = toPane.style.display;
  1397. const prevPos = toPane.style.position;
  1398. const prevVis = toPane.style.visibility;
  1399. const prevPointer = toPane.style.pointerEvents;
  1400. toPane.style.display = '';
  1401. toPane.style.position = 'absolute';
  1402. toPane.style.visibility = 'hidden';
  1403. toPane.style.pointerEvents = 'none';
  1404. toPane.style.left = '0';
  1405. toPane.style.right = '0';
  1406. const newH = toPane.getBoundingClientRect().height;
  1407. // Restore pane styles (keep hidden until animation starts)
  1408. toPane.style.position = prevPos;
  1409. toPane.style.visibility = prevVis;
  1410. toPane.style.pointerEvents = prevPointer;
  1411. toPane.style.display = prevDisplay; // usually 'none'
  1412. // Apply new options (title/buttons/icon policy) before showing the pane
  1413. this._flowIndex = nextIndex;
  1414. this.params = nextOptions;
  1415. // Update icon/title/buttons without recreating DOM
  1416. try {
  1417. if (this.dom.title) this.dom.title.textContent = this.params.title || '';
  1418. } catch {}
  1419. // Icon updates (only if needed)
  1420. try {
  1421. const wantsIcon = !!this.params.icon;
  1422. if (!wantsIcon && this.dom.icon) {
  1423. this._destroyLottieIcon();
  1424. this.dom.icon.remove();
  1425. this.dom.icon = null;
  1426. } else if (wantsIcon) {
  1427. const same = this._isSameIconType(this.dom.icon, this.params.icon);
  1428. if (!this.dom.icon || !same) {
  1429. if (this.dom.icon) {
  1430. this._destroyLottieIcon();
  1431. this.dom.icon.remove();
  1432. }
  1433. this.dom.icon = this._createIcon(this.params.icon);
  1434. // icon should be on top (before title)
  1435. popup.insertBefore(this.dom.icon, this.dom.title || popup.firstChild);
  1436. }
  1437. }
  1438. } catch {}
  1439. this._updateFlowActions();
  1440. // Switch panes with directional slide (next: left, prev: right)
  1441. const isNext = direction !== 'prev';
  1442. const enterFrom = isNext ? 100 : -100;
  1443. const exitTo = isNext ? -100 : 100;
  1444. // Prepare panes for animation
  1445. toPane.style.display = '';
  1446. toPane.style.position = 'absolute';
  1447. toPane.style.left = '0';
  1448. toPane.style.right = '0';
  1449. toPane.style.top = '0';
  1450. toPane.style.transform = `translateX(${enterFrom}%)`;
  1451. toPane.style.opacity = '0';
  1452. toPane.style.pointerEvents = 'none';
  1453. fromPane.style.position = 'absolute';
  1454. fromPane.style.left = '0';
  1455. fromPane.style.right = '0';
  1456. fromPane.style.top = '0';
  1457. fromPane.style.transform = 'translateX(0%)';
  1458. fromPane.style.opacity = '1';
  1459. fromPane.style.pointerEvents = 'none';
  1460. // Lock content height during transition
  1461. content.style.height = oldH + 'px';
  1462. content.style.overflow = 'hidden';
  1463. const slideDuration = 320;
  1464. let heightAnim = null;
  1465. let fromAnim = null;
  1466. let toAnim = null;
  1467. try {
  1468. heightAnim = content.animate(
  1469. [{ height: oldH + 'px' }, { height: newH + 'px' }],
  1470. { duration: 220, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
  1471. );
  1472. } catch {}
  1473. try {
  1474. if (fromPane.animate && toPane.animate) {
  1475. fromAnim = fromPane.animate(
  1476. [
  1477. { transform: 'translateX(0%)', opacity: 1 },
  1478. { transform: `translateX(${exitTo}%)`, opacity: 0.1 }
  1479. ],
  1480. { duration: slideDuration, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
  1481. );
  1482. toAnim = toPane.animate(
  1483. [
  1484. { transform: `translateX(${enterFrom}%)`, opacity: 0.2 },
  1485. { transform: 'translateX(0%)', opacity: 1 }
  1486. ],
  1487. { duration: slideDuration, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
  1488. );
  1489. }
  1490. } catch {}
  1491. try {
  1492. await Promise.all([
  1493. heightAnim && heightAnim.finished ? heightAnim.finished.catch(() => {}) : Promise.resolve(),
  1494. fromAnim && fromAnim.finished ? fromAnim.finished.catch(() => {}) : Promise.resolve(),
  1495. toAnim && toAnim.finished ? toAnim.finished.catch(() => {}) : Promise.resolve()
  1496. ]);
  1497. } catch {}
  1498. // Cleanup styles and hide old pane
  1499. fromPane.style.display = 'none';
  1500. fromPane.style.position = '';
  1501. fromPane.style.left = '';
  1502. fromPane.style.right = '';
  1503. fromPane.style.top = '';
  1504. fromPane.style.transform = '';
  1505. fromPane.style.opacity = '';
  1506. fromPane.style.pointerEvents = '';
  1507. toPane.style.position = 'relative';
  1508. toPane.style.left = '';
  1509. toPane.style.right = '';
  1510. toPane.style.top = '';
  1511. toPane.style.transform = '';
  1512. toPane.style.opacity = '';
  1513. toPane.style.pointerEvents = '';
  1514. content.style.height = '';
  1515. content.style.overflow = '';
  1516. // Re-adjust ring background if icon exists (rare in flow)
  1517. try { this._adjustRingBackgroundColor(); } catch {}
  1518. }
  1519. _isSameIconType(iconEl, type) {
  1520. if (!iconEl) return false;
  1521. const raw = String(type || '').trim();
  1522. if (isLottieIcon(raw)) {
  1523. const name = getLottieIconName(raw);
  1524. return iconEl.dataset && iconEl.dataset.lottie === name;
  1525. }
  1526. try {
  1527. return iconEl.classList && iconEl.classList.contains(raw);
  1528. } catch {
  1529. return false;
  1530. }
  1531. }
  1532. _destroyLottieIcon() {
  1533. const icon = this.dom && this.dom.icon;
  1534. if (!icon) return;
  1535. const inst = icon._lottieInstance;
  1536. if (inst && typeof inst.destroy === 'function') {
  1537. try { inst.destroy(); } catch {}
  1538. }
  1539. try { icon._lottieInstance = null; } catch {}
  1540. }
  1541. _initLottieIcon(iconEl, name) {
  1542. if (!iconEl) return;
  1543. const container = iconEl.querySelector(`.${PREFIX}lottie`);
  1544. if (!container) return;
  1545. const cleanName = String(name || '').trim();
  1546. if (!cleanName) return;
  1547. const url = resolveLottieJsonUrl(cleanName);
  1548. if (!url) return;
  1549. const wantsLoop = (this.params && typeof this.params.iconLoop === 'boolean')
  1550. ? this.params.iconLoop
  1551. : true;
  1552. const wantsAutoplay = !(this.params && this.params.iconAnimation === false);
  1553. const showError = (msg) => {
  1554. try {
  1555. container.textContent = String(msg || '');
  1556. container.classList.add(`${PREFIX}lottie-error`);
  1557. } catch {}
  1558. };
  1559. const loadData = () => new Promise((resolve, reject) => {
  1560. try {
  1561. if (typeof fetch === 'function') {
  1562. fetch(url).then((res) => {
  1563. if (!res || !res.ok) throw new Error('fetch failed');
  1564. return res.json();
  1565. }).then(resolve).catch(reject);
  1566. return;
  1567. }
  1568. const xhr = new XMLHttpRequest();
  1569. xhr.open('GET', url, true);
  1570. xhr.responseType = 'json';
  1571. xhr.onload = () => {
  1572. if (xhr.status >= 200 && xhr.status < 300) {
  1573. const data = xhr.response || (xhr.responseText ? JSON.parse(xhr.responseText) : null);
  1574. resolve(data);
  1575. } else {
  1576. reject(new Error('xhr failed'));
  1577. }
  1578. };
  1579. xhr.onerror = () => reject(new Error('xhr error'));
  1580. xhr.send();
  1581. } catch (e) {
  1582. reject(e);
  1583. }
  1584. });
  1585. (async () => {
  1586. const lottie = await ensureLottieLib();
  1587. if (!lottie) {
  1588. showError('Lottie lib not loaded.');
  1589. return;
  1590. }
  1591. if (!iconEl.isConnected) return;
  1592. let data = null;
  1593. try { data = await loadData(); } catch {}
  1594. if (!data) {
  1595. showError('Failed to load animation.');
  1596. return;
  1597. }
  1598. if (!iconEl.isConnected) return;
  1599. try {
  1600. const anim = lottie.loadAnimation({
  1601. container,
  1602. renderer: 'svg',
  1603. loop: wantsLoop,
  1604. autoplay: wantsAutoplay,
  1605. animationData: data
  1606. });
  1607. iconEl._lottieInstance = anim;
  1608. if (!wantsAutoplay && anim && typeof anim.goToAndStop === 'function') {
  1609. anim.goToAndStop(0, true);
  1610. }
  1611. } catch {
  1612. showError('Failed to init animation.');
  1613. }
  1614. })();
  1615. }
  1616. _rerenderInside() {
  1617. this._destroyLottieIcon();
  1618. // Update popup content without recreating overlay (used in flow transitions)
  1619. const popup = this.dom && this.dom.popup;
  1620. if (!popup) return;
  1621. // Clear popup (but keep reference)
  1622. while (popup.firstChild) popup.removeChild(popup.firstChild);
  1623. // Icon
  1624. this.dom.icon = null;
  1625. if (this.params.icon) {
  1626. this.dom.icon = this._createIcon(this.params.icon);
  1627. popup.appendChild(this.dom.icon);
  1628. }
  1629. // Title
  1630. this.dom.title = null;
  1631. if (this.params.title) {
  1632. this.dom.title = el('h2', `${PREFIX}title`, this.params.title);
  1633. popup.appendChild(this.dom.title);
  1634. }
  1635. // Content
  1636. this.dom.content = null;
  1637. if (this.params.text || this.params.html || this.params.content || this.params.dom) {
  1638. this.dom.content = el('div', `${PREFIX}content`);
  1639. const domSpec = this._normalizeDomSpec(this.params);
  1640. if (domSpec) this._mountDomContent(domSpec);
  1641. else if (this.params.html) this.dom.content.innerHTML = this.params.html;
  1642. else this.dom.content.textContent = this.params.text;
  1643. popup.appendChild(this.dom.content);
  1644. }
  1645. // Actions / buttons
  1646. this.dom.actions = el('div', `${PREFIX}actions`);
  1647. this.dom.cancelBtn = null;
  1648. if (this.params.showCancelButton) {
  1649. this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
  1650. this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText);
  1651. this.dom.cancelBtn.onclick = () => this._handleCancel();
  1652. this.dom.actions.appendChild(this.dom.cancelBtn);
  1653. }
  1654. this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
  1655. this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText);
  1656. this.dom.confirmBtn.onclick = () => this._handleConfirm();
  1657. this.dom.actions.appendChild(this.dom.confirmBtn);
  1658. popup.appendChild(this.dom.actions);
  1659. // Re-run open hooks for each step
  1660. try {
  1661. if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
  1662. } catch {}
  1663. }
  1664. }
  1665. return Layer;
  1666. })));