@innovastudio/contentbuilder 1.5.157 → 1.5.159

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.
Files changed (45) hide show
  1. package/index.d.ts +6 -0
  2. package/package.json +2 -1
  3. package/public/contentbuilder/contentbuilder.css +149 -6
  4. package/public/contentbuilder/contentbuilder.esm.js +968 -3
  5. package/public/contentbuilder/contentbuilder.min.js +6 -6
  6. package/public/contentbuilder/themes/colored-blue.css +3 -2
  7. package/public/contentbuilder/themes/colored-blue2.css +3 -2
  8. package/public/contentbuilder/themes/colored-blue3.css +3 -2
  9. package/public/contentbuilder/themes/colored-blue4.css +3 -2
  10. package/public/contentbuilder/themes/colored-blue5.css +3 -2
  11. package/public/contentbuilder/themes/colored-blue6.css +3 -2
  12. package/public/contentbuilder/themes/colored-blue7.css +3 -2
  13. package/public/contentbuilder/themes/colored-blue8.css +3 -2
  14. package/public/contentbuilder/themes/colored-darkblue.css +3 -2
  15. package/public/contentbuilder/themes/colored-gray.css +3 -2
  16. package/public/contentbuilder/themes/colored-green.css +3 -2
  17. package/public/contentbuilder/themes/colored-green2.css +3 -2
  18. package/public/contentbuilder/themes/colored-green3.css +3 -2
  19. package/public/contentbuilder/themes/colored-green4.css +3 -2
  20. package/public/contentbuilder/themes/colored-green5.css +3 -2
  21. package/public/contentbuilder/themes/colored-magenta.css +3 -2
  22. package/public/contentbuilder/themes/colored-orange.css +3 -2
  23. package/public/contentbuilder/themes/colored-orange2.css +3 -2
  24. package/public/contentbuilder/themes/colored-orange3.css +3 -2
  25. package/public/contentbuilder/themes/colored-pink.css +3 -2
  26. package/public/contentbuilder/themes/colored-pink2.css +3 -2
  27. package/public/contentbuilder/themes/colored-pink3.css +3 -2
  28. package/public/contentbuilder/themes/colored-pink4.css +3 -2
  29. package/public/contentbuilder/themes/colored-purple.css +3 -2
  30. package/public/contentbuilder/themes/colored-purple2.css +3 -2
  31. package/public/contentbuilder/themes/colored-red.css +3 -2
  32. package/public/contentbuilder/themes/colored-red2.css +3 -2
  33. package/public/contentbuilder/themes/colored-red3.css +3 -2
  34. package/public/contentbuilder/themes/colored-red4.css +3 -2
  35. package/public/contentbuilder/themes/colored-red5.css +3 -2
  36. package/public/contentbuilder/themes/colored-yellow.css +3 -2
  37. package/public/contentbuilder/themes/colored-yellow2.css +3 -2
  38. package/public/contentbuilder/themes/dark-blue.css +3 -2
  39. package/public/contentbuilder/themes/dark-blue2.css +3 -2
  40. package/public/contentbuilder/themes/dark-blue3.css +3 -2
  41. package/public/contentbuilder/themes/dark-gray.css +3 -2
  42. package/public/contentbuilder/themes/dark-pink.css +3 -2
  43. package/public/contentbuilder/themes/dark-purple.css +3 -2
  44. package/public/contentbuilder/themes/dark-red.css +3 -2
  45. package/public/contentbuilder/themes/dark.css +16 -2
@@ -14428,7 +14428,7 @@ class HtmlUtil {
14428
14428
  this.builder.opts.onRender();
14429
14429
 
14430
14430
  // Re-select
14431
- if (row.firstChild) row.firstChild.click();
14431
+ // if(row.firstChild) row.firstChild.click();
14432
14432
  //Change to row selection
14433
14433
  row.classList.remove('row-outline');
14434
14434
  //Hide Column tool (new!)
@@ -15040,6 +15040,7 @@ class HtmlUtil {
15040
15040
 
15041
15041
  // Remove runtime-added attributes
15042
15042
  element.removeAttribute('data-cb-original-content');
15043
+ element.removeAttribute('data-cb-loaded');
15043
15044
 
15044
15045
  // element.removeAttribute('data-cb-logo-loop-initialized');
15045
15046
 
@@ -50724,6 +50725,815 @@ class Spacer {
50724
50725
  }
50725
50726
  }
50726
50727
 
50728
+ /**
50729
+ * ContentBox Settings UI Generator
50730
+ * Automatically generates settings form based on plugin schema
50731
+ *
50732
+ * Usage in ContentBox Editor:
50733
+ * const generator = new SettingsUIGenerator(runtime);
50734
+ * const form = generator.generateForm('logo-loop', currentElement);
50735
+ * panel.appendChild(form);
50736
+ */
50737
+
50738
+ class SettingsUIGenerator {
50739
+ constructor(runtime, builder) {
50740
+ this.runtime = runtime;
50741
+ this.builder = builder;
50742
+ }
50743
+
50744
+ /**
50745
+ * Generate settings form for a plugin
50746
+ * @param {string} pluginName - Plugin name
50747
+ * @param {HTMLElement} element - Current element being edited
50748
+ * @returns {HTMLElement} Form element
50749
+ */
50750
+ generateForm(pluginName, element, onChange) {
50751
+ const plugin = this.runtime.getPlugin(pluginName);
50752
+ if (!plugin || !plugin.settings) {
50753
+ console.warn(`Plugin "${pluginName}" has no settings schema`);
50754
+ return this.createEmptyForm();
50755
+ }
50756
+ const form = document.createElement('div');
50757
+ form.className = 'cb-settings-form';
50758
+
50759
+ // Get current values from element
50760
+ const currentValues = this.getCurrentValues(element, plugin.settings);
50761
+
50762
+ // Check if plugin has grouped settings
50763
+ if (plugin.settings._groups) {
50764
+ this.generateGroupedForm(form, plugin.settings, currentValues);
50765
+ } else {
50766
+ this.generateFlatForm(form, plugin.settings, currentValues);
50767
+ }
50768
+
50769
+ // Attach listeners for auto-update
50770
+ if (typeof onChange === 'function') {
50771
+ form.addEventListener('input', () => {
50772
+ const values = this.getFormValues(form);
50773
+ onChange(values, element);
50774
+ });
50775
+ form.addEventListener('change', () => {
50776
+ const values = this.getFormValues(form);
50777
+ onChange(values, element);
50778
+ });
50779
+ }
50780
+ return form;
50781
+ }
50782
+
50783
+ /**
50784
+ * Generate flat form (no groups)
50785
+ */
50786
+ generateFlatForm(container, settings, currentValues) {
50787
+ Object.keys(settings).forEach(key => {
50788
+ if (key.startsWith('_')) return; // Skip meta keys
50789
+
50790
+ const field = settings[key];
50791
+ const fieldElement = this.createField(key, field, currentValues[key]);
50792
+ container.appendChild(fieldElement);
50793
+ });
50794
+ }
50795
+
50796
+ /**
50797
+ * Generate grouped form
50798
+ */
50799
+ generateGroupedForm(container, settings, currentValues) {
50800
+ const groups = settings._groups || [];
50801
+ groups.forEach(group => {
50802
+ const groupElement = document.createElement('div');
50803
+ groupElement.className = 'cb-settings-group';
50804
+ const groupTitle = document.createElement('h3');
50805
+ groupTitle.className = 'cb-settings-group-title';
50806
+ groupTitle.style.cssText = 'font-size: 17px;font-weight: 500;';
50807
+ groupTitle.textContent = group.label;
50808
+ groupElement.appendChild(groupTitle);
50809
+ group.fields.forEach(fieldKey => {
50810
+ const field = settings[fieldKey];
50811
+ if (!field) return;
50812
+ const fieldElement = this.createField(fieldKey, field, currentValues[fieldKey]);
50813
+ groupElement.appendChild(fieldElement);
50814
+ });
50815
+ container.appendChild(groupElement);
50816
+ });
50817
+ }
50818
+
50819
+ /**
50820
+ * Create a single form field
50821
+ */
50822
+ createField(name, config, value) {
50823
+ const wrapper = document.createElement('div');
50824
+ wrapper.className = 'cb-settings-field';
50825
+ wrapper.style.marginBottom = '20px';
50826
+ wrapper.setAttribute('data-field', name);
50827
+
50828
+ // Add conditional display logic
50829
+ if (config.dependsOn) {
50830
+ wrapper.setAttribute('data-depends-on', JSON.stringify(config.dependsOn));
50831
+ wrapper.style.display = 'none'; // Hidden by default, shown by JS
50832
+ }
50833
+
50834
+ // Label
50835
+ const label = document.createElement('label');
50836
+ label.className = 'cb-settings-label';
50837
+ label.style.fontWeight = 500;
50838
+ label.textContent = config.label || name;
50839
+ // wrapper.appendChild(label);
50840
+
50841
+ // Input based on type
50842
+ const input = this.createInput(name, config, value);
50843
+ wrapper.appendChild(input);
50844
+
50845
+ // Description
50846
+ // if (config.description) {
50847
+ // const desc = document.createElement('p');
50848
+ // desc.className = 'cb-settings-description';
50849
+ // desc.textContent = config.description;
50850
+ // wrapper.appendChild(desc);
50851
+ // }
50852
+
50853
+ return wrapper;
50854
+ }
50855
+
50856
+ /**
50857
+ * Create input element based on field type
50858
+ */
50859
+ createInput(name, config, value) {
50860
+ const currentValue = value !== undefined ? value : config.default;
50861
+ switch (config.type) {
50862
+ case 'number':
50863
+ return this.createNumberInput(name, config, currentValue);
50864
+ case 'boolean':
50865
+ return this.createCheckboxInput(name, config, currentValue);
50866
+ case 'select':
50867
+ return this.createSelectInput(name, config, currentValue);
50868
+ case 'radio':
50869
+ return this.createRadioInput(name, config, currentValue);
50870
+ case 'textarea':
50871
+ return this.createTextareaInput(name, config, currentValue);
50872
+ case 'color':
50873
+ return this.createColorInput(name, config, currentValue);
50874
+ case 'range':
50875
+ return this.createRangeInput(name, config, currentValue);
50876
+ case 'text':
50877
+ default:
50878
+ return this.createTextInput(name, config, currentValue);
50879
+ }
50880
+ }
50881
+ createTextInput(name, config, value) {
50882
+ const label = document.createElement('label');
50883
+ label.className = 'label';
50884
+ label.style.fontWeight = 500;
50885
+ const labelText = document.createElement('div');
50886
+ labelText.textContent = config.label || name;
50887
+ label.appendChild(labelText);
50888
+ const input = document.createElement('input');
50889
+ input.type = 'text';
50890
+ input.name = name;
50891
+ input.value = value || '';
50892
+ if (config.placeholder) input.placeholder = config.placeholder;
50893
+ label.appendChild(input);
50894
+
50895
+ // Add description if exists
50896
+ if (config.description) {
50897
+ const desc = document.createElement('small');
50898
+ desc.style.cssText = 'display: block;margin-top: 4px;';
50899
+ desc.textContent = config.description;
50900
+ label.appendChild(desc);
50901
+ }
50902
+ return label;
50903
+ }
50904
+ createNumberInput(name, config, value) {
50905
+ const label = document.createElement('label');
50906
+ label.className = 'label';
50907
+ label.style.fontWeight = 500;
50908
+ const labelText = document.createElement('div');
50909
+ labelText.textContent = config.label || name;
50910
+ if (config.unit) {
50911
+ labelText.textContent += ` (${config.unit})`;
50912
+ }
50913
+ label.appendChild(labelText);
50914
+ const input = document.createElement('input');
50915
+ input.type = 'number';
50916
+ input.style.cssText = 'width: 100px;';
50917
+ input.name = name;
50918
+ input.value = value !== undefined ? value : config.default;
50919
+ if (config.min !== undefined) input.min = config.min;
50920
+ if (config.max !== undefined) input.max = config.max;
50921
+ if (config.step !== undefined) input.step = config.step;
50922
+ label.appendChild(input);
50923
+ if (config.description) {
50924
+ const desc = document.createElement('small');
50925
+ desc.style.cssText = 'display: block;margin-top: 4px;';
50926
+ desc.textContent = config.description;
50927
+ label.appendChild(desc);
50928
+ }
50929
+ return label;
50930
+ }
50931
+ createCheckboxInput(name, config, value) {
50932
+ const label = document.createElement('label');
50933
+ label.className = 'label checkbox';
50934
+ const div = document.createElement('div');
50935
+ const input = document.createElement('input');
50936
+ input.type = 'checkbox';
50937
+ input.name = name;
50938
+ input.checked = value !== undefined ? value : config.default;
50939
+ const span = document.createElement('span');
50940
+ span.textContent = config.label || name;
50941
+ label.appendChild(input);
50942
+ label.appendChild(span);
50943
+ div.appendChild(label);
50944
+ if (config.description) {
50945
+ const desc = document.createElement('small');
50946
+ desc.style.cssText = 'display: block;margin-top: 4px;';
50947
+ desc.textContent = config.description;
50948
+ div.appendChild(desc);
50949
+ }
50950
+ return label;
50951
+ }
50952
+ createSelectInput(name, config, value) {
50953
+ const label = document.createElement('label');
50954
+ label.className = 'label';
50955
+ label.style.fontWeight = 500;
50956
+ const labelText = document.createTextNode((config.label || name) + ':');
50957
+ label.appendChild(labelText);
50958
+ const select = document.createElement('select');
50959
+ select.name = name;
50960
+ config.options.forEach(option => {
50961
+ const opt = document.createElement('option');
50962
+ opt.value = option.value;
50963
+ opt.textContent = option.label;
50964
+ if (option.value === value) opt.selected = true;
50965
+ select.appendChild(opt);
50966
+ });
50967
+ label.appendChild(select);
50968
+ if (config.description) {
50969
+ const desc = document.createElement('small');
50970
+ desc.style.cssText = 'display: block;margin-top: 4px;';
50971
+ desc.textContent = config.description;
50972
+ label.appendChild(desc);
50973
+ }
50974
+ return label;
50975
+ }
50976
+ createRadioInput(name, config, value) {
50977
+ const wrapper = document.createElement('div');
50978
+ const title = document.createElement('div');
50979
+ title.textContent = config.label || name;
50980
+ title.style.cssText = 'margin-bottom: 8px;font-weight: 590;';
50981
+ wrapper.appendChild(title);
50982
+ config.options.forEach(option => {
50983
+ const label = document.createElement('label');
50984
+ label.className = 'label checkbox';
50985
+ const input = document.createElement('input');
50986
+ input.type = 'radio';
50987
+ input.name = name;
50988
+ input.value = option.value;
50989
+ input.checked = option.value === value;
50990
+ const span = document.createElement('span');
50991
+ span.textContent = option.label;
50992
+ label.appendChild(input);
50993
+ label.appendChild(span);
50994
+ wrapper.appendChild(label);
50995
+ });
50996
+ if (config.description) {
50997
+ const desc = document.createElement('small');
50998
+ desc.style.cssText = 'display: block;margin-top: 4px;';
50999
+ desc.textContent = config.description;
51000
+ wrapper.appendChild(desc);
51001
+ }
51002
+ return wrapper;
51003
+ }
51004
+ createTextareaInput(name, config, value) {
51005
+ const label = document.createElement('label');
51006
+ label.className = 'label';
51007
+ label.style.fontWeight = 500;
51008
+ const labelText = document.createElement('div');
51009
+ labelText.textContent = config.label || name;
51010
+ label.appendChild(labelText);
51011
+ const textarea = document.createElement('textarea');
51012
+ textarea.name = name;
51013
+ textarea.value = value || '';
51014
+ textarea.rows = config.rows || 4;
51015
+ if (config.placeholder) textarea.placeholder = config.placeholder;
51016
+ label.appendChild(textarea);
51017
+ if (config.description) {
51018
+ const desc = document.createElement('small');
51019
+ desc.style.cssText = 'display: block;margin-top: 4px;';
51020
+ desc.textContent = config.description;
51021
+ label.appendChild(desc);
51022
+ }
51023
+ return label;
51024
+ }
51025
+ createColorInput(name, config, value) {
51026
+ const container = document.createElement('div');
51027
+ const label = document.createElement('label');
51028
+ label.className = 'label';
51029
+ label.style.fontWeight = 500;
51030
+ label.style.cursor = 'pointer';
51031
+ const labelText = document.createElement('div');
51032
+ labelText.textContent = config.label || name;
51033
+ label.appendChild(labelText);
51034
+ const input = document.createElement('input');
51035
+ input.type = 'hidden';
51036
+ input.name = name;
51037
+ input.value = value || config.default || '#000000';
51038
+ const group = document.createElement('div');
51039
+ group.className = 'group';
51040
+ group.style.cssText = 'width: 52px; margin:0; overflow: visible;';
51041
+ const colorBtn = document.createElement('button');
51042
+ colorBtn.type = 'button';
51043
+ colorBtn.title = config.label || 'Color';
51044
+ colorBtn.className = 'btn-color is-btn-color';
51045
+ colorBtn.style.backgroundColor = input.value;
51046
+ colorBtn.setAttribute('aria-label', `Choose color for ${config.label || name}`);
51047
+ const openPicker = e => {
51048
+ e.preventDefault();
51049
+ this.builder.openColorPicker(input.value, color => {
51050
+ input.value = color;
51051
+ colorBtn.style.backgroundColor = color;
51052
+ input.dispatchEvent(new Event('change', {
51053
+ bubbles: true
51054
+ }));
51055
+ }, colorBtn);
51056
+ };
51057
+ colorBtn.addEventListener('click', openPicker);
51058
+ // Make label click trigger the button
51059
+ label.addEventListener('click', e => {
51060
+ if (e.target === label || e.target === labelText) {
51061
+ colorBtn.click();
51062
+ }
51063
+ });
51064
+ group.appendChild(colorBtn);
51065
+ label.appendChild(input);
51066
+ if (config.description) {
51067
+ const desc = document.createElement('small');
51068
+ desc.style.cssText = 'display: block;margin-top: 4px;';
51069
+ desc.textContent = config.description;
51070
+ label.appendChild(desc);
51071
+ }
51072
+ container.appendChild(label);
51073
+ container.appendChild(group);
51074
+ return container;
51075
+ }
51076
+ createColorInput2(name, config, value) {
51077
+ const label = document.createElement('label');
51078
+ label.className = 'label';
51079
+ label.style.fontWeight = 500;
51080
+ const labelText = document.createElement('div');
51081
+ labelText.textContent = config.label || name;
51082
+ label.appendChild(labelText);
51083
+ const input = document.createElement('input');
51084
+ input.type = 'color';
51085
+ input.name = name;
51086
+ input.style.cssText = 'width:40px;height:40px';
51087
+ input.value = value || config.default || '#000000';
51088
+ label.appendChild(input);
51089
+ if (config.description) {
51090
+ const desc = document.createElement('small');
51091
+ desc.style.cssText = 'display: block;margin-top: 4px;';
51092
+ desc.textContent = config.description;
51093
+ label.appendChild(desc);
51094
+ }
51095
+ return label;
51096
+ }
51097
+ createRangeInput(name, config, value) {
51098
+ const label = document.createElement('label');
51099
+ label.className = 'label';
51100
+ label.style.fontWeight = 500;
51101
+ const currentVal = value !== undefined ? value : config.default;
51102
+ const labelText = document.createElement('div');
51103
+ labelText.textContent = (config.label || name) + ': ' + currentVal + (config.unit || '');
51104
+ label.appendChild(labelText);
51105
+ const input = document.createElement('input');
51106
+ input.type = 'range';
51107
+ input.className = 'is-rangeslider';
51108
+ input.name = name;
51109
+ input.value = currentVal;
51110
+ if (config.min !== undefined) input.min = config.min;
51111
+ if (config.max !== undefined) input.max = config.max;
51112
+ if (config.step !== undefined) input.step = config.step;
51113
+ input.addEventListener('input', () => {
51114
+ labelText.textContent = (config.label || name) + ': ' + input.value + (config.unit || '');
51115
+ });
51116
+ label.appendChild(input);
51117
+ if (config.description) {
51118
+ const desc = document.createElement('small');
51119
+ desc.style.cssText = 'display: block;margin-top: 4px;';
51120
+ desc.textContent = config.description;
51121
+ label.appendChild(desc);
51122
+ }
51123
+ return label;
51124
+ }
51125
+
51126
+ /**
51127
+ * Get current values from element attributes
51128
+ */
51129
+ getCurrentValues(element, settings) {
51130
+ const values = {};
51131
+ Object.keys(settings).forEach(key => {
51132
+ if (key.startsWith('_')) return;
51133
+ const attrName = 'data-cb-' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
51134
+ const attrValue = element.getAttribute(attrName);
51135
+ if (attrValue !== null) {
51136
+ values[key] = this.parseValue(attrValue, settings[key].type);
51137
+ }
51138
+ });
51139
+ return values;
51140
+ }
51141
+
51142
+ /**
51143
+ * Parse attribute value based on type
51144
+ */
51145
+ parseValue(value, type) {
51146
+ switch (type) {
51147
+ case 'number':
51148
+ return parseFloat(value);
51149
+ case 'boolean':
51150
+ return value === 'true';
51151
+ default:
51152
+ return value;
51153
+ }
51154
+ }
51155
+
51156
+ /**
51157
+ * Get form values
51158
+ */
51159
+ getFormValues(form) {
51160
+ const values = {};
51161
+ const inputs = form.querySelectorAll('input, select, textarea');
51162
+ inputs.forEach(input => {
51163
+ const name = input.name;
51164
+ if (!name) return;
51165
+ if (input.type === 'checkbox') {
51166
+ values[name] = input.checked;
51167
+ } else if (input.type === 'radio') {
51168
+ if (input.checked) {
51169
+ values[name] = input.value;
51170
+ }
51171
+ } else {
51172
+ values[name] = input.value;
51173
+ }
51174
+ });
51175
+ return values;
51176
+ }
51177
+
51178
+ /**
51179
+ * Apply form values to element
51180
+ */
51181
+ applyValues(element, values) {
51182
+ Object.keys(values).forEach(key => {
51183
+ const attrName = 'data-cb-' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
51184
+ element.setAttribute(attrName, values[key]);
51185
+ });
51186
+ }
51187
+ createEmptyForm() {
51188
+ const form = document.createElement('div');
51189
+ form.className = 'cb-settings-form';
51190
+ form.innerHTML = '<p>This plugin has no configurable settings.</p>';
51191
+ return form;
51192
+ }
51193
+ }
51194
+
51195
+ class Plugin {
51196
+ constructor(builder) {
51197
+ this.builder = builder;
51198
+ }
51199
+ renderTool() {
51200
+ const builderStuff = this.builder.builderStuff;
51201
+ const contentStuff = this.builder.contentStuff;
51202
+ const util = this.builder.util;
51203
+ const dom = this.builder.dom;
51204
+ let pluginTool = builderStuff.querySelector('.is-plugin-tool');
51205
+ if (!pluginTool) {
51206
+ let html = `
51207
+ <div class="is-tool is-plugin-tool">
51208
+ <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>
51209
+ </div>
51210
+ `;
51211
+ dom.appendHtml(builderStuff, html);
51212
+ if (!this.builder.iframe) {
51213
+ pluginTool = builderStuff.querySelector('.is-plugin-tool');
51214
+ } else {
51215
+ pluginTool = contentStuff.querySelector('.is-plugin-tool');
51216
+ }
51217
+ this.pluginTool = pluginTool;
51218
+ let btn = pluginTool.querySelector('button');
51219
+ dom.addEventListener(btn, 'click', () => {
51220
+ // old 10317
51221
+
51222
+ if (this.pluginModal && this.pluginModal.classList.contains('active')) {
51223
+ this.hidePluginEditor();
51224
+ } else {
51225
+ this.renderPanel();
51226
+ this.showPluginEditor();
51227
+ }
51228
+ btn.setAttribute('data-focus', true);
51229
+ });
51230
+ }
51231
+ }
51232
+ renderPanel() {
51233
+ const builderStuff = this.builder.builderStuff;
51234
+ const util = this.builder.util;
51235
+ const dom = this.builder.dom;
51236
+ let pluginModal = builderStuff.querySelector('.is-modal.pluginsettings');
51237
+ if (!pluginModal) {
51238
+ let html = `
51239
+ <div class="is-modal is-modal-content pluginsettings" tabindex="-1" role="dialog" aria-modal="true" aria-hidden="true">
51240
+ <div class="is-modal-bar is-draggable">
51241
+ <span>${util.out('Plugin Settings')}</span>
51242
+ <button class="is-modal-close" tabindex="-1" title="${util.out('Close')}">&#10005;</button>
51243
+ </div>
51244
+ <div class="is-modal-controls" style="box-sizing:border-box;padding:30px;overflow-y: auto;height: calc(100% - 30px);">
51245
+ <div class="is-tabs" data-group="table">
51246
+ <a title="${util.out('Style')}" id="tabTableGeneral" href="" data-content="divTableGeneral" class="active">${util.out('Style')}</a>
51247
+ <a title="${util.out('Layout')}" id="tabTableLayout" href="" data-content="divTableLayout">${util.out('Layout')}</a>
51248
+ </div>
51249
+ <div id="divTableGeneral" class="is-tab-content active" tabindex="-1" data-group="table" style="display:block">
51250
+
51251
+ <div style="display:flex;padding-bottom:12px">
51252
+ <div style="padding-right:15px">
51253
+ <div>${util.out('Background')}:</div>
51254
+ <div>
51255
+ <button title="${util.out('Background Color')}" class="input-table-bgcolor is-btn-color"></button>
51256
+ </div>
51257
+ </div>
51258
+ <div>
51259
+ <div>${util.out('Text Color')}:</div>
51260
+ <div>
51261
+ <button title="${util.out('Text Color')}" class="input-table-textcolor is-btn-color"></button>
51262
+ </div>
51263
+ </div>
51264
+ </div>
51265
+
51266
+ <div style="padding-bottom:12px;">
51267
+ <div>${util.out('Border Thickness')}:</div>
51268
+ <div>
51269
+ <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>
51270
+ </div>
51271
+ </div>
51272
+
51273
+ <div style="padding-bottom:12px;">
51274
+ <div>${util.out('Border Color')}:</div>
51275
+ <div>
51276
+ <button title="${util.out('Border Color')}" class="input-table-bordercolor is-btn-color"></button>
51277
+ </div>
51278
+ </div>
51279
+
51280
+ <div style="padding-bottom:12px;">
51281
+ <div>${util.out('Apply To')}:</div>
51282
+ <div>
51283
+ <select id="selTableApplyTo" style="width:120px;">
51284
+ <option value="table">${util.out('Table')}</option>
51285
+ <option value="currentrow">${util.out('Current Row')}</option>
51286
+ <option value="currentcol">${util.out('Current Column')}</option>
51287
+ <option value="evenrows">${util.out('Even Rows')}</option>
51288
+ <option value="oddrows">${util.out('Odd Rows')}</option>
51289
+ <option value="currentcell">${util.out('Current Cell')}</option>
51290
+ </select>
51291
+ </div>
51292
+ </div>
51293
+
51294
+ </div>
51295
+
51296
+ <div id="divTableLayout" class="is-tab-content" tabindex="-1" data-group="table">
51297
+
51298
+ <div style="padding-bottom:12px;">
51299
+ <div>${util.out('Insert Row')}:</div>
51300
+ <div style="display:flex">
51301
+ <button class="classic" title="${util.out('Above')}" data-table-cmd="rowabove" title="${util.out('Above')}" style="margin-right:15px"> ${util.out('Above')} </button>
51302
+ <button class="classic" title="${util.out('Below')}" data-table-cmd="rowbelow" title="${util.out('Below')}" style=""> ${util.out('Below')} </button>
51303
+ </div>
51304
+ </div>
51305
+
51306
+ <div style="padding-bottom:15px;">
51307
+ <div>${util.out('Insert Column')}:</div>
51308
+ <div style="display:flex">
51309
+ <button class="classic" title="${util.out('Left')}" data-table-cmd="columnleft" title="${util.out('Left')}" style="margin-right:15px"> ${util.out('Left')} </button>
51310
+ <button class="classic" title="${util.out('Right')}" data-table-cmd="columnright" title="${util.out('Right')}" style=""> ${util.out('Right')} </button>
51311
+ </div>
51312
+ </div>
51313
+
51314
+ <div style="padding-bottom:15px;">
51315
+ <button class="classic" title="${util.out('Delete Row')}" data-table-cmd="delrow" title="Delete Row" style=""> ${util.out('Delete Row')} </button>
51316
+ </div>
51317
+
51318
+ <div style="padding-bottom:15px;">
51319
+ <button class="classic" title="${util.out('Delete Column')}" data-table-cmd="delcolumn" title="Delete Column" style=""> ${util.out('Delete Column')} </button>
51320
+ </div>
51321
+
51322
+ <div>
51323
+ <button class="classic" title="${util.out('Merge Cell')}" data-table-cmd="mergecell" style="">${util.out('Merge Cell')}</button>
51324
+ </div>
51325
+ </div>
51326
+ </div>
51327
+ </div>
51328
+ `;
51329
+ dom.appendHtml(builderStuff, html);
51330
+ pluginModal = builderStuff.querySelector('.is-modal.pluginsettings');
51331
+ this.pluginModal = pluginModal;
51332
+ let btnOk = pluginModal.querySelector('.input-ok');
51333
+ dom.addEventListener(btnOk, 'click', () => {
51334
+ this.builder.uo.saveForUndo();
51335
+ this.builder.opts.onChange();
51336
+ util.hideModal(pluginModal);
51337
+ });
51338
+ let btnCancel = pluginModal.querySelector('.input-cancel');
51339
+ dom.addEventListener(btnCancel, 'click', () => {
51340
+ util.hideModal(pluginModal);
51341
+ });
51342
+ new Tabs({
51343
+ element: pluginModal
51344
+ });
51345
+ new Draggable$2({
51346
+ selector: '.is-modal.pluginsettings .is-draggable'
51347
+ });
51348
+ }
51349
+ }
51350
+ showPluginEditor() {
51351
+ const pluginModal = this.pluginModal;
51352
+ this.builder.util.showModal(pluginModal);
51353
+ this.realtime();
51354
+ const handlePluginClick = e => {
51355
+ const clrPicker = document.querySelector('.pop-picker.active') || document.querySelector('.pickgradientcolor.active');
51356
+ let elm = e.target;
51357
+ let elmRemoved = !document.contains(elm); // e.g. when deleting a card in card list plugin
51358
+
51359
+ if (this.builder.doc.activeElement) {
51360
+ if (this.builder.doc.activeElement.closest('.is-modal')) {
51361
+ // prevent modal close when mouseup outside the modal
51362
+ return;
51363
+ }
51364
+ }
51365
+ if (!elm) return;
51366
+ 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) {
51367
+ // click outside
51368
+
51369
+ // hide
51370
+ this.hidePluginEditor();
51371
+ // console.log('HIDE');
51372
+
51373
+ document.removeEventListener('click', handlePluginClick);
51374
+ if (this.builder.iframeDocument) {
51375
+ this.builder.doc.removeEventListener('click', handlePluginClick);
51376
+ }
51377
+ this.builder.handlePluginClick_ = false;
51378
+ }
51379
+ if (elm.closest('[data-cb-type]')) {
51380
+ this.realtime();
51381
+ }
51382
+ };
51383
+ if (!this.builder.handlePluginClick_) {
51384
+ document.addEventListener('click', handlePluginClick);
51385
+ if (this.builder.iframeDocument) {
51386
+ this.builder.doc.addEventListener('click', handlePluginClick);
51387
+ }
51388
+ this.builder.handlePluginClick_ = true;
51389
+ }
51390
+ }
51391
+ hidePluginEditor() {
51392
+ const pluginModal = this.pluginModal;
51393
+ if (pluginModal) this.builder.util.hideModal(pluginModal);
51394
+ }
51395
+ realtime() {
51396
+ const util = this.builder.util;
51397
+ const pluginModal = this.pluginModal;
51398
+ let currentElement = this.builder.activePlugin;
51399
+ const runtime = this.builder.win.builderRuntime;
51400
+ if (currentElement && runtime) {
51401
+ const pluginName = currentElement.getAttribute('data-cb-type');
51402
+ const plugin = runtime.getPlugin(pluginName);
51403
+ let titleHtml = util.out(plugin.displayName || plugin.name);
51404
+ const modalBar = pluginModal.querySelector('.is-modal-bar > span');
51405
+ modalBar.innerHTML = titleHtml;
51406
+ const container = pluginModal.querySelector('.is-modal-controls');
51407
+
51408
+ // Clear panel
51409
+ container.innerHTML = '';
51410
+ const div = document.createElement('div');
51411
+ div.classList.add('submain');
51412
+ container.appendChild(div);
51413
+
51414
+ // Store references
51415
+ this.currentElement = currentElement;
51416
+ this.generator = new SettingsUIGenerator(runtime, this.builder);
51417
+
51418
+ // Check if plugin has custom content editor
51419
+ const hasContentEditor = plugin && plugin.editor && plugin.editor.openContentEditor;
51420
+ if (hasContentEditor) {
51421
+ // Get original content for the editor to work with
51422
+ const originalContent = currentElement.getAttribute('data-cb-original-content');
51423
+
51424
+ // Create a temporary element for editing (so editor can manipulate it)
51425
+ let editableClone = document.querySelector('.editable-clone');
51426
+ if (editableClone) editableClone.remove();
51427
+ // editableClone = document.createElement('div');
51428
+ editableClone = currentElement.cloneNode(false);
51429
+ editableClone.innerHTML = originalContent;
51430
+ editableClone.className = 'editable-clone';
51431
+ editableClone.style.display = 'none'; // Hidden, just for editing
51432
+ document.body.appendChild(editableClone);
51433
+ const originalElement = editableClone.cloneNode(false);
51434
+
51435
+ // Let plugin handle everything - pass the editable clone
51436
+ const editorUI = plugin.editor.openContentEditor(editableClone, this.builder, async () => {
51437
+ this.builder.uo.saveForUndo();
51438
+
51439
+ // Store edited content from clone
51440
+ currentElement.setAttribute('data-cb-original-content', editableClone.innerHTML);
51441
+
51442
+ // Also performs similar value updates like the applyChanges()
51443
+ // currentElement.setAttribute('data-cb-content', editableClone.getAttribute('data-cb-content'));
51444
+ const getChangedDataAttributes = (el1, el2) => {
51445
+ const changed = [];
51446
+ for (const key of Object.keys(el1.dataset)) {
51447
+ if (key in el2.dataset && el1.dataset[key] !== el2.dataset[key]) {
51448
+ changed.push(key);
51449
+ }
51450
+ }
51451
+ return changed;
51452
+ };
51453
+ const changedAttributes = getChangedDataAttributes(originalElement, editableClone);
51454
+ // let hasChange = changedAttributes.length > 0;
51455
+ for (const attrName of changedAttributes) {
51456
+ // console.log(attrName);
51457
+ currentElement.dataset[attrName] = editableClone.dataset[attrName];
51458
+ // update form (not working)
51459
+ // const convertCbKey = (str) => {
51460
+ // return str.replace(/^cb/, '').replace(/^./, c => c.toLowerCase());
51461
+ // }
51462
+ // const name = convertCbKey(attrName);
51463
+ // this.currentForm.querySelector(`[name="${name}"]`).value = editableClone.dataset[attrName];
51464
+ }
51465
+ // console.log(hasChange)
51466
+ // -------
51467
+
51468
+ currentElement.innerHTML = editableClone.innerHTML; // Update current content
51469
+
51470
+ // Remove temporary clone
51471
+ editableClone.remove();
51472
+
51473
+ // update form (working)
51474
+ // currentElement.click();
51475
+ // return;
51476
+
51477
+ // Reinitialize plugin with new content
51478
+ await runtime.reinitialize(currentElement.parentElement);
51479
+ this.builder.onChange();
51480
+ });
51481
+ div.appendChild(editorUI);
51482
+ }
51483
+
51484
+ // Generate form
51485
+ this.currentForm = this.generator.generateForm(pluginName, currentElement, () => {
51486
+ this.builder.uo.saveForUndo();
51487
+ this.applyChanges(runtime);
51488
+ });
51489
+ this.currentForm.style.marginTop = '20px';
51490
+ div.appendChild(this.currentForm);
51491
+ }
51492
+ }
51493
+ async applyChanges(runtime) {
51494
+ if (!this.currentElement || !this.currentForm) return;
51495
+
51496
+ // 1. Get form values
51497
+ const values = this.generator.getFormValues(this.currentForm);
51498
+
51499
+ // 2. Apply to element attributes
51500
+ this.generator.applyValues(this.currentElement, values);
51501
+
51502
+ // 3. Reinitialize component
51503
+ const container = this.currentElement.parentElement || this.currentElement.closest('.is-wrapper');
51504
+ await runtime.reinitialize(container);
51505
+
51506
+ //Trigger Change event
51507
+ this.builder.opts.onChange();
51508
+
51509
+ // console.log('Settings applied and component reinitialized');
51510
+ }
51511
+
51512
+ click(e) {
51513
+ const plugin = e.target.closest('[data-cb-type]');
51514
+ if (plugin) {
51515
+ this.builder.activePlugin = plugin;
51516
+ this.renderTool();
51517
+ this.pluginTool.style.display = 'flex';
51518
+ let _toolwidth = this.pluginTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
51519
+
51520
+ let w = plugin.offsetWidth * this.builder.opts.zoom;
51521
+ let top = plugin.getBoundingClientRect().top + this.builder.win.pageYOffset;
51522
+ let left = plugin.getBoundingClientRect().left - 2;
51523
+ left = left + (w - _toolwidth);
51524
+
51525
+ //Adjust left in case an element is outside the screen
51526
+ const _screenwidth = window.innerWidth;
51527
+ if (_toolwidth + left > _screenwidth) left = plugin.getBoundingClientRect().left;
51528
+ this.pluginTool.style.top = top + 'px';
51529
+ this.pluginTool.style.left = left + 'px';
51530
+ } else {
51531
+ this.builder.activePlugin = null;
51532
+ if (this.pluginTool) this.pluginTool.style.display = '';
51533
+ }
51534
+ }
51535
+ }
51536
+
50727
51537
  class Module {
50728
51538
  constructor(builder) {
50729
51539
  this.builder = builder;
@@ -52565,6 +53375,7 @@ class Element$1 {
52565
53375
  this.button = new Button(builder);
52566
53376
  this.image = new Image$1(builder);
52567
53377
  this.spacer = new Spacer(builder);
53378
+ this.plugin = new Plugin(builder);
52568
53379
  this.module = new Module(builder);
52569
53380
  this.code = new Code(builder);
52570
53381
  this.iframe = new Iframe(builder);
@@ -52603,6 +53414,10 @@ class Element$1 {
52603
53414
 
52604
53415
  // Set contentEditable FALSE on special elements
52605
53416
 
53417
+ const plugins = col.querySelectorAll('[data-cb-type]'); // plugins
53418
+ Array.prototype.forEach.call(plugins, plugin => {
53419
+ plugin.contentEditable = false;
53420
+ });
52606
53421
  let sociallinks = col.querySelectorAll('.is-social');
52607
53422
  Array.prototype.forEach.call(sociallinks, sociallink => {
52608
53423
  sociallink.contentEditable = false;
@@ -52767,6 +53582,9 @@ class Element$1 {
52767
53582
 
52768
53583
  // Module
52769
53584
  this.module.click(col, e);
53585
+
53586
+ // Plugin
53587
+ this.plugin.click(e);
52770
53588
  }
52771
53589
  }
52772
53590
 
@@ -61922,6 +62740,12 @@ class ElementTool {
61922
62740
  const btnMore = elementTool.querySelector('.elm-more');
61923
62741
  dom.addEventListener(btnMore, 'click', () => {
61924
62742
  const viewportHeight = window.innerHeight;
62743
+ const elmSettings = elementMore.querySelector('.elm-settings');
62744
+ if (this.builder.activeElement.hasAttribute('data-cb-type')) {
62745
+ elmSettings.style.display = 'none';
62746
+ } else {
62747
+ elmSettings.style.display = '';
62748
+ }
61925
62749
 
61926
62750
  /*
61927
62751
  let top, left;
@@ -62187,7 +63011,10 @@ class ElementTool {
62187
63011
  if (dom.parentsHasClass(elm, 'is-subblock')) {
62188
63012
  subblock = true;
62189
63013
  }
62190
- if ((customcode || noedit || _protected) && !subblock) ; else {
63014
+ const plugin = elm.closest('[data-cb-type]');
63015
+ if ((customcode || noedit || _protected) && !subblock) ; else if (plugin) {
63016
+ activeElement = plugin;
63017
+ } else {
62191
63018
  const tagName = elm.tagName.toLowerCase();
62192
63019
  // LATER: label, code, figcaption ?
62193
63020
  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')) {
@@ -62339,7 +63166,8 @@ class ElementTool {
62339
63166
  let elm = this.builder.activeElement;
62340
63167
  if (!elm) return;
62341
63168
  if (elm.closest('.is-dock')) return;
62342
- if (elm.closest('[data-cb-type]')) return;
63169
+ // if(elm.closest('[data-cb-type]')) return;
63170
+
62343
63171
  let top, left;
62344
63172
  if (!this.builder.iframe) {
62345
63173
  top = elm.getBoundingClientRect().top + window.pageYOffset;
@@ -97180,6 +98008,143 @@ Add an image for each feature.`, 'Create a new content showcasing a photo galler
97180
98008
  fromViewToActual(html) {
97181
98009
  return this.htmlutil.fromViewToActual(html);
97182
98010
  }
98011
+ openColorPicker(currentColor, callback, btn) {
98012
+ this.colorPicker.open(callback, currentColor, () => {}, btn);
98013
+ }
98014
+ openIconPicker(callback) {
98015
+ let iconModal = this.builderStuff.querySelector('.is-modal.iconselect');
98016
+ if (!iconModal) {
98017
+ // Create modal with search input and event listeners once
98018
+ let html = `
98019
+ <style>
98020
+ .is-modal.iconselect .icon-search-wrapper {
98021
+ position: sticky;
98022
+ top: 0;
98023
+ background: white;
98024
+ padding: 20px;
98025
+ border-bottom: 1px solid #ddd;
98026
+ z-index: 10;
98027
+ }
98028
+ .is-modal.iconselect .icon-search {
98029
+ width: 100%;
98030
+ padding: 10px 15px;
98031
+ font-size: 16px;
98032
+ border: 1px solid #ccc;
98033
+ border-radius: 4px;
98034
+ box-sizing: border-box;
98035
+ }
98036
+ .is-modal.iconselect .icon-search:focus {
98037
+ outline: none;
98038
+ border-color: #007bff;
98039
+ }
98040
+ .is-modal.iconselect .icon-list {
98041
+ display: flex;
98042
+ gap: 10px;
98043
+ flex-flow: wrap;
98044
+ padding: 20px;
98045
+ }
98046
+ .is-modal.iconselect .icon-list > button {
98047
+ display: flex !important;
98048
+ width: 80px !important;
98049
+ height: 80px !important;
98050
+ flex: none;
98051
+ font-size: 30px !important;
98052
+ box-shadow: none !important;
98053
+ align-items: center;
98054
+ justify-content: center;
98055
+ }
98056
+ .is-modal.iconselect .icon-list > button.hidden {
98057
+ display: none !important;
98058
+ }
98059
+ .is-modal.iconselect .no-results {
98060
+ padding: 40px 20px;
98061
+ text-align: center;
98062
+ color: #666;
98063
+ display: none;
98064
+ }
98065
+ .is-modal.iconselect .no-results.show {
98066
+ display: block;
98067
+ }
98068
+ </style>
98069
+ <div class="is-modal iconselect" tabindex="-1" role="dialog" aria-modal="true" aria-hidden="true">
98070
+ <div class="is-modal-content scroll" style="width: 90vw; max-width: 908px; height: 70vh; min-height: 525px; padding: 0;">
98071
+ <div style="height: 100%; overflow-y: auto; display: flex; flex-direction: column;">
98072
+ <div class="icon-search-wrapper">
98073
+ <input type="text" class="icon-search" placeholder="Search icons... (e.g., alarm, wrench)" />
98074
+ </div>
98075
+ <div class="icon-list"></div>
98076
+ <div class="no-results">No icons found matching your search.</div>
98077
+ </div>
98078
+ </div>
98079
+ </div>
98080
+ `;
98081
+ this.dom.appendHtml(this.builderStuff, html);
98082
+ iconModal = this.builderStuff.querySelector('.is-modal.iconselect');
98083
+ this.iconModal = iconModal;
98084
+ const iconList = this.builderStuff.querySelector('.icon-list');
98085
+ const searchInput = this.builderStuff.querySelector('.icon-search');
98086
+ const noResults = this.builderStuff.querySelector('.no-results');
98087
+ iconList.innerHTML = this.rte.getIcons2() + this.rte.getIcons();
98088
+ let icons = iconList.querySelectorAll('button');
98089
+
98090
+ // Add click handlers for icons
98091
+ icons.forEach(icon => {
98092
+ icon.addEventListener('click', () => {
98093
+ // Get the current callback from the modal's data
98094
+ const currentCallback = iconModal._currentCallback;
98095
+ if (currentCallback) {
98096
+ currentCallback(icon.innerHTML);
98097
+ }
98098
+ this.util.hideModal(iconModal);
98099
+ });
98100
+ });
98101
+
98102
+ // Add search/filter functionality
98103
+ searchInput.addEventListener('input', e => {
98104
+ const searchTerm = e.target.value.toLowerCase().trim();
98105
+ let visibleCount = 0;
98106
+ icons.forEach(icon => {
98107
+ const iconElement = icon.querySelector('i');
98108
+ if (!iconElement) return;
98109
+
98110
+ // Get class names from the icon
98111
+ const iconClasses = iconElement.className.toLowerCase();
98112
+
98113
+ // Check if search term matches any part of the icon classes
98114
+ if (searchTerm === '' || iconClasses.includes(searchTerm)) {
98115
+ icon.classList.remove('hidden');
98116
+ visibleCount++;
98117
+ } else {
98118
+ icon.classList.add('hidden');
98119
+ }
98120
+ });
98121
+
98122
+ // Show/hide no results message
98123
+ if (visibleCount === 0 && searchTerm !== '') {
98124
+ noResults.classList.add('show');
98125
+ } else {
98126
+ noResults.classList.remove('show');
98127
+ }
98128
+ });
98129
+
98130
+ // Clear search when modal is opened
98131
+ iconModal.addEventListener('modalshow', () => {
98132
+ searchInput.value = '';
98133
+ searchInput.dispatchEvent(new Event('input'));
98134
+ });
98135
+ }
98136
+
98137
+ // Store the current callback and reset search
98138
+ iconModal._currentCallback = callback;
98139
+ const searchInput = iconModal.querySelector('.icon-search');
98140
+ if (searchInput) {
98141
+ searchInput.value = '';
98142
+ searchInput.dispatchEvent(new Event('input'));
98143
+ // Focus search input after a brief delay to ensure modal is visible
98144
+ setTimeout(() => searchInput.focus(), 100);
98145
+ }
98146
+ this.util.showModal(iconModal);
98147
+ }
97183
98148
  colorpicker(onPick, defaultcolor) {
97184
98149
  // return new ColorPicker({
97185
98150
  // onPick: onPick,