(() => { '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 }; })();