(() => {
'use strict';
if (window.WorkflowModels && window.WorkflowModelUtils) return;
const registry = new Map();
function register(type, spec) {
if (!type || !spec) throw new Error('Workflow model registration requires type and spec.');
registry.set(String(type), spec);
return spec;
}
function get(type) {
return registry.get(String(type)) || null;
}
function has(type) {
return registry.has(String(type));
}
function list() {
return Array.from(registry.keys());
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function deepClone(value) {
return JSON.parse(JSON.stringify(value));
}
const SAFE_NAME_RE = /^[a-z0-9](?:[a-z0-9_-]{1,62}[a-z0-9])?$/;
function canonicalizeName(raw, { max = 64, fallback = '' } = {}) {
let s = String(raw || '').trim();
if (!s) return fallback;
s = s.replace(/\s+/g, '_');
try {
s = s.normalize('NFKD');
} catch {}
s = s.replace(/[^\w-]+/g, '');
s = s.replace(/_+/g, '_').toLowerCase();
s = s.replace(/^[^a-z0-9]+/, '');
s = s.replace(/[^a-z0-9]+$/, '');
return s.slice(0, max);
}
function dispatchValueEvents(el) {
if (!el) return;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function formatBytes(bytes) {
const n = Number(bytes);
if (!Number.isFinite(n) || n <= 0) return '';
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1).replace(/\.0$/, '')} KB`;
return `${(n / (1024 * 1024)).toFixed(2).replace(/\.00$/, '')} MB`;
}
function readTextFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(String(e.target?.result || ''));
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsText(file);
});
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000;
let binary = '';
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
function defaultMetaHtml({
label = 'Loaded content',
sizeBytes,
content = '',
includeLines = true,
includeChars = true,
extraBits = []
} = {}) {
const bits = [String(label || 'Loaded content')];
const sizeLabel = formatBytes(sizeBytes);
if (sizeLabel) bits.push(sizeLabel);
if (includeLines) {
const lines = String(content || '').split(/\r?\n/).length;
bits.push(`${lines} lines`);
}
if (includeChars) {
bits.push(`${String(content || '').length.toLocaleString()} chars`);
}
(Array.isArray(extraBits) ? extraBits : []).forEach((bit) => {
if (bit != null && String(bit).trim()) bits.push(String(bit));
});
return bits
.map((v, i) => {
const isSize = /(?:\d+\s?(?:KB|MB|B))$/.test(v);
if (i === 0) return escapeHtml(v);
return `• ${escapeHtml(v)}`;
})
.join(' ');
}
function createDropZoneController(config = {}) {
const scope = config.scope || document;
const refs = {
block: typeof config.block === 'string' ? scope.querySelector(config.block) : config.block,
drop: typeof config.dropZone === 'string' ? scope.querySelector(config.dropZone) : config.dropZone,
input: typeof config.fileInput === 'string' ? scope.querySelector(config.fileInput) : config.fileInput,
meta: typeof config.metaEl === 'string' ? scope.querySelector(config.metaEl) : config.metaEl,
removeBtn: typeof config.removeBtn === 'string' ? scope.querySelector(config.removeBtn) : config.removeBtn,
contentField: typeof config.contentField === 'string' ? scope.querySelector(config.contentField) : config.contentField,
filenameField: typeof config.filenameField === 'string' ? scope.querySelector(config.filenameField) : config.filenameField,
sourceField: typeof config.sourceField === 'string' ? scope.querySelector(config.sourceField) : config.sourceField,
sizeField: typeof config.sizeField === 'string' ? scope.querySelector(config.sizeField) : config.sizeField
};
if (!refs.drop || !refs.input) return null;
const emptyMetaText = config.emptyMetaText || 'Drop file here or click to upload';
const readFile = typeof config.readFile === 'function' ? config.readFile : readTextFile;
const beforeRead = typeof config.beforeRead === 'function' ? config.beforeRead : null;
const onSet = typeof config.onSet === 'function' ? config.onSet : null;
const onClear = typeof config.onClear === 'function' ? config.onClear : null;
const onError = typeof config.onError === 'function' ? config.onError : null;
const renderFilledMeta = typeof config.renderFilledMeta === 'function' ? config.renderFilledMeta : null;
const uploadSourceValue = config.uploadSourceValue || 'upload';
const clearInputOnSet = config.clearInputOnSet !== false;
const listeners = [];
function addListener(el, type, fn, options) {
if (!el) return;
el.addEventListener(type, fn, options);
listeners.push(() => el.removeEventListener(type, fn, options));
}
function getState() {
const content = String(refs.contentField?.value || '');
const filename = String(refs.filenameField?.value || '').trim();
const source = String(refs.sourceField?.value || '').trim();
const sizeBytes = Number(refs.sizeField?.value || NaN);
return {
content,
filename,
source,
sizeBytes,
hasContent: content.trim().length > 0
};
}
function renderMeta() {
if (!refs.meta || !refs.drop) return;
const state = getState();
refs.drop.classList.toggle('is-filled', state.hasContent);
if (!state.hasContent) {
refs.meta.textContent = emptyMetaText;
return;
}
if (renderFilledMeta) {
const custom = renderFilledMeta({ state, refs, controller });
if (typeof custom === 'string') {
refs.meta.innerHTML = custom;
return;
}
if (custom && typeof custom === 'object') {
refs.meta.innerHTML = defaultMetaHtml({
label: custom.label,
sizeBytes: custom.sizeBytes ?? state.sizeBytes,
content: custom.content ?? state.content,
includeLines: custom.includeLines !== false,
includeChars: custom.includeChars !== false,
extraBits: custom.extraBits || []
});
return;
}
}
refs.meta.innerHTML = defaultMetaHtml({
label: state.filename || 'Loaded file',
sizeBytes: state.sizeBytes,
content: state.content
});
}
function setValue(text, meta = {}) {
if (refs.contentField) refs.contentField.value = String(text || '');
if (refs.filenameField) refs.filenameField.value = String(meta.name || '');
if (refs.sourceField) refs.sourceField.value = String(meta.source || uploadSourceValue);
if (refs.sizeField) refs.sizeField.value = meta.sizeBytes != null ? String(meta.sizeBytes) : '';
if (clearInputOnSet && refs.input) refs.input.value = '';
dispatchValueEvents(refs.contentField);
dispatchValueEvents(refs.filenameField);
dispatchValueEvents(refs.sourceField);
dispatchValueEvents(refs.sizeField);
renderMeta();
if (onSet) onSet({ text: String(text || ''), meta, refs, controller });
}
function clearValue(extra = {}) {
if (refs.contentField) refs.contentField.value = '';
if (refs.filenameField) refs.filenameField.value = '';
if (refs.sourceField) refs.sourceField.value = '';
if (refs.sizeField) refs.sizeField.value = '';
if (refs.input) refs.input.value = '';
dispatchValueEvents(refs.contentField);
dispatchValueEvents(refs.filenameField);
dispatchValueEvents(refs.sourceField);
dispatchValueEvents(refs.sizeField);
renderMeta();
if (onClear) onClear({ refs, controller, extra });
}
async function readAndSet(file) {
if (!file) return;
try {
if (beforeRead) {
const ok = await beforeRead({ file, refs, controller });
if (ok === false) return;
}
const raw = await readFile(file);
setValue(raw, {
name: file.name || '',
source: uploadSourceValue,
sizeBytes: file.size
});
} catch (err) {
console.error(err);
if (onError) onError(err, { file, refs, controller });
else window.showToast?.('error', err?.message || 'Failed to read file.');
}
}
function bind() {
addListener(refs.drop, 'click', () => refs.input.click());
addListener(refs.drop, 'keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
refs.input.click();
}
});
addListener(refs.drop, 'dragover', (e) => {
const isFile = Array.from(e.dataTransfer?.types || []).includes('Files');
if (!isFile) return;
e.preventDefault();
refs.drop.classList.add('dragover');
});
addListener(refs.drop, 'dragleave', () => {
refs.drop.classList.remove('dragover');
});
addListener(refs.drop, 'drop', (e) => {
const files = Array.from(e.dataTransfer?.files || []);
const file = files[0];
if (!file) return;
e.preventDefault();
refs.drop.classList.remove('dragover');
readAndSet(file);
});
addListener(refs.input, 'change', () => {
const file = refs.input.files?.[0];
if (file) readAndSet(file);
});
addListener(refs.removeBtn, 'click', (e) => {
e.preventDefault();
e.stopPropagation();
clearValue({ fromRemoveButton: true });
});
renderMeta();
}
function destroy() {
listeners.splice(0).forEach((off) => {
try { off(); } catch {}
});
}
const controller = {
refs,
bind,
destroy,
getState,
setValue,
clearValue,
readAndSet,
refresh: renderMeta
};
return controller;
}
function clearViciLookupForBlock(block) {
if (!block) return;
const panel = block.querySelector('.vici-lookup') || block.closest('.molecule-block')?.querySelector('.vici-lookup');
if (!panel) return;
try {
window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: false });
} catch (err) {
console.error(err);
}
}
function resetViciLookups(scope = document) {
scope.querySelectorAll('.vici-lookup').forEach((panel) => {
try {
window.ViciLookup?.clear?.(panel, { keepInput: false, keepTarget: false });
} catch (err) {
console.error(err);
}
});
}
window.WorkflowModels = {
register,
get,
has,
list
};
window.WorkflowModelUtils = {
escapeHtml,
deepClone,
SAFE_NAME_RE,
canonicalizeName,
dispatchValueEvents,
formatBytes,
readTextFile,
arrayBufferToBase64,
createDropZoneController,
clearViciLookupForBlock,
resetViciLookups,
defaultMetaHtml
};
})();