|
@@ -133,6 +133,7 @@
|
|
|
this._flowBase = null; // base options merged into each step
|
|
this._flowBase = null; // base options merged into each step
|
|
|
this._flowResolved = null; // resolved merged steps
|
|
this._flowResolved = null; // resolved merged steps
|
|
|
this._flowMountedList = []; // mounted DOM records for move-mode steps
|
|
this._flowMountedList = []; // mounted DOM records for move-mode steps
|
|
|
|
|
+ this._flowAutoForm = null; // { enabled: true } when using step container shorthand
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Constructor helper when called as function: const popup = Layer({...})
|
|
// Constructor helper when called as function: const popup = Layer({...})
|
|
@@ -188,16 +189,27 @@
|
|
|
|
|
|
|
|
// Instance entry point (chainable)
|
|
// Instance entry point (chainable)
|
|
|
run(options) {
|
|
run(options) {
|
|
|
- // Flow mode: if configured via .step()/.steps(), ignore per-call options and use steps
|
|
|
|
|
|
|
+ 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 (this._flowSteps && this._flowSteps.length) {
|
|
|
- if (options !== undefined && options !== null) {
|
|
|
|
|
|
|
+ if (hasArgs) {
|
|
|
// allow providing base options at fire-time
|
|
// allow providing base options at fire-time
|
|
|
- this.params = { ...(this.params || {}), ...normalizeOptions(options, arguments[1], arguments[2]) };
|
|
|
|
|
|
|
+ this.params = { ...(this.params || {}), ...this._stripStepOptions(merged) };
|
|
|
}
|
|
}
|
|
|
return this._fireFlow();
|
|
return this._fireFlow();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const merged = (options === undefined) ? (this.params || {}) : options;
|
|
|
|
|
return this._fire(merged);
|
|
return this._fire(merged);
|
|
|
}
|
|
}
|
|
|
// Backward-compatible alias
|
|
// Backward-compatible alias
|
|
@@ -275,6 +287,50 @@
|
|
|
return (typeof x === 'function') ? x : null;
|
|
return (typeof x === 'function') ? x : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ _stripStepOptions(options) {
|
|
|
|
|
+ const out = { ...(options || {}) };
|
|
|
|
|
+ try { delete out.step; } catch {}
|
|
|
|
|
+ try { delete out.stepItem; } catch {}
|
|
|
|
|
+ return out;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _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 } = 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 };
|
|
|
|
|
+ try {
|
|
|
|
|
+ const title =
|
|
|
|
|
+ (item.getAttribute('data-step-title') || item.getAttribute('data-layer-title') || item.getAttribute('title') || '').trim();
|
|
|
|
|
+ if (title) step.title = title;
|
|
|
|
|
+ } catch {}
|
|
|
|
|
+ return step;
|
|
|
|
|
+ });
|
|
|
|
|
+ this._flowAutoForm = { enabled: true };
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
_render(meta = null) {
|
|
_render(meta = null) {
|
|
|
// Remove existing if any (but first, try to restore any mounted DOM from the previous instance)
|
|
// Remove existing if any (but first, try to restore any mounted DOM from the previous instance)
|
|
|
const existing = document.querySelector(`.${PREFIX}overlay`);
|
|
const existing = document.querySelector(`.${PREFIX}overlay`);
|
|
@@ -939,13 +995,86 @@
|
|
|
} catch {}
|
|
} catch {}
|
|
|
|
|
|
|
|
const value = (this._flowSteps && this._flowSteps.length) ? (this._flowValues || []) : undefined;
|
|
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) {
|
|
if (isConfirmed === true) {
|
|
|
- this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false, value });
|
|
|
|
|
|
|
+ const payload = { isConfirmed: true, isDenied: false, isDismissed: false, value };
|
|
|
|
|
+ if (data !== undefined) payload.data = data;
|
|
|
|
|
+ this.resolve(payload);
|
|
|
} else if (isConfirmed === false) {
|
|
} else if (isConfirmed === false) {
|
|
|
- this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason || 'cancel', value });
|
|
|
|
|
|
|
+ const payload = { isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason || 'cancel', value };
|
|
|
|
|
+ if (data !== undefined) payload.data = data;
|
|
|
|
|
+ this.resolve(payload);
|
|
|
} else {
|
|
} else {
|
|
|
- this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: reason || 'backdrop', value });
|
|
|
|
|
|
|
+ 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) {
|
|
_normalizeDomSpec(params) {
|