@innovastudio/contentbox 1.6.161 → 1.6.163

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.
@@ -12300,6 +12300,616 @@ class PanelSpacer {
12300
12300
 
12301
12301
  }
12302
12302
 
12303
+ /**
12304
+ * ContentBox Settings UI Generator
12305
+ * Automatically generates settings form based on plugin schema
12306
+ *
12307
+ * Usage in ContentBox Editor:
12308
+ * const generator = new SettingsUIGenerator(runtime);
12309
+ * const form = generator.generateForm('logo-loop', currentElement);
12310
+ * panel.appendChild(form);
12311
+ */
12312
+ class SettingsUIGenerator$1 {
12313
+ constructor(runtime, builder) {
12314
+ this.runtime = runtime;
12315
+ this.builder = builder;
12316
+ }
12317
+ /**
12318
+ * Generate settings form for a plugin
12319
+ * @param {string} pluginName - Plugin name
12320
+ * @param {HTMLElement} element - Current element being edited
12321
+ * @returns {HTMLElement} Form element
12322
+ */
12323
+
12324
+
12325
+ generateForm(pluginName, element, onChange) {
12326
+ const plugin = this.runtime.getPlugin(pluginName);
12327
+
12328
+ if (!plugin || !plugin.settings) {
12329
+ console.warn(`Plugin "${pluginName}" has no settings schema`);
12330
+ return this.createEmptyForm();
12331
+ }
12332
+
12333
+ const form = document.createElement('div');
12334
+ form.className = 'cb-settings-form'; // Get current values from element
12335
+
12336
+ const currentValues = this.getCurrentValues(element, plugin.settings); // Check if plugin has grouped settings
12337
+
12338
+ if (plugin.settings._groups) {
12339
+ this.generateGroupedForm(form, plugin.settings, currentValues);
12340
+ } else {
12341
+ this.generateFlatForm(form, plugin.settings, currentValues);
12342
+ } // Attach listeners for auto-update
12343
+
12344
+
12345
+ if (typeof onChange === 'function') {
12346
+ form.addEventListener('input', () => {
12347
+ const values = this.getFormValues(form);
12348
+ onChange(values, element);
12349
+ });
12350
+ form.addEventListener('change', () => {
12351
+ const values = this.getFormValues(form);
12352
+ onChange(values, element);
12353
+ });
12354
+ }
12355
+
12356
+ return form;
12357
+ }
12358
+ /**
12359
+ * Generate flat form (no groups)
12360
+ */
12361
+
12362
+
12363
+ generateFlatForm(container, settings, currentValues) {
12364
+ Object.keys(settings).forEach(key => {
12365
+ if (key.startsWith('_')) return; // Skip meta keys
12366
+
12367
+ const field = settings[key];
12368
+ const fieldElement = this.createField(key, field, currentValues[key]);
12369
+ container.appendChild(fieldElement);
12370
+ });
12371
+ }
12372
+ /**
12373
+ * Generate grouped form
12374
+ */
12375
+
12376
+
12377
+ generateGroupedForm(container, settings, currentValues) {
12378
+ const groups = settings._groups || [];
12379
+ groups.forEach(group => {
12380
+ const groupElement = document.createElement('div');
12381
+ groupElement.className = 'cb-settings-group';
12382
+ const groupTitle = document.createElement('h3');
12383
+ groupTitle.className = 'cb-settings-group-title';
12384
+ groupTitle.style.cssText = 'font-size: 17px;font-weight: 500;';
12385
+ groupTitle.textContent = group.label;
12386
+ groupElement.appendChild(groupTitle);
12387
+ group.fields.forEach(fieldKey => {
12388
+ const field = settings[fieldKey];
12389
+ if (!field) return;
12390
+ const fieldElement = this.createField(fieldKey, field, currentValues[fieldKey]);
12391
+ groupElement.appendChild(fieldElement);
12392
+ });
12393
+ container.appendChild(groupElement);
12394
+ });
12395
+ }
12396
+ /**
12397
+ * Create a single form field
12398
+ */
12399
+
12400
+
12401
+ createField(name, config, value) {
12402
+ const wrapper = document.createElement('div');
12403
+ wrapper.className = 'cb-settings-field';
12404
+ wrapper.style.marginBottom = '20px';
12405
+ wrapper.setAttribute('data-field', name); // Add conditional display logic
12406
+
12407
+ if (config.dependsOn) {
12408
+ wrapper.setAttribute('data-depends-on', JSON.stringify(config.dependsOn));
12409
+ wrapper.style.display = 'none'; // Hidden by default, shown by JS
12410
+ } // Label
12411
+
12412
+
12413
+ const label = document.createElement('label');
12414
+ label.className = 'cb-settings-label';
12415
+ label.style.fontWeight = 500;
12416
+ label.textContent = config.label || name; // wrapper.appendChild(label);
12417
+ // Input based on type
12418
+
12419
+ const input = this.createInput(name, config, value);
12420
+ wrapper.appendChild(input); // Description
12421
+ // if (config.description) {
12422
+ // const desc = document.createElement('p');
12423
+ // desc.className = 'cb-settings-description';
12424
+ // desc.textContent = config.description;
12425
+ // wrapper.appendChild(desc);
12426
+ // }
12427
+
12428
+ return wrapper;
12429
+ }
12430
+ /**
12431
+ * Create input element based on field type
12432
+ */
12433
+
12434
+
12435
+ createInput(name, config, value) {
12436
+ const currentValue = value !== undefined ? value : config.default;
12437
+
12438
+ switch (config.type) {
12439
+ case 'number':
12440
+ return this.createNumberInput(name, config, currentValue);
12441
+
12442
+ case 'boolean':
12443
+ return this.createCheckboxInput(name, config, currentValue);
12444
+
12445
+ case 'select':
12446
+ return this.createSelectInput(name, config, currentValue);
12447
+
12448
+ case 'radio':
12449
+ return this.createRadioInput(name, config, currentValue);
12450
+
12451
+ case 'textarea':
12452
+ return this.createTextareaInput(name, config, currentValue);
12453
+
12454
+ case 'color':
12455
+ return this.createColorInput(name, config, currentValue);
12456
+
12457
+ case 'range':
12458
+ return this.createRangeInput(name, config, currentValue);
12459
+
12460
+ case 'text':
12461
+ default:
12462
+ return this.createTextInput(name, config, currentValue);
12463
+ }
12464
+ }
12465
+
12466
+ createTextInput(name, config, value) {
12467
+ const label = document.createElement('label');
12468
+ label.className = 'label';
12469
+ label.style.fontWeight = 500;
12470
+ const labelText = document.createElement('div');
12471
+ labelText.textContent = config.label || name;
12472
+ label.appendChild(labelText);
12473
+ const input = document.createElement('input');
12474
+ input.type = 'text';
12475
+ input.name = name;
12476
+ input.value = value || '';
12477
+ if (config.placeholder) input.placeholder = config.placeholder;
12478
+ label.appendChild(input); // Add description if exists
12479
+
12480
+ if (config.description) {
12481
+ const desc = document.createElement('small');
12482
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12483
+ desc.textContent = config.description;
12484
+ label.appendChild(desc);
12485
+ }
12486
+
12487
+ return label;
12488
+ }
12489
+
12490
+ createNumberInput(name, config, value) {
12491
+ const label = document.createElement('label');
12492
+ label.className = 'label';
12493
+ label.style.fontWeight = 500;
12494
+ const labelText = document.createElement('div');
12495
+ labelText.textContent = config.label || name;
12496
+
12497
+ if (config.unit) {
12498
+ labelText.textContent += ` (${config.unit})`;
12499
+ }
12500
+
12501
+ label.appendChild(labelText);
12502
+ const input = document.createElement('input');
12503
+ input.type = 'number';
12504
+ input.style.cssText = 'width: 100px;';
12505
+ input.name = name;
12506
+ input.value = value !== undefined ? value : config.default;
12507
+ if (config.min !== undefined) input.min = config.min;
12508
+ if (config.max !== undefined) input.max = config.max;
12509
+ if (config.step !== undefined) input.step = config.step;
12510
+ label.appendChild(input);
12511
+
12512
+ if (config.description) {
12513
+ const desc = document.createElement('small');
12514
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12515
+ desc.textContent = config.description;
12516
+ label.appendChild(desc);
12517
+ }
12518
+
12519
+ return label;
12520
+ }
12521
+
12522
+ createCheckboxInput(name, config, value) {
12523
+ const label = document.createElement('label');
12524
+ label.className = 'label checkbox';
12525
+ const div = document.createElement('div');
12526
+ const input = document.createElement('input');
12527
+ input.type = 'checkbox';
12528
+ input.name = name;
12529
+ input.checked = value !== undefined ? value : config.default;
12530
+ const span = document.createElement('span');
12531
+ span.textContent = config.label || name;
12532
+ label.appendChild(input);
12533
+ label.appendChild(span);
12534
+ div.appendChild(label);
12535
+
12536
+ if (config.description) {
12537
+ const desc = document.createElement('small');
12538
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12539
+ desc.textContent = config.description;
12540
+ div.appendChild(desc);
12541
+ }
12542
+
12543
+ return label;
12544
+ }
12545
+
12546
+ createSelectInput(name, config, value) {
12547
+ const label = document.createElement('label');
12548
+ label.className = 'label';
12549
+ label.style.fontWeight = 500;
12550
+ const labelText = document.createTextNode((config.label || name) + ':');
12551
+ label.appendChild(labelText);
12552
+ const select = document.createElement('select');
12553
+ select.name = name;
12554
+ config.options.forEach(option => {
12555
+ const opt = document.createElement('option');
12556
+ opt.value = option.value;
12557
+ opt.textContent = option.label;
12558
+ if (option.value === value) opt.selected = true;
12559
+ select.appendChild(opt);
12560
+ });
12561
+ label.appendChild(select);
12562
+
12563
+ if (config.description) {
12564
+ const desc = document.createElement('small');
12565
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12566
+ desc.textContent = config.description;
12567
+ label.appendChild(desc);
12568
+ }
12569
+
12570
+ return label;
12571
+ }
12572
+
12573
+ createRadioInput(name, config, value) {
12574
+ const wrapper = document.createElement('div');
12575
+ const title = document.createElement('div');
12576
+ title.textContent = config.label || name;
12577
+ title.style.cssText = 'margin-bottom: 8px;font-weight: 590;';
12578
+ wrapper.appendChild(title);
12579
+ config.options.forEach(option => {
12580
+ const label = document.createElement('label');
12581
+ label.className = 'label checkbox';
12582
+ const input = document.createElement('input');
12583
+ input.type = 'radio';
12584
+ input.name = name;
12585
+ input.value = option.value;
12586
+ input.checked = option.value === value;
12587
+ const span = document.createElement('span');
12588
+ span.textContent = option.label;
12589
+ label.appendChild(input);
12590
+ label.appendChild(span);
12591
+ wrapper.appendChild(label);
12592
+ });
12593
+
12594
+ if (config.description) {
12595
+ const desc = document.createElement('small');
12596
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12597
+ desc.textContent = config.description;
12598
+ wrapper.appendChild(desc);
12599
+ }
12600
+
12601
+ return wrapper;
12602
+ }
12603
+
12604
+ createTextareaInput(name, config, value) {
12605
+ const label = document.createElement('label');
12606
+ label.className = 'label';
12607
+ label.style.fontWeight = 500;
12608
+ const labelText = document.createElement('div');
12609
+ labelText.textContent = config.label || name;
12610
+ label.appendChild(labelText);
12611
+ const textarea = document.createElement('textarea');
12612
+ textarea.name = name;
12613
+ textarea.value = value || '';
12614
+ textarea.rows = config.rows || 4;
12615
+ if (config.placeholder) textarea.placeholder = config.placeholder;
12616
+ label.appendChild(textarea);
12617
+
12618
+ if (config.description) {
12619
+ const desc = document.createElement('small');
12620
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12621
+ desc.textContent = config.description;
12622
+ label.appendChild(desc);
12623
+ }
12624
+
12625
+ return label;
12626
+ }
12627
+
12628
+ createColorInput(name, config, value) {
12629
+ const container = document.createElement('div');
12630
+ const label = document.createElement('label');
12631
+ label.className = 'label';
12632
+ label.style.fontWeight = 500;
12633
+ label.style.cursor = 'pointer';
12634
+ const labelText = document.createElement('div');
12635
+ labelText.textContent = config.label || name;
12636
+ label.appendChild(labelText);
12637
+ const input = document.createElement('input');
12638
+ input.type = 'hidden';
12639
+ input.name = name;
12640
+ input.value = value || config.default || '#000000';
12641
+ const group = document.createElement('div');
12642
+ group.className = 'group';
12643
+ group.style.cssText = 'width: 52px; margin:0; overflow: visible;';
12644
+ const colorBtn = document.createElement('button');
12645
+ colorBtn.type = 'button';
12646
+ colorBtn.title = config.label || 'Color';
12647
+ colorBtn.className = 'btn-color is-btn-color';
12648
+ colorBtn.style.backgroundColor = input.value;
12649
+ colorBtn.setAttribute('aria-label', `Choose color for ${config.label || name}`);
12650
+
12651
+ const openPicker = e => {
12652
+ e.preventDefault();
12653
+ this.builder.openColorPicker(input.value, color => {
12654
+ input.value = color;
12655
+ colorBtn.style.backgroundColor = color;
12656
+ input.dispatchEvent(new Event('change', {
12657
+ bubbles: true
12658
+ }));
12659
+ }, colorBtn);
12660
+ };
12661
+
12662
+ colorBtn.addEventListener('click', openPicker); // Make label click trigger the button
12663
+
12664
+ label.addEventListener('click', e => {
12665
+ if (e.target === label || e.target === labelText) {
12666
+ colorBtn.click();
12667
+ }
12668
+ });
12669
+ group.appendChild(colorBtn);
12670
+ label.appendChild(input);
12671
+
12672
+ if (config.description) {
12673
+ const desc = document.createElement('small');
12674
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12675
+ desc.textContent = config.description;
12676
+ label.appendChild(desc);
12677
+ }
12678
+
12679
+ container.appendChild(label);
12680
+ container.appendChild(group);
12681
+ return container;
12682
+ }
12683
+
12684
+ createColorInput2(name, config, value) {
12685
+ const label = document.createElement('label');
12686
+ label.className = 'label';
12687
+ label.style.fontWeight = 500;
12688
+ const labelText = document.createElement('div');
12689
+ labelText.textContent = config.label || name;
12690
+ label.appendChild(labelText);
12691
+ const input = document.createElement('input');
12692
+ input.type = 'color';
12693
+ input.name = name;
12694
+ input.style.cssText = 'width:40px;height:40px';
12695
+ input.value = value || config.default || '#000000';
12696
+ label.appendChild(input);
12697
+
12698
+ if (config.description) {
12699
+ const desc = document.createElement('small');
12700
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12701
+ desc.textContent = config.description;
12702
+ label.appendChild(desc);
12703
+ }
12704
+
12705
+ return label;
12706
+ }
12707
+
12708
+ createRangeInput(name, config, value) {
12709
+ const label = document.createElement('label');
12710
+ label.className = 'label';
12711
+ label.style.fontWeight = 500;
12712
+ const currentVal = value !== undefined ? value : config.default;
12713
+ const labelText = document.createElement('div');
12714
+ labelText.textContent = (config.label || name) + ': ' + currentVal + (config.unit || '');
12715
+ label.appendChild(labelText);
12716
+ const input = document.createElement('input');
12717
+ input.type = 'range';
12718
+ input.className = 'is-rangeslider';
12719
+ input.name = name;
12720
+ input.value = currentVal;
12721
+ if (config.min !== undefined) input.min = config.min;
12722
+ if (config.max !== undefined) input.max = config.max;
12723
+ if (config.step !== undefined) input.step = config.step;
12724
+ input.addEventListener('input', () => {
12725
+ labelText.textContent = (config.label || name) + ': ' + input.value + (config.unit || '');
12726
+ });
12727
+ label.appendChild(input);
12728
+
12729
+ if (config.description) {
12730
+ const desc = document.createElement('small');
12731
+ desc.style.cssText = 'display: block;margin-top: 4px;';
12732
+ desc.textContent = config.description;
12733
+ label.appendChild(desc);
12734
+ }
12735
+
12736
+ return label;
12737
+ }
12738
+ /**
12739
+ * Get current values from element attributes
12740
+ */
12741
+
12742
+
12743
+ getCurrentValues(element, settings) {
12744
+ const values = {};
12745
+ Object.keys(settings).forEach(key => {
12746
+ if (key.startsWith('_')) return;
12747
+ const attrName = 'data-cb-' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
12748
+ const attrValue = element.getAttribute(attrName);
12749
+
12750
+ if (attrValue !== null) {
12751
+ values[key] = this.parseValue(attrValue, settings[key].type);
12752
+ }
12753
+ });
12754
+ return values;
12755
+ }
12756
+ /**
12757
+ * Parse attribute value based on type
12758
+ */
12759
+
12760
+
12761
+ parseValue(value, type) {
12762
+ switch (type) {
12763
+ case 'number':
12764
+ return parseFloat(value);
12765
+
12766
+ case 'boolean':
12767
+ return value === 'true';
12768
+
12769
+ default:
12770
+ return value;
12771
+ }
12772
+ }
12773
+ /**
12774
+ * Get form values
12775
+ */
12776
+
12777
+
12778
+ getFormValues(form) {
12779
+ const values = {};
12780
+ const inputs = form.querySelectorAll('input, select, textarea');
12781
+ inputs.forEach(input => {
12782
+ const name = input.name;
12783
+ if (!name) return;
12784
+
12785
+ if (input.type === 'checkbox') {
12786
+ values[name] = input.checked;
12787
+ } else if (input.type === 'radio') {
12788
+ if (input.checked) {
12789
+ values[name] = input.value;
12790
+ }
12791
+ } else {
12792
+ values[name] = input.value;
12793
+ }
12794
+ });
12795
+ return values;
12796
+ }
12797
+ /**
12798
+ * Apply form values to element
12799
+ */
12800
+
12801
+
12802
+ applyValues(element, values) {
12803
+ Object.keys(values).forEach(key => {
12804
+ const attrName = 'data-cb-' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
12805
+ element.setAttribute(attrName, values[key]);
12806
+ });
12807
+ }
12808
+
12809
+ createEmptyForm() {
12810
+ const form = document.createElement('div');
12811
+ form.className = 'cb-settings-form';
12812
+ form.innerHTML = '<p>This plugin has no configurable settings.</p>';
12813
+ return form;
12814
+ }
12815
+
12816
+ }
12817
+
12818
+ class PanelPlugin {
12819
+ constructor(panel, builder) {
12820
+ this.builder = builder;
12821
+ this.panel = panel;
12822
+ this.generator = null;
12823
+ this.currentElement = null;
12824
+ this.currentForm = null;
12825
+ }
12826
+
12827
+ render(pluginName, currentElement, runtime) {
12828
+ // Clear panel
12829
+ this.panel.innerHTML = '';
12830
+ const div = document.createElement('div');
12831
+ div.classList.add('submain');
12832
+ this.panel.appendChild(div); // Store references
12833
+
12834
+ this.currentElement = currentElement;
12835
+ this.generator = new SettingsUIGenerator$1(runtime, this.builder); // Check if plugin has custom content editor
12836
+
12837
+ const plugin = runtime.getPlugin(pluginName);
12838
+ const hasContentEditor = plugin && plugin.editor && plugin.editor.openContentEditor;
12839
+
12840
+ if (hasContentEditor) {
12841
+ // Get original content for the editor to work with
12842
+ const originalContent = currentElement.getAttribute('data-cb-original-content'); // Create a temporary element for editing (so editor can manipulate it)
12843
+
12844
+ let editableClone = document.querySelector('.editable-clone');
12845
+ if (editableClone) editableClone.remove(); // editableClone = document.createElement('div');
12846
+
12847
+ editableClone = currentElement.cloneNode(false);
12848
+ editableClone.innerHTML = originalContent;
12849
+ editableClone.className = 'editable-clone';
12850
+ editableClone.style.display = 'none'; // Hidden, just for editing
12851
+
12852
+ document.body.appendChild(editableClone);
12853
+ const originalElement = editableClone.cloneNode(false); // Let plugin handle everything - pass the editable clone
12854
+
12855
+ const editorUI = plugin.editor.openContentEditor(editableClone, this.builder, async () => {
12856
+ this.builder.editor.saveForUndo(); // Store edited content from clone
12857
+
12858
+ currentElement.setAttribute('data-cb-original-content', editableClone.innerHTML); // Also performs similar value updates like the applyChanges()
12859
+ // currentElement.setAttribute('data-cb-content', editableClone.getAttribute('data-cb-content'));
12860
+
12861
+ const getChangedDataAttributes = (el1, el2) => {
12862
+ const changed = [];
12863
+
12864
+ for (const key of Object.keys(el1.dataset)) {
12865
+ if (key in el2.dataset && el1.dataset[key] !== el2.dataset[key]) {
12866
+ changed.push(key);
12867
+ }
12868
+ }
12869
+
12870
+ return changed;
12871
+ };
12872
+
12873
+ const changedAttributes = getChangedDataAttributes(originalElement, editableClone);
12874
+ changedAttributes.forEach(attrName => {
12875
+ // console.log(attrName)
12876
+ // console.log(editableClone.dataset[attrName])
12877
+ currentElement.dataset[attrName] = editableClone.dataset[attrName];
12878
+ }); // -------
12879
+
12880
+ currentElement.innerHTML = editableClone.innerHTML; // Update current content
12881
+ // Remove temporary clone
12882
+
12883
+ editableClone.remove(); // Reinitialize plugin with new content
12884
+
12885
+ await runtime.reinitialize(currentElement.parentElement);
12886
+ this.builder.onChange();
12887
+ });
12888
+ div.appendChild(editorUI);
12889
+ } // Generate form
12890
+
12891
+
12892
+ this.currentForm = this.generator.generateForm(pluginName, currentElement, () => {
12893
+ this.builder.editor.saveForUndo();
12894
+ this.applyChanges(runtime);
12895
+ });
12896
+ div.appendChild(this.currentForm);
12897
+ }
12898
+
12899
+ async applyChanges(runtime) {
12900
+ if (!this.currentElement || !this.currentForm) return; // 1. Get form values
12901
+
12902
+ const values = this.generator.getFormValues(this.currentForm); // 2. Apply to element attributes
12903
+
12904
+ this.generator.applyValues(this.currentElement, values); // 3. Reinitialize component
12905
+
12906
+ const container = this.currentElement.parentElement || this.currentElement.closest('.is-wrapper');
12907
+ await runtime.reinitialize(container);
12908
+ this.builder.onChange(); // console.log('Settings applied and component reinitialized');
12909
+ }
12910
+
12911
+ }
12912
+
12303
12913
  class PanelIcon {
12304
12914
  constructor(panel, builder) {
12305
12915
  this.builder = builder;
@@ -25494,6 +26104,7 @@ class ControlPanel {
25494
26104
  <div class="panel-audio"></div>
25495
26105
  <div class="panel-iframe"></div>
25496
26106
  <div class="panel-spacer"></div>
26107
+ <div class="panel-plugin"></div>
25497
26108
  <div class="panel-column"></div>
25498
26109
  <div class="panel-row"></div>
25499
26110
  <div class="panel-container"></div>
@@ -25544,6 +26155,7 @@ class ControlPanel {
25544
26155
  this.panelAudio = controlPanel.querySelector('.panel-audio');
25545
26156
  this.panelIframe = controlPanel.querySelector('.panel-iframe');
25546
26157
  this.panelSpacer = controlPanel.querySelector('.panel-spacer');
26158
+ this.panelPlugin = controlPanel.querySelector('.panel-plugin');
25547
26159
  this.panelIcon = controlPanel.querySelector('.panel-icon');
25548
26160
  this.panelSvg = controlPanel.querySelector('.panel-svg');
25549
26161
  this.panelCode = controlPanel.querySelector('.panel-code');
@@ -25584,6 +26196,7 @@ class ControlPanel {
25584
26196
  this.objPanelAudio = new PanelAudio(this.panelAudio, this.builder);
25585
26197
  this.objPanelIframe = new PanelIframe(this.panelIframe, this.builder);
25586
26198
  this.objPanelSpacer = new PanelSpacer(this.panelSpacer, this.builder);
26199
+ this.objPanelPlugin = new PanelPlugin(this.panelPlugin, this.builder);
25587
26200
  this.objPanelIcon = new PanelIcon(this.panelIcon, this.builder);
25588
26201
  this.objPanelSvg = new PanelSvg(this.panelSvg, this.builder);
25589
26202
  this.objPanelCode = new PanelCode(this.panelCode, this.builder);
@@ -26240,6 +26853,7 @@ class ControlPanel {
26240
26853
  this.panelAudio.classList.remove('active');
26241
26854
  this.panelIframe.classList.remove('active');
26242
26855
  this.panelSpacer.classList.remove('active');
26856
+ this.panelPlugin.classList.remove('active');
26243
26857
  this.panelIcon.classList.remove('active');
26244
26858
  this.panelSvg.classList.remove('active');
26245
26859
  this.panelCode.classList.remove('active');
@@ -26302,13 +26916,32 @@ class ControlPanel {
26302
26916
  this.title.innerHTML = titleHtml;
26303
26917
  this.panelCode.classList.add('active');
26304
26918
  this.objPanelCode.getState();
26305
- } else if (element.closest('[data-noedit]') && !element.closest('[data-html]')) {
26919
+ } else if (element.closest('[data-noedit]') && !element.closest('[data-html]') && !element.closest('[data-cb-type]')) {
26306
26920
  let titleHtml = out('Locked');
26307
26921
  this.title.innerHTML = titleHtml;
26308
26922
  } else {
26309
26923
  if (element.querySelector('.icon') && element.childElementCount === 1) ;
26310
26924
 
26311
- if (element.tagName.toLowerCase() === 'img') {
26925
+ if (element.closest('[data-cb-type]')) {
26926
+ const runtime = this.builder.win.builderRuntime;
26927
+
26928
+ if (runtime) {
26929
+ const pluginElement = element.closest('[data-cb-type]');
26930
+ const pluginName = pluginElement.getAttribute('data-cb-type');
26931
+
26932
+ if (!pluginElement.hasAttribute('data-cb-editmode')) {
26933
+ pluginElement.setAttribute('contentEditable', false);
26934
+ }
26935
+
26936
+ const plugin = runtime.getPlugin(pluginName);
26937
+ let titleHtml = out(plugin.displayName || plugin.name);
26938
+ this.title.innerHTML = titleHtml; // this.title.style.cssText = 'margin-bottom: 0';
26939
+
26940
+ this.objPanelPlugin.render(pluginName, pluginElement, runtime);
26941
+ }
26942
+
26943
+ this.panelPlugin.classList.add('active');
26944
+ } else if (element.tagName.toLowerCase() === 'img') {
26312
26945
  // Image
26313
26946
  let titleHtml = out('Image');
26314
26947
  this.title.innerHTML = titleHtml;
@@ -32700,15 +33333,16 @@ class Util$1 {
32700
33333
  rtepop.style.display = '';
32701
33334
  dom.removeClass(rtepop, 'active');
32702
33335
  dom.removeClass(rtepop, 'deactive');
32703
- // dom.addClass(rtepop, 'deactive');
32704
33336
  });
32705
33337
  }
32706
-
32707
33338
  let pops = builderStuff.querySelectorAll('.is-pop');
32708
33339
  Array.prototype.forEach.call(pops, pop => {
32709
33340
  pop.style.display = '';
32710
33341
  dom.removeClass(pop, 'active');
32711
- pop.setAttribute('aria-hidden', true);
33342
+ this.builder.doc.body.focus();
33343
+ setTimeout(() => {
33344
+ pop.setAttribute('aria-hidden', true);
33345
+ }, 0);
32712
33346
  });
32713
33347
  this.builder.colTool.lockIndicator.style.display = '';
32714
33348
  if (this.builder.resize) {
@@ -40969,7 +41603,7 @@ class HtmlUtil {
40969
41603
  this.builder.opts.onRender();
40970
41604
 
40971
41605
  // Re-select
40972
- if (row.firstChild) row.firstChild.click();
41606
+ // if(row.firstChild) row.firstChild.click();
40973
41607
  //Change to row selection
40974
41608
  row.classList.remove('row-outline');
40975
41609
  //Hide Column tool (new!)
@@ -41022,6 +41656,8 @@ class HtmlUtil {
41022
41656
  }
41023
41657
  util.clearControls(); // NEW
41024
41658
 
41659
+ // Re-init plugins
41660
+ if (this.builder.win.builderRuntime) this.builder.win.builderRuntime.reinitialize();
41025
41661
  this.builder.hideModal(modal);
41026
41662
  }
41027
41663
  viewHtmlExternal() {
@@ -41568,6 +42204,34 @@ class HtmlUtil {
41568
42204
  elm.innerHTML = '';
41569
42205
  });
41570
42206
  }
42207
+
42208
+ // Clean all interactive components
42209
+ const components = tmp.querySelectorAll('[data-cb-type]');
42210
+ components.forEach(element => {
42211
+ const original = element.getAttribute('data-cb-original-content');
42212
+ if (original) {
42213
+ // Restore original HTML
42214
+ element.innerHTML = original;
42215
+
42216
+ // Remove runtime-added attributes
42217
+ element.removeAttribute('data-cb-original-content');
42218
+
42219
+ // element.removeAttribute('data-cb-logo-loop-initialized');
42220
+
42221
+ Array.from(element.attributes).forEach(attr => {
42222
+ const name = attr.name;
42223
+ // Remove any attribute that starts with "data-cb-" and ends with "-initialized"
42224
+ if (name.startsWith('data-cb-') && name.endsWith('-initialized')) {
42225
+ element.removeAttribute(name);
42226
+ }
42227
+ });
42228
+
42229
+ // Remove runtime-added classes
42230
+ const type = element.getAttribute('data-cb-type');
42231
+ element.classList.remove(`cb-${type}`);
42232
+ }
42233
+ element.removeAttribute('data-cb-loaded');
42234
+ });
41571
42235
  html = '';
41572
42236
  if (multiple) {
41573
42237
  //ContentBox
@@ -42423,7 +43087,7 @@ const prepareSvgIcons = builder => {
42423
43087
  <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
42424
43088
  <path d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2"></path>
42425
43089
  </symbol>
42426
- <symbol id="icon-folder" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
43090
+ <symbol id="icon-folder" viewBox="0 0 24 24" stroke-width="1.4" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
42427
43091
  <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
42428
43092
  <path d="M11 19h-6a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v2.5"></path>
42429
43093
  <path d="M18 18m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
@@ -42908,6 +43572,9 @@ class Grid {
42908
43572
  cell.click(); //refresh active cell/row
42909
43573
  }, 30);
42910
43574
  this.builder.opts.onChange();
43575
+
43576
+ // Re-init plugins
43577
+ if (this.builder.win.builderRuntime) this.builder.win.builderRuntime.reinitialize(cellElement);
42911
43578
  }
42912
43579
  removeColumn() {
42913
43580
  let util = this.util;
@@ -43681,6 +44348,9 @@ class Grid {
43681
44348
  builder.applyBehaviorOn(builderActive);
43682
44349
  this.rowTool.position(rowElement);
43683
44350
  this.builder.opts.onChange();
44351
+
44352
+ // Re-init plugins
44353
+ if (this.builder.win.builderRuntime) this.builder.win.builderRuntime.reinitialize(rowElement);
43684
44354
  }
43685
44355
  }
43686
44356
  class RowTool$1 {
@@ -77235,6 +77905,817 @@ class Spacer {
77235
77905
  }
77236
77906
  }
77237
77907
 
77908
+ /**
77909
+ * ContentBox Settings UI Generator
77910
+ * Automatically generates settings form based on plugin schema
77911
+ *
77912
+ * Usage in ContentBox Editor:
77913
+ * const generator = new SettingsUIGenerator(runtime);
77914
+ * const form = generator.generateForm('logo-loop', currentElement);
77915
+ * panel.appendChild(form);
77916
+ */
77917
+
77918
+ class SettingsUIGenerator {
77919
+ constructor(runtime, builder) {
77920
+ this.runtime = runtime;
77921
+ this.builder = builder;
77922
+ }
77923
+
77924
+ /**
77925
+ * Generate settings form for a plugin
77926
+ * @param {string} pluginName - Plugin name
77927
+ * @param {HTMLElement} element - Current element being edited
77928
+ * @returns {HTMLElement} Form element
77929
+ */
77930
+ generateForm(pluginName, element, onChange) {
77931
+ const plugin = this.runtime.getPlugin(pluginName);
77932
+ if (!plugin || !plugin.settings) {
77933
+ console.warn(`Plugin "${pluginName}" has no settings schema`);
77934
+ return this.createEmptyForm();
77935
+ }
77936
+ const form = document.createElement('div');
77937
+ form.className = 'cb-settings-form';
77938
+
77939
+ // Get current values from element
77940
+ const currentValues = this.getCurrentValues(element, plugin.settings);
77941
+
77942
+ // Check if plugin has grouped settings
77943
+ if (plugin.settings._groups) {
77944
+ this.generateGroupedForm(form, plugin.settings, currentValues);
77945
+ } else {
77946
+ this.generateFlatForm(form, plugin.settings, currentValues);
77947
+ }
77948
+
77949
+ // Attach listeners for auto-update
77950
+ if (typeof onChange === 'function') {
77951
+ form.addEventListener('input', () => {
77952
+ const values = this.getFormValues(form);
77953
+ onChange(values, element);
77954
+ });
77955
+ form.addEventListener('change', () => {
77956
+ const values = this.getFormValues(form);
77957
+ onChange(values, element);
77958
+ });
77959
+ }
77960
+ return form;
77961
+ }
77962
+
77963
+ /**
77964
+ * Generate flat form (no groups)
77965
+ */
77966
+ generateFlatForm(container, settings, currentValues) {
77967
+ Object.keys(settings).forEach(key => {
77968
+ if (key.startsWith('_')) return; // Skip meta keys
77969
+
77970
+ const field = settings[key];
77971
+ const fieldElement = this.createField(key, field, currentValues[key]);
77972
+ container.appendChild(fieldElement);
77973
+ });
77974
+ }
77975
+
77976
+ /**
77977
+ * Generate grouped form
77978
+ */
77979
+ generateGroupedForm(container, settings, currentValues) {
77980
+ const groups = settings._groups || [];
77981
+ groups.forEach(group => {
77982
+ const groupElement = document.createElement('div');
77983
+ groupElement.className = 'cb-settings-group';
77984
+ const groupTitle = document.createElement('h3');
77985
+ groupTitle.className = 'cb-settings-group-title';
77986
+ groupTitle.style.cssText = 'font-size: 17px;font-weight: 500;';
77987
+ groupTitle.textContent = group.label;
77988
+ groupElement.appendChild(groupTitle);
77989
+ group.fields.forEach(fieldKey => {
77990
+ const field = settings[fieldKey];
77991
+ if (!field) return;
77992
+ const fieldElement = this.createField(fieldKey, field, currentValues[fieldKey]);
77993
+ groupElement.appendChild(fieldElement);
77994
+ });
77995
+ container.appendChild(groupElement);
77996
+ });
77997
+ }
77998
+
77999
+ /**
78000
+ * Create a single form field
78001
+ */
78002
+ createField(name, config, value) {
78003
+ const wrapper = document.createElement('div');
78004
+ wrapper.className = 'cb-settings-field';
78005
+ wrapper.style.marginBottom = '20px';
78006
+ wrapper.setAttribute('data-field', name);
78007
+
78008
+ // Add conditional display logic
78009
+ if (config.dependsOn) {
78010
+ wrapper.setAttribute('data-depends-on', JSON.stringify(config.dependsOn));
78011
+ wrapper.style.display = 'none'; // Hidden by default, shown by JS
78012
+ }
78013
+
78014
+ // Label
78015
+ const label = document.createElement('label');
78016
+ label.className = 'cb-settings-label';
78017
+ label.style.fontWeight = 500;
78018
+ label.textContent = config.label || name;
78019
+ // wrapper.appendChild(label);
78020
+
78021
+ // Input based on type
78022
+ const input = this.createInput(name, config, value);
78023
+ wrapper.appendChild(input);
78024
+
78025
+ // Description
78026
+ // if (config.description) {
78027
+ // const desc = document.createElement('p');
78028
+ // desc.className = 'cb-settings-description';
78029
+ // desc.textContent = config.description;
78030
+ // wrapper.appendChild(desc);
78031
+ // }
78032
+
78033
+ return wrapper;
78034
+ }
78035
+
78036
+ /**
78037
+ * Create input element based on field type
78038
+ */
78039
+ createInput(name, config, value) {
78040
+ const currentValue = value !== undefined ? value : config.default;
78041
+ switch (config.type) {
78042
+ case 'number':
78043
+ return this.createNumberInput(name, config, currentValue);
78044
+ case 'boolean':
78045
+ return this.createCheckboxInput(name, config, currentValue);
78046
+ case 'select':
78047
+ return this.createSelectInput(name, config, currentValue);
78048
+ case 'radio':
78049
+ return this.createRadioInput(name, config, currentValue);
78050
+ case 'textarea':
78051
+ return this.createTextareaInput(name, config, currentValue);
78052
+ case 'color':
78053
+ return this.createColorInput(name, config, currentValue);
78054
+ case 'range':
78055
+ return this.createRangeInput(name, config, currentValue);
78056
+ case 'text':
78057
+ default:
78058
+ return this.createTextInput(name, config, currentValue);
78059
+ }
78060
+ }
78061
+ createTextInput(name, config, value) {
78062
+ const label = document.createElement('label');
78063
+ label.className = 'label';
78064
+ label.style.fontWeight = 500;
78065
+ const labelText = document.createElement('div');
78066
+ labelText.textContent = config.label || name;
78067
+ label.appendChild(labelText);
78068
+ const input = document.createElement('input');
78069
+ input.type = 'text';
78070
+ input.name = name;
78071
+ input.value = value || '';
78072
+ if (config.placeholder) input.placeholder = config.placeholder;
78073
+ label.appendChild(input);
78074
+
78075
+ // Add description if exists
78076
+ if (config.description) {
78077
+ const desc = document.createElement('small');
78078
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78079
+ desc.textContent = config.description;
78080
+ label.appendChild(desc);
78081
+ }
78082
+ return label;
78083
+ }
78084
+ createNumberInput(name, config, value) {
78085
+ const label = document.createElement('label');
78086
+ label.className = 'label';
78087
+ label.style.fontWeight = 500;
78088
+ const labelText = document.createElement('div');
78089
+ labelText.textContent = config.label || name;
78090
+ if (config.unit) {
78091
+ labelText.textContent += ` (${config.unit})`;
78092
+ }
78093
+ label.appendChild(labelText);
78094
+ const input = document.createElement('input');
78095
+ input.type = 'number';
78096
+ input.style.cssText = 'width: 100px;';
78097
+ input.name = name;
78098
+ input.value = value !== undefined ? value : config.default;
78099
+ if (config.min !== undefined) input.min = config.min;
78100
+ if (config.max !== undefined) input.max = config.max;
78101
+ if (config.step !== undefined) input.step = config.step;
78102
+ label.appendChild(input);
78103
+ if (config.description) {
78104
+ const desc = document.createElement('small');
78105
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78106
+ desc.textContent = config.description;
78107
+ label.appendChild(desc);
78108
+ }
78109
+ return label;
78110
+ }
78111
+ createCheckboxInput(name, config, value) {
78112
+ const label = document.createElement('label');
78113
+ label.className = 'label checkbox';
78114
+ const div = document.createElement('div');
78115
+ const input = document.createElement('input');
78116
+ input.type = 'checkbox';
78117
+ input.name = name;
78118
+ input.checked = value !== undefined ? value : config.default;
78119
+ const span = document.createElement('span');
78120
+ span.textContent = config.label || name;
78121
+ label.appendChild(input);
78122
+ label.appendChild(span);
78123
+ div.appendChild(label);
78124
+ if (config.description) {
78125
+ const desc = document.createElement('small');
78126
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78127
+ desc.textContent = config.description;
78128
+ div.appendChild(desc);
78129
+ }
78130
+ return label;
78131
+ }
78132
+ createSelectInput(name, config, value) {
78133
+ const label = document.createElement('label');
78134
+ label.className = 'label';
78135
+ label.style.fontWeight = 500;
78136
+ const labelText = document.createTextNode((config.label || name) + ':');
78137
+ label.appendChild(labelText);
78138
+ const select = document.createElement('select');
78139
+ select.name = name;
78140
+ config.options.forEach(option => {
78141
+ const opt = document.createElement('option');
78142
+ opt.value = option.value;
78143
+ opt.textContent = option.label;
78144
+ if (option.value === value) opt.selected = true;
78145
+ select.appendChild(opt);
78146
+ });
78147
+ label.appendChild(select);
78148
+ if (config.description) {
78149
+ const desc = document.createElement('small');
78150
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78151
+ desc.textContent = config.description;
78152
+ label.appendChild(desc);
78153
+ }
78154
+ return label;
78155
+ }
78156
+ createRadioInput(name, config, value) {
78157
+ const wrapper = document.createElement('div');
78158
+ const title = document.createElement('div');
78159
+ title.textContent = config.label || name;
78160
+ title.style.cssText = 'margin-bottom: 8px;font-weight: 590;';
78161
+ wrapper.appendChild(title);
78162
+ config.options.forEach(option => {
78163
+ const label = document.createElement('label');
78164
+ label.className = 'label checkbox';
78165
+ const input = document.createElement('input');
78166
+ input.type = 'radio';
78167
+ input.name = name;
78168
+ input.value = option.value;
78169
+ input.checked = option.value === value;
78170
+ const span = document.createElement('span');
78171
+ span.textContent = option.label;
78172
+ label.appendChild(input);
78173
+ label.appendChild(span);
78174
+ wrapper.appendChild(label);
78175
+ });
78176
+ if (config.description) {
78177
+ const desc = document.createElement('small');
78178
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78179
+ desc.textContent = config.description;
78180
+ wrapper.appendChild(desc);
78181
+ }
78182
+ return wrapper;
78183
+ }
78184
+ createTextareaInput(name, config, value) {
78185
+ const label = document.createElement('label');
78186
+ label.className = 'label';
78187
+ label.style.fontWeight = 500;
78188
+ const labelText = document.createElement('div');
78189
+ labelText.textContent = config.label || name;
78190
+ label.appendChild(labelText);
78191
+ const textarea = document.createElement('textarea');
78192
+ textarea.name = name;
78193
+ textarea.value = value || '';
78194
+ textarea.rows = config.rows || 4;
78195
+ if (config.placeholder) textarea.placeholder = config.placeholder;
78196
+ label.appendChild(textarea);
78197
+ if (config.description) {
78198
+ const desc = document.createElement('small');
78199
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78200
+ desc.textContent = config.description;
78201
+ label.appendChild(desc);
78202
+ }
78203
+ return label;
78204
+ }
78205
+ createColorInput(name, config, value) {
78206
+ const container = document.createElement('div');
78207
+ const label = document.createElement('label');
78208
+ label.className = 'label';
78209
+ label.style.fontWeight = 500;
78210
+ label.style.cursor = 'pointer';
78211
+ const labelText = document.createElement('div');
78212
+ labelText.textContent = config.label || name;
78213
+ label.appendChild(labelText);
78214
+ const input = document.createElement('input');
78215
+ input.type = 'hidden';
78216
+ input.name = name;
78217
+ input.value = value || config.default || '#000000';
78218
+ const group = document.createElement('div');
78219
+ group.className = 'group';
78220
+ group.style.cssText = 'width: 52px; margin:0; overflow: visible;';
78221
+ const colorBtn = document.createElement('button');
78222
+ colorBtn.type = 'button';
78223
+ colorBtn.title = config.label || 'Color';
78224
+ colorBtn.className = 'btn-color is-btn-color';
78225
+ colorBtn.style.backgroundColor = input.value;
78226
+ colorBtn.setAttribute('aria-label', `Choose color for ${config.label || name}`);
78227
+ const openPicker = e => {
78228
+ e.preventDefault();
78229
+ this.builder.openColorPicker(input.value, color => {
78230
+ input.value = color;
78231
+ colorBtn.style.backgroundColor = color;
78232
+ input.dispatchEvent(new Event('change', {
78233
+ bubbles: true
78234
+ }));
78235
+ }, colorBtn);
78236
+ };
78237
+ colorBtn.addEventListener('click', openPicker);
78238
+ // Make label click trigger the button
78239
+ label.addEventListener('click', e => {
78240
+ if (e.target === label || e.target === labelText) {
78241
+ colorBtn.click();
78242
+ }
78243
+ });
78244
+ group.appendChild(colorBtn);
78245
+ label.appendChild(input);
78246
+ if (config.description) {
78247
+ const desc = document.createElement('small');
78248
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78249
+ desc.textContent = config.description;
78250
+ label.appendChild(desc);
78251
+ }
78252
+ container.appendChild(label);
78253
+ container.appendChild(group);
78254
+ return container;
78255
+ }
78256
+ createColorInput2(name, config, value) {
78257
+ const label = document.createElement('label');
78258
+ label.className = 'label';
78259
+ label.style.fontWeight = 500;
78260
+ const labelText = document.createElement('div');
78261
+ labelText.textContent = config.label || name;
78262
+ label.appendChild(labelText);
78263
+ const input = document.createElement('input');
78264
+ input.type = 'color';
78265
+ input.name = name;
78266
+ input.style.cssText = 'width:40px;height:40px';
78267
+ input.value = value || config.default || '#000000';
78268
+ label.appendChild(input);
78269
+ if (config.description) {
78270
+ const desc = document.createElement('small');
78271
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78272
+ desc.textContent = config.description;
78273
+ label.appendChild(desc);
78274
+ }
78275
+ return label;
78276
+ }
78277
+ createRangeInput(name, config, value) {
78278
+ const label = document.createElement('label');
78279
+ label.className = 'label';
78280
+ label.style.fontWeight = 500;
78281
+ const currentVal = value !== undefined ? value : config.default;
78282
+ const labelText = document.createElement('div');
78283
+ labelText.textContent = (config.label || name) + ': ' + currentVal + (config.unit || '');
78284
+ label.appendChild(labelText);
78285
+ const input = document.createElement('input');
78286
+ input.type = 'range';
78287
+ input.className = 'is-rangeslider';
78288
+ input.name = name;
78289
+ input.value = currentVal;
78290
+ if (config.min !== undefined) input.min = config.min;
78291
+ if (config.max !== undefined) input.max = config.max;
78292
+ if (config.step !== undefined) input.step = config.step;
78293
+ input.addEventListener('input', () => {
78294
+ labelText.textContent = (config.label || name) + ': ' + input.value + (config.unit || '');
78295
+ });
78296
+ label.appendChild(input);
78297
+ if (config.description) {
78298
+ const desc = document.createElement('small');
78299
+ desc.style.cssText = 'display: block;margin-top: 4px;';
78300
+ desc.textContent = config.description;
78301
+ label.appendChild(desc);
78302
+ }
78303
+ return label;
78304
+ }
78305
+
78306
+ /**
78307
+ * Get current values from element attributes
78308
+ */
78309
+ getCurrentValues(element, settings) {
78310
+ const values = {};
78311
+ Object.keys(settings).forEach(key => {
78312
+ if (key.startsWith('_')) return;
78313
+ const attrName = 'data-cb-' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
78314
+ const attrValue = element.getAttribute(attrName);
78315
+ if (attrValue !== null) {
78316
+ values[key] = this.parseValue(attrValue, settings[key].type);
78317
+ }
78318
+ });
78319
+ return values;
78320
+ }
78321
+
78322
+ /**
78323
+ * Parse attribute value based on type
78324
+ */
78325
+ parseValue(value, type) {
78326
+ switch (type) {
78327
+ case 'number':
78328
+ return parseFloat(value);
78329
+ case 'boolean':
78330
+ return value === 'true';
78331
+ default:
78332
+ return value;
78333
+ }
78334
+ }
78335
+
78336
+ /**
78337
+ * Get form values
78338
+ */
78339
+ getFormValues(form) {
78340
+ const values = {};
78341
+ const inputs = form.querySelectorAll('input, select, textarea');
78342
+ inputs.forEach(input => {
78343
+ const name = input.name;
78344
+ if (!name) return;
78345
+ if (input.type === 'checkbox') {
78346
+ values[name] = input.checked;
78347
+ } else if (input.type === 'radio') {
78348
+ if (input.checked) {
78349
+ values[name] = input.value;
78350
+ }
78351
+ } else {
78352
+ values[name] = input.value;
78353
+ }
78354
+ });
78355
+ return values;
78356
+ }
78357
+
78358
+ /**
78359
+ * Apply form values to element
78360
+ */
78361
+ applyValues(element, values) {
78362
+ Object.keys(values).forEach(key => {
78363
+ const attrName = 'data-cb-' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
78364
+ element.setAttribute(attrName, values[key]);
78365
+ });
78366
+ }
78367
+ createEmptyForm() {
78368
+ const form = document.createElement('div');
78369
+ form.className = 'cb-settings-form';
78370
+ form.innerHTML = '<p>This plugin has no configurable settings.</p>';
78371
+ return form;
78372
+ }
78373
+ }
78374
+
78375
+ class Plugin {
78376
+ constructor(builder) {
78377
+ this.builder = builder;
78378
+ }
78379
+ renderTool() {
78380
+ const builderStuff = this.builder.builderStuff;
78381
+ const contentStuff = this.builder.contentStuff;
78382
+ const util = this.builder.util;
78383
+ const dom = this.builder.dom;
78384
+ let pluginTool = builderStuff.querySelector('.is-plugin-tool');
78385
+ if (!pluginTool) {
78386
+ let html = `
78387
+ <div class="is-tool is-plugin-tool">
78388
+ <button title="${util.out('Settings')}" data-title="${util.out('Settings')}" style="width:40px;height:40px;background:none;"><svg class="is-icon-flex"><use xlink:href="#icon-cog"></use></svg></button>
78389
+ </div>
78390
+ `;
78391
+ dom.appendHtml(builderStuff, html);
78392
+ if (!this.builder.iframe) {
78393
+ pluginTool = builderStuff.querySelector('.is-plugin-tool');
78394
+ } else {
78395
+ pluginTool = contentStuff.querySelector('.is-plugin-tool');
78396
+ }
78397
+ this.pluginTool = pluginTool;
78398
+ let btn = pluginTool.querySelector('button');
78399
+ dom.addEventListener(btn, 'click', () => {
78400
+ // old 10317
78401
+
78402
+ if (this.pluginModal && this.pluginModal.classList.contains('active')) {
78403
+ this.hidePluginEditor();
78404
+ } else {
78405
+ this.renderPanel();
78406
+ this.showPluginEditor();
78407
+ }
78408
+ btn.setAttribute('data-focus', true);
78409
+ });
78410
+ }
78411
+ }
78412
+ renderPanel() {
78413
+ const builderStuff = this.builder.builderStuff;
78414
+ const util = this.builder.util;
78415
+ const dom = this.builder.dom;
78416
+ let pluginModal = builderStuff.querySelector('.is-modal.pluginsettings');
78417
+ if (!pluginModal) {
78418
+ let html = `
78419
+ <div class="is-modal is-modal-content pluginsettings" tabindex="-1" role="dialog" aria-modal="true" aria-hidden="true">
78420
+ <div class="is-modal-bar is-draggable">
78421
+ <span>${util.out('Plugin Settings')}</span>
78422
+ <button class="is-modal-close" tabindex="-1" title="${util.out('Close')}">&#10005;</button>
78423
+ </div>
78424
+ <div class="is-modal-controls" style="box-sizing:border-box;padding:30px;overflow-y: auto;height: calc(100% - 30px);">
78425
+ <div class="is-tabs" data-group="table">
78426
+ <a title="${util.out('Style')}" id="tabTableGeneral" href="" data-content="divTableGeneral" class="active">${util.out('Style')}</a>
78427
+ <a title="${util.out('Layout')}" id="tabTableLayout" href="" data-content="divTableLayout">${util.out('Layout')}</a>
78428
+ </div>
78429
+ <div id="divTableGeneral" class="is-tab-content active" tabindex="-1" data-group="table" style="display:block">
78430
+
78431
+ <div style="display:flex;padding-bottom:12px">
78432
+ <div style="padding-right:15px">
78433
+ <div>${util.out('Background')}:</div>
78434
+ <div>
78435
+ <button title="${util.out('Background Color')}" class="input-table-bgcolor is-btn-color"></button>
78436
+ </div>
78437
+ </div>
78438
+ <div>
78439
+ <div>${util.out('Text Color')}:</div>
78440
+ <div>
78441
+ <button title="${util.out('Text Color')}" class="input-table-textcolor is-btn-color"></button>
78442
+ </div>
78443
+ </div>
78444
+ </div>
78445
+
78446
+ <div style="padding-bottom:12px;">
78447
+ <div>${util.out('Border Thickness')}:</div>
78448
+ <div>
78449
+ <select id="selCellBorderWidth" style="width:120px;"><option value="0">No Border</option><option value="1">1</option><option value="2">2</option><option value="3">3</option></select>
78450
+ </div>
78451
+ </div>
78452
+
78453
+ <div style="padding-bottom:12px;">
78454
+ <div>${util.out('Border Color')}:</div>
78455
+ <div>
78456
+ <button title="${util.out('Border Color')}" class="input-table-bordercolor is-btn-color"></button>
78457
+ </div>
78458
+ </div>
78459
+
78460
+ <div style="padding-bottom:12px;">
78461
+ <div>${util.out('Apply To')}:</div>
78462
+ <div>
78463
+ <select id="selTableApplyTo" style="width:120px;">
78464
+ <option value="table">${util.out('Table')}</option>
78465
+ <option value="currentrow">${util.out('Current Row')}</option>
78466
+ <option value="currentcol">${util.out('Current Column')}</option>
78467
+ <option value="evenrows">${util.out('Even Rows')}</option>
78468
+ <option value="oddrows">${util.out('Odd Rows')}</option>
78469
+ <option value="currentcell">${util.out('Current Cell')}</option>
78470
+ </select>
78471
+ </div>
78472
+ </div>
78473
+
78474
+ </div>
78475
+
78476
+ <div id="divTableLayout" class="is-tab-content" tabindex="-1" data-group="table">
78477
+
78478
+ <div style="padding-bottom:12px;">
78479
+ <div>${util.out('Insert Row')}:</div>
78480
+ <div style="display:flex">
78481
+ <button class="classic" title="${util.out('Above')}" data-table-cmd="rowabove" title="${util.out('Above')}" style="margin-right:15px"> ${util.out('Above')} </button>
78482
+ <button class="classic" title="${util.out('Below')}" data-table-cmd="rowbelow" title="${util.out('Below')}" style=""> ${util.out('Below')} </button>
78483
+ </div>
78484
+ </div>
78485
+
78486
+ <div style="padding-bottom:15px;">
78487
+ <div>${util.out('Insert Column')}:</div>
78488
+ <div style="display:flex">
78489
+ <button class="classic" title="${util.out('Left')}" data-table-cmd="columnleft" title="${util.out('Left')}" style="margin-right:15px"> ${util.out('Left')} </button>
78490
+ <button class="classic" title="${util.out('Right')}" data-table-cmd="columnright" title="${util.out('Right')}" style=""> ${util.out('Right')} </button>
78491
+ </div>
78492
+ </div>
78493
+
78494
+ <div style="padding-bottom:15px;">
78495
+ <button class="classic" title="${util.out('Delete Row')}" data-table-cmd="delrow" title="Delete Row" style=""> ${util.out('Delete Row')} </button>
78496
+ </div>
78497
+
78498
+ <div style="padding-bottom:15px;">
78499
+ <button class="classic" title="${util.out('Delete Column')}" data-table-cmd="delcolumn" title="Delete Column" style=""> ${util.out('Delete Column')} </button>
78500
+ </div>
78501
+
78502
+ <div>
78503
+ <button class="classic" title="${util.out('Merge Cell')}" data-table-cmd="mergecell" style="">${util.out('Merge Cell')}</button>
78504
+ </div>
78505
+ </div>
78506
+ </div>
78507
+ </div>
78508
+ `;
78509
+ dom.appendHtml(builderStuff, html);
78510
+ pluginModal = builderStuff.querySelector('.is-modal.pluginsettings');
78511
+ this.pluginModal = pluginModal;
78512
+ let btnOk = pluginModal.querySelector('.input-ok');
78513
+ dom.addEventListener(btnOk, 'click', () => {
78514
+ this.builder.uo.saveForUndo();
78515
+ this.builder.opts.onChange();
78516
+ util.hideModal(pluginModal);
78517
+ });
78518
+ let btnCancel = pluginModal.querySelector('.input-cancel');
78519
+ dom.addEventListener(btnCancel, 'click', () => {
78520
+ util.hideModal(pluginModal);
78521
+ });
78522
+ new Tabs({
78523
+ element: pluginModal
78524
+ });
78525
+ new Draggable$2({
78526
+ selector: '.is-modal.pluginsettings .is-draggable'
78527
+ });
78528
+ }
78529
+ }
78530
+ showPluginEditor() {
78531
+ const pluginModal = this.pluginModal;
78532
+ this.builder.util.showModal(pluginModal);
78533
+ this.realtime();
78534
+ const handlePluginClick = e => {
78535
+ const clrPicker = document.querySelector('.pop-picker.active') || document.querySelector('.pickgradientcolor.active');
78536
+ let elm = e.target;
78537
+ let elmRemoved = !document.contains(elm); // e.g. when deleting a card in card list plugin
78538
+
78539
+ if (this.builder.doc.activeElement) {
78540
+ if (this.builder.doc.activeElement.closest('.is-modal')) {
78541
+ // prevent modal close when mouseup outside the modal
78542
+ return;
78543
+ }
78544
+ }
78545
+ if (!elm) return;
78546
+ if (!elm.closest('.is-plugin-tool') && !elm.closest('.is-sidebar') && !elm.closest('.is-modal') && !elm.closest('.keep-selection') && !elm.closest('[data-cb-type]') && !clrPicker && !elmRemoved) {
78547
+ // click outside
78548
+
78549
+ // hide
78550
+ this.hidePluginEditor();
78551
+ // console.log('HIDE');
78552
+
78553
+ document.removeEventListener('click', handlePluginClick);
78554
+ if (this.builder.iframeDocument) {
78555
+ this.builder.doc.removeEventListener('click', handlePluginClick);
78556
+ }
78557
+ this.builder.handlePluginClick_ = false;
78558
+ }
78559
+ if (elm.closest('[data-cb-type]')) {
78560
+ this.realtime();
78561
+ }
78562
+ };
78563
+ if (!this.builder.handlePluginClick_) {
78564
+ document.addEventListener('click', handlePluginClick);
78565
+ if (this.builder.iframeDocument) {
78566
+ this.builder.doc.addEventListener('click', handlePluginClick);
78567
+ }
78568
+ this.builder.handlePluginClick_ = true;
78569
+ }
78570
+ }
78571
+ hidePluginEditor() {
78572
+ const pluginModal = this.pluginModal;
78573
+ if (pluginModal) this.builder.util.hideModal(pluginModal);
78574
+ }
78575
+ realtime() {
78576
+ const util = this.builder.util;
78577
+ const pluginModal = this.pluginModal;
78578
+ let currentElement = this.builder.activePlugin;
78579
+ const runtime = this.builder.win.builderRuntime;
78580
+ if (currentElement && runtime) {
78581
+ const pluginName = currentElement.getAttribute('data-cb-type');
78582
+ const plugin = runtime.getPlugin(pluginName);
78583
+ let titleHtml = util.out(plugin.displayName || plugin.name);
78584
+ const modalBar = pluginModal.querySelector('.is-modal-bar > span');
78585
+ modalBar.innerHTML = titleHtml;
78586
+ const container = pluginModal.querySelector('.is-modal-controls');
78587
+
78588
+ // Clear panel
78589
+ container.innerHTML = '';
78590
+ const div = document.createElement('div');
78591
+ div.classList.add('submain');
78592
+ container.appendChild(div);
78593
+
78594
+ // Store references
78595
+ this.currentElement = currentElement;
78596
+ this.generator = new SettingsUIGenerator(runtime, this.builder);
78597
+
78598
+ // Check if plugin has custom content editor
78599
+ const hasContentEditor = plugin && plugin.editor && plugin.editor.openContentEditor;
78600
+ if (hasContentEditor) {
78601
+ // Get original content for the editor to work with
78602
+ const originalContent = currentElement.getAttribute('data-cb-original-content');
78603
+
78604
+ // Create a temporary element for editing (so editor can manipulate it)
78605
+ let editableClone = document.querySelector('.editable-clone');
78606
+ if (editableClone) editableClone.remove();
78607
+ // editableClone = document.createElement('div');
78608
+ editableClone = currentElement.cloneNode(false);
78609
+ editableClone.innerHTML = originalContent;
78610
+ editableClone.className = 'editable-clone';
78611
+ editableClone.style.display = 'none'; // Hidden, just for editing
78612
+ document.body.appendChild(editableClone);
78613
+ const originalElement = editableClone.cloneNode(false);
78614
+
78615
+ // Let plugin handle everything - pass the editable clone
78616
+ const editorUI = plugin.editor.openContentEditor(editableClone, this.builder, async () => {
78617
+ this.builder.uo.saveForUndo();
78618
+
78619
+ // Store edited content from clone
78620
+ currentElement.setAttribute('data-cb-original-content', editableClone.innerHTML);
78621
+
78622
+ // Also performs similar value updates like the applyChanges()
78623
+ // currentElement.setAttribute('data-cb-content', editableClone.getAttribute('data-cb-content'));
78624
+ const getChangedDataAttributes = (el1, el2) => {
78625
+ const changed = [];
78626
+ for (const key of Object.keys(el1.dataset)) {
78627
+ if (key in el2.dataset && el1.dataset[key] !== el2.dataset[key]) {
78628
+ changed.push(key);
78629
+ }
78630
+ }
78631
+ return changed;
78632
+ };
78633
+ const changedAttributes = getChangedDataAttributes(originalElement, editableClone);
78634
+ // let hasChange = changedAttributes.length > 0;
78635
+ for (const attrName of changedAttributes) {
78636
+ // console.log(attrName);
78637
+ currentElement.dataset[attrName] = editableClone.dataset[attrName];
78638
+ // update form (not working)
78639
+ // const convertCbKey = (str) => {
78640
+ // return str.replace(/^cb/, '').replace(/^./, c => c.toLowerCase());
78641
+ // }
78642
+ // const name = convertCbKey(attrName);
78643
+ // this.currentForm.querySelector(`[name="${name}"]`).value = editableClone.dataset[attrName];
78644
+ }
78645
+ // console.log(hasChange)
78646
+ // -------
78647
+
78648
+ currentElement.innerHTML = editableClone.innerHTML; // Update current content
78649
+
78650
+ // Remove temporary clone
78651
+ editableClone.remove();
78652
+
78653
+ // update form (working)
78654
+ // currentElement.click();
78655
+ // return;
78656
+
78657
+ // Reinitialize plugin with new content
78658
+ await runtime.reinitialize(currentElement.parentElement);
78659
+ this.builder.onChange();
78660
+ });
78661
+ div.appendChild(editorUI);
78662
+ }
78663
+
78664
+ // Generate form
78665
+ this.currentForm = this.generator.generateForm(pluginName, currentElement, () => {
78666
+ this.builder.uo.saveForUndo();
78667
+ this.applyChanges(runtime);
78668
+ });
78669
+ this.currentForm.style.marginTop = '20px';
78670
+ div.appendChild(this.currentForm);
78671
+ }
78672
+ }
78673
+ async applyChanges(runtime) {
78674
+ if (!this.currentElement || !this.currentForm) return;
78675
+
78676
+ // 1. Get form values
78677
+ const values = this.generator.getFormValues(this.currentForm);
78678
+
78679
+ // 2. Apply to element attributes
78680
+ this.generator.applyValues(this.currentElement, values);
78681
+
78682
+ // 3. Reinitialize component
78683
+ const container = this.currentElement.parentElement || this.currentElement.closest('.is-wrapper');
78684
+ await runtime.reinitialize(container);
78685
+
78686
+ //Trigger Change event
78687
+ this.builder.opts.onChange();
78688
+
78689
+ // console.log('Settings applied and component reinitialized');
78690
+ }
78691
+
78692
+ click(e) {
78693
+ const plugin = e.target.closest('[data-cb-type]');
78694
+ if (plugin) {
78695
+ this.builder.activePlugin = plugin;
78696
+ if (!this.builder.isContentBox) {
78697
+ this.renderTool();
78698
+ this.pluginTool.style.display = 'flex';
78699
+ let _toolwidth = this.pluginTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
78700
+
78701
+ let w = plugin.offsetWidth * this.builder.opts.zoom;
78702
+ let top = plugin.getBoundingClientRect().top + this.builder.win.pageYOffset;
78703
+ let left = plugin.getBoundingClientRect().left - 2;
78704
+ left = left + (w - _toolwidth);
78705
+
78706
+ //Adjust left in case an element is outside the screen
78707
+ const _screenwidth = window.innerWidth;
78708
+ if (_toolwidth + left > _screenwidth) left = plugin.getBoundingClientRect().left;
78709
+ this.pluginTool.style.top = top + 'px';
78710
+ this.pluginTool.style.left = left + 'px';
78711
+ }
78712
+ } else {
78713
+ this.builder.activePlugin = null;
78714
+ if (this.pluginTool) this.pluginTool.style.display = '';
78715
+ }
78716
+ }
78717
+ }
78718
+
77238
78719
  class Module {
77239
78720
  constructor(builder) {
77240
78721
  this.builder = builder;
@@ -79076,6 +80557,7 @@ class Element$1 {
79076
80557
  this.button = new Button(builder);
79077
80558
  this.image = new Image$1(builder);
79078
80559
  this.spacer = new Spacer(builder);
80560
+ this.plugin = new Plugin(builder);
79079
80561
  this.module = new Module(builder);
79080
80562
  this.code = new Code(builder);
79081
80563
  this.iframe = new Iframe(builder);
@@ -79114,6 +80596,10 @@ class Element$1 {
79114
80596
 
79115
80597
  // Set contentEditable FALSE on special elements
79116
80598
 
80599
+ const plugins = col.querySelectorAll('[data-cb-type]'); // plugins
80600
+ Array.prototype.forEach.call(plugins, plugin => {
80601
+ plugin.contentEditable = false;
80602
+ });
79117
80603
  let sociallinks = col.querySelectorAll('.is-social');
79118
80604
  Array.prototype.forEach.call(sociallinks, sociallink => {
79119
80605
  sociallink.contentEditable = false;
@@ -79205,6 +80691,9 @@ class Element$1 {
79205
80691
  subblock = true;
79206
80692
  }
79207
80693
  }
80694
+ if (elm.closest('[data-cb-type]')) {
80695
+ customcode = true;
80696
+ }
79208
80697
  if (!customcode && !noedit && !_protected || subblock) {
79209
80698
  //previously this is commented: && !noedit && !_protected
79210
80699
 
@@ -79275,6 +80764,9 @@ class Element$1 {
79275
80764
 
79276
80765
  // Module
79277
80766
  this.module.click(col, e);
80767
+
80768
+ // Plugin
80769
+ this.plugin.click(e);
79278
80770
  }
79279
80771
  }
79280
80772
 
@@ -88430,6 +89922,12 @@ class ElementTool {
88430
89922
  const btnMore = elementTool.querySelector('.elm-more');
88431
89923
  dom.addEventListener(btnMore, 'click', () => {
88432
89924
  const viewportHeight = window.innerHeight;
89925
+ const elmSettings = elementMore.querySelector('.elm-settings');
89926
+ if (this.builder.activeElement.hasAttribute('data-cb-type')) {
89927
+ elmSettings.style.display = 'none';
89928
+ } else {
89929
+ elmSettings.style.display = '';
89930
+ }
88433
89931
 
88434
89932
  /*
88435
89933
  let top, left;
@@ -88562,6 +90060,9 @@ class ElementTool {
88562
90060
 
88563
90061
  //Trigger Change event
88564
90062
  this.builder.opts.onChange();
90063
+
90064
+ // Re-init plugins
90065
+ if (this.builder.win.builderRuntime) this.builder.win.builderRuntime.reinitialize(newelm.parentNode);
88565
90066
  }, 100); //Timeout needed by Safari
88566
90067
  });
88567
90068
 
@@ -88695,7 +90196,10 @@ class ElementTool {
88695
90196
  if (dom.parentsHasClass(elm, 'is-subblock')) {
88696
90197
  subblock = true;
88697
90198
  }
88698
- if ((customcode || noedit || _protected) && !subblock) ; else {
90199
+ const plugin = elm.closest('[data-cb-type]');
90200
+ if ((customcode || noedit || _protected) && !subblock) ; else if (plugin) {
90201
+ activeElement = plugin;
90202
+ } else {
88699
90203
  const tagName = elm.tagName.toLowerCase();
88700
90204
  // LATER: label, code, figcaption ?
88701
90205
  if (!elm.classList.contains('cell-active') && (tagName === 'h1' || tagName === 'h2' || tagName === 'h3' || tagName === 'h4' || tagName === 'h5' || tagName === 'h6' || tagName === 'p' || tagName === 'div' || tagName === 'pre' || tagName === 'blockquote' || tagName === 'li' || tagName === 'img' || tagName === 'iframe')) {
@@ -88847,6 +90351,8 @@ class ElementTool {
88847
90351
  let elm = this.builder.activeElement;
88848
90352
  if (!elm) return;
88849
90353
  if (elm.closest('.is-dock')) return;
90354
+ // if(elm.closest('[data-cb-type]')) return;
90355
+
88850
90356
  let top, left;
88851
90357
  if (!this.builder.iframe) {
88852
90358
  top = elm.getBoundingClientRect().top + window.pageYOffset;
@@ -106495,6 +108001,9 @@ class ContentStuff$1 {
106495
108001
  <div class="is-tool is-iframe-tool">
106496
108002
  <button title="${util.out('Settings')}" data-title="${util.out('Settings')}" style="width:40px;height:40px;background:none;"><svg class="is-icon-flex"><use xlink:href="#icon-cog"></use></svg></button>
106497
108003
  </div>
108004
+ <div class="is-tool is-plugin-tool">
108005
+ <button title="${util.out('Settings')}" data-title="${util.out('Settings')}" style="width:40px;height:40px;background:none;"><svg class="is-icon-flex"><use xlink:href="#icon-cog"></use></svg></button>
108006
+ </div>
106498
108007
  <div class="is-tool is-module-tool">
106499
108008
  <button class="btn-module-refresh" title="${util.out('Refresh')}" data-title="${util.out('Refresh')}" style="width:40px;height:40px;"><svg class="is-icon-flex"><use xlink:href="#icon-reload"></use></svg></button>
106500
108009
  <button class="btn-module-settings" title="${util.out('Settings')}" data-title="${util.out('Settings')}" style="width:40px;height:40px;"><svg class="is-icon-flex"><use xlink:href="#icon-cog"></use></svg></button>
@@ -106900,20 +108409,24 @@ class ContentStuff$1 {
106900
108409
  /*
106901
108410
  .is-tool.is-video-tool,
106902
108411
  .is-tool.is-audio-tool,
108412
+ .is-tool.is-plugin-tool,
106903
108413
  .is-tool.is-iframe-tool {
106904
108414
  background: rgba(0, 0, 0, 0.15);
106905
108415
  border: transparent 1px solid;
106906
108416
  }
106907
108417
  .is-tool.is-video-tool > div,
106908
108418
  .is-tool.is-audio-tool > div,
108419
+ .is-tool.is-plugin-tool > div,
106909
108420
  .is-tool.is-iframe-tool > div,
106910
108421
  .is-tool.is-video-tool > button,
106911
108422
  .is-tool.is-audio-tool > button,
108423
+ .is-tool.is-plugin-tool > button,
106912
108424
  .is-tool.is-iframe-tool > button {
106913
108425
  background: transparent;
106914
108426
  }
106915
108427
  .is-tool.is-video-tool svg,
106916
108428
  .is-tool.is-audio-tool svg,
108429
+ .is-tool.is-plugin-tool svg,
106917
108430
  .is-tool.is-iframe-tool svg {
106918
108431
  fill: #fff;
106919
108432
  }
@@ -106923,6 +108436,7 @@ class ContentStuff$1 {
106923
108436
  .is-tool.is-table-tool,
106924
108437
  .is-tool.is-code-tool,
106925
108438
  .is-tool.is-module-tool,
108439
+ .is-tool.is-plugin-tool,
106926
108440
  .is-tool#divLinkTool,
106927
108441
  .is-tool#divButtonTool,
106928
108442
  .is-tool.is-svg-tool {
@@ -106936,6 +108450,7 @@ class ContentStuff$1 {
106936
108450
  .is-tool.is-table-tool > button,
106937
108451
  .is-tool.is-code-tool > button,
106938
108452
  .is-tool.is-module-tool > button,
108453
+ .is-tool.is-plugin-tool > button,
106939
108454
  .is-tool#divLinkTool > button,
106940
108455
  .is-tool#divButtonTool > button,
106941
108456
  .is-tool.is-svg-tool > button {
@@ -106948,6 +108463,7 @@ class ContentStuff$1 {
106948
108463
  .is-tool.is-table-tool > button svg,
106949
108464
  .is-tool.is-code-tool > button svg,
106950
108465
  .is-tool.is-module-tool > button svg,
108466
+ .is-tool.is-plugin-tool > button svg,
106951
108467
  .is-tool#divLinkTool > button svg,
106952
108468
  .is-tool#divButtonTool > button svg,
106953
108469
  .is-tool.is-svg-tool > button svg {
@@ -106962,6 +108478,7 @@ class ContentStuff$1 {
106962
108478
 
106963
108479
  .is-tool.is-video-tool,
106964
108480
  .is-tool.is-audio-tool,
108481
+ .is-tool.is-plugin-tool,
106965
108482
  .is-tool.is-iframe-tool {
106966
108483
  background: rgba(255, 255, 255, 0.97) !important;
106967
108484
  border: transparent 1px solid;
@@ -106973,6 +108490,7 @@ class ContentStuff$1 {
106973
108490
  .is-tool.is-video-tool > button,
106974
108491
  .is-tool.is-audio-tool > div,
106975
108492
  .is-tool.is-audio-tool > button,
108493
+ .is-tool.is-plugin-tool > div,
106976
108494
  .is-tool.is-iframe-tool > div,
106977
108495
  .is-tool.is-iframe-tool > button {
106978
108496
  width: 35px !important;
@@ -106982,6 +108500,7 @@ class ContentStuff$1 {
106982
108500
 
106983
108501
  .is-tool.is-video-tool svg,
106984
108502
  .is-tool.is-audio-tool svg,
108503
+ .is-tool.is-plugin-tool svg,
106985
108504
  .is-tool.is-iframe-tool svg {
106986
108505
  width: 17px;
106987
108506
  height: 17px;
@@ -123603,6 +125122,9 @@ Add an image for each feature.`, 'Create a new content showcasing a photo galler
123603
125122
 
123604
125123
  // Zoom
123605
125124
  this.setZoomOnControl(builder);
125125
+
125126
+ // Re-init plugins
125127
+ if (this.win.builderRuntime) this.win.builderRuntime.reinitialize(builder);
123606
125128
  }
123607
125129
  applySortableGrid() {
123608
125130
  if (this.iframe) {
@@ -123687,6 +125209,143 @@ Add an image for each feature.`, 'Create a new content showcasing a photo galler
123687
125209
  fromViewToActual(html) {
123688
125210
  return this.htmlutil.fromViewToActual(html);
123689
125211
  }
125212
+ openColorPicker(currentColor, callback, btn) {
125213
+ this.colorPicker.open(callback, currentColor, () => {}, btn);
125214
+ }
125215
+ openIconPicker(callback) {
125216
+ let iconModal = this.builderStuff.querySelector('.is-modal.iconselect');
125217
+ if (!iconModal) {
125218
+ // Create modal with search input and event listeners once
125219
+ let html = `
125220
+ <style>
125221
+ .is-modal.iconselect .icon-search-wrapper {
125222
+ position: sticky;
125223
+ top: 0;
125224
+ background: white;
125225
+ padding: 20px;
125226
+ border-bottom: 1px solid #ddd;
125227
+ z-index: 10;
125228
+ }
125229
+ .is-modal.iconselect .icon-search {
125230
+ width: 100%;
125231
+ padding: 10px 15px;
125232
+ font-size: 16px;
125233
+ border: 1px solid #ccc;
125234
+ border-radius: 4px;
125235
+ box-sizing: border-box;
125236
+ }
125237
+ .is-modal.iconselect .icon-search:focus {
125238
+ outline: none;
125239
+ border-color: #007bff;
125240
+ }
125241
+ .is-modal.iconselect .icon-list {
125242
+ display: flex;
125243
+ gap: 10px;
125244
+ flex-flow: wrap;
125245
+ padding: 20px;
125246
+ }
125247
+ .is-modal.iconselect .icon-list > button {
125248
+ display: flex !important;
125249
+ width: 80px !important;
125250
+ height: 80px !important;
125251
+ flex: none;
125252
+ font-size: 30px !important;
125253
+ box-shadow: none !important;
125254
+ align-items: center;
125255
+ justify-content: center;
125256
+ }
125257
+ .is-modal.iconselect .icon-list > button.hidden {
125258
+ display: none !important;
125259
+ }
125260
+ .is-modal.iconselect .no-results {
125261
+ padding: 40px 20px;
125262
+ text-align: center;
125263
+ color: #666;
125264
+ display: none;
125265
+ }
125266
+ .is-modal.iconselect .no-results.show {
125267
+ display: block;
125268
+ }
125269
+ </style>
125270
+ <div class="is-modal iconselect" tabindex="-1" role="dialog" aria-modal="true" aria-hidden="true">
125271
+ <div class="is-modal-content scroll" style="width: 90vw; max-width: 908px; height: 70vh; min-height: 525px; padding: 0;">
125272
+ <div style="height: 100%; overflow-y: auto; display: flex; flex-direction: column;">
125273
+ <div class="icon-search-wrapper">
125274
+ <input type="text" class="icon-search" placeholder="Search icons... (e.g., alarm, wrench)" />
125275
+ </div>
125276
+ <div class="icon-list"></div>
125277
+ <div class="no-results">No icons found matching your search.</div>
125278
+ </div>
125279
+ </div>
125280
+ </div>
125281
+ `;
125282
+ this.dom.appendHtml(this.builderStuff, html);
125283
+ iconModal = this.builderStuff.querySelector('.is-modal.iconselect');
125284
+ this.iconModal = iconModal;
125285
+ const iconList = this.builderStuff.querySelector('.icon-list');
125286
+ const searchInput = this.builderStuff.querySelector('.icon-search');
125287
+ const noResults = this.builderStuff.querySelector('.no-results');
125288
+ iconList.innerHTML = this.rte.getIcons2() + this.rte.getIcons();
125289
+ let icons = iconList.querySelectorAll('button');
125290
+
125291
+ // Add click handlers for icons
125292
+ icons.forEach(icon => {
125293
+ icon.addEventListener('click', () => {
125294
+ // Get the current callback from the modal's data
125295
+ const currentCallback = iconModal._currentCallback;
125296
+ if (currentCallback) {
125297
+ currentCallback(icon.innerHTML);
125298
+ }
125299
+ this.util.hideModal(iconModal);
125300
+ });
125301
+ });
125302
+
125303
+ // Add search/filter functionality
125304
+ searchInput.addEventListener('input', e => {
125305
+ const searchTerm = e.target.value.toLowerCase().trim();
125306
+ let visibleCount = 0;
125307
+ icons.forEach(icon => {
125308
+ const iconElement = icon.querySelector('i');
125309
+ if (!iconElement) return;
125310
+
125311
+ // Get class names from the icon
125312
+ const iconClasses = iconElement.className.toLowerCase();
125313
+
125314
+ // Check if search term matches any part of the icon classes
125315
+ if (searchTerm === '' || iconClasses.includes(searchTerm)) {
125316
+ icon.classList.remove('hidden');
125317
+ visibleCount++;
125318
+ } else {
125319
+ icon.classList.add('hidden');
125320
+ }
125321
+ });
125322
+
125323
+ // Show/hide no results message
125324
+ if (visibleCount === 0 && searchTerm !== '') {
125325
+ noResults.classList.add('show');
125326
+ } else {
125327
+ noResults.classList.remove('show');
125328
+ }
125329
+ });
125330
+
125331
+ // Clear search when modal is opened
125332
+ iconModal.addEventListener('modalshow', () => {
125333
+ searchInput.value = '';
125334
+ searchInput.dispatchEvent(new Event('input'));
125335
+ });
125336
+ }
125337
+
125338
+ // Store the current callback and reset search
125339
+ iconModal._currentCallback = callback;
125340
+ const searchInput = iconModal.querySelector('.icon-search');
125341
+ if (searchInput) {
125342
+ searchInput.value = '';
125343
+ searchInput.dispatchEvent(new Event('input'));
125344
+ // Focus search input after a brief delay to ensure modal is visible
125345
+ setTimeout(() => searchInput.focus(), 100);
125346
+ }
125347
+ this.util.showModal(iconModal);
125348
+ }
123690
125349
  colorpicker(onPick, defaultcolor) {
123691
125350
  // return new ColorPicker({
123692
125351
  // onPick: onPick,
@@ -124338,6 +125997,9 @@ Add an image for each feature.`, 'Create a new content showcasing a photo galler
124338
125997
  }
124339
125998
  });
124340
125999
  }
126000
+ openFilePicker(type, callback) {
126001
+ this.openAssetSelect(type, callback);
126002
+ }
124341
126003
  openAssetSelect(targetAssetType, callback, defaultValue) {
124342
126004
  const inpUrl = document.createElement('input');
124343
126005
 
@@ -143456,7 +145118,10 @@ class Section {
143456
145118
  if (this.builder.eb) this.builder.eb.refresh(); // freeform
143457
145119
 
143458
145120
  this.builder.onRender();
143459
- this.builder.onChange();
145121
+ this.builder.onChange(); // Re-init plugins
145122
+
145123
+ const runtime = this.builder.win.builderRuntime;
145124
+ if (runtime) runtime.reinitialize(section);
143460
145125
  }
143461
145126
 
143462
145127
  sectionDelete() {
@@ -162390,7 +164055,8 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
162390
164055
  blockContainer: '.is-box',
162391
164056
  slider: this.settings.slider,
162392
164057
  onRender: () => {
162393
- this.settings.onRender();
164058
+ this.settings.onRender(); // Re-init plugins
164059
+ // if(this.win.builderRuntime) this.win.builderRuntime.reinitialize();
162394
164060
  },
162395
164061
  onChange: () => {
162396
164062
  this.settings.onChange();
@@ -164137,6 +165803,18 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
164137
165803
  } // init
164138
165804
 
164139
165805
 
165806
+ openFilePicker(type, callback) {
165807
+ this.editor.openAssetSelect(type, callback);
165808
+ }
165809
+
165810
+ openColorPicker(currentColor, callback, btn) {
165811
+ this.editor.openColorPicker(currentColor, callback, btn);
165812
+ }
165813
+
165814
+ openIconPicker(callback) {
165815
+ this.editor.openIconPicker(callback);
165816
+ }
165817
+
164140
165818
  openPreview() {
164141
165819
  this.animateScroll.openPreview();
164142
165820
  }
@@ -164738,7 +166416,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
164738
166416
  this.markScrollTarget(this.activeSection); // adding class 'scroll-target'
164739
166417
  }
164740
166418
 
164741
- doUndoRedo() {
166419
+ async doUndoRedo() {
164742
166420
  /*
164743
166421
  // Clean
164744
166422
  const elms = this.wrapperEl.querySelectorAll(`[data-center],[data-center-top],[data--50-bottom],
@@ -164807,6 +166485,9 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
164807
166485
  if (this.controlPanel) {
164808
166486
  this.controlpanel.select('');
164809
166487
  }
166488
+
166489
+ const runtime = this.win.builderRuntime;
166490
+ if (runtime) runtime.reinitialize();
164810
166491
  }
164811
166492
 
164812
166493
  refreshAnim() {
@@ -165051,7 +166732,9 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
165051
166732
  });
165052
166733
  this.editor.applyBehavior(); // Re-apply
165053
166734
 
165054
- this.settings.onRender();
166735
+ this.settings.onRender(); // Re-init plugins
166736
+
166737
+ if (this.win.builderRuntime) this.win.builderRuntime.reinitialize();
165055
166738
  const scrollButton = this.wrapperEl.querySelectorAll('.is-arrow-down a');
165056
166739
  scrollButton.forEach(btn => {
165057
166740
  btn.addEventListener('click', e => {
@@ -165838,7 +167521,9 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
165838
167521
  this.pageSetupDone = true; //prevent duplicate for traditional pageSetup on init
165839
167522
  // Re-apply
165840
167523
 
165841
- this.settings.onRender();
167524
+ this.settings.onRender(); // Re-init plugins
167525
+
167526
+ if (this.win.builderRuntime) this.win.builderRuntime.reinitialize();
165842
167527
  const scrollButton = wrapper.querySelectorAll('.is-arrow-down a');
165843
167528
  scrollButton.forEach(btn => {
165844
167529
  btn.addEventListener('click', e => {