| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415 |
- (function (global, factory) {
- const LayerClass = factory();
- // Allow usage as `Layer({...})` or `new Layer()`
- // But Layer is a class. We can wrap it in a proxy or factory function.
-
- function LayerFactory(options) {
- if (options && typeof options === 'object') {
- return LayerClass.$(options);
- }
- return new LayerClass();
- }
-
- // Copy static methods (including non-enumerable class statics like `fire` / `$`)
- // Class static methods are non-enumerable by default, so Object.assign() would miss them.
- const copyStatic = (to, from) => {
- try {
- Object.getOwnPropertyNames(from).forEach((k) => {
- if (k === 'prototype' || k === 'name' || k === 'length') return;
- const desc = Object.getOwnPropertyDescriptor(from, k);
- if (!desc) return;
- Object.defineProperty(to, k, desc);
- });
- } catch (e) {
- // Best-effort fallback
- try { Object.assign(to, from); } catch {}
- }
- };
- copyStatic(LayerFactory, LayerClass);
- // Also copy prototype for instanceof checks if needed (though tricky with factory)
- LayerFactory.prototype = LayerClass.prototype;
-
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = LayerFactory :
- typeof define === 'function' && define.amd ? define(() => LayerFactory) :
- (global.Layer = LayerFactory);
- }(this, (function () {
- 'use strict';
- const PREFIX = 'layer-';
- const RING_TYPES = new Set(['success', 'error', 'warning', 'info', 'question']);
- const RING_BG_PARTS_SELECTOR = `.${PREFIX}success-circular-line-left, .${PREFIX}success-circular-line-right, .${PREFIX}success-fix`;
- const SVGANI_PREFIX = 'svg:';
- const BG_SVG_PREFIX = 'svg(';
- const BG_CSS_PREFIX = 'css(';
- const THEME_VALUES = new Set(['auto', 'dark', 'light']);
- const normalizeTheme = (value) => {
- if (typeof value !== 'string') return 'auto';
- const v = value.trim().toLowerCase();
- return THEME_VALUES.has(v) ? v : 'auto';
- };
- const normalizeOptions = (options, text, icon) => {
- if (typeof options !== 'string') return options || {};
- const o = { title: options };
- if (text) o.text = text;
- if (icon) o.icon = icon;
- return o;
- };
-
- const el = (tag, className, text) => {
- const node = document.createElement(tag);
- if (className) node.className = className;
- if (text !== undefined) node.textContent = text;
- return node;
- };
-
- const svgEl = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag);
-
- // Ensure library CSS is loaded (xjs.css)
- // - CSS is centralized in xjs.css (no runtime <style> injection).
- // - We still auto-load it for convenience/compat, since consumers may forget the <link>.
- let _xjsCssReady = null; // Promise<void>
- const waitForStylesheet = (link) => new Promise((resolve) => {
- if (!link) return resolve();
- if (link.sheet) return resolve();
- let done = false;
- const finish = () => {
- if (done) return;
- done = true;
- resolve();
- };
- try { link.addEventListener('load', finish, { once: true }); } catch {}
- try { link.addEventListener('error', finish, { once: true }); } catch {}
- // Fallback timeout: avoid blocking forever if the load event is missed.
- setTimeout(finish, 200);
- });
- const ensureXjsCss = () => {
- try {
- if (_xjsCssReady) return _xjsCssReady;
- if (typeof document === 'undefined') return Promise.resolve();
- if (!document.head) return Promise.resolve();
- const existingLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
- .find((l) => /(^|\/)xjs\.css(\?|#|$)/.test((l.getAttribute('href') || '').trim()));
- if (existingLink) return (_xjsCssReady = waitForStylesheet(existingLink));
- const id = 'xjs-css';
- const existing = document.getElementById(id);
- if (existing) return (_xjsCssReady = waitForStylesheet(existing));
- const scripts = Array.from(document.getElementsByTagName('script'));
- const scriptSrc = scripts
- .map((s) => s && s.src)
- .find((src) => /(^|\/)(xjs|layer)\.js(\?|#|$)/.test(String(src || '')));
- let href = 'xjs.css';
- if (scriptSrc) {
- href = String(scriptSrc)
- .replace(/(^|\/)xjs\.js(\?|#|$)/, '$1xjs.css$2')
- .replace(/(^|\/)layer\.js(\?|#|$)/, '$1xjs.css$2');
- }
- const link = document.createElement('link');
- link.id = id;
- link.rel = 'stylesheet';
- link.href = href;
- _xjsCssReady = waitForStylesheet(link);
- document.head.appendChild(link);
- return _xjsCssReady;
- } catch {
- // ignore
- return Promise.resolve();
- }
- };
- // SvgAni (svg.js) lazy loader for "svg:*" icon type
- let _svgAniReady = null;
- let _svgAniPrefetchScheduled = false;
- const isSvgAniIcon = (icon) => {
- if (typeof icon !== 'string') return false;
- const s = icon.trim();
- return s.startsWith(SVGANI_PREFIX);
- };
- const getSvgAniIconName = (icon) => {
- if (!isSvgAniIcon(icon)) return '';
- let name = String(icon || '').trim().slice(SVGANI_PREFIX.length).trim();
- if (name === 'loadding') name = 'loading';
- return name;
- };
- const normalizeSvgAniName = (raw) => {
- let name = String(raw || '').trim();
- if (!name) return '';
- if (name.startsWith(SVGANI_PREFIX)) name = getSvgAniIconName(name);
- if (name === 'loadding') name = 'loading';
- return name;
- };
- const getLayerScriptBase = () => {
- try {
- if (typeof document === 'undefined') return '';
- const scripts = Array.from(document.getElementsByTagName('script'));
- const src = scripts
- .map((s) => s && s.src)
- .find((s) => /(^|\/)(xjs|layer)\.js(\?|#|$)/.test(String(s || '')));
- if (!src) return '';
- return String(src)
- .replace(/[#?].*$/, '')
- .replace(/\/[^\/]*$/, '/');
- } catch {
- return '';
- }
- };
- const resolveSvgAniScriptUrl = () => {
- const base = getLayerScriptBase();
- return base ? (base + 'svg/svg.js') : 'svg/svg.js';
- };
- const resolveSvgAniJsonUrl = (name) => {
- const raw = String(name || '').trim();
- if (!raw) return '';
- if (/^(https?:)?\/\//.test(raw)) return raw;
- if (raw.startsWith('/') || raw.startsWith('./') || raw.startsWith('../')) return raw;
- const base = getLayerScriptBase();
- const file = raw.endsWith('.json') ? raw : (raw + '.json');
- return (base ? base : '') + 'svg/' + file;
- };
- const ensureSvgAniLib = () => {
- try {
- if (typeof window === 'undefined') return Promise.resolve(null);
- if (window.lottie) return Promise.resolve(window.lottie);
- if (typeof document === 'undefined' || !document.head) return Promise.resolve(null);
- if (_svgAniReady) return _svgAniReady;
- const existing = document.getElementById('layer-svgani-lib');
- _svgAniReady = new Promise((resolve) => {
- const finish = () => resolve(window.lottie || null);
- if (existing) {
- try { existing.addEventListener('load', finish, { once: true }); } catch {}
- try { existing.addEventListener('error', finish, { once: true }); } catch {}
- setTimeout(finish, 1200);
- return;
- }
- const script = document.createElement('script');
- script.id = 'layer-svgani-lib';
- script.async = true;
- script.src = resolveSvgAniScriptUrl();
- try { script.addEventListener('load', finish, { once: true }); } catch {}
- try { script.addEventListener('error', finish, { once: true }); } catch {}
- setTimeout(finish, 1800);
- document.head.appendChild(script);
- });
- return _svgAniReady;
- } catch {
- return Promise.resolve(null);
- }
- };
- const scheduleSvgAniPrefetch = () => {
- if (_svgAniPrefetchScheduled) return;
- _svgAniPrefetchScheduled = true;
- try {
- if (typeof window === 'undefined') return;
- const start = () => {
- setTimeout(() => {
- try { ensureSvgAniLib(); } catch {}
- }, 0);
- };
- if (document.readyState === 'complete') start();
- else window.addEventListener('load', start, { once: true });
- } catch {}
- };
- const waitForConnected = (el) => new Promise((resolve) => {
- if (!el) return resolve(false);
- if (el.isConnected) return resolve(true);
- let done = false;
- const finish = () => {
- if (done) return;
- done = true;
- resolve(!!el.isConnected);
- };
- try {
- requestAnimationFrame(() => requestAnimationFrame(finish));
- } catch {
- setTimeout(finish, 0);
- return;
- }
- setTimeout(finish, 120);
- });
- class Layer {
- constructor() {
- this._cssReady = ensureXjsCss();
- this.params = {};
- this.dom = {};
- this.promise = null;
- this.resolve = null;
- this.reject = null;
- this._onKeydown = null;
- this._mounted = null; // { kind, originalEl, placeholder, parent, nextSibling, prevHidden, prevInlineDisplay }
- this._isClosing = false;
- this._isReplace = false;
- // Step/flow support
- this._flowSteps = null; // Array<options>
- this._flowIndex = 0;
- this._flowValues = [];
- this._flowBase = null; // base options merged into each step
- this._flowResolved = null; // resolved merged steps
- this._flowMountedList = []; // mounted DOM records for move-mode steps
- this._flowAutoForm = null; // { enabled: true } when using step container shorthand
- this._backgroundSpec = null;
- }
- // Constructor helper when called as function: const popup = Layer({...})
- static get isProxy() { return true; }
-
- // Static entry point
- static run(options) {
- const instance = new Layer();
- return instance.run(options);
- }
- // Chainable entry point (builder-style)
- // Example:
- // Layer.$({ title: 'Hi' }).run().then(...)
- // Layer.$().config({ title: 'Hi' }).run()
- static $(options) {
- const instance = new Layer();
- if (options !== undefined) instance.config(options);
- return instance;
- }
-
- // Chainable config helper (does not render until `.run()` is called)
- config(options = {}) {
- // Support the same shorthand as Layer.run(title, text, icon)
- options = normalizeOptions(options, arguments[1], arguments[2]);
- this.params = { ...(this.params || {}), ...options };
- return this;
- }
- // Add a single step (chainable)
- // Usage:
- // Layer.$().step({ title:'A', dom:'#step1' }).step({ title:'B', dom:'#step2' }).run()
- step(options = {}) {
- if (!this._flowSteps) this._flowSteps = [];
- this._flowSteps.push(normalizeOptions(options, arguments[1], arguments[2]));
- return this;
- }
- // Add multiple steps at once (chainable)
- steps(steps = []) {
- if (!Array.isArray(steps)) return this;
- if (!this._flowSteps) this._flowSteps = [];
- steps.forEach((s) => this.step(s));
- return this;
- }
- // Convenience static helper: Layer.flow([steps], baseOptions?)
- static flow(steps = [], baseOptions = {}) {
- return Layer.$(baseOptions).steps(steps).run();
- }
-
- // Instance entry point (chainable)
- run(options) {
- const hasArgs = (options !== undefined && options !== null);
- const normalized = hasArgs ? normalizeOptions(options, arguments[1], arguments[2]) : null;
- const merged = hasArgs ? normalized : (this.params || {});
- // If no explicit steps yet, allow shorthand: { step: '#container', stepItem: '.item' }
- if (!this._flowSteps || !this._flowSteps.length) {
- const didInit = this._initFlowFromStepContainer(merged);
- if (didInit) {
- this.params = { ...(this.params || {}), ...this._stripStepOptions(merged) };
- }
- }
- // Flow mode: if configured via .step()/.steps() or step container shorthand, ignore per-call options and use steps
- if (this._flowSteps && this._flowSteps.length) {
- if (hasArgs) {
- // allow providing base options at fire-time
- this.params = { ...(this.params || {}), ...this._stripStepOptions(merged) };
- }
- return this._fireFlow();
- }
- return this._fire(merged);
- }
- _fire(options = {}) {
- options = normalizeOptions(options, arguments[1], arguments[2]);
- this.params = {
- title: '',
- text: '',
- icon: null,
- iconSize: null, // e.g. '6em' / '72px'
- iconLoop: null, // svgAni only: true/false/null (auto)
- confirmButtonText: 'OK',
- cancelButtonText: 'Cancel',
- showCancelButton: false,
- closeOnClickOutside: true,
- closeOnEsc: true,
- iconAnimation: true,
- popupAnimation: true,
- theme: 'auto',
- background: null,
- width: null,
- height: null,
- iwidth: null,
- iheight: null,
- nextIcon: null, // flow-only: icon for Next button
- prevIcon: null, // flow-only: icon for Back button
- // Content:
- // - text: plain text
- // - html: innerHTML
- // - dom: selector / Element / <template> (preferred)
- // - content: backward compat for selector/Element OR advanced { dom, mode, clone }
- dom: null,
- domMode: 'move', // 'move' (default) | 'clone'
- // Hook for confirming (also used in flow steps)
- // Return false to prevent close / next.
- // Return a value to be attached as `value`.
- // May return Promise.
- preConfirm: null,
- ...options
- };
- this.promise = new Promise((resolve, reject) => {
- this.resolve = resolve;
- this.reject = reject;
- });
- this._render();
- return this.promise;
- }
- _fireFlow() {
- this._flowIndex = 0;
- this._flowValues = [];
- // In flow mode:
- // - keep iconAnimation off by default (avoids jitter)
- // - BUT allow popupAnimation by default so the first step has the same entrance feel
- const base = { ...(this.params || {}) };
- if (!('popupAnimation' in base)) base.popupAnimation = true;
- if (!('iconAnimation' in base)) base.iconAnimation = false;
- this._flowBase = base;
- this.promise = new Promise((resolve, reject) => {
- this.resolve = resolve;
- this.reject = reject;
- });
- this._flowResolved = (this._flowSteps || []).map((_, i) => this._getFlowStepOptions(i));
- const first = this._flowResolved[0] || this._getFlowStepOptions(0);
- this.params = first;
- this._render({ flow: true });
- return this.promise;
- }
- static _getXjs() {
- // Prefer $ (main), fallback to xjs/animal (compat)
- const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
- if (!g) return null;
- const x = g.$ || g.xjs || g.animal;
- return (typeof x === 'function') ? x : null;
- }
- _stripStepOptions(options) {
- const out = { ...(options || {}) };
- try { delete out.step; } catch {}
- try { delete out.stepItem; } catch {}
- return out;
- }
- _parseStepSizeAttr(raw) {
- if (raw === undefined || raw === null) return null;
- const s = String(raw).trim();
- if (!s) return null;
- if (/^-?\d+(\.\d+)?$/.test(s)) return parseFloat(s);
- return s;
- }
- _readStepAttr(item, container, names) {
- const list = Array.isArray(names) ? names : [names];
- const readFrom = (el) => {
- if (!el || !el.getAttribute) return '';
- for (let i = 0; i < list.length; i++) {
- const v = el.getAttribute(list[i]);
- if (v != null && String(v).trim() !== '') return String(v).trim();
- }
- return '';
- };
- return readFrom(item) || readFrom(container) || '';
- }
- _normalizeStepContainer(options) {
- const opts = options || {};
- const raw = opts.step;
- if (!raw || typeof document === 'undefined') return null;
- const container = (typeof raw === 'string') ? document.querySelector(raw) : raw;
- if (!container || (typeof Element !== 'undefined' && !(container instanceof Element))) return null;
- const itemSelector = opts.stepItem;
- let items = [];
- if (typeof itemSelector === 'string' && itemSelector.trim()) {
- try { items = Array.from(container.querySelectorAll(itemSelector)); } catch {}
- } else {
- try { items = Array.from(container.children || []); } catch {}
- }
- return { container, items };
- }
- _initFlowFromStepContainer(options) {
- const spec = this._normalizeStepContainer(options);
- if (!spec) return false;
- const { items, container } = spec;
- if (!items || !items.length) {
- try { console.warn('Layer step container has no items.'); } catch {}
- return false;
- }
- this._flowSteps = items.map((item) => {
- const step = { dom: item, __fromContainer: true };
- try {
- const title =
- (item.getAttribute('data-step-title') || item.getAttribute('data-layer-title') || item.getAttribute('title') || '').trim();
- if (title) step.title = title;
- } catch {}
- try {
- const background = this._readStepAttr(item, container, ['data-step-background', 'data-layer-background', 'data-background']);
- if (background) step.background = background;
- const icon = this._readStepAttr(item, container, ['data-step-icon', 'data-layer-icon', 'data-icon']);
- if (icon) step.icon = icon;
- const theme = this._readStepAttr(item, container, ['data-step-theme', 'data-layer-theme', 'data-theme']);
- if (theme) step.theme = theme;
- const width = this._readStepAttr(item, container, ['data-step-width', 'data-layer-width', 'data-width']);
- const height = this._readStepAttr(item, container, ['data-step-height', 'data-layer-height', 'data-height']);
- const iwidth = this._readStepAttr(item, container, ['data-step-iwidth', 'data-layer-iwidth', 'data-iwidth']);
- const iheight = this._readStepAttr(item, container, ['data-step-iheight', 'data-layer-iheight', 'data-iheight']);
- const w = this._parseStepSizeAttr(width);
- const h = this._parseStepSizeAttr(height);
- const iw = this._parseStepSizeAttr(iwidth);
- const ih = this._parseStepSizeAttr(iheight);
- if (w != null) step.width = w;
- if (h != null) step.height = h;
- if (iw != null) step.iwidth = iw;
- if (ih != null) step.iheight = ih;
- } catch {}
- return step;
- });
- this._flowAutoForm = { enabled: true };
- return true;
- }
- _clearPopup() {
- const popup = this.dom && this.dom.popup;
- if (!popup) return null;
- while (popup.firstChild) popup.removeChild(popup.firstChild);
- return popup;
- }
- _bindOverlayClick(handler) {
- const overlay = this.dom && this.dom.overlay;
- if (!overlay) return;
- try {
- if (overlay._layerOnClick) {
- overlay.removeEventListener('click', overlay._layerOnClick);
- }
- } catch {}
- overlay._layerOnClick = null;
- if (!handler) return;
- overlay._layerOnClick = handler;
- overlay.addEventListener('click', handler);
- }
- _mountOverlay() {
- const overlay = this.dom && this.dom.overlay;
- if (!overlay) return;
- // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
- // then show on a clean frame so opacity transitions are smooth.
- const ready = this._cssReady && typeof this._cssReady.then === 'function' ? this._cssReady : Promise.resolve();
- ready.then(() => {
- try { overlay.style.visibility = ''; } catch {}
- if (!overlay.parentNode) {
- document.body.appendChild(overlay);
- }
- // Double-rAF gives the browser a chance to apply styles before animating.
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- overlay.classList.add('show');
- this._didOpen();
- });
- });
- });
- }
- _applyPopupBasics(options, { forceBackground = false } = {}) {
- this._applyPopupSize(options);
- this._applyPopupTheme(options);
- this._applyBackgroundForOptions(options, { force: forceBackground });
- }
- _buildActions({ flow = false } = {}) {
- this.dom.actions = el('div', `${PREFIX}actions`);
- this.dom.popup.appendChild(this.dom.actions);
- if (flow) {
- this._updateFlowActions();
- return;
- }
- // Cancel Button
- if (this.params.showCancelButton) {
- this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
- this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText);
- this.dom.cancelBtn.onclick = () => this._handleCancel();
- this.dom.actions.appendChild(this.dom.cancelBtn);
- }
- // Confirm Button
- this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
- this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText);
- this.dom.confirmBtn.onclick = () => this._handleConfirm();
- this.dom.actions.appendChild(this.dom.confirmBtn);
- }
- _render(meta = null) {
- this._destroySvgAniIcon();
- // Remove existing if any (but first, try to restore any mounted DOM from the previous instance)
- const existing = document.querySelector(`.${PREFIX}overlay`);
- const wantReplace = !!(this.params && this.params.replace);
- this._isReplace = false;
- if (existing && wantReplace) {
- try {
- const prev = existing._layerInstance;
- if (prev && typeof prev._forceDestroy === 'function') prev._forceDestroy('replace');
- } catch {}
- try {
- if (existing._layerCloseTimer) {
- clearTimeout(existing._layerCloseTimer);
- existing._layerCloseTimer = null;
- }
- } catch {}
- try {
- if (existing._layerOnClick) {
- existing.removeEventListener('click', existing._layerOnClick);
- existing._layerOnClick = null;
- }
- } catch {}
- try {
- while (existing.firstChild) existing.removeChild(existing.firstChild);
- } catch {}
- try {
- existing.style.visibility = '';
- existing.classList.add('show');
- // Reset any inline fade state from the previous close
- existing.style.transition = 'none';
- existing.style.opacity = '1';
- requestAnimationFrame(() => {
- try { existing.style.transition = ''; } catch {}
- });
- } catch {}
- this.dom.overlay = existing;
- this.dom.overlay._layerInstance = this;
- this._isReplace = true;
- } else {
- if (existing) {
- try {
- const prev = existing._layerInstance;
- if (prev && typeof prev._forceDestroy === 'function') prev._forceDestroy('replace');
- } catch {}
- try { existing.remove(); } catch {}
- }
- // Create Overlay
- this.dom.overlay = el('div', `${PREFIX}overlay`);
- this.dom.overlay._layerInstance = this;
- }
- // Create Popup
- this.dom.popup = el('div', `${PREFIX}popup`);
- this.dom.overlay.appendChild(this.dom.popup);
- this._applyPopupBasics(this.params, { forceBackground: true });
- // Flow mode: pre-mount all steps and switch by hide/show (DOM continuity, less jitter)
- if (meta && meta.flow) {
- this._renderFlowUI();
- return;
- }
- // Icon
- if (this.params.icon) {
- this.dom.icon = this._createIcon(this.params.icon);
- this.dom.popup.appendChild(this.dom.icon);
- }
- // Title
- if (this.params.title) {
- this.dom.title = el('h2', `${PREFIX}title`, this.params.title);
- this.dom.popup.appendChild(this.dom.title);
- }
- // Content (Text / HTML / Element)
- if (this.params.text || this.params.html || this.params.content || this.params.dom) {
- this.dom.content = el('div', `${PREFIX}content`);
- // DOM content (preferred: dom, backward: content)
- const domSpec = this._normalizeDomSpec(this.params);
- if (domSpec) {
- this._mountDomContent(domSpec);
- } else if (this.params.html) {
- this.dom.content.innerHTML = this.params.html;
- } else {
- this.dom.content.textContent = this.params.text;
- }
-
- this.dom.popup.appendChild(this.dom.content);
- }
- // Actions
- this._buildActions();
- // Event Listeners
- if (this.params.closeOnClickOutside) {
- this._bindOverlayClick((e) => {
- if (e.target === this.dom.overlay) this._close(null); // Dismiss
- });
- } else {
- this._bindOverlayClick(null);
- }
- this._mountOverlay();
- }
- _renderFlowUI() {
- this._destroySvgAniIcon();
- // Icon/title/content/actions are stable; steps are pre-mounted and toggled.
- const popup = this.dom.popup;
- if (!popup) return;
- // Clear popup
- this._clearPopup();
- this._applyPopupBasics(this.params, { forceBackground: true });
- // Icon (in flow: suppressed by default unless question or summary)
- this.dom.icon = null;
- if (this.params.icon) {
- this.dom.icon = this._createIcon(this.params.icon);
- popup.appendChild(this.dom.icon);
- }
- // Title (always present for flow)
- this.dom.title = el('h2', `${PREFIX}title`, this.params.title || '');
- popup.appendChild(this.dom.title);
- // Content container
- this.dom.content = el('div', `${PREFIX}content`);
- // In flow, stretch content so slide distance is visible.
- this.dom.content.style.alignSelf = 'stretch';
- this.dom.content.style.width = '100%';
- // Stack container: keep panes absolute so we can cross-fade without layout thrash
- this.dom.stepStack = el('div', `${PREFIX}step-stack`);
- this.dom.stepStack.style.position = 'relative';
- this.dom.stepStack.style.width = '100%';
- this.dom.stepPanes = [];
- this._flowMountedList = [];
- const steps = this._flowResolved || (this._flowSteps || []).map((_, i) => this._getFlowStepOptions(i));
- steps.forEach((opt, i) => {
- const pane = el('div', `${PREFIX}step-pane`);
- pane.style.width = '100%';
- pane.style.boxSizing = 'border-box';
- pane.style.display = (i === this._flowIndex) ? '' : 'none';
- // Fill pane: dom/html/text
- const domSpec = this._normalizeDomSpec(opt);
- if (domSpec) {
- this._mountDomContentInto(pane, domSpec, { collectFlow: true });
- } else if (opt.html) {
- pane.innerHTML = opt.html;
- } else if (opt.text) {
- pane.textContent = opt.text;
- }
- this.dom.stepStack.appendChild(pane);
- this.dom.stepPanes.push(pane);
- });
- this.dom.content.appendChild(this.dom.stepStack);
- popup.appendChild(this.dom.content);
- // Actions
- this._buildActions({ flow: true });
- // Event Listeners
- if (this.params.closeOnClickOutside) {
- this._bindOverlayClick((e) => {
- if (e.target === this.dom.overlay) this._close(null, 'backdrop');
- });
- } else {
- this._bindOverlayClick(null);
- }
- this._mountOverlay();
- }
- _updateFlowActions() {
- const actions = this.dom && this.dom.actions;
- if (!actions) return;
- while (actions.firstChild) actions.removeChild(actions.firstChild);
- this.dom.cancelBtn = null;
- const total = (this._flowSteps && this._flowSteps.length) ? this._flowSteps.length : 0;
- const isLast = total > 0 ? (this._flowIndex >= (total - 1)) : true;
- if (this.params.showCancelButton) {
- const prevIcon = (this._flowIndex > 0) ? this.params.prevIcon : null;
- this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
- this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText, prevIcon, 'start');
- this.dom.cancelBtn.onclick = () => this._handleCancel();
- actions.appendChild(this.dom.cancelBtn);
- }
- const nextIcon = (!isLast) ? this.params.nextIcon : null;
- this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
- this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText, nextIcon, 'end');
- this.dom.confirmBtn.onclick = () => this._handleConfirm();
- actions.appendChild(this.dom.confirmBtn);
- }
- _createIcon(type) {
- const rawType = String(type || '').trim();
- const icon = el('div', `${PREFIX}icon ${rawType}`);
- const applyIconBoxSize = () => {
- if (!this.params) return;
- const w = this._normalizeSizeValue(this.params.iwidth);
- const h = this._normalizeSizeValue(this.params.iheight);
- if (w) icon.style.width = w;
- if (h) icon.style.height = h;
- };
- const applyIconSize = (mode) => {
- if (!(this.params && this.params.iconSize)) return;
- try {
- const s = String(this.params.iconSize).trim();
- if (!s) return;
- // For SweetAlert2-style success icon, scale via font-size so all `em`-based parts remain proportional.
- if (mode === 'font') {
- const m = s.match(/^(-?\d*\.?\d+)\s*(px|em|rem)$/i);
- if (m) {
- const n = parseFloat(m[1]);
- const unit = m[2];
- if (Number.isFinite(n) && n > 0) {
- icon.style.fontSize = (n / 5) + unit; // icon is 5em wide/tall
- return;
- }
- }
- }
- // Fallback: directly size the box (works great for SVG icons)
- icon.style.width = s;
- icon.style.height = s;
- } catch {}
- };
-
- const appendRingParts = () => {
- // Use the same "success-like" ring parts for every built-in icon
- icon.appendChild(el('div', `${PREFIX}success-circular-line-left`));
- icon.appendChild(el('div', `${PREFIX}success-ring`));
- icon.appendChild(el('div', `${PREFIX}success-fix`));
- icon.appendChild(el('div', `${PREFIX}success-circular-line-right`));
- };
-
- const createInnerMarkSvg = () => {
- const svg = svgEl('svg');
- svg.setAttribute('viewBox', '0 0 80 80');
- svg.setAttribute('aria-hidden', 'true');
- svg.setAttribute('focusable', 'false');
- return svg;
- };
-
- const addMarkPath = (svg, d, extraClass) => {
- const p = svgEl('path');
- p.setAttribute('d', d);
- p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
- p.setAttribute('stroke', 'currentColor');
- svg.appendChild(p);
- return p;
- };
-
- const addDot = (svg, cx, cy) => {
- const dot = svgEl('circle');
- dot.setAttribute('class', `${PREFIX}svg-dot`);
- dot.setAttribute('cx', String(cx));
- dot.setAttribute('cy', String(cy));
- dot.setAttribute('r', '3.2');
- dot.setAttribute('fill', 'currentColor');
- svg.appendChild(dot);
- return dot;
- };
-
- const appendBuiltInInnerMark = (svg) => {
- if (rawType === 'error') {
- addMarkPath(svg, 'M28 28 L52 52', `${PREFIX}svg-error-left`);
- addMarkPath(svg, 'M52 28 L28 52', `${PREFIX}svg-error-right`);
- } else if (rawType === 'warning') {
- addMarkPath(svg, 'M40 20 L40 46', `${PREFIX}svg-warning-line`);
- addDot(svg, 40, 58);
- } else if (rawType === 'info') {
- addMarkPath(svg, 'M40 34 L40 56', `${PREFIX}svg-info-line`);
- addDot(svg, 40, 25);
- } else if (rawType === 'question') {
- 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`);
- addDot(svg, 42, 61);
- }
- };
-
- if (isSvgAniIcon(rawType)) {
- // SvgAni animation icon: svg:<name>
- const name = getSvgAniIconName(rawType);
- icon.className = `${PREFIX}icon ${PREFIX}icon-svgAni`;
- icon.dataset.svgani = name;
- const container = el('div', `${PREFIX}svgAni`);
- icon.appendChild(container);
- applyIconSize('box');
- applyIconBoxSize();
- scheduleSvgAniPrefetch();
- this._initSvgAniIcon(icon, name);
- return icon;
- }
- if (rawType === 'success') {
- // SweetAlert2-compatible DOM structure (circle + tick) for exact style parity
- // <div class="...success-circular-line-left"></div>
- // <div class="...success-mark"><span class="...success-line-tip"></span><span class="...success-line-long"></span></div>
- // <div class="...success-ring"></div>
- // <div class="...success-fix"></div>
- // <div class="...success-circular-line-right"></div>
- icon.appendChild(el('div', `${PREFIX}success-circular-line-left`));
- const mark = el('div', `${PREFIX}success-mark`);
- mark.appendChild(el('span', `${PREFIX}success-line-tip`));
- mark.appendChild(el('span', `${PREFIX}success-line-long`));
- icon.appendChild(mark);
- icon.appendChild(el('div', `${PREFIX}success-ring`));
- icon.appendChild(el('div', `${PREFIX}success-fix`));
- icon.appendChild(el('div', `${PREFIX}success-circular-line-right`));
- applyIconSize('font');
- return icon;
- }
- if (rawType === 'error' || rawType === 'warning' || rawType === 'info' || rawType === 'question') {
- // Use the same "success-like" ring parts for every icon
- appendRingParts();
- applyIconSize('font');
- applyIconBoxSize();
- // SVG only draws the inner symbol (no SVG ring)
- const svg = createInnerMarkSvg();
- appendBuiltInInnerMark(svg);
- icon.appendChild(svg);
- return icon;
- }
- // Default to SVG icons for other/custom types
- applyIconSize('box');
- applyIconBoxSize();
- const svg = createInnerMarkSvg();
- const ring = svgEl('circle');
- ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
- ring.setAttribute('cx', '40');
- ring.setAttribute('cy', '40');
- ring.setAttribute('r', '34');
- ring.setAttribute('stroke', 'currentColor');
- svg.appendChild(ring);
- // For custom types, we draw ring only by default (no inner mark).
- icon.appendChild(svg);
-
- return icon;
- }
- _normalizeSizeValue(v) {
- if (v === undefined || v === null) return null;
- if (typeof v === 'number' && Number.isFinite(v)) return v + 'px';
- if (typeof v === 'string') {
- const s = v.trim();
- return s ? s : null;
- }
- return null;
- }
- _applyPopupSize(options) {
- const popup = this.dom && this.dom.popup;
- if (!popup) return;
- const w = this._normalizeSizeValue(options && options.width);
- const h = this._normalizeSizeValue(options && options.height);
- if (!w && !h) {
- popup.style.width = '';
- popup.style.height = '';
- popup.style.maxWidth = '';
- popup.style.overflow = '';
- return;
- }
- if (w) {
- popup.style.width = w;
- // If width is explicitly set, remove default CSS max-width clamp.
- popup.style.maxWidth = 'none';
- } else {
- popup.style.width = '';
- popup.style.maxWidth = '';
- }
- if (h) popup.style.height = h;
- else popup.style.height = '';
- popup.style.overflow = 'auto';
- }
- _applyPopupTheme(options) {
- const popup = this.dom && this.dom.popup;
- if (!popup) return;
- const theme = normalizeTheme(options && options.theme);
- try {
- popup.classList.remove(`${PREFIX}theme-auto`, `${PREFIX}theme-light`, `${PREFIX}theme-dark`);
- } catch {}
- try {
- popup.classList.add(`${PREFIX}theme-${theme}`);
- } catch {}
- }
- _normalizeBackgroundSpec(raw) {
- if (!raw || typeof raw !== 'string') return null;
- const s = raw.trim();
- if (!s) return null;
- if (s.startsWith(BG_SVG_PREFIX) && s.endsWith(')')) {
- const name = normalizeSvgAniName(s.slice(BG_SVG_PREFIX.length, -1));
- if (!name) return null;
- return { type: 'svg', name, key: `svg:${name}` };
- }
- if (s.startsWith(SVGANI_PREFIX)) {
- const name = normalizeSvgAniName(s);
- if (!name) return null;
- return { type: 'svg', name, key: `svg:${name}` };
- }
- if (/^url\(/i.test(s)) {
- return { type: 'image', value: s, key: s };
- }
- if (s.startsWith(BG_CSS_PREFIX) && s.endsWith(')')) {
- const css = s.slice(BG_CSS_PREFIX.length, -1).trim();
- if (!css) return null;
- return { type: 'css', css, key: `css:${css}` };
- }
- // Fallback: treat as raw css string
- return { type: 'css', css: s, key: `css:${s}` };
- }
- _isSameBackgroundSpec(a, b) {
- if (!a && !b) return true;
- if (!a || !b) return false;
- return a.key === b.key;
- }
- _insertBackgroundEl(bgEl) {
- const popup = this.dom && this.dom.popup;
- if (!popup || !bgEl) return;
- if (popup.firstChild) popup.insertBefore(bgEl, popup.firstChild);
- else popup.appendChild(bgEl);
- }
- _createBackgroundEl(spec) {
- const bg = el('div', `${PREFIX}bg`);
- bg.dataset.bgKey = spec.key;
- if (spec.type === 'image') {
- bg.style.backgroundImage = spec.value;
- } else if (spec.type === 'css') {
- try { bg.style.cssText += ';' + spec.css; } catch {}
- } else if (spec.type === 'svg') {
- const container = el('div', `${PREFIX}svgAni`);
- bg.appendChild(container);
- this._initSvgAniBackground(bg, spec.name);
- }
- return bg;
- }
- _applyBackgroundForOptions(options, { force = false } = {}) {
- const popup = this.dom && this.dom.popup;
- if (!popup) return;
- const spec = this._normalizeBackgroundSpec(options && options.background);
- if (!spec) {
- if (this.dom.background) {
- this._destroySvgAniBackground(this.dom.background);
- try { this.dom.background.remove(); } catch {}
- }
- this.dom.background = null;
- this._backgroundSpec = null;
- popup.style.background = '';
- return;
- }
- if (!force && this._isSameBackgroundSpec(this._backgroundSpec, spec)) return;
- if (this.dom.background) {
- this._destroySvgAniBackground(this.dom.background);
- try { this.dom.background.remove(); } catch {}
- }
- const bgEl = this._createBackgroundEl(spec);
- this.dom.background = bgEl;
- this._backgroundSpec = spec;
- popup.style.background = 'transparent';
- this._insertBackgroundEl(bgEl);
- }
- async _transitionBackground(nextSpec, direction) {
- const popup = this.dom && this.dom.popup;
- if (!popup) return;
- if (this._isSameBackgroundSpec(this._backgroundSpec, nextSpec)) return;
- const fromEl = this.dom.background;
- let toEl = null;
- if (nextSpec) {
- toEl = this._createBackgroundEl(nextSpec);
- this._insertBackgroundEl(toEl);
- popup.style.background = 'transparent';
- } else {
- popup.style.background = '';
- }
- const isNext = direction !== 'prev';
- const enterFrom = isNext ? 100 : -100;
- const exitTo = isNext ? -100 : 100;
- const duration = 320;
- if (toEl) {
- toEl.style.transform = `translateX(${enterFrom}%)`;
- toEl.style.opacity = '0.2';
- }
- if (fromEl) {
- fromEl.style.transform = 'translateX(0%)';
- fromEl.style.opacity = '1';
- }
- let fromAnim = null;
- let toAnim = null;
- try {
- if (fromEl && fromEl.animate) {
- fromAnim = fromEl.animate(
- [
- { transform: 'translateX(0%)', opacity: 1 },
- { transform: `translateX(${exitTo}%)`, opacity: 0.1 }
- ],
- { duration, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
- );
- }
- } catch {}
- try {
- if (toEl && toEl.animate) {
- toAnim = toEl.animate(
- [
- { transform: `translateX(${enterFrom}%)`, opacity: 0.2 },
- { transform: 'translateX(0%)', opacity: 1 }
- ],
- { duration, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
- );
- }
- } catch {}
- try {
- await Promise.all([
- fromAnim && fromAnim.finished ? fromAnim.finished.catch(() => {}) : Promise.resolve(),
- toAnim && toAnim.finished ? toAnim.finished.catch(() => {}) : Promise.resolve()
- ]);
- } catch {}
- if (fromEl) {
- this._destroySvgAniBackground(fromEl);
- try { fromEl.remove(); } catch {}
- }
- if (toEl) {
- toEl.style.transform = '';
- toEl.style.opacity = '';
- }
- this.dom.background = toEl;
- this._backgroundSpec = nextSpec || null;
- }
- _destroySvgAniBackground(bgEl) {
- if (!bgEl) return;
- const inst = bgEl._svgAniInstance;
- if (inst && typeof inst.destroy === 'function') {
- try { inst.destroy(); } catch {}
- }
- try { bgEl._svgAniInstance = null; } catch {}
- }
- _initSvgAniBackground(bgEl, name) {
- if (!bgEl) return;
- const container = bgEl.querySelector(`.${PREFIX}svgAni`);
- if (!container) return;
- const cleanName = normalizeSvgAniName(name);
- if (!cleanName) return;
- const url = resolveSvgAniJsonUrl(cleanName);
- if (!url) return;
- const showError = (msg) => {
- try {
- container.textContent = String(msg || '');
- container.classList.add(`${PREFIX}svgAni-error`);
- } catch {}
- };
- const loadData = () => new Promise((resolve, reject) => {
- try {
- if (typeof fetch === 'function') {
- fetch(url).then((res) => {
- if (!res || !res.ok) throw new Error('fetch failed');
- return res.json();
- }).then(resolve).catch(reject);
- return;
- }
- const xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.responseType = 'json';
- xhr.onload = () => {
- if (xhr.status >= 200 && xhr.status < 300) {
- const data = xhr.response || (xhr.responseText ? JSON.parse(xhr.responseText) : null);
- resolve(data);
- } else {
- reject(new Error('xhr failed'));
- }
- };
- xhr.onerror = () => reject(new Error('xhr failed'));
- xhr.send();
- } catch (e) {
- reject(e);
- }
- });
- const boot = async () => {
- const svgAniLib = await ensureSvgAniLib();
- if (!svgAniLib) {
- showError('SvgAni lib not available');
- return;
- }
- try {
- const data = await loadData();
- if (!data) throw new Error('empty json');
- try { container.textContent = ''; } catch {}
- const anim = svgAniLib.loadAnimation({
- container,
- renderer: 'svg',
- loop: true,
- autoplay: true,
- animationData: data,
- // Fill background container even if aspect ratio differs
- rendererSettings: { preserveAspectRatio: 'xMidYMid slice' }
- });
- bgEl._svgAniInstance = anim;
- } catch (e) {
- showError('SvgAni load failed');
- }
- };
- boot();
- }
- _adjustRingBackgroundColor() {
- try {
- const icon = this.dom && this.dom.icon;
- const popup = this.dom && this.dom.popup;
- if (!icon || !popup) return;
- const bg = getComputedStyle(popup).backgroundColor;
- const parts = icon.querySelectorAll(RING_BG_PARTS_SELECTOR);
- parts.forEach((el) => {
- try { el.style.backgroundColor = bg; } catch {}
- });
- } catch {}
- }
- _didOpen() {
- // Keyboard close (ESC)
- if (this.params.closeOnEsc) {
- this._onKeydown = (e) => {
- if (!e) return;
- if (e.key === 'Escape') this._close(null, 'esc');
- };
- document.addEventListener('keydown', this._onKeydown);
- }
- // Keep the "success-like" ring perfectly blended with popup bg
- this._adjustRingBackgroundColor();
- // Popup animation (optional)
- if (this.params.popupAnimation) {
- if (this.dom.popup) {
- // Use WAAPI directly to guarantee the slide+fade effect is visible,
- // independent from xjs.animate implementation details.
- try {
- const popup = this.dom.popup;
- try { popup.classList.add(`${PREFIX}popup-anim-slide`); } catch {}
- const rect = popup.getBoundingClientRect();
- // Default entrance: from above center by "more than half" of popup height.
- // Clamp to viewport so very tall popups don't start far off-screen.
- const vh = (typeof window !== 'undefined' && window && window.innerHeight) ? window.innerHeight : rect.height;
- const baseH = Math.min(rect.height, vh);
- const y0 = -Math.round(Math.max(18, baseH * 0.75));
- // Disable CSS transform transition so it won't fight with WAAPI.
- popup.style.transition = 'none';
- popup.style.willChange = 'transform, opacity';
- // If WAAPI is available, prefer it (most consistent).
- if (popup.animate) {
- const anim = popup.animate(
- [
- { transform: `translateY(${y0}px) scale(0.92)`, opacity: 0 },
- { transform: 'translateY(0px) scale(1)', opacity: 1 }
- ],
- {
- duration: 220,
- easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)',
- fill: 'forwards'
- }
- );
- anim.finished
- .catch(() => {})
- .finally(() => {
- try { popup.style.willChange = ''; } catch {}
- });
- } else {
- // Fallback: try xjs.animate if WAAPI isn't available.
- const X = Layer._getXjs();
- if (X) {
- popup.style.opacity = '0';
- popup.style.transform = `translateY(${y0}px) scale(0.92)`;
- X(popup).animate({
- y: [y0, 0],
- scale: [0.92, 1],
- opacity: [0, 1],
- duration: 220,
- easing: 'ease-out'
- });
- }
- setTimeout(() => {
- try { popup.style.willChange = ''; } catch {}
- }, 260);
- }
- } catch {}
- }
- }
- // When popupAnimation is disabled, use a subtle scale-pop fade-in at current position.
- if (!this.params.popupAnimation && !this._isReplace && this.dom.popup) {
- try {
- const popup = this.dom.popup;
- popup.style.transition = 'none';
- popup.style.willChange = 'transform, opacity';
- if (popup.animate) {
- const anim = popup.animate(
- [
- { transform: 'scale(0.8)', opacity: 0, offset: 0, easing: 'ease-out' },
- { transform: 'scale(1.1)', opacity: 1, offset: 0.67, easing: 'ease-in-out' },
- { transform: 'scale(1)', opacity: 1, offset: 1 }
- ],
- {
- duration: 240,
- fill: 'forwards'
- }
- );
- anim.finished
- .catch(() => {})
- .finally(() => {
- try { popup.style.willChange = ''; } catch {}
- try { popup.style.transition = ''; } catch {}
- try { popup.style.opacity = ''; } catch {}
- try { popup.style.transform = ''; } catch {}
- });
- } else {
- const X = Layer._getXjs();
- if (X) {
- popup.style.opacity = '0';
- popup.style.transform = 'scale(0.8)';
- X(popup).animate({
- scale: [0.8, 1.1],
- opacity: [0, 1],
- duration: 160,
- easing: 'ease-out'
- });
- setTimeout(() => {
- try {
- X(popup).animate({
- scale: [1.1, 1],
- duration: 80,
- easing: 'ease-in-out'
- });
- } catch {}
- }, 160);
- setTimeout(() => {
- try { popup.style.willChange = ''; } catch {}
- try { popup.style.transition = ''; } catch {}
- try { popup.style.opacity = ''; } catch {}
- try { popup.style.transform = ''; } catch {}
- }, 270);
- } else {
- setTimeout(() => {
- try { popup.style.willChange = ''; } catch {}
- try { popup.style.transition = ''; } catch {}
- }, 260);
- }
- }
- } catch {}
- }
- // Replace mode: if popupAnimation is off, do a soft fade+scale to avoid a hard cut.
- if (!this.params.popupAnimation && this._isReplace && this.dom.popup) {
- try {
- const popup = this.dom.popup;
- popup.style.transition = 'none';
- popup.style.opacity = '0';
- popup.style.transform = 'scale(0.98)';
- requestAnimationFrame(() => {
- popup.style.transition = 'opacity 0.22s ease, transform 0.22s ease';
- popup.style.opacity = '1';
- popup.style.transform = 'scale(1)';
- setTimeout(() => {
- try { popup.style.transition = ''; } catch {}
- try { popup.style.opacity = ''; } catch {}
- try { popup.style.transform = ''; } catch {}
- }, 260);
- });
- } catch {}
- }
- // Icon SVG draw animation
- if (this.params.iconAnimation) {
- this._animateIcon();
- }
- // User hook (SweetAlert-ish naming)
- try {
- if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
- } catch {}
- }
- _animateIcon() {
- const icon = this.dom.icon;
- if (!icon) return;
- const type = (this.params && this.params.icon) || '';
- if (isSvgAniIcon(type)) return;
- // Ring animation (same as success) for all built-in icons
- if (RING_TYPES.has(type)) this._adjustRingBackgroundColor();
- try { icon.classList.remove(`${PREFIX}icon-show`); } catch {}
- requestAnimationFrame(() => {
- try { icon.classList.add(`${PREFIX}icon-show`); } catch {}
- });
- // Success tick is CSS-driven; others still draw their SVG mark after the ring starts.
- if (type === 'success') return;
- const X = Layer._getXjs();
- if (!X) return;
- const svg = icon.querySelector('svg');
- if (!svg) return;
- const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
- const dot = svg.querySelector(`.${PREFIX}svg-dot`);
- // If this is a built-in icon, keep the SweetAlert-like order:
- // ring sweep first (~0.51s), then draw the inner mark.
- const baseDelay = RING_TYPES.has(type) ? 520 : 0;
- if (type === 'error') {
- // Draw order: left-top -> right-bottom, then right-top -> left-bottom
- // NOTE: A tiny delay (like 70ms) looks simultaneous; make it strictly sequential.
- const a = svg.querySelector(`.${PREFIX}svg-error-left`) || marks[0]; // M28 28 L52 52
- const b = svg.querySelector(`.${PREFIX}svg-error-right`) || marks[1]; // M52 28 L28 52
- const dur = 320;
- const gap = 60;
- try { if (a) X(a).draw({ duration: dur, easing: 'ease-out', delay: baseDelay }); } catch {}
- try { if (b) X(b).draw({ duration: dur, easing: 'ease-out', delay: baseDelay + dur + gap }); } catch {}
- } else {
- // warning / info / question (single stroke) or custom SVG symbols
- marks.forEach((m, i) => {
- try { X(m).draw({ duration: 420, easing: 'ease-out', delay: baseDelay + i * 60 }); } catch {}
- });
- }
- if (dot) {
- try {
- dot.style.opacity = '0';
- // Keep dot pop after the ring begins
- const d = baseDelay + 140;
- if (type === 'info') {
- X(dot).animate({ opacity: [0, 1], y: [-8, 0], scale: [0.2, 1], duration: 420, delay: d, easing: { stiffness: 300, damping: 14 } });
- } else {
- X(dot).animate({ opacity: [0, 1], scale: [0.2, 1], duration: 320, delay: d, easing: { stiffness: 320, damping: 18 } });
- }
- } catch {}
- }
- }
- _forceDestroy(reason = 'replace') {
- try { this._destroySvgAniIcon(); } catch {}
- try { this._destroySvgAniBackground(this.dom && this.dom.background); } catch {}
- // Restore mounted DOM (if any) and cleanup listeners; also resolve the promise so it doesn't hang.
- try {
- if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
- else this._unmountDomContent();
- } catch {}
- if (this._onKeydown) {
- try { document.removeEventListener('keydown', this._onKeydown); } catch {}
- this._onKeydown = null;
- }
- try {
- if (this.resolve) {
- this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason });
- }
- } catch {}
- }
- _close(isConfirmed, reason) {
- if (this._isClosing) return;
- this._isClosing = true;
- try { this._destroySvgAniIcon(); } catch {}
- try { this._destroySvgAniBackground(this.dom && this.dom.background); } catch {}
- const shouldDelayUnmount = !!(
- (this._mounted && this._mounted.kind === 'move') ||
- (this._flowSteps && this._flowSteps.length && this._flowMountedList && this._flowMountedList.length)
- ) || !this.params.popupAnimation;
- const doUnmount = () => {
- try {
- if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
- else this._unmountDomContent();
- } catch {}
- };
- if (!shouldDelayUnmount) {
- // Restore mounted DOM (moved into popup) before removing overlay
- doUnmount();
- }
- let customClose = false;
- // Soft close for non-popupAnimation cases (avoid abrupt cut)
- if (!this.params.popupAnimation) {
- try {
- const popup = this.dom.popup;
- if (popup) {
- popup.style.transition = 'opacity 0.22s ease';
- popup.style.opacity = '0';
- popup.style.transform = '';
- }
- if (this.dom.overlay) {
- const overlay = this.dom.overlay;
- // Use inline opacity to avoid class-based jumps
- overlay.style.transition = 'opacity 0.22s ease';
- overlay.style.opacity = '1';
- requestAnimationFrame(() => {
- try {
- if (overlay._layerInstance !== this) return;
- overlay.style.opacity = '0';
- } catch {}
- });
- }
- customClose = true;
- } catch {}
- }
- if (!customClose) {
- this.dom.overlay.classList.remove('show');
- }
- try {
- if (this.dom.overlay) {
- const overlay = this.dom.overlay;
- const delay = customClose ? 240 : 300;
- overlay._layerCloseTimer = setTimeout(() => {
- if (shouldDelayUnmount) {
- doUnmount();
- }
- if (overlay._layerInstance !== this) return;
- if (overlay.parentNode) {
- overlay.parentNode.removeChild(overlay);
- }
- }, delay);
- }
- } catch {}
- if (this._onKeydown) {
- try { document.removeEventListener('keydown', this._onKeydown); } catch {}
- this._onKeydown = null;
- }
- try {
- if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
- } catch {}
- try {
- if (typeof this.params.didClose === 'function') this.params.didClose();
- } catch {}
- const value = (this._flowSteps && this._flowSteps.length) ? (this._flowValues || []) : undefined;
- let data;
- if (this._flowSteps && this._flowSteps.length && this._flowAutoForm && this._flowAutoForm.enabled) {
- const root = (this.dom && (this.dom.stepStack || this.dom.content || this.dom.popup)) || null;
- data = this._collectFormData(root);
- }
- if (isConfirmed === true) {
- const payload = { isConfirmed: true, isDenied: false, isDismissed: false, value };
- if (data !== undefined) payload.data = data;
- this.resolve(payload);
- } else if (isConfirmed === false) {
- const payload = { isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason || 'cancel', value };
- if (data !== undefined) payload.data = data;
- this.resolve(payload);
- } else {
- const payload = { isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason || 'backdrop', value };
- if (data !== undefined) payload.data = data;
- this.resolve(payload);
- }
- }
- _assignFormValue(data, name, value, forceArray) {
- if (!data || !name) return;
- let key = name;
- let asArray = !!forceArray;
- if (key.endsWith('[]')) {
- key = key.slice(0, -2);
- asArray = true;
- }
- if (!(key in data)) {
- data[key] = asArray ? [value] : value;
- return;
- }
- if (Array.isArray(data[key])) {
- data[key].push(value);
- return;
- }
- data[key] = [data[key], value];
- }
- _collectFormData(root) {
- const data = {};
- if (!root || !root.querySelectorAll) return data;
- const fields = root.querySelectorAll('input, select, textarea');
- fields.forEach((el) => {
- try {
- if (!el || el.disabled) return;
- const name = (el.getAttribute('name') || '').trim();
- if (!name) return;
- const tag = (el.tagName || '').toLowerCase();
- if (tag === 'select') {
- if (el.multiple) {
- const values = Array.from(el.options || []).filter((o) => o.selected).map((o) => o.value);
- values.forEach((v) => this._assignFormValue(data, name, v, true));
- } else {
- this._assignFormValue(data, name, el.value, false);
- }
- return;
- }
- if (tag === 'textarea') {
- this._assignFormValue(data, name, el.value, false);
- return;
- }
- const type = (el.getAttribute('type') || 'text').toLowerCase();
- if (type === 'radio') {
- if (el.checked) this._assignFormValue(data, name, el.value, false);
- return;
- }
- if (type === 'checkbox') {
- if (el.checked) this._assignFormValue(data, name, el.value, true);
- return;
- }
- if (type === 'file') {
- const files = el.files ? Array.from(el.files) : [];
- this._assignFormValue(data, name, files, true);
- return;
- }
- this._assignFormValue(data, name, el.value, false);
- } catch {}
- });
- return data;
- }
- _normalizeDomSpec(params) {
- // Preferred: params.dom (selector/Element/template)
- // Backward: params.content (selector/Element) OR advanced object { dom, mode, clone }
- const p = params || {};
- let dom = p.dom;
- let mode = p.domMode || 'move';
- if (dom == null && p.content != null) {
- if (typeof p.content === 'object' && !(p.content instanceof Element)) {
- if (p.content && (p.content.dom != null || p.content.selector != null)) {
- dom = (p.content.dom != null) ? p.content.dom : p.content.selector;
- if (p.content.mode) mode = p.content.mode;
- if (p.content.clone === true) mode = 'clone';
- }
- } else {
- dom = p.content;
- }
- }
- if (dom == null) return null;
- return { dom, mode };
- }
- _mountDomContentInto(target, domSpec, opts = null) {
- // Like _mountDomContent, but mounts into the provided container.
- const collectFlow = !!(opts && opts.collectFlow);
- const originalContent = this.dom.content;
- // Temporarily redirect this.dom.content for reuse of internal logic.
- try { this.dom.content = target; } catch {}
- try {
- const before = this._mounted;
- this._mountDomContent(domSpec);
- const rec = this._mounted;
- // If we mounted in move-mode, _mounted holds record; detach it from single-mode tracking.
- if (collectFlow && rec && rec.kind === 'move') {
- this._flowMountedList.push(rec);
- this._mounted = before; // restore previous single record (usually null)
- }
- } finally {
- try { this.dom.content = originalContent; } catch {}
- }
- }
- _unmountFlowMounted() {
- // Restore all moved DOM nodes for flow steps
- const list = Array.isArray(this._flowMountedList) ? this._flowMountedList : [];
- this._flowMountedList = [];
- list.forEach((m) => {
- try {
- if (!m || m.kind !== 'move') return;
- // Reuse single unmount logic by swapping _mounted
- const prev = this._mounted;
- this._mounted = m;
- this._unmountDomContent();
- this._mounted = prev;
- } catch {}
- });
- }
- _mountDomContent(domSpec) {
- try {
- if (!this.dom.content) return;
- this._unmountDomContent(); // ensure only one mount at a time
- const forceVisible = (el) => {
- if (!el) return { prevHidden: false, prevInlineDisplay: '' };
- const prevHidden = !!el.hidden;
- const prevInlineDisplay = (el.style && typeof el.style.display === 'string') ? el.style.display : '';
- try { el.hidden = false; } catch {}
- try {
- const cs = (typeof getComputedStyle === 'function') ? getComputedStyle(el) : null;
- const display = cs ? String(cs.display || '') : '';
- // If element is hidden via CSS (e.g. .hidden-dom{display:none}),
- // add an inline override so it becomes visible inside the popup.
- if (display === 'none') el.style.display = 'block';
- else if (el.style && el.style.display === 'none') el.style.display = 'block';
- } catch {}
- return { prevHidden, prevInlineDisplay };
- };
- let node = domSpec.dom;
- if (typeof node === 'string') node = document.querySelector(node);
- if (!node) return;
- // <template> support: always clone template content
- if (typeof HTMLTemplateElement !== 'undefined' && node instanceof HTMLTemplateElement) {
- const frag = node.content.cloneNode(true);
- this.dom.content.appendChild(frag);
- this._mounted = { kind: 'template' };
- return;
- }
- if (!(node instanceof Element)) return;
- if (domSpec.mode === 'clone') {
- const clone = node.cloneNode(true);
- // If original is hidden (via attr or CSS), the clone may inherit; force show it in popup.
- try { clone.hidden = false; } catch {}
- try {
- const cs = (typeof getComputedStyle === 'function') ? getComputedStyle(node) : null;
- const display = cs ? String(cs.display || '') : '';
- if (display === 'none') clone.style.display = 'block';
- } catch {}
- this.dom.content.appendChild(clone);
- this._mounted = { kind: 'clone' };
- return;
- }
- // Default: move into popup but restore on close
- const placeholder = document.createComment('layer-dom-placeholder');
- const parent = node.parentNode;
- const nextSibling = node.nextSibling;
- if (!parent) {
- // Detached node: moving would lose it when overlay is removed; clone instead.
- const clone = node.cloneNode(true);
- try { clone.hidden = false; } catch {}
- try { if (clone.style && clone.style.display === 'none') clone.style.display = ''; } catch {}
- this.dom.content.appendChild(clone);
- this._mounted = { kind: 'clone' };
- return;
- }
- try { parent.insertBefore(placeholder, nextSibling); } catch {}
- const { prevHidden, prevInlineDisplay } = forceVisible(node);
- this.dom.content.appendChild(node);
- this._mounted = { kind: 'move', originalEl: node, placeholder, parent, nextSibling, prevHidden, prevInlineDisplay };
- } catch {
- // ignore
- }
- }
- _unmountDomContent() {
- const m = this._mounted;
- if (!m) return;
- this._mounted = null;
- if (m.kind !== 'move') return;
- const node = m.originalEl;
- if (!node) return;
- // Restore hidden/display
- try { node.hidden = !!m.prevHidden; } catch {}
- try {
- if (node.style && typeof m.prevInlineDisplay === 'string') node.style.display = m.prevInlineDisplay;
- } catch {}
- // Move back to original position
- try {
- const ph = m.placeholder;
- if (ph && ph.parentNode) {
- ph.parentNode.insertBefore(node, ph);
- ph.parentNode.removeChild(ph);
- return;
- }
- } catch {}
- // Fallback: append to original parent
- try {
- if (m.parent) m.parent.appendChild(node);
- } catch {}
- }
- _setButtonsDisabled(disabled) {
- try {
- if (this.dom && this.dom.confirmBtn) this.dom.confirmBtn.disabled = !!disabled;
- if (this.dom && this.dom.cancelBtn) this.dom.cancelBtn.disabled = !!disabled;
- } catch {}
- }
- _normalizeButtonIcon(icon) {
- if (!icon) return null;
- try {
- if (icon && icon.nodeType) return icon.cloneNode(true);
- } catch {}
- if (typeof icon === 'string') {
- const s = icon.trim();
- if (!s) return null;
- if (s.indexOf('<') !== -1 && s.indexOf('>') !== -1) {
- const wrap = document.createElement('span');
- wrap.innerHTML = s;
- return wrap;
- }
- return document.createTextNode(s);
- }
- return null;
- }
- _setButtonContent(btn, text, icon, iconPosition) {
- if (!btn) return;
- const labelText = (text === undefined || text === null) ? '' : String(text);
- if (!icon) {
- try { btn.textContent = labelText; } catch {}
- return;
- }
- while (btn.firstChild) btn.removeChild(btn.firstChild);
- const iconNode = this._normalizeButtonIcon(icon);
- const iconWrap = el('span', `${PREFIX}btn-icon`);
- if (iconNode) iconWrap.appendChild(iconNode);
- const label = el('span', `${PREFIX}btn-label`, labelText);
- const atEnd = iconPosition === 'end';
- if (atEnd) {
- btn.appendChild(label);
- btn.appendChild(iconWrap);
- } else {
- btn.appendChild(iconWrap);
- btn.appendChild(label);
- }
- }
- async _handleConfirm() {
- // Flow next / finalize
- if (this._flowSteps && this._flowSteps.length) {
- return this._flowNext();
- }
- // Single popup: support async preConfirm
- const pre = this.params && this.params.preConfirm;
- if (typeof pre === 'function') {
- try {
- this._setButtonsDisabled(true);
- const r = pre(this.dom && this.dom.popup);
- const v = (r && typeof r.then === 'function') ? await r : r;
- if (v === false) {
- this._setButtonsDisabled(false);
- return;
- }
- // store value in non-flow mode as single value
- this._flowValues = [v];
- } catch (e) {
- console.error(e);
- this._setButtonsDisabled(false);
- return;
- }
- }
- this._close(true, 'confirm');
- }
- _handleCancel() {
- if (this._flowSteps && this._flowSteps.length) {
- // default: if not first step, cancel acts as "back"
- if (this._flowIndex > 0) {
- this._flowPrev();
- return;
- }
- }
- this._close(false, 'cancel');
- }
- _getFlowStepOptions(index) {
- const step = (this._flowSteps && this._flowSteps[index]) ? this._flowSteps[index] : {};
- const base = this._flowBase || {};
- const merged = { ...base, ...step };
- // Default button texts for flow
- const isLast = index >= (this._flowSteps.length - 1);
- if (!('confirmButtonText' in step)) merged.confirmButtonText = isLast ? (base.confirmButtonText || 'OK') : 'Next';
- // Show cancel as Back after first step (unless step explicitly overrides)
- if (index > 0) {
- if (!('showCancelButton' in step)) merged.showCancelButton = true;
- if (!('cancelButtonText' in step)) merged.cancelButtonText = 'Back';
- } else {
- // First step default keeps base settings
- if (!('cancelButtonText' in step) && merged.showCancelButton) merged.cancelButtonText = merged.cancelButtonText || 'Cancel';
- }
- // Icon/animation policy for flow:
- // - During steps: no icon/animations by default (avoids distraction + layout jitter)
- // - Allow icon only if step explicitly uses `icon:'question'`, or step is marked as summary.
- const isSummary = !!(step && (step.summary === true || step.isSummary === true));
- const explicitIcon = ('icon' in step) ? step.icon : undefined;
- const baseIcon = ('icon' in base) ? base.icon : undefined;
- const chosenIcon = (explicitIcon !== undefined) ? explicitIcon : baseIcon;
- const isContainerStep = !!(step && step.__fromContainer);
- const allowContainerIcon = isContainerStep && explicitIcon !== undefined;
- if (!isSummary && !isLast) {
- if (allowContainerIcon) {
- merged.icon = chosenIcon;
- } else {
- merged.icon = (chosenIcon === 'question') ? 'question' : null;
- merged.iconAnimation = false;
- }
- } else if (!isSummary && isLast) {
- // last step: still suppress unless summary/question/container icon
- if (allowContainerIcon) {
- merged.icon = chosenIcon;
- } else {
- merged.icon = (chosenIcon === 'question') ? 'question' : null;
- merged.iconAnimation = false;
- }
- } else {
- // summary step: allow icon; animation follows explicit config (default true only if provided elsewhere)
- if (!('icon' in step) && chosenIcon === undefined) merged.icon = null;
- }
- if (allowContainerIcon) {
- if (!('iconAnimation' in step) && !('iconAnimation' in base)) {
- merged.iconAnimation = true;
- }
- }
- return merged;
- }
- async _flowNext() {
- const idx = this._flowIndex;
- const total = this._flowSteps.length;
- const isLast = idx >= (total - 1);
- // preConfirm hook for current step
- const pre = this.params && this.params.preConfirm;
- if (typeof pre === 'function') {
- try {
- this._setButtonsDisabled(true);
- const r = pre(this.dom && this.dom.popup, idx);
- const v = (r && typeof r.then === 'function') ? await r : r;
- if (v === false) {
- this._setButtonsDisabled(false);
- return;
- }
- this._flowValues[idx] = v;
- } catch (e) {
- console.error(e);
- this._setButtonsDisabled(false);
- return;
- }
- }
- if (isLast) {
- this._close(true, 'confirm');
- return;
- }
- this._setButtonsDisabled(false);
- await this._flowGo(idx + 1, 'next');
- }
- async _flowPrev() {
- const idx = this._flowIndex;
- if (idx <= 0) return;
- await this._flowGo(idx - 1, 'prev');
- }
- async _flowGo(index, direction) {
- const next = (this._flowResolved && this._flowResolved[index]) ? this._flowResolved[index] : this._getFlowStepOptions(index);
- await this._transitionToFlow(index, next, direction);
- }
- async _transitionToFlow(nextIndex, nextOptions, direction) {
- const popup = this.dom && this.dom.popup;
- const content = this.dom && this.dom.content;
- const panes = this.dom && this.dom.stepPanes;
- if (!popup || !content || !panes || !panes.length) {
- this._flowIndex = nextIndex;
- this.params = nextOptions;
- this._render({ flow: true });
- return;
- }
- const fromIndex = this._flowIndex;
- const fromPane = panes[fromIndex];
- const toPane = panes[nextIndex];
- if (!fromPane || !toPane) {
- this._flowIndex = nextIndex;
- this.params = nextOptions;
- this._render({ flow: true });
- return;
- }
- // Measure current content height
- const oldH = content.getBoundingClientRect().height;
- // Prepare target pane for measurement without affecting layout
- const prevDisplay = toPane.style.display;
- const prevPos = toPane.style.position;
- const prevVis = toPane.style.visibility;
- const prevPointer = toPane.style.pointerEvents;
- toPane.style.display = '';
- toPane.style.position = 'absolute';
- toPane.style.visibility = 'hidden';
- toPane.style.pointerEvents = 'none';
- toPane.style.left = '0';
- toPane.style.right = '0';
- const newH = toPane.getBoundingClientRect().height;
- // Restore pane styles (keep hidden until animation starts)
- toPane.style.position = prevPos;
- toPane.style.visibility = prevVis;
- toPane.style.pointerEvents = prevPointer;
- toPane.style.display = prevDisplay; // usually 'none'
- // Apply new options (title/buttons/icon policy) before showing the pane
- this._flowIndex = nextIndex;
- this.params = nextOptions;
- this._applyPopupTheme(this.params);
- const isNext = direction !== 'prev';
- const enterFrom = isNext ? 100 : -100;
- const exitTo = isNext ? -100 : 100;
- const slideDuration = 320;
- // Update title with directional slide (keep in sync with panes)
- let titleAnim = null;
- let titleCleanup = null;
- try {
- const titleEl = this.dom && this.dom.title;
- if (titleEl) {
- const nextTitle = this.params.title || '';
- const prevTitle = titleEl.textContent || '';
- if (prevTitle !== nextTitle) {
- const clone = titleEl.cloneNode(true);
- try { clone.textContent = prevTitle; } catch {}
- try {
- const popupRect = popup.getBoundingClientRect();
- const titleRect = titleEl.getBoundingClientRect();
- clone.style.position = 'absolute';
- clone.style.left = '0';
- clone.style.right = '0';
- clone.style.top = Math.max(0, titleRect.top - popupRect.top) + 'px';
- clone.style.width = '100%';
- clone.style.pointerEvents = 'none';
- clone.style.willChange = 'transform, opacity';
- popup.appendChild(clone);
- } catch {}
- titleEl.textContent = nextTitle;
- try {
- titleEl.style.willChange = 'transform, opacity';
- titleEl.style.transform = `translateX(${enterFrom}%)`;
- titleEl.style.opacity = '0';
- } catch {}
- if (titleEl.animate && clone.animate) {
- const inAnim = titleEl.animate(
- [
- { transform: `translateX(${enterFrom}%)`, opacity: 0.2 },
- { transform: 'translateX(0%)', opacity: 1 }
- ],
- { duration: slideDuration, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
- );
- const outAnim = clone.animate(
- [
- { transform: 'translateX(0%)', opacity: 1 },
- { transform: `translateX(${exitTo}%)`, opacity: 0.1 }
- ],
- { duration: slideDuration, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
- );
- titleAnim = Promise.all([
- inAnim.finished ? inAnim.finished.catch(() => {}) : Promise.resolve(),
- outAnim.finished ? outAnim.finished.catch(() => {}) : Promise.resolve()
- ]);
- } else {
- const ease = 'cubic-bezier(0.2, 0.9, 0.2, 1)';
- const transition = `transform ${slideDuration}ms ${ease}, opacity ${slideDuration}ms ${ease}`;
- try { titleEl.style.transition = transition; } catch {}
- try { clone.style.transition = transition; } catch {}
- requestAnimationFrame(() => {
- try {
- titleEl.style.transform = 'translateX(0%)';
- titleEl.style.opacity = '1';
- clone.style.transform = `translateX(${exitTo}%)`;
- clone.style.opacity = '0.1';
- } catch {}
- });
- titleAnim = new Promise((resolve) => setTimeout(resolve, slideDuration + 40));
- }
- titleCleanup = () => {
- try { if (clone && clone.parentNode) clone.parentNode.removeChild(clone); } catch {}
- try { titleEl.style.willChange = ''; } catch {}
- try { titleEl.style.transition = ''; } catch {}
- try { titleEl.style.transform = ''; } catch {}
- try { titleEl.style.opacity = ''; } catch {}
- };
- } else {
- titleEl.textContent = nextTitle;
- }
- }
- } catch {}
- const nextBgSpec = this._normalizeBackgroundSpec(this.params && this.params.background);
- const bgTransition = this._transitionBackground(nextBgSpec, direction);
- this._applyPopupSize(this.params);
- // Icon updates (only if needed)
- try {
- const wantsIcon = !!this.params.icon;
- if (!wantsIcon && this.dom.icon) {
- this._destroySvgAniIcon();
- this.dom.icon.remove();
- this.dom.icon = null;
- } else if (wantsIcon) {
- const same = this._isSameIconType(this.dom.icon, this.params.icon);
- if (!this.dom.icon || !same) {
- if (this.dom.icon) {
- this._destroySvgAniIcon();
- this.dom.icon.remove();
- }
- this.dom.icon = this._createIcon(this.params.icon);
- // icon should be on top (before title)
- popup.insertBefore(this.dom.icon, this.dom.title || popup.firstChild);
- }
- }
- } catch {}
- this._updateFlowActions();
- // Prepare panes for animation
- toPane.style.display = '';
- toPane.style.position = 'absolute';
- toPane.style.left = '0';
- toPane.style.right = '0';
- toPane.style.top = '0';
- toPane.style.transform = `translateX(${enterFrom}%)`;
- toPane.style.opacity = '0';
- toPane.style.pointerEvents = 'none';
- fromPane.style.position = 'absolute';
- fromPane.style.left = '0';
- fromPane.style.right = '0';
- fromPane.style.top = '0';
- fromPane.style.transform = 'translateX(0%)';
- fromPane.style.opacity = '1';
- fromPane.style.pointerEvents = 'none';
- // Lock content height during transition
- content.style.height = oldH + 'px';
- content.style.overflow = 'hidden';
- let heightAnim = null;
- let fromAnim = null;
- let toAnim = null;
- let slideCleanup = null;
- try {
- heightAnim = content.animate(
- [{ height: oldH + 'px' }, { height: newH + 'px' }],
- { duration: 220, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
- );
- } catch {}
- // Ensure the new pane is in layout before starting slide (fixes missing transitions on some browsers).
- try { void toPane.offsetWidth; } catch {}
- await new Promise((resolve) => requestAnimationFrame(resolve));
- try {
- if (fromPane.animate && toPane.animate) {
- fromAnim = fromPane.animate(
- [
- { transform: 'translateX(0%)', opacity: 1 },
- { transform: `translateX(${exitTo}%)`, opacity: 0.1 }
- ],
- { duration: slideDuration, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
- );
- toAnim = toPane.animate(
- [
- { transform: `translateX(${enterFrom}%)`, opacity: 0.2 },
- { transform: 'translateX(0%)', opacity: 1 }
- ],
- { duration: slideDuration, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', fill: 'forwards' }
- );
- } else {
- const ease = 'cubic-bezier(0.2, 0.9, 0.2, 1)';
- const transition = `transform ${slideDuration}ms ${ease}, opacity ${slideDuration}ms ${ease}`;
- fromPane.style.transition = transition;
- toPane.style.transition = transition;
- slideCleanup = () => {
- try { fromPane.style.transition = ''; } catch {}
- try { toPane.style.transition = ''; } catch {}
- };
- // Force a layout so transitions apply reliably.
- void fromPane.offsetWidth;
- requestAnimationFrame(() => {
- try {
- fromPane.style.transform = `translateX(${exitTo}%)`;
- fromPane.style.opacity = '0.1';
- toPane.style.transform = 'translateX(0%)';
- toPane.style.opacity = '1';
- } catch {}
- });
- const fallback = new Promise((resolve) => setTimeout(resolve, slideDuration + 40));
- fromAnim = fallback;
- toAnim = fallback;
- }
- } catch {}
- try {
- await Promise.all([
- heightAnim && heightAnim.finished ? heightAnim.finished.catch(() => {}) : Promise.resolve(),
- fromAnim && fromAnim.finished ? fromAnim.finished.catch(() => {}) : Promise.resolve(),
- toAnim && toAnim.finished ? toAnim.finished.catch(() => {}) : Promise.resolve(),
- bgTransition && typeof bgTransition.then === 'function' ? bgTransition.catch(() => {}) : Promise.resolve(),
- titleAnim && typeof titleAnim.then === 'function' ? titleAnim.catch(() => {}) : Promise.resolve()
- ]);
- } catch {}
- try { if (titleCleanup) titleCleanup(); } catch {}
- try { if (slideCleanup) slideCleanup(); } catch {}
- // Cleanup styles and hide old pane
- fromPane.style.display = 'none';
- fromPane.style.position = '';
- fromPane.style.left = '';
- fromPane.style.right = '';
- fromPane.style.top = '';
- fromPane.style.transform = '';
- fromPane.style.opacity = '';
- fromPane.style.pointerEvents = '';
- toPane.style.position = 'relative';
- toPane.style.left = '';
- toPane.style.right = '';
- toPane.style.top = '';
- toPane.style.transform = '';
- toPane.style.opacity = '';
- toPane.style.pointerEvents = '';
- content.style.height = '';
- content.style.overflow = '';
- // Re-adjust ring background if icon exists (rare in flow)
- try { this._adjustRingBackgroundColor(); } catch {}
- }
- _isSameIconType(iconEl, type) {
- if (!iconEl) return false;
- const raw = String(type || '').trim();
- if (isSvgAniIcon(raw)) {
- const name = getSvgAniIconName(raw);
- return iconEl.dataset && iconEl.dataset.svgani === name;
- }
- try {
- return iconEl.classList && iconEl.classList.contains(raw);
- } catch {
- return false;
- }
- }
- _destroySvgAniIcon() {
- const icon = this.dom && this.dom.icon;
- if (!icon) return;
- const inst = icon._svgAniInstance;
- if (inst && typeof inst.destroy === 'function') {
- try { inst.destroy(); } catch {}
- }
- try { icon._svgAniInstance = null; } catch {}
- }
- _initSvgAniIcon(iconEl, name) {
- if (!iconEl) return;
- const container = iconEl.querySelector(`.${PREFIX}svgAni`);
- if (!container) return;
- const cleanName = String(name || '').trim();
- if (!cleanName) return;
- const url = resolveSvgAniJsonUrl(cleanName);
- if (!url) return;
- const wantsLoop = (this.params && typeof this.params.iconLoop === 'boolean')
- ? this.params.iconLoop
- : true;
- const wantsAutoplay = !(this.params && this.params.iconAnimation === false);
- const showError = (msg) => {
- try {
- container.textContent = String(msg || '');
- container.classList.add(`${PREFIX}svgAni-error`);
- } catch {}
- };
- const loadData = () => new Promise((resolve, reject) => {
- try {
- if (typeof fetch === 'function') {
- fetch(url).then((res) => {
- if (!res || !res.ok) throw new Error('fetch failed');
- return res.json();
- }).then(resolve).catch(reject);
- return;
- }
- const xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.responseType = 'json';
- xhr.onload = () => {
- if (xhr.status >= 200 && xhr.status < 300) {
- const data = xhr.response || (xhr.responseText ? JSON.parse(xhr.responseText) : null);
- resolve(data);
- } else {
- reject(new Error('xhr failed'));
- }
- };
- xhr.onerror = () => reject(new Error('xhr error'));
- xhr.send();
- } catch (e) {
- reject(e);
- }
- });
- (async () => {
- const svgAniLib = await ensureSvgAniLib();
- if (!svgAniLib) {
- showError('SvgAni lib not loaded.');
- return;
- }
- const connected = await waitForConnected(iconEl);
- if (!connected) return;
- let data = null;
- try { data = await loadData(); } catch {}
- if (!data) {
- showError('Failed to load animation.');
- return;
- }
- if (!iconEl.isConnected) return;
- try {
- const anim = svgAniLib.loadAnimation({
- container,
- renderer: 'svg',
- loop: wantsLoop,
- autoplay: wantsAutoplay,
- animationData: data
- });
- iconEl._svgAniInstance = anim;
- if (!wantsAutoplay && anim && typeof anim.goToAndStop === 'function') {
- anim.goToAndStop(0, true);
- }
- } catch {
- showError('Failed to init animation.');
- }
- })();
- }
- _rerenderInside() {
- this._destroySvgAniIcon();
- // Update popup content without recreating overlay (used in flow transitions)
- const popup = this._clearPopup();
- if (!popup) return;
- this._applyPopupTheme(this.params);
- // Icon
- this.dom.icon = null;
- if (this.params.icon) {
- this.dom.icon = this._createIcon(this.params.icon);
- popup.appendChild(this.dom.icon);
- }
- // Title
- this.dom.title = null;
- if (this.params.title) {
- this.dom.title = el('h2', `${PREFIX}title`, this.params.title);
- popup.appendChild(this.dom.title);
- }
- // Content
- this.dom.content = null;
- if (this.params.text || this.params.html || this.params.content || this.params.dom) {
- this.dom.content = el('div', `${PREFIX}content`);
- const domSpec = this._normalizeDomSpec(this.params);
- if (domSpec) this._mountDomContent(domSpec);
- else if (this.params.html) this.dom.content.innerHTML = this.params.html;
- else this.dom.content.textContent = this.params.text;
- popup.appendChild(this.dom.content);
- }
- // Actions / buttons
- this.dom.actions = el('div', `${PREFIX}actions`);
- this.dom.cancelBtn = null;
- if (this.params.showCancelButton) {
- this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
- this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText);
- this.dom.cancelBtn.onclick = () => this._handleCancel();
- this.dom.actions.appendChild(this.dom.cancelBtn);
- }
- this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
- this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText);
- this.dom.confirmBtn.onclick = () => this._handleConfirm();
- this.dom.actions.appendChild(this.dom.confirmBtn);
- popup.appendChild(this.dom.actions);
- // Re-run open hooks for each step
- try {
- if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
- } catch {}
- }
- }
- return Layer;
- })));
|