pict-section-formeditor 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,6 +47,10 @@ class FormEditorExampleApplication extends libPictApplication
47
47
  ManifestDataAddress: 'AppData.FormConfig',
48
48
  DefaultDestinationAddress: '#FormEditor-Container',
49
49
  ActiveTab: 'visual',
50
+ ExtendedDescriptorProperties:
51
+ [
52
+ { Name: 'Units', Address: 'PictForm.Units', DataType: 'String', Description: 'Unit of measure (e.g. kg, lbs, meters)' }
53
+ ],
50
54
  Renderables:
51
55
  [
52
56
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-formeditor",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Pict visual editor for pict-section-form configurations",
5
5
  "main": "source/Pict-Section-FormEditor.js",
6
6
  "scripts": {
@@ -26,13 +26,13 @@
26
26
  "dependencies": {
27
27
  "pict-section-code": "^1.0.2",
28
28
  "pict-section-content": "^0.0.6",
29
- "pict-section-form": "^1.0.189",
29
+ "pict-section-form": "^1.0.190",
30
30
  "pict-section-markdowneditor": "^1.0.0",
31
31
  "pict-section-objecteditor": "^1.0.0",
32
32
  "pict-view": "^1.0.66"
33
33
  },
34
34
  "devDependencies": {
35
- "pict": "^1.0.350",
35
+ "pict": "^1.0.351",
36
36
  "quackage": "^1.0.56"
37
37
  },
38
38
  "mocha": {
@@ -13,6 +13,24 @@ module.exports = (
13
13
  // Which tab is active by default: 'visual', 'objecteditor', 'json'
14
14
  ActiveTab: 'visual',
15
15
 
16
+ // Extended descriptor properties to display in the Input properties panel.
17
+ // Each entry defines a custom field that maps to a dot-notation address
18
+ // within the Descriptor object (e.g. 'PictForm.Units' for Descriptor.PictForm.Units).
19
+ //
20
+ // Example:
21
+ // [
22
+ // { Name: 'Units', Address: 'PictForm.Units', DataType: 'String' },
23
+ // { Name: 'Extra Data', Address: 'ExtraData', DataType: 'String' },
24
+ // { Name: 'Entity', Address: 'PictForm.Configuration.Entity', DataType: 'String' }
25
+ // ]
26
+ //
27
+ // Each entry supports:
28
+ // Name - Display label in the properties panel
29
+ // Address - Dot-notation path relative to the Descriptor (required)
30
+ // DataType - 'String' (default), 'Number', or 'Boolean'
31
+ // Description - Optional tooltip / placeholder text
32
+ ExtendedDescriptorProperties: [],
33
+
16
34
  CSS: /*css*/`
17
35
  .pict-formeditor
18
36
  {
@@ -3397,6 +3415,48 @@ module.exports = (
3397
3415
  line-height: 1.5;
3398
3416
  }
3399
3417
 
3418
+ /* ---- Export Buttons ---- */
3419
+ .pict-fe-export-buttons
3420
+ {
3421
+ display: flex;
3422
+ gap: 10px;
3423
+ flex-wrap: wrap;
3424
+ }
3425
+ .pict-fe-export-btn
3426
+ {
3427
+ display: inline-flex;
3428
+ align-items: center;
3429
+ gap: 6px;
3430
+ padding: 8px 16px;
3431
+ border: 1px solid #D4C4A8;
3432
+ border-radius: 6px;
3433
+ background: #FDFCFA;
3434
+ color: #3D3229;
3435
+ font-size: 13px;
3436
+ font-weight: 600;
3437
+ cursor: pointer;
3438
+ transition: border-color 0.15s, background 0.15s;
3439
+ }
3440
+ .pict-fe-export-btn:hover
3441
+ {
3442
+ border-color: #9E6B47;
3443
+ background: #FAF5EE;
3444
+ }
3445
+ .pict-fe-export-btn:active
3446
+ {
3447
+ background: #F3EAE0;
3448
+ }
3449
+ .pict-fe-export-btn svg
3450
+ {
3451
+ flex-shrink: 0;
3452
+ }
3453
+ .pict-fe-import-export-divider
3454
+ {
3455
+ border: none;
3456
+ border-top: 1px solid #E8E0D4;
3457
+ margin: 4px 0;
3458
+ }
3459
+
3400
3460
  /* ---- Toast Notifications ---- */
3401
3461
  .pict-fe-toast-container
3402
3462
  {
@@ -729,6 +729,15 @@ class PictProviderFormEditorIconography
729
729
  '<circle cx="9" cy="18" r="' + tmpR + '" fill="currentColor"/>' +
730
730
  '<circle cx="15" cy="18" r="' + tmpR + '" fill="currentColor"/>');
731
731
  };
732
+
733
+ // Download — downward arrow into a tray (uses currentColor)
734
+ this._Icons.Action['Download'] = function(pSize, pColors, pSW)
735
+ {
736
+ return _svg(pSize,
737
+ '<path d="M12 3 L12 15" stroke="currentColor" stroke-width="' + pSW + '"/>' +
738
+ '<path d="M8 11 L12 15 L16 11" stroke="currentColor" stroke-width="' + pSW + '" fill="none"/>' +
739
+ '<path d="M4 17 L4 20 L20 20 L20 17" stroke="currentColor" stroke-width="' + pSW + '" fill="none"/>');
740
+ };
732
741
  }
733
742
 
734
743
  /* ======================================================================== */
@@ -605,6 +605,25 @@ class FormEditorRendering extends libPictProvider
605
605
  let tmpHTML = '';
606
606
 
607
607
  tmpHTML += '<div class="pict-fe-import-container">';
608
+
609
+ // Export section
610
+ tmpHTML += '<h3 class="pict-fe-import-title">Export Form Configuration</h3>';
611
+ tmpHTML += '<p class="pict-fe-import-description">Download the current form configuration as a JSON manifest or CSV spreadsheet.</p>';
612
+ tmpHTML += '<div class="pict-fe-export-buttons">';
613
+ tmpHTML += `<button class="pict-fe-export-btn" onclick="${tmpViewRef}.exportJSON()">`;
614
+ tmpHTML += `${tmpParent._IconographyProvider.getIcon('Action', 'Download', 18)}`;
615
+ tmpHTML += '<span>Export JSON</span>';
616
+ tmpHTML += '</button>';
617
+ tmpHTML += `<button class="pict-fe-export-btn" onclick="${tmpViewRef}.exportCSV()">`;
618
+ tmpHTML += `${tmpParent._IconographyProvider.getIcon('Action', 'Download', 18)}`;
619
+ tmpHTML += '<span>Export CSV</span>';
620
+ tmpHTML += '</button>';
621
+ tmpHTML += '</div>';
622
+
623
+ // Divider
624
+ tmpHTML += '<hr class="pict-fe-import-export-divider" />';
625
+
626
+ // Import section
608
627
  tmpHTML += '<h3 class="pict-fe-import-title">Import Form Configuration</h3>';
609
628
  tmpHTML += '<p class="pict-fe-import-description">Drop a CSV or JSON file below to load a form configuration. CSV files are processed through ManifestFactory. JSON files are loaded directly as manifests. If the file contains multiple forms, the first will be loaded and the rest will be available in the Load Configuration selector.</p>';
610
629
 
@@ -47,12 +47,15 @@ class PictViewFormEditorInlineEditing extends libPictView
47
47
  // Replace the span with an inline editor
48
48
  let tmpEditorHTML = '';
49
49
 
50
+ // Build the commit call referencing the inline editing child view
51
+ let tmpInlineRef = `${tmpViewRef}._InlineEditingView`;
52
+
50
53
  if (pProperty === 'Layout')
51
54
  {
52
55
  // Layout uses a select dropdown
53
56
  // onclick stopPropagation prevents the parent span's onclick from re-calling beginEditProperty
54
57
  let tmpLayouts = ['Record', 'Tabular', 'RecordSet'];
55
- let tmpCommitCall = `${tmpViewRef}.commitEditProperty('${pType}', ${pSectionIndex}, ${pGroupIndex}, '${pProperty}')`;
58
+ let tmpCommitCall = `${tmpInlineRef}.commitEditProperty('${pType}', ${pSectionIndex}, ${pGroupIndex}, '${pProperty}')`;
56
59
  tmpEditorHTML += `<select class="pict-fe-inline-edit-select" id="${tmpElementId}-Input" onclick="event.stopPropagation()" onchange="${tmpCommitCall}" onblur="setTimeout(function(){${tmpCommitCall}},150)" onkeydown="if(event.key==='Escape'){this.dataset.cancelled='true';this.blur();}">`;
57
60
  for (let i = 0; i < tmpLayouts.length; i++)
58
61
  {
@@ -66,7 +69,7 @@ class PictViewFormEditorInlineEditing extends libPictView
66
69
  // Name and Hash use a text input
67
70
  // onclick stopPropagation prevents the parent span's onclick from re-calling beginEditProperty
68
71
  let tmpHashClass = (pProperty === 'Hash') ? ' pict-fe-inline-edit-hash' : '';
69
- tmpEditorHTML += `<input class="pict-fe-inline-edit-input${tmpHashClass}" id="${tmpElementId}-Input" type="text" value="${this._ParentFormEditor._UtilitiesProvider._escapeAttr(tmpCurrentValue)}" onclick="event.stopPropagation()" onblur="${tmpViewRef}.commitEditProperty('${pType}', ${pSectionIndex}, ${pGroupIndex}, '${pProperty}')" onkeydown="if(event.key==='Enter'){this.blur();}if(event.key==='Escape'){this.dataset.cancelled='true';this.blur();}" />`;
72
+ tmpEditorHTML += `<input class="pict-fe-inline-edit-input${tmpHashClass}" id="${tmpElementId}-Input" type="text" value="${this._ParentFormEditor._UtilitiesProvider._escapeAttr(tmpCurrentValue)}" onclick="event.stopPropagation()" onblur="${tmpInlineRef}.commitEditProperty('${pType}', ${pSectionIndex}, ${pGroupIndex}, '${pProperty}')" onkeydown="if(event.key==='Enter'){this.blur();}if(event.key==='Escape'){this.dataset.cancelled='true';this.blur();}" />`;
70
73
  }
71
74
 
72
75
  this.pict.ContentAssignment.assignContent(`#${tmpElementId}`, tmpEditorHTML);
@@ -224,7 +227,8 @@ class PictViewFormEditorInlineEditing extends libPictView
224
227
  }
225
228
  }
226
229
 
227
- let tmpCommitCall = `${tmpViewRef}.commitEditInputDataType(${pSectionIndex}, ${pGroupIndex}, ${pRowIndex}, ${pInputIndex})`;
230
+ let tmpInlineRef = `${tmpViewRef}._InlineEditingView`;
231
+ let tmpCommitCall = `${tmpInlineRef}.commitEditInputDataType(${pSectionIndex}, ${pGroupIndex}, ${pRowIndex}, ${pInputIndex})`;
228
232
  let tmpEditorHTML = `<select class="pict-fe-inline-edit-select" id="${tmpElementId}-Input" onclick="event.stopPropagation()" onchange="${tmpCommitCall}" onblur="setTimeout(function(){${tmpCommitCall}},150)" onkeydown="if(event.key==='Escape'){this.dataset.cancelled='true';this.blur();}">`;
229
233
  for (let i = 0; i < this._ParentFormEditor._ManyfestDataTypes.length; i++)
230
234
  {
@@ -3024,6 +3024,9 @@ class PictViewFormEditorPropertiesPanel extends libPictView
3024
3024
  tmpHTML += '<div class="pict-fe-props-section-divider"></div>';
3025
3025
  tmpHTML += this._renderInputTypeProperties(tmpInputType, tmpDescriptor, tmpPanelViewRef);
3026
3026
 
3027
+ // Extended descriptor properties (configured via options or programmatic API)
3028
+ tmpHTML += this._renderExtendedDescriptorProperties(tmpDescriptor, tmpPanelViewRef);
3029
+
3027
3030
  // Solver assignment and references for this input
3028
3031
  tmpHTML += this._renderInputSolverInfo(tmpInputHash);
3029
3032
 
@@ -3749,6 +3752,195 @@ class PictViewFormEditorPropertiesPanel extends libPictView
3749
3752
  this._ParentFormEditor.renderVisualEditor();
3750
3753
  }
3751
3754
 
3755
+ /* -------------------------------------------------------------------------- */
3756
+ /* Extended Descriptor Properties */
3757
+ /* -------------------------------------------------------------------------- */
3758
+
3759
+ /**
3760
+ * Render extended descriptor properties defined via the
3761
+ * ExtendedDescriptorProperties configuration option or the
3762
+ * addExtendedDescriptorProperty() API.
3763
+ *
3764
+ * Each entry maps a display name to a dot-notation address within the
3765
+ * Descriptor object. For example { Name: 'Units', Address: 'PictForm.Units' }
3766
+ * reads from and writes to Descriptor.PictForm.Units.
3767
+ *
3768
+ * @param {object} pDescriptor - The full Descriptor object for the selected input
3769
+ * @param {string} pPanelViewRef - The browser-accessible view reference string
3770
+ * @returns {string} HTML string
3771
+ */
3772
+ _renderExtendedDescriptorProperties(pDescriptor, pPanelViewRef)
3773
+ {
3774
+ let tmpExtended = this._ParentFormEditor._ExtendedDescriptorProperties;
3775
+ if (!tmpExtended || tmpExtended.length === 0)
3776
+ {
3777
+ return '';
3778
+ }
3779
+
3780
+ let tmpHTML = '';
3781
+ tmpHTML += '<div class="pict-fe-props-section-divider"></div>';
3782
+ tmpHTML += '<div class="pict-fe-props-section-header">Extended Properties</div>';
3783
+
3784
+ for (let i = 0; i < tmpExtended.length; i++)
3785
+ {
3786
+ let tmpProp = tmpExtended[i];
3787
+ let tmpCurrentValue = this._resolveDescriptorAddress(pDescriptor, tmpProp.Address);
3788
+ let tmpDataType = tmpProp.DataType || 'String';
3789
+ let tmpDescription = tmpProp.Description || '';
3790
+ // Escape the address for safe embedding in an onclick attribute
3791
+ let tmpEscapedAddress = this._escapeAttr(tmpProp.Address);
3792
+
3793
+ tmpHTML += '<div class="pict-fe-props-field">';
3794
+ tmpHTML += `<div class="pict-fe-props-label" title="${this._escapeAttr(tmpDescription)}">${this._escapeHTML(tmpProp.Name)}</div>`;
3795
+
3796
+ if (tmpDataType === 'Boolean')
3797
+ {
3798
+ let tmpChecked = tmpCurrentValue ? ' checked' : '';
3799
+ tmpHTML += '<label class="pict-fe-props-checkbox-label">';
3800
+ tmpHTML += `<input type="checkbox" class="pict-fe-props-checkbox"${tmpChecked} onchange="${pPanelViewRef}.commitExtendedPropertyChange('${tmpEscapedAddress}', this.checked, 'Boolean')" />`;
3801
+ if (tmpDescription)
3802
+ {
3803
+ tmpHTML += ` ${this._escapeHTML(tmpDescription)}`;
3804
+ }
3805
+ tmpHTML += '</label>';
3806
+ }
3807
+ else if (tmpDataType === 'Number')
3808
+ {
3809
+ let tmpDisplayValue = (typeof tmpCurrentValue === 'number') ? String(tmpCurrentValue) : '';
3810
+ tmpHTML += `<input class="pict-fe-props-input" type="number" value="${this._escapeAttr(tmpDisplayValue)}" placeholder="${this._escapeAttr(tmpDescription)}" onchange="${pPanelViewRef}.commitExtendedPropertyChange('${tmpEscapedAddress}', this.value, 'Number')" />`;
3811
+ }
3812
+ else
3813
+ {
3814
+ let tmpDisplayValue = (typeof tmpCurrentValue === 'string') ? tmpCurrentValue : (tmpCurrentValue !== null && tmpCurrentValue !== undefined ? String(tmpCurrentValue) : '');
3815
+ tmpHTML += `<input class="pict-fe-props-input" type="text" value="${this._escapeAttr(tmpDisplayValue)}" placeholder="${this._escapeAttr(tmpDescription)}" onchange="${pPanelViewRef}.commitExtendedPropertyChange('${tmpEscapedAddress}', this.value, 'String')" />`;
3816
+ }
3817
+
3818
+ tmpHTML += '</div>';
3819
+ }
3820
+
3821
+ return tmpHTML;
3822
+ }
3823
+
3824
+ /**
3825
+ * Resolve a dot-notation address within a Descriptor object.
3826
+ *
3827
+ * @param {object} pDescriptor - The Descriptor object
3828
+ * @param {string} pAddress - Dot-notation path (e.g. 'PictForm.Units')
3829
+ * @returns {*} The resolved value, or undefined if not found
3830
+ */
3831
+ _resolveDescriptorAddress(pDescriptor, pAddress)
3832
+ {
3833
+ if (!pDescriptor || typeof pAddress !== 'string')
3834
+ {
3835
+ return undefined;
3836
+ }
3837
+
3838
+ let tmpSegments = pAddress.split('.');
3839
+ let tmpCurrent = pDescriptor;
3840
+
3841
+ for (let i = 0; i < tmpSegments.length; i++)
3842
+ {
3843
+ if (tmpCurrent === null || tmpCurrent === undefined || typeof tmpCurrent !== 'object')
3844
+ {
3845
+ return undefined;
3846
+ }
3847
+ tmpCurrent = tmpCurrent[tmpSegments[i]];
3848
+ }
3849
+
3850
+ return tmpCurrent;
3851
+ }
3852
+
3853
+ /**
3854
+ * Set a value at a dot-notation address within a Descriptor object,
3855
+ * creating intermediate objects as needed.
3856
+ *
3857
+ * @param {object} pDescriptor - The Descriptor object
3858
+ * @param {string} pAddress - Dot-notation path (e.g. 'PictForm.Units')
3859
+ * @param {*} pValue - The value to set
3860
+ */
3861
+ _setDescriptorAddress(pDescriptor, pAddress, pValue)
3862
+ {
3863
+ if (!pDescriptor || typeof pAddress !== 'string')
3864
+ {
3865
+ return;
3866
+ }
3867
+
3868
+ let tmpSegments = pAddress.split('.');
3869
+ let tmpCurrent = pDescriptor;
3870
+
3871
+ // Navigate to (and create) intermediate objects
3872
+ for (let i = 0; i < tmpSegments.length - 1; i++)
3873
+ {
3874
+ if (!tmpCurrent.hasOwnProperty(tmpSegments[i]) || typeof tmpCurrent[tmpSegments[i]] !== 'object')
3875
+ {
3876
+ tmpCurrent[tmpSegments[i]] = {};
3877
+ }
3878
+ tmpCurrent = tmpCurrent[tmpSegments[i]];
3879
+ }
3880
+
3881
+ let tmpFinalKey = tmpSegments[tmpSegments.length - 1];
3882
+
3883
+ if (pValue === undefined || pValue === null || pValue === '')
3884
+ {
3885
+ delete tmpCurrent[tmpFinalKey];
3886
+ }
3887
+ else
3888
+ {
3889
+ tmpCurrent[tmpFinalKey] = pValue;
3890
+ }
3891
+ }
3892
+
3893
+ /**
3894
+ * Commit a change to an extended descriptor property.
3895
+ *
3896
+ * @param {string} pAddress - Dot-notation path within the Descriptor (e.g. 'PictForm.Units')
3897
+ * @param {*} pValue - The new value from the form control
3898
+ * @param {string} pDataType - 'String', 'Number', or 'Boolean'
3899
+ */
3900
+ commitExtendedPropertyChange(pAddress, pValue, pDataType)
3901
+ {
3902
+ if (!this._SelectedInput || !this._ParentFormEditor)
3903
+ {
3904
+ return;
3905
+ }
3906
+
3907
+ let tmpResolved = this._resolveSelectedDescriptor();
3908
+ if (!tmpResolved || !tmpResolved.Descriptor)
3909
+ {
3910
+ return;
3911
+ }
3912
+
3913
+ let tmpFinalValue;
3914
+
3915
+ switch (pDataType)
3916
+ {
3917
+ case 'Boolean':
3918
+ tmpFinalValue = !!pValue;
3919
+ break;
3920
+
3921
+ case 'Number':
3922
+ {
3923
+ let tmpNumValue = parseFloat(pValue);
3924
+ if (isNaN(tmpNumValue) || pValue === '')
3925
+ {
3926
+ tmpFinalValue = undefined;
3927
+ }
3928
+ else
3929
+ {
3930
+ tmpFinalValue = tmpNumValue;
3931
+ }
3932
+ break;
3933
+ }
3934
+
3935
+ default: // String
3936
+ tmpFinalValue = (typeof pValue === 'string' && pValue.length > 0) ? pValue : undefined;
3937
+ break;
3938
+ }
3939
+
3940
+ this._setDescriptorAddress(tmpResolved.Descriptor, pAddress, tmpFinalValue);
3941
+ this._ParentFormEditor.renderVisualEditor();
3942
+ }
3943
+
3752
3944
  /* -------------------------------------------------------------------------- */
3753
3945
  /* Options Tab */
3754
3946
  /* -------------------------------------------------------------------------- */
@@ -16,6 +16,7 @@ const libChildPictManager = require('../providers/Pict-Provider-ChildPictManager
16
16
  const libPreviewCSS = require('../providers/Pict-Provider-PreviewCSS.js');
17
17
  const libFormEditorDocumentation = require('../providers/Pict-Provider-FormEditorDocumentation.js');
18
18
  const libManifestFactory = require('pict-section-form').ManifestFactory;
19
+ const libManifestConversionToCSV = require('pict-section-form').ManifestConversionToCSV;
19
20
 
20
21
  class PictViewFormEditor extends libPictView
21
22
  {
@@ -44,6 +45,28 @@ class PictViewFormEditor extends libPictView
44
45
  this._ContentEditorView = null;
45
46
  this._ContentEditorContext = null;
46
47
 
48
+ // Extended descriptor properties for the Input properties panel.
49
+ // Populated from options.ExtendedDescriptorProperties and can be
50
+ // modified at runtime via addExtendedDescriptorProperty().
51
+ this._ExtendedDescriptorProperties = [];
52
+ if (Array.isArray(tmpOptions.ExtendedDescriptorProperties))
53
+ {
54
+ for (let i = 0; i < tmpOptions.ExtendedDescriptorProperties.length; i++)
55
+ {
56
+ let tmpProp = tmpOptions.ExtendedDescriptorProperties[i];
57
+ if (tmpProp && typeof tmpProp.Address === 'string' && tmpProp.Address.length > 0)
58
+ {
59
+ this._ExtendedDescriptorProperties.push(
60
+ {
61
+ Name: tmpProp.Name || tmpProp.Address,
62
+ Address: tmpProp.Address,
63
+ DataType: tmpProp.DataType || 'String',
64
+ Description: tmpProp.Description || ''
65
+ });
66
+ }
67
+ }
68
+ }
69
+
47
70
  // Supported Manyfest DataTypes
48
71
  this._ManyfestDataTypes =
49
72
  [
@@ -108,7 +131,7 @@ class PictViewFormEditor extends libPictView
108
131
  );
109
132
  this._RenderingProvider._ParentFormEditor = this;
110
133
 
111
- // Create the documentation provider for the embedded help system
134
+ // Create the documentation provider for the embedded help system.
112
135
  let tmpDocumentationHash = `${pServiceHash || 'FormEditor'}-Documentation`;
113
136
  this._DocumentationProvider = this.pict.addProvider(
114
137
  tmpDocumentationHash,
@@ -208,6 +231,12 @@ class PictViewFormEditor extends libPictView
208
231
  this.fable.addServiceType('ManifestFactory', libManifestFactory);
209
232
  }
210
233
 
234
+ // Register ManifestConversionToCSV service type if not already present (needed for CSV export)
235
+ if (!this.fable.servicesMap.hasOwnProperty('ManifestConversionToCSV'))
236
+ {
237
+ this.fable.addServiceType('ManifestConversionToCSV', libManifestConversionToCSV);
238
+ }
239
+
211
240
  // Ensure the manifest data address exists in AppData
212
241
  let tmpManifest = this._resolveManifestData();
213
242
  if (!tmpManifest)
@@ -951,6 +980,167 @@ class PictViewFormEditor extends libPictView
951
980
  this.pict.ContentAssignment.assignContent(`#FormEditor-ImportStatus-${tmpHash}`, tmpHTML);
952
981
  }
953
982
 
983
+ /* -------------------------------------------------------------------------- */
984
+ /* Code Section: Export (CSV / JSON) */
985
+ /* -------------------------------------------------------------------------- */
986
+
987
+ /**
988
+ * Export the current manifest as a JSON file download.
989
+ */
990
+ exportJSON()
991
+ {
992
+ let tmpManifest = this._resolveManifestData();
993
+ if (!tmpManifest)
994
+ {
995
+ this._showToast('error', 'No manifest data to export.');
996
+ return;
997
+ }
998
+
999
+ let tmpJSON = JSON.stringify(tmpManifest, null, '\t');
1000
+ let tmpFileName = tmpManifest.Scope || tmpManifest.Form || 'manifest';
1001
+ this._triggerFileDownload(tmpFileName + '.json', tmpJSON, 'application/json');
1002
+ this._showToast('success', `Exported JSON: ${tmpFileName}.json`);
1003
+ }
1004
+
1005
+ /**
1006
+ * Export the current manifest as a CSV file download.
1007
+ *
1008
+ * Uses ManifestConversionToCSV from pict-section-form to convert the
1009
+ * manifest to a tabular array, then formats as a CSV string.
1010
+ */
1011
+ exportCSV()
1012
+ {
1013
+ let tmpManifest = this._resolveManifestData();
1014
+ if (!tmpManifest)
1015
+ {
1016
+ this._showToast('error', 'No manifest data to export.');
1017
+ return;
1018
+ }
1019
+
1020
+ // Instantiate the conversion service without registering it globally
1021
+ let tmpConverter = this.fable.instantiateServiceProviderWithoutRegistration('ManifestConversionToCSV', {}, `${this.UUID}-CSVExport`);
1022
+
1023
+ let tmpCSVDataArray = tmpConverter.createTabularArrayFromManifests(tmpManifest);
1024
+
1025
+ if (!tmpCSVDataArray || tmpCSVDataArray.length === 0)
1026
+ {
1027
+ this._showToast('error', 'Could not convert manifest to CSV.');
1028
+ return;
1029
+ }
1030
+
1031
+ // Convert the 2D array to a CSV string with proper escaping
1032
+ let tmpCSVLines = [];
1033
+ for (let i = 0; i < tmpCSVDataArray.length; i++)
1034
+ {
1035
+ let tmpRow = tmpCSVDataArray[i];
1036
+ let tmpEscapedRow = [];
1037
+ for (let j = 0; j < tmpRow.length; j++)
1038
+ {
1039
+ let tmpCell = tmpRow[j];
1040
+ if ((typeof tmpCell === 'string') && ((tmpCell.indexOf(',') >= 0) || (tmpCell.indexOf('"') >= 0) || (tmpCell.indexOf('\n') >= 0)))
1041
+ {
1042
+ tmpEscapedRow.push('"' + tmpCell.replace(/"/g, '""') + '"');
1043
+ }
1044
+ else
1045
+ {
1046
+ tmpEscapedRow.push(tmpCell);
1047
+ }
1048
+ }
1049
+ tmpCSVLines.push(tmpEscapedRow.join(','));
1050
+ }
1051
+ let tmpCSVString = tmpCSVLines.join('\n');
1052
+
1053
+ let tmpFileName = tmpManifest.Scope || tmpManifest.Form || 'manifest';
1054
+ this._triggerFileDownload(tmpFileName + '.csv', tmpCSVString, 'text/csv');
1055
+ this._showToast('success', `Exported CSV: ${tmpFileName}.csv`);
1056
+ }
1057
+
1058
+ /**
1059
+ * Trigger a browser file download from an in-memory string.
1060
+ *
1061
+ * @param {string} pFileName - The suggested file name
1062
+ * @param {string} pContent - The file content
1063
+ * @param {string} pMimeType - MIME type for the blob
1064
+ */
1065
+ _triggerFileDownload(pFileName, pContent, pMimeType)
1066
+ {
1067
+ if (typeof document === 'undefined')
1068
+ {
1069
+ return;
1070
+ }
1071
+
1072
+ let tmpBlob = new Blob([pContent], { type: pMimeType });
1073
+ let tmpURL = URL.createObjectURL(tmpBlob);
1074
+
1075
+ let tmpLink = document.createElement('a');
1076
+ tmpLink.href = tmpURL;
1077
+ tmpLink.download = pFileName;
1078
+ tmpLink.style.display = 'none';
1079
+ document.body.appendChild(tmpLink);
1080
+ tmpLink.click();
1081
+
1082
+ // Clean up
1083
+ document.body.removeChild(tmpLink);
1084
+ URL.revokeObjectURL(tmpURL);
1085
+ }
1086
+
1087
+ /* -------------------------------------------------------------------------- */
1088
+ /* Code Section: Extended Descriptor Properties */
1089
+ /* -------------------------------------------------------------------------- */
1090
+
1091
+ /**
1092
+ * Add an extended descriptor property to the Input properties panel.
1093
+ *
1094
+ * @param {string} pAddress - Dot-notation path relative to the Descriptor (e.g. 'PictForm.Units')
1095
+ * @param {string} [pName] - Display label (defaults to the last segment of the address)
1096
+ * @param {string} [pDataType] - 'String' (default), 'Number', or 'Boolean'
1097
+ * @param {string} [pDescription] - Tooltip / placeholder text
1098
+ */
1099
+ addExtendedDescriptorProperty(pAddress, pName, pDataType, pDescription)
1100
+ {
1101
+ if (typeof pAddress !== 'string' || pAddress.length === 0)
1102
+ {
1103
+ return;
1104
+ }
1105
+
1106
+ // Avoid duplicates
1107
+ for (let i = 0; i < this._ExtendedDescriptorProperties.length; i++)
1108
+ {
1109
+ if (this._ExtendedDescriptorProperties[i].Address === pAddress)
1110
+ {
1111
+ return;
1112
+ }
1113
+ }
1114
+
1115
+ let tmpSegments = pAddress.split('.');
1116
+ let tmpDefaultName = tmpSegments[tmpSegments.length - 1];
1117
+
1118
+ this._ExtendedDescriptorProperties.push(
1119
+ {
1120
+ Name: pName || tmpDefaultName,
1121
+ Address: pAddress,
1122
+ DataType: pDataType || 'String',
1123
+ Description: pDescription || ''
1124
+ });
1125
+ }
1126
+
1127
+ /**
1128
+ * Remove an extended descriptor property by address.
1129
+ *
1130
+ * @param {string} pAddress - Dot-notation path to remove
1131
+ */
1132
+ removeExtendedDescriptorProperty(pAddress)
1133
+ {
1134
+ for (let i = 0; i < this._ExtendedDescriptorProperties.length; i++)
1135
+ {
1136
+ if (this._ExtendedDescriptorProperties[i].Address === pAddress)
1137
+ {
1138
+ this._ExtendedDescriptorProperties.splice(i, 1);
1139
+ return;
1140
+ }
1141
+ }
1142
+ }
1143
+
954
1144
  /**
955
1145
  * Show a floating toast notification inside the form editor.
956
1146
  *