UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

315 lines (274 loc) 10.9 kB
class JoeWorkflowWidget extends HTMLElement { constructor() { super(); this.updateTimer = null; this._rendering = false; this._renderScheduled = false; this.config = this.getDefaultConfig(); this.joeIndex = null; this.widgetFieldName = null; this._liveObj = null; // constructed snapshot for live completion when autoUpdate enabled } static get observedAttributes() { return ['schema', '_id']; } connectedCallback() { this.schemaName = this.getAttribute('schema'); this.objectId = this.getAttribute('_id'); this.classList.add('joe-workflow-widget'); // Get workflow config from field definition this.workflowField = this.closest('.joe-object-field'); if (this.workflowField) { var fieldName = this.workflowField.getAttribute('data-name'); this.widgetFieldName = fieldName; if (fieldName && window._joe) { var fieldDef = window._joe.getField(fieldName); this.config = (fieldDef && fieldDef.workflow_config) || this.getDefaultConfig(); } } this.config = this.config || this.getDefaultConfig(); // Determine joeIndex for navigation and updates try{ var overlay = this.closest('.joe-overlay'); if(overlay && overlay.getAttribute('data-joeindex') !== null){ this.joeIndex = parseInt(overlay.getAttribute('data-joeindex'), 10); if(isNaN(this.joeIndex)){ this.joeIndex = null; } } }catch(e){} // Get schema name from JOE if not provided if (!this.schemaName && window._joe && window._joe.current && window._joe.current.schema) { this.schemaName = window._joe.current.schema.__schemaname || window._joe.current.schema.name; } this.scheduleRender(); this.setupUpdateListener(); } getDefaultConfig() { return { sections: 'all', fields: 'all', excludeSections: ['system'], excludeFields: ['_id', 'created', 'joeUpdated', 'itemtype', 'tags', 'status'], mustBeTrue: [], autoUpdate: false // Disabled by default to prevent lockups }; } setupUpdateListener() { // Only set up listeners if autoUpdate is enabled if (!this.config || !this.config.autoUpdate) { return; } // Simplest live update (no tech debt): // On change, compute a constructed snapshot once (debounced) and re-render from that. // Critically, we never call _jco(true) from render() to avoid re-entrancy loops. if(this.joeIndex === null || !window.getJoe){ return; } var self = this; var overlay = this.closest('.joe-overlay'); if(!overlay){ return; } this._onFormChange = function(ev){ if(ev && ev.target && self.contains(ev.target)){ return; } clearTimeout(self.updateTimer); self.updateTimer = setTimeout(function(){ try{ var j = getJoe(self.joeIndex); self._liveObj = (j && j.constructObjectFromFields) ? j.constructObjectFromFields(self.joeIndex) : null; }catch(e){ self._liveObj = null; } self.scheduleRender(); }, 350); }; overlay.addEventListener('change', this._onFormChange, true); } disconnectedCallback() { try{ var overlay = this.closest('.joe-overlay'); if(overlay && this._onFormChange){ overlay.removeEventListener('change', this._onFormChange, true); } }catch(e){} if (this.updateTimer) { clearTimeout(this.updateTimer); } } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { if (attr === 'schema') { this.schemaName = newValue; } else if (attr === '_id') { this.objectId = newValue; } this.scheduleRender(); } } scheduleRender() { if (this._renderScheduled) { return; } this._renderScheduled = true; var self = this; // Defer to avoid re-entrancy with JOE render/construct cycles setTimeout(function() { self._renderScheduled = false; self.render(); }, 0); } render() { var self = this; if (this._rendering) { return; } this._rendering = true; var joe = window._joe; if (!joe || !joe.current) { this.innerHTML = '<div class="joe-workflow-error">Workflow widget requires active JOE instance</div>'; this._rendering = false; return; } var fields = joe.current.fields || []; var sections = joe.current.sections || {}; // IMPORTANT: Do NOT call _jco(true) here. It triggers constructObjectFromFields which can // cause re-entrant rerenders / attributeChangedCallback loops and lock up the UI. // Use the live object snapshot. (When autoUpdate is re-enabled later, we can switch to a safer source.) var currentObj = this._liveObj || joe.current.object || {}; // Filter sections to show var sectionsToShow = []; if (this.config.sections === 'all') { for (var secId in sections) { if (this.config.excludeSections && this.config.excludeSections.indexOf(secId) !== -1) continue; if (secId === 'system') continue; if (!sections[secId] || !sections[secId].fields || !sections[secId].fields.length) continue; sectionsToShow.push(secId); } } else if (Array.isArray(this.config.sections)) { sectionsToShow = this.config.sections.filter(function(secId) { return sections[secId] && (!self.config.excludeSections || self.config.excludeSections.indexOf(secId) === -1); }); } // Calculate progress for each section var sectionProgress = {}; sectionsToShow.forEach(function(sectionId) { var section = sections[sectionId]; if (!section) return; var fieldsInSection = section.fields || []; if (fieldsInSection.length === 0) return; var completed = 0; var totalCount = 0; var missing = []; fieldsInSection.forEach(function(fname) { var field = joe.getField(fname) || { name: fname, type: 'text' }; var ftype = (joe.propAsFuncOrValue && joe.propAsFuncOrValue(field.type, currentObj)) || field.type || 'text'; ftype = (ftype || '').toString().toLowerCase(); // Never include content fields in completion counts (widgets, labels, etc). if(ftype === 'content'){ return; } if(self.config.excludeFields && self.config.excludeFields.indexOf(field.name) !== -1){ return; } if(self.config.fields === 'requiredOnly' && !joe.propAsFuncOrValue(field.required, currentObj)){ return; } if(field.hidden && joe.propAsFuncOrValue(field.hidden, currentObj)){ return; } if(field.condition && !joe.propAsFuncOrValue(field.condition, currentObj)){ return; } totalCount++; if (self.isFieldComplete(field, currentObj)) { completed++; }else{ missing.push(field.display || field.label || field.name); } }); if(!totalCount){ return; } var percentage = Math.round((completed / totalCount) * 100); sectionProgress[sectionId] = { name: section.name || sectionId, completed: completed, total: totalCount, percentage: percentage, isComplete: percentage === 100, anchor: section.anchor || sectionId, missing: missing }; }); // Render widget HTML var widgetTitle = this.config.title; if (!widgetTitle && this.schemaName) { // Capitalize first letter of schema name widgetTitle = (this.schemaName.charAt(0).toUpperCase() + this.schemaName.slice(1)) + ' Workflow'; } if (!widgetTitle) { widgetTitle = 'Workflow'; // fallback } var html = '<div class="joe-workflow-widget-header">' + widgetTitle + '</div>'; var hasSections = false; sectionsToShow.forEach(function(sectionId) { var progress = sectionProgress[sectionId]; if (!progress) return; hasSections = true; var statusHtml = progress.isComplete ? '<span class="joe-workflow-checkmark" title="Complete">✓</span>' : '<span class="joe-workflow-percentage" title="' + progress.completed + ' of ' + progress.total + ' fields complete">' + progress.percentage + '%</span>'; var ji = (self.joeIndex !== null ? self.joeIndex : 0); var safeSectionIdHtml = (sectionId || '').toString() .replace(/&/g, '&amp;') .replace(/"/g, '&quot;'); // Use double-quotes inside gotoSection(...) to avoid JS escaping issues. var clickAction = 'onclick="try{ getJoe(' + ji + ').gotoSection(&quot;' + safeSectionIdHtml + '&quot;); }catch(e){}" style="cursor:pointer;"'; var tip = ''; if(progress.missing && progress.missing.length){ tip = progress.missing.join(' | ') .replace(/&/g, '&amp;') .replace(/"/g, '&quot;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); } html += '<div class="joe-workflow-section" ' + clickAction + (tip ? (' title="' + tip + '"') : '') + '>'; html += '<div class="joe-workflow-section-label">' + progress.name + '</div>'; html += '<div class="joe-workflow-section-status">' + statusHtml + '</div>'; html += '</div>'; }); if (!hasSections) { html += '<div class="joe-workflow-empty">No sections to track</div>'; } this.innerHTML = html; this._rendering = false; } isFieldComplete(field, obj) { var value = obj[field.name]; var fieldType = (field.type || '').toLowerCase(); var mustBeTrue = this.config.mustBeTrue && Array.isArray(this.config.mustBeTrue) && this.config.mustBeTrue.indexOf(field.name) !== -1; if (value === undefined || value === null) { return false; } switch (fieldType) { case 'text': case 'rendering': case 'code': case 'wysiwyg': case 'url': return typeof value === 'string' && value.trim().length > 0; case 'objectreference': return Array.isArray(value) ? value.length > 0 : !!value; case 'objectlist': return Array.isArray(value) && value.length > 0; case 'number': return typeof value === 'number' && !isNaN(value); case 'boolean': if (mustBeTrue) { return value === true; } return typeof value === 'boolean'; case 'date': case 'date-time': return !!value; case 'select': return value !== '' && value !== null && value !== undefined; case 'uploader': return Array.isArray(value) && value.length > 0; default: if (Array.isArray(value)) { return value.length > 0; } if (typeof value === 'object') { return Object.keys(value).length > 0; } return !!value; } } } window.customElements.define('joe-workflow-widget', JoeWorkflowWidget);