@innovastudio/contentbox 1.6.203 → 1.6.204

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.
@@ -627,7 +627,14 @@ class CustomSelect$1 {
627
627
  this.selectStyled.addEventListener('keydown', this.handleKeydown.bind(this));
628
628
  this.selectStyled.addEventListener('click', this.toggleOptions.bind(this));
629
629
  this.selectOptions.addEventListener('click', this.handleOptionClick.bind(this));
630
- this.selectSearch.addEventListener('input', this.handleFilterInput.bind(this)); // this.selectStyled.addEventListener('mousedown', ()=>{
630
+ this.selectSearch.addEventListener('input', this.handleFilterInput.bind(this)); // Keep the content's selection: without an iframe the content shares this
631
+ // document, so a press here would move the selection into the panel and
632
+ // the chosen value would have no text to apply to.
633
+
634
+ const keepSelection = e => e.preventDefault();
635
+
636
+ this.selectStyled.addEventListener('mousedown', keepSelection);
637
+ this.selectOptions.addEventListener('mousedown', keepSelection); // this.selectStyled.addEventListener('mousedown', ()=>{
631
638
  // this.select.classList.add('clicked');
632
639
  // });
633
640
  }
@@ -11471,7 +11478,7 @@ class Text {
11471
11478
  if (!anchorNode) return false;
11472
11479
  let container = anchorNode.nodeType !== Node.TEXT_NODE && anchorNode.nodeType !== Node.COMMENT_NODE ? anchorNode : anchorNode.parentElement;
11473
11480
 
11474
- if (container && container.closest('.is-overlay') && this.builder.controlpanel.activeElement && !container.closest('[data-cb-type]')) {
11481
+ if (container && container.closest('.is-overlay') && this.builder.controlpanel.activeElement && this.builder.controlpanel.activeElement.isConnected && !container.closest('[data-cb-type]')) {
11475
11482
  return this.builder.controlpanel.activeElement; // container => is-overlay when drag & drop bloxk
11476
11483
  }
11477
11484
  /*
@@ -11495,9 +11502,13 @@ class Text {
11495
11502
  getSelection() works only if element selectable. If not, it uses previous selected element which is incorrect.
11496
11503
  So we check if activeElement is different with getSelection() => means incorrect
11497
11504
  */
11505
+ // An edit can take the active element off the page (clearing a size
11506
+ // unwraps its span); the selection is then the right answer.
11498
11507
 
11499
11508
 
11500
- if (this.builder.controlpanel.activeElement.contains(container)) ; else {
11509
+ const activeElement = this.builder.controlpanel.activeElement;
11510
+
11511
+ if (!activeElement || !activeElement.isConnected || activeElement.contains(container)) ; else {
11501
11512
  container = this.builder.controlpanel.activeElement; // If incorrect, uses activeElement
11502
11513
  }
11503
11514
 
@@ -18781,6 +18792,11 @@ class PanelOverlay {
18781
18792
 
18782
18793
  <div class="inp-text" role="textbox"></div>
18783
18794
 
18795
+ <div class="label mt-4">${out('Color')}:</div>
18796
+ <div class="group">
18797
+ <button type="button" title="${out('Color')}" class="btn-ovl-color cb-colorbtn"><span></span></button>
18798
+ </div>
18799
+
18784
18800
  <div class="label-note mt-4 note-motion">${out('This text is part of an animated layer. Change the wording here; its movement lives on the Motion tab above.')}</div>
18785
18801
 
18786
18802
  <div class="label-note mt-4 note-plain" style="display:none">${out('Change the wording here. Its placement and styling come from the section design.')}</div>
@@ -18954,6 +18970,38 @@ class PanelOverlay {
18954
18970
  attrs: ['alt']
18955
18971
  });
18956
18972
  this.builder.editor.onChange();
18973
+ }); // --- Text colour ---
18974
+ //
18975
+ // Written inline on the selected element, so it overrides the design's
18976
+ // colour for this text only; clearing it in the picker returns the
18977
+ // colour it had when selected. One undo step per picker session.
18978
+
18979
+ this.origColors = new WeakMap();
18980
+ const btnColor = panel.querySelector('.btn-ovl-color');
18981
+ btnColor.addEventListener('click', () => {
18982
+ const el = this.target();
18983
+ if (!el) return;
18984
+ let fresh = true;
18985
+ const current = el.style.color || this.builder.win.getComputedStyle(el).color;
18986
+ const colorpicker = this.builder.editor.colorpicker();
18987
+ colorpicker.open(color => {
18988
+ const el = this.target();
18989
+ if (!el) return;
18990
+
18991
+ if (fresh) {
18992
+ this.builder.editor.saveForUndo();
18993
+ fresh = false;
18994
+ } // Cleared: back to the colour the text had when it was selected,
18995
+ // which may itself be inline (set by the design) or none.
18996
+
18997
+
18998
+ el.style.color = color || this.origColors.get(el) || '';
18999
+ syncToCodeSource(el, {
19000
+ styles: ['color']
19001
+ });
19002
+ this.paintColor(el);
19003
+ this.builder.editor.onChange();
19004
+ }, current, () => {}, btnColor, true); // overlay=true
18957
19005
  }); // --- Text ---
18958
19006
 
18959
19007
  this.textEditor = panel.querySelector('.inp-text'); // No formatting UI, deliberately: this panel edits AI-generated overlay
@@ -18996,6 +19044,16 @@ class PanelOverlay {
18996
19044
  sanitize: false
18997
19045
  });
18998
19046
  });
19047
+ } // The swatch shows the colour the text has now: its own if set, otherwise
19048
+ // the one it gets from the design.
19049
+
19050
+
19051
+ paintColor(el) {
19052
+ const btn = this.panel.querySelector('.btn-ovl-color');
19053
+ if (!btn || !el) return;
19054
+ const color = el.style.color || this.builder.win.getComputedStyle(el).color;
19055
+ const editor = this.builder.editor;
19056
+ if (editor && typeof editor.paintColorButton === 'function') editor.paintColorButton(btn, color);else btn.style.backgroundColor = color;
18999
19057
  }
19000
19058
 
19001
19059
  escapeHtml(text) {
@@ -19327,6 +19385,9 @@ class PanelOverlay {
19327
19385
  // it (which would move their caret to the end on every keystroke).
19328
19386
 
19329
19387
  if (panel.ownerDocument.activeElement !== this.textEditor) {
19388
+ // Opening a plugin's panel tears down every panel editor
19389
+ // (destroyPanelEditors), this field included: bring it back.
19390
+ if (!this.ed || !this.textEditor.isContentEditable) this.setupTextEditor();
19330
19391
  this._loadingText = true;
19331
19392
  if (this.ed) this.ed.setHTML(el.innerHTML);else this.textEditor.innerHTML = el.innerHTML;
19332
19393
  this._loadingText = false;
@@ -19342,6 +19403,8 @@ class PanelOverlay {
19342
19403
  this.textEditor.style.fontWeight = computed.fontWeight;
19343
19404
  this.textEditor.style.fontStyle = computed.fontStyle;
19344
19405
  this.textEditor.style.textTransform = computed.textTransform;
19406
+ if (!this.origColors.has(el)) this.origColors.set(el, el.style.color);
19407
+ this.paintColor(el);
19345
19408
  }
19346
19409
 
19347
19410
  getStateSource(el) {
@@ -30004,7 +30067,7 @@ class PanelBox {
30004
30067
  <button title="${out('Color')}" class="btn-color is-btn-color"></button>
30005
30068
  </div>
30006
30069
  </div>
30007
- <div>
30070
+ <div class="div-box-gradient">
30008
30071
  <div class="label mt-3">${out('Gradient')}:</div>
30009
30072
  <div class="group">
30010
30073
  <button title="${out('Gradient')}" class="btn-gradient is-btn-color"></button>
@@ -30074,10 +30137,9 @@ class PanelBox {
30074
30137
 
30075
30138
  </div>
30076
30139
 
30077
- <div class="submain box-plugin-settings panel-plugin pluginsettings">
30078
-
30079
- <hr style="width: 100%;border-bottom: #ededed 2px solid;margin-top: 20px;">
30080
-
30140
+ <div class="submain div-box-plugins" style="display:none">
30141
+ <div class="label mt-3">${out('In this box')}:</div>
30142
+ <div class="overlay-list mt-1"></div>
30081
30143
  </div>
30082
30144
 
30083
30145
  <button class="accordion-item btn-accordion-content" aria-expanded="false" aria-controls="boxcontent1">
@@ -31134,6 +31196,13 @@ class PanelBox {
31134
31196
 
31135
31197
  static css() {
31136
31198
  return `
31199
+ .cb-navbar-box .boxedit,
31200
+ .cb-navbar-box .div-box-gradient,
31201
+ .cb-navbar-box .box-bg-image,
31202
+ .cb-navbar-box > .accordion-item,
31203
+ .cb-navbar-box > .accordion-content {
31204
+ display: none !important;
31205
+ }
31137
31206
  [data-cb-ui] .cb-contentpos .cb-fieldhead {
31138
31207
  display: flex;
31139
31208
  align-items: baseline;
@@ -31445,7 +31514,10 @@ class PanelBox {
31445
31514
  const overlayBg = activeBox.querySelector('.is-overlay-bg');
31446
31515
  const overlayContent = activeBox.querySelector('.is-overlay-content');
31447
31516
  const container = this.box.contentContainer(activeBox);
31448
- const panel = this.panel;
31517
+ const panel = this.panel; // A navigation bar keeps only its background colour (the bar's colour once the
31518
+ // header has scrolled by): no duplicate, extra box, gradient, image or box tabs
31519
+
31520
+ panel.classList.toggle('cb-navbar-box', !!activeBox.closest('.is-section-navbar'));
31449
31521
  const divEditModule = panel.querySelector('.group.editmodule');
31450
31522
 
31451
31523
  if (overlayContent && overlayContent.getAttribute('data-module')) {
@@ -31789,12 +31861,12 @@ class PanelBox {
31789
31861
  chkAutoLayout.checked = true;
31790
31862
  } else {
31791
31863
  chkAutoLayout.checked = false;
31792
- } // PLUGIN SETTINGS
31864
+ } // PLUGINS
31865
+ // A plugin in the box overlay has its own panel: a click on it opens that.
31866
+ // Here the box lists them, and each can hide box settings it has no use for.
31793
31867
 
31794
31868
 
31795
- const divPluginSettings = panel.querySelector('.box-plugin-settings');
31796
- this.divPluginSettings = divPluginSettings;
31797
- const runtime = this.builder.win.builderRuntime; // Reset (can be changed by renderPluginSettings)
31869
+ const runtime = this.builder.win.builderRuntime; // Reset (can be changed by applyPluginOptions)
31798
31870
 
31799
31871
  const divBgColor = this.panel.querySelector('.box-bg-color');
31800
31872
  divBgColor.style.display = '';
@@ -31805,39 +31877,55 @@ class PanelBox {
31805
31877
  const divPanelTitle = this.panel.parentNode.querySelector('.part-title');
31806
31878
  divPanelTitle.style.display = '';
31807
31879
  this.divPanelTitle = divPanelTitle;
31880
+ const divPlugins = panel.querySelector('.div-box-plugins');
31881
+ const list = divPlugins.querySelector('.overlay-list');
31882
+ list.innerHTML = ''; // The box's own plugins: not the ones inside its content areas, nor inside another plugin
31808
31883
 
31809
- if (overlayContent && runtime) {
31810
- const currentElement = overlayContent.querySelector('[data-cb-type]');
31884
+ const plugins = overlayContent && runtime ? Array.from(overlayContent.querySelectorAll('[data-cb-type]')).filter(el => !el.closest('.is-container') && !(el.parentElement && el.parentElement.closest('[data-cb-type]'))) : []; // Several of the same plugin are numbered, so their rows can be told apart
31811
31885
 
31812
- if (currentElement) {
31813
- divPluginSettings.style.display = '';
31814
- this.renderPluginSettings(currentElement);
31815
- } else {
31816
- divPluginSettings.style.display = 'none';
31817
- }
31818
- } else {
31819
- divPluginSettings.style.display = 'none';
31820
- }
31821
- }
31886
+ const counts = {},
31887
+ seen = {};
31888
+ plugins.forEach(el => {
31889
+ const t = el.getAttribute('data-cb-type');
31890
+ counts[t] = (counts[t] || 0) + 1;
31891
+ });
31892
+ plugins.forEach(el => {
31893
+ const type = el.getAttribute('data-cb-type');
31894
+ const plugin = runtime.getPlugin(type);
31895
+ if (!plugin) return;
31896
+ seen[type] = (seen[type] || 0) + 1;
31897
+ this.applyPluginOptions(el, plugin);
31898
+ const row = this.panel.ownerDocument.createElement('button');
31899
+ row.type = 'button';
31900
+ row.className = 'ovl-row';
31901
+ const label = this.panel.ownerDocument.createElement('span');
31902
+ label.className = 'ovl-row-text';
31903
+ label.textContent = out(plugin.displayName || plugin.name) + (counts[type] > 1 ? ' ' + seen[type] : '');
31904
+ row.appendChild(label);
31905
+ row.addEventListener('click', () => {
31906
+ this.builder.controlpanel.markOverlayHover(null);
31907
+ this.builder.controlpanel.selectOverlayElement(el);
31908
+ });
31909
+ row.addEventListener('mouseenter', () => this.builder.controlpanel.markOverlayHover(el));
31910
+ row.addEventListener('mouseleave', () => this.builder.controlpanel.markOverlayHover(null));
31911
+ list.appendChild(row);
31912
+ });
31913
+ divPlugins.style.display = list.childElementCount ? '' : 'none';
31914
+ } // True when the plugin element fills (practically) the whole box overlay.
31822
31915
 
31823
- renderPluginSettings(currentElement) {
31824
- // Same panel lifecycle as panel-plugin.js / ContentBuilder's
31825
- // elementplugin.js: destroy the previous render's panel editors before
31826
- // their elements go away, and publish the plugin being edited.
31827
- this.builder.destroyPanelEditors();
31828
- this.builder.activePlugin = currentElement;
31829
- this.currentElement = currentElement;
31830
- this.generator = null;
31831
- this.currentForm = null;
31832
- const divPluginSettings = this.divPluginSettings;
31833
- divPluginSettings.innerHTML = '';
31834
- const runtime = this.builder.win.builderRuntime;
31835
- const pluginName = currentElement.getAttribute('data-cb-type');
31836
- const plugin = runtime.getPlugin(pluginName);
31837
- if (!plugin) return;
31838
- let displayName = out(plugin.displayName || plugin.name);
31839
31916
 
31840
- if (currentElement.querySelector('.is-container')) {
31917
+ pluginCoversBox(element) {
31918
+ const overlay = element.closest('.is-overlay');
31919
+ if (!overlay) return true;
31920
+ const box = overlay.getBoundingClientRect();
31921
+ const el = element.getBoundingClientRect();
31922
+ if (!box.width || !box.height) return true;
31923
+ return el.width >= box.width * 0.98 && el.height >= box.height * 0.98;
31924
+ } // The box settings a plugin in this box has no use for (its contentbox options)
31925
+
31926
+
31927
+ applyPluginOptions(currentElement, plugin) {
31928
+ if (plugin.contentbox && currentElement.querySelector('.is-container')) {
31841
31929
  // if plugin element has editable content
31842
31930
  plugin.contentbox.boxTabText = false;
31843
31931
  plugin.contentbox.boxTabContent = false;
@@ -31860,16 +31948,21 @@ class PanelBox {
31860
31948
 
31861
31949
 
31862
31950
  if (plugin.contentbox) {
31863
- if (plugin.contentbox.bg !== undefined && plugin.contentbox.bg !== null && plugin.contentbox.bg === false) {
31951
+ // A plugin hides the box background (bg: false) because it covers it,
31952
+ // like a full-bleed video. When the plugin only fills part of the box
31953
+ // (a framed video, a card), the background is visible and stays editable.
31954
+ const coversBox = this.pluginCoversBox(currentElement);
31955
+
31956
+ if (coversBox && plugin.contentbox.bg !== undefined && plugin.contentbox.bg !== null && plugin.contentbox.bg === false) {
31864
31957
  this.divBgColor.style.display = 'none';
31865
31958
  this.divBgImage.style.display = 'none';
31866
31959
  }
31867
31960
 
31868
- if (plugin.contentbox.bgColor !== undefined && plugin.contentbox.bgColor !== null && plugin.contentbox.bgColor === false) {
31961
+ if (coversBox && plugin.contentbox.bgColor !== undefined && plugin.contentbox.bgColor !== null && plugin.contentbox.bgColor === false) {
31869
31962
  this.divBgColor.style.display = 'none';
31870
31963
  }
31871
31964
 
31872
- if (plugin.contentbox.bgImage !== undefined && plugin.contentbox.bgImage !== null && plugin.contentbox.bgImage === false) {
31965
+ if (coversBox && plugin.contentbox.bgImage !== undefined && plugin.contentbox.bgImage !== null && plugin.contentbox.bgImage === false) {
31873
31966
  this.divBgImage.style.display = 'none';
31874
31967
  }
31875
31968
 
@@ -31941,111 +32034,6 @@ class PanelBox {
31941
32034
  });
31942
32035
  }
31943
32036
  }
31944
-
31945
- const divTitle = document.createElement('h3');
31946
- divTitle.style.cssText = 'font-family: sans-serif;font-weight: 300;font-size: 20px;margin: 0 0 12px;';
31947
- divTitle.innerText = displayName;
31948
- divPluginSettings.appendChild(divTitle);
31949
- this.generator = new SettingsUIGenerator$1(runtime, this.builder);
31950
- const hasContentEditor = plugin && plugin.editor && plugin.editor.openContentEditor;
31951
-
31952
- if (hasContentEditor) {
31953
- // Get original content for the editor to work with
31954
- const originalContent = currentElement.getAttribute('data-cb-original-content'); // Create a temporary element for editing (so editor can manipulate it)
31955
-
31956
- let editableClone = document.querySelector('.editable-clone');
31957
- if (editableClone) editableClone.remove(); // editableClone = document.createElement('div');
31958
-
31959
- editableClone = currentElement.cloneNode(false);
31960
- editableClone.innerHTML = originalContent;
31961
- editableClone.className = 'editable-clone';
31962
- editableClone.style.display = 'none'; // Hidden, just for editing
31963
-
31964
- document.body.appendChild(editableClone);
31965
- const originalElement = editableClone.cloneNode(false); // Let plugin handle everything - pass the editable clone
31966
-
31967
- const editorUI = plugin.editor.openContentEditor(editableClone, this.builder, async () => {
31968
- this.builder.editor.saveForUndo(); // Do the same process like reinitialize, needed for newly added item (which requires data-cb-original-content update)
31969
- // Add index for sortable grid (for easy reorder)
31970
-
31971
- let grid = editableClone.querySelector('.grid-sortable');
31972
-
31973
- if (!grid) {
31974
- if (currentElement.classList.contains('grid-sortable')) {
31975
- grid = editableClone;
31976
- }
31977
- }
31978
-
31979
- if (grid) {
31980
- Array.from(grid.children).forEach((child, index) => {
31981
- if (child.nodeType === 1 && child.tagName !== 'STYLE' && child.tagName !== 'SCRIPT') child.setAttribute('data-index', index);
31982
- });
31983
- } //----
31984
- // Add data-edit-id for editable area
31985
-
31986
-
31987
- let editables = editableClone.querySelectorAll('.edit');
31988
- editables.forEach((area, index) => {
31989
- const id = `content-${Date.now()}-${index}`;
31990
- area.setAttribute('data-edit-id', id);
31991
- }); //-----
31992
- // Store edited content from clone
31993
-
31994
- currentElement.setAttribute('data-cb-original-content', editableClone.innerHTML); // Also performs similar value updates like the applyChanges()
31995
- // currentElement.setAttribute('data-cb-content', editableClone.getAttribute('data-cb-content'));
31996
-
31997
- const getChangedDataAttributes = (el1, el2) => {
31998
- const changed = [];
31999
-
32000
- for (const key of Object.keys(el1.dataset)) {
32001
- if (key in el2.dataset && el1.dataset[key] !== el2.dataset[key]) {
32002
- changed.push(key);
32003
- }
32004
- }
32005
-
32006
- return changed;
32007
- };
32008
-
32009
- const changedAttributes = getChangedDataAttributes(originalElement, editableClone);
32010
- changedAttributes.forEach(attrName => {
32011
- // console.log(attrName)
32012
- // console.log(editableClone.dataset[attrName])
32013
- currentElement.dataset[attrName] = editableClone.dataset[attrName];
32014
- }); // -------
32015
-
32016
- currentElement.innerHTML = editableClone.innerHTML; // Update current content
32017
- // Remove temporary clone
32018
-
32019
- editableClone.remove(); // Reinitialize plugin with new content
32020
-
32021
- await runtime.reinitialize(currentElement.parentElement);
32022
- this.builder.onChange();
32023
- this.builder.editor.hideElementTools();
32024
- this.builder.makeSortable(currentElement.parentElement);
32025
- });
32026
- divPluginSettings.appendChild(editorUI);
32027
- } // Generate form
32028
-
32029
-
32030
- this.currentForm = this.generator.generateForm(pluginName, currentElement, () => {
32031
- this.builder.editor.saveForUndo();
32032
- this.applyChanges(runtime);
32033
- });
32034
- divPluginSettings.appendChild(this.currentForm);
32035
- }
32036
-
32037
- async applyChanges(runtime) {
32038
- if (!this.currentElement || !this.currentForm) return; // 1. Get form values
32039
-
32040
- const values = this.generator.getFormValues(this.currentForm); // 2. Apply to element attributes
32041
-
32042
- this.generator.applyValues(this.currentElement, values); // 3. Reinitialize component
32043
-
32044
- const container = this.currentElement.parentElement || this.currentElement.closest('.is-wrapper');
32045
- await runtime.reinitialize(container);
32046
- this.builder.onChange(); // console.log('Settings applied and component reinitialized');
32047
-
32048
- this.builder.makeSortable(container);
32049
32037
  }
32050
32038
 
32051
32039
  getStateTargeted() {
@@ -33713,6 +33701,31 @@ class PanelSection {
33713
33701
 
33714
33702
  </div>
33715
33703
 
33704
+ <button class="accordion-item btn-accordion-bar" aria-expanded="false" aria-controls="sectioncontent5">
33705
+ ${out('Navigation Bar')}
33706
+ <span><svg><use xlink:href="#icon-chevron-down"></use></svg></span>
33707
+ </button>
33708
+ <div class="accordion-content" id="sectioncontent5" aria-hidden="true">
33709
+
33710
+ <div class="subpanel">
33711
+ <div class="label mt-2">${out('Bar over this section')}:</div>
33712
+ <div role="group" aria-label="${out('Bar over this section')}" class="group sectionbar">
33713
+ <button title="${out('Default')}" data-bar="">${out('Default')}</button>
33714
+ <button title="${out('Light')}" data-bar="light">${out('Light')}</button>
33715
+ <button title="${out('Dark')}" data-bar="dark">${out('Dark')}</button>
33716
+ <button title="${out('Light glass')}" data-bar="light-glass">${out('Light glass')}</button>
33717
+ <button title="${out('Dark glass')}" data-bar="dark-glass">${out('Dark glass')}</button>
33718
+ </div>
33719
+
33720
+ <div class="label mt-3">${out('Text Color')}:</div>
33721
+ <div class="group">
33722
+ <button title="${out('Text Color')}" class="btn-barink is-btn-color"></button>
33723
+ <button title="${out('Clear')}" class="btn-clear btn-barink-clear"><svg><use xlink:href="#icon-eraser"></use></svg></button>
33724
+ </div>
33725
+ </div>
33726
+
33727
+ </div>
33728
+
33716
33729
  <button class="accordion-item" aria-expanded="false" aria-controls="sectioncontent3">
33717
33730
  ${out('Responsive')}
33718
33731
  <span><svg><use xlink:href="#icon-chevron-down"></use></svg></span>
@@ -33904,6 +33917,40 @@ class PanelSection {
33904
33917
  btnScrollPreset.forEach(btn => btn.classList.remove('on'));
33905
33918
  if (s !== '') btn.classList.add('on');
33906
33919
  });
33920
+ }); // Navigation Bar: how the page's navigation bar looks while it passes over this
33921
+ // section. The section only says it (data-bar, data-bar-ink); the menu in the bar
33922
+ // does the work.
33923
+
33924
+ const btnBarLooks = panel.querySelectorAll('[data-bar]');
33925
+ btnBarLooks.forEach(btn => {
33926
+ btn.addEventListener('click', () => {
33927
+ this.builder.editor.saveForUndo();
33928
+ const section = this.builder.activeSection;
33929
+ const s = btn.getAttribute('data-bar');
33930
+ if (s) section.setAttribute('data-bar', s);else section.removeAttribute('data-bar');
33931
+ btnBarLooks.forEach(btn => btn.classList.remove('on'));
33932
+ btn.classList.add('on');
33933
+ this.builder.onChange();
33934
+ });
33935
+ });
33936
+ const btnBarInk = panel.querySelector('.btn-barink');
33937
+ btnBarInk.addEventListener('click', () => {
33938
+ this.builder.editor.saveForUndo(true); // checkLater = true
33939
+
33940
+ const section = this.builder.activeSection;
33941
+ const colorpicker = this.builder.editor.colorpicker();
33942
+ colorpicker.open(s => {
33943
+ if (s) section.setAttribute('data-bar-ink', s);else section.removeAttribute('data-bar-ink');
33944
+ btnBarInk.style.backgroundColor = s; // preview
33945
+
33946
+ this.builder.onChange();
33947
+ }, btnBarInk.style.backgroundColor, () => {}, btnBarInk, true);
33948
+ });
33949
+ panel.querySelector('.btn-barink-clear').addEventListener('click', () => {
33950
+ this.builder.editor.saveForUndo();
33951
+ this.builder.activeSection.removeAttribute('data-bar-ink');
33952
+ btnBarInk.style.backgroundColor = '';
33953
+ this.builder.onChange();
33907
33954
  }); // Responsive
33908
33955
 
33909
33956
  bindDeviceRow(panel.querySelector('.div-target'), this.builder);
@@ -34142,6 +34189,26 @@ class PanelSection {
34142
34189
  btnGradient.style.backgroundImage = gradient; //preview
34143
34190
  } else {
34144
34191
  btnGradient.style.backgroundImage = '';
34192
+ } // A navigation bar section: no height presets and no section tabs
34193
+
34194
+
34195
+ panel.classList.toggle('cb-navbar-section', activeSection.classList.contains('is-section-navbar')); // Navigation Bar: only when the page's bar holds a plugin that reads the looks
34196
+ // (one that says barLooks: true)
34197
+
34198
+ const btnAccordionBar = panel.querySelector('.btn-accordion-bar');
34199
+ const runtime = this.builder.win.builderRuntime;
34200
+ const barPlugins = this.builder.wrapperEl ? Array.from(this.builder.wrapperEl.querySelectorAll('.is-section-navbar [data-cb-type]')) : [];
34201
+ const pageBar = !!runtime && barPlugins.some(el => {
34202
+ const plugin = runtime.getPlugin(el.getAttribute('data-cb-type'));
34203
+ return !!(plugin && plugin.barLooks);
34204
+ });
34205
+ const showBar = pageBar && !activeSection.classList.contains('is-section-navbar');
34206
+ btnAccordionBar.classList.toggle('is-bar-off', !showBar);
34207
+
34208
+ if (showBar) {
34209
+ const look = activeSection.getAttribute('data-bar') || '';
34210
+ panel.querySelectorAll('[data-bar]').forEach(btn => btn.classList.toggle('on', btn.getAttribute('data-bar') === look));
34211
+ panel.querySelector('.btn-barink').style.backgroundColor = activeSection.getAttribute('data-bar-ink') || '';
34145
34212
  }
34146
34213
 
34147
34214
  this.realtimeVisibility(activeSection, true);
@@ -34246,6 +34313,13 @@ class PanelSection {
34246
34313
 
34247
34314
  static css() {
34248
34315
  return `
34316
+ .btn-accordion-bar.is-bar-off,
34317
+ .btn-accordion-bar.is-bar-off + .accordion-content ,
34318
+ .cb-navbar-section .div-section-height,
34319
+ .cb-navbar-section > .accordion-item,
34320
+ .cb-navbar-section > .accordion-content {
34321
+ display: none !important;
34322
+ }
34249
34323
  .cb-sectionanim .cb-anim-note {
34250
34324
  font-size: 11.5px;
34251
34325
  line-height: 1.5;
@@ -43492,6 +43566,15 @@ class ControlPanel {
43492
43566
  // elmTool, whose duplicate/delete/move actions would let a user
43493
43567
  // restructure a generated layer.
43494
43568
  this.builder.editor.inspectedElement = overlayTarget.element;
43569
+ } else {
43570
+ // A plugin in the box overlay opens its own panel (Section > Box > the
43571
+ // plugin), like overlay text. Its editable parts and content areas are
43572
+ // edited where they are.
43573
+ const plugin = e.target.closest ? e.target.closest('.is-overlay-content [data-cb-type]') : null;
43574
+
43575
+ if (plugin && !e.target.closest('.is-container') && !e.target.closest('.is-subblock')) {
43576
+ this.builder.editor.inspectedElement = plugin;
43577
+ }
43495
43578
  }
43496
43579
 
43497
43580
  if (old) old.call(this);
@@ -43636,8 +43719,92 @@ class ControlPanel {
43636
43719
  this.builder.editor.controlpanel = this; // This overide ContentBuilder controlpanel object
43637
43720
 
43638
43721
  this.bindEscape();
43722
+ this.bindLayerPassThrough();
43639
43723
  this.showTooltip();
43640
43724
  }
43725
+ /**
43726
+ * Clicks go through overlay layers that are faded out.
43727
+ *
43728
+ * A crossfade keeps every layer stacked at full size and only turns the
43729
+ * hidden ones transparent, so a click on the text you can see lands on an
43730
+ * invisible layer above it: an empty wrapper gets selected, or the caret
43731
+ * goes into text that is not on screen. hitTest() already sees through
43732
+ * these for the overlay panel; this does the same for the browser's own
43733
+ * hit test, which is what places the caret in a .is-container.
43734
+ *
43735
+ * Marks only exist under the pointer and are recomputed on every move and
43736
+ * scroll, so a layer that fades back in is clickable again. A faded layer
43737
+ * is only passed through when something visible lies below it; a stack
43738
+ * scrubbed to a moment where everything is faded stays selectable.
43739
+ * The marker is stripped from the saved HTML in html().
43740
+ */
43741
+
43742
+
43743
+ bindLayerPassThrough() {
43744
+ if (!this.passThroughHandler) {
43745
+ let frame = 0;
43746
+ let x = -1,
43747
+ y = -1;
43748
+
43749
+ const update = () => {
43750
+ frame = 0;
43751
+ this.updateLayerPassThrough(x, y);
43752
+ };
43753
+
43754
+ this.passThroughHandler = e => {
43755
+ if (typeof e.clientX === 'number') {
43756
+ x = e.clientX;
43757
+ y = e.clientY;
43758
+ }
43759
+
43760
+ if (e.type === 'pointerdown') {
43761
+ // a touch has no move before it
43762
+ update();
43763
+ return;
43764
+ }
43765
+
43766
+ if (!frame) frame = this.builder.win.requestAnimationFrame(update);
43767
+ };
43768
+ }
43769
+
43770
+ const types = ['pointermove', 'pointerdown', 'scroll'];
43771
+ if (this.passThroughDoc) types.forEach(type => this.passThroughDoc.removeEventListener(type, this.passThroughHandler, true));
43772
+ this.passThroughDoc = this.builder.doc;
43773
+ if (this.passThroughDoc) types.forEach(type => this.passThroughDoc.addEventListener(type, this.passThroughHandler, true));
43774
+ }
43775
+
43776
+ updateLayerPassThrough(x, y) {
43777
+ const doc = this.builder.doc;
43778
+ if (!doc || !doc.elementsFromPoint) return;
43779
+ doc.querySelectorAll('[data-overlay-passthrough]').forEach(elm => {
43780
+ elm.removeAttribute('data-overlay-passthrough');
43781
+ });
43782
+ if (x < 0) return;
43783
+ const selector = this.builder.overlayEditableSelector;
43784
+ const stack = doc.elementsFromPoint(x, y);
43785
+ stack.forEach((el, i) => {
43786
+ const overlay = el.closest(selector); // Faded counts through the ancestors: text in a layer that is
43787
+ // faded out as a whole is painted itself, yet invisible.
43788
+
43789
+ if (!overlay || el === overlay || this.isVisibleIn(el, overlay)) return;
43790
+ const covered = stack.slice(i + 1).some(below => overlay.contains(below) && below !== overlay && !el.contains(below) && this.isLayerContent(below) && this.isVisibleIn(below, overlay));
43791
+ if (covered) el.setAttribute('data-overlay-passthrough', '');
43792
+ });
43793
+ } // Something a click is meant for: text, a picture, or a media element.
43794
+
43795
+
43796
+ isLayerContent(el) {
43797
+ return this.builder.hasOwnText(el) || el.matches('img,video,iframe,svg,a,button') || this.hasBackgroundImage(el);
43798
+ } // Painted itself and through every ancestor up to the overlay.
43799
+
43800
+
43801
+ isVisibleIn(el, overlay) {
43802
+ for (let node = el; node && node !== overlay; node = node.parentElement) {
43803
+ if (!this.isPainted(node)) return false;
43804
+ }
43805
+
43806
+ return true;
43807
+ }
43641
43808
  /**
43642
43809
  * Escape closes the open dialog (Element, Icons, Shadow, Image Adjust,
43643
43810
  * Group, Block Options, Symbols, Settings).
@@ -44177,7 +44344,8 @@ class ControlPanel {
44177
44344
  const el = stack[i];
44178
44345
 
44179
44346
  if (match(el)) {
44180
- if (this.isPainted(el)) return el; // Crossfade stacks (three images or three captions at the same
44347
+ const overlay = el.closest(this.builder.overlayEditableSelector);
44348
+ if (overlay ? this.isVisibleIn(el, overlay) : this.isPainted(el)) return el; // Crossfade stacks (three images or three captions at the same
44181
44349
  // spot, cycled by opacity keyframes) put a FULLY TRANSPARENT
44182
44350
  // layer on top for most of the scene. Stacking order would hand
44183
44351
  // back that one every time, so the user edits a layer they
@@ -44424,6 +44592,8 @@ class ControlPanel {
44424
44592
  sectionHeightAllowed() {
44425
44593
  const section = this.builder.activeSection;
44426
44594
  if (!section || !section.classList.contains('is-box')) return true;
44595
+ if (section.classList.contains('is-section-navbar')) return false; // a navigation bar has no height presets
44596
+
44427
44597
  const el = section.querySelector('.is-overlay-content [data-cb-type]');
44428
44598
  if (!el) return true;
44429
44599
  const runtime = this.builder.win.builderRuntime;
@@ -50657,6 +50827,10 @@ class ToolbarKeys {
50657
50827
  */
50658
50828
  _hold(container, control) {
50659
50829
  if (!control) return;
50830
+
50831
+ // Where the control was: if the command removes what it sat on, the caret
50832
+ // goes to what took its place
50833
+ const spot = (control.closest('.row, [data-cb-block]') || control).getBoundingClientRect();
50660
50834
  setTimeout(() => {
50661
50835
  // Not bailing when the container has gone: deleting the last column
50662
50836
  // takes the strip with it, and that is exactly when the focus needs
@@ -50714,8 +50888,10 @@ class ToolbarKeys {
50714
50888
  // A tick later again: emptying an area makes the editor put its
50715
50889
  // own button there, and that has not happened yet.
50716
50890
  setTimeout(() => {
50717
- const somewhere = this._somewhereUsable();
50718
- if (somewhere) somewhere.focus();
50891
+ const somewhere = this._somewhereUsable(spot);
50892
+ if (somewhere) somewhere.focus({
50893
+ preventScroll: true
50894
+ });
50719
50895
  }, 30);
50720
50896
  }
50721
50897
  }, 0);
@@ -50786,7 +50962,9 @@ class ToolbarKeys {
50786
50962
  // the trip to the toolbar after one button. Stay on it, so the next format
50787
50963
  // is a key away, and leave deliberately with Escape. Anything that claims
50788
50964
  // the focus for itself, a panel or a dialog, is left to do so.
50965
+ // A mouse click (detail > 0) is left alone, so the mouse works as it always did.
50789
50966
  tool.addEventListener('click', e => {
50967
+ if (e.detail > 0) return;
50790
50968
  const control = this._controlFrom(e, tool, BUTTONS);
50791
50969
  if (control) this._hold(tool, control);
50792
50970
  });
@@ -50928,13 +51106,26 @@ class ToolbarKeys {
50928
51106
  * go dead with the focus stuck on a strip. Falls back through what the editor
50929
51107
  * still calls active, then to the first column on the page.
50930
51108
  */
50931
- currentColumn() {
51109
+ currentColumn(near) {
50932
51110
  // It has to be able to hold the focus. A locked column is still the active
50933
51111
  // one but cannot take it, and focusing it silently does nothing.
50934
51112
  const usable = el => el && el.isConnected && (el.isContentEditable || el.hasAttribute('tabindex')) ? el : null;
50935
51113
  const active = usable(this.builder.util.cellSelected()) || usable(this.builder.activeCol);
50936
51114
  if (active) return active;
50937
51115
  const rows = this.builder.doc.querySelectorAll(`${this.builder.opts.container} > *`);
51116
+
51117
+ // Near a place on screen (where a removed row was): the column that took its
51118
+ // place, or the last one before it
51119
+ if (near) {
51120
+ let before = null;
51121
+ for (const row of rows) {
51122
+ const cell = ToolbarKeys.cells(row).find(usable);
51123
+ if (!cell) continue;
51124
+ if (cell.getBoundingClientRect().top >= near.top - 1) return cell;
51125
+ before = cell;
51126
+ }
51127
+ if (before) return before;
51128
+ }
50938
51129
  for (const row of rows) {
50939
51130
  const cell = ToolbarKeys.cells(row).find(usable);
50940
51131
  if (cell) return cell;
@@ -50948,8 +51139,8 @@ class ToolbarKeys {
50948
51139
  * editing away: in both the focus would otherwise land on the body, where no
50949
51140
  * key reaches the editor at all.
50950
51141
  */
50951
- _somewhereUsable() {
50952
- const column = this.currentColumn();
51142
+ _somewhereUsable(near) {
51143
+ const column = this.currentColumn(near);
50953
51144
  if (column) return column;
50954
51145
  const doc = this.builder.doc;
50955
51146
 
@@ -52122,6 +52313,18 @@ class Util$1 {
52122
52313
  // A toolbar button that opens a dialog often moves the focus first, so the
52123
52314
  // strip fills this in from the button actually used (toolbarkeys.js).
52124
52315
 
52316
+ // Opened from a control inside the content iframe: this page never saw
52317
+ // that mouse click, so the browser would ring the dialog's first control
52318
+ // as if the keyboard had been used. Carry the opener's own state over.
52319
+ let focusOpts;
52320
+ if (opener && opener.tagName === 'IFRAME') {
52321
+ try {
52322
+ const inner = opener.contentDocument.activeElement;
52323
+ if (!(inner && inner.matches(':focus-visible'))) focusOpts = {
52324
+ focusVisible: false
52325
+ };
52326
+ } catch (e) {/* another origin */}
52327
+ }
52125
52328
  if (overflowHidden) document.body.style.overflow = 'hidden';
52126
52329
  const handleKeyDown = e => {
52127
52330
  if (e.keyCode === 27) {
@@ -52139,11 +52342,11 @@ class Util$1 {
52139
52342
  modal.addEventListener('keydown', handleKeyDown);
52140
52343
  let content = modal.querySelector('.is-tab-content.active');
52141
52344
  if (content) {
52142
- content.focus();
52345
+ content.focus(focusOpts);
52143
52346
  this.builder.tabs.setupTabKey(content);
52144
52347
  } else {
52145
52348
  this.setupTabKeys(modal);
52146
- modal.focus();
52349
+ modal.focus(focusOpts);
52147
52350
  }
52148
52351
 
52149
52352
  // Land on a control rather than on the dialog itself: focus on the dialog
@@ -52163,7 +52366,7 @@ class Util$1 {
52163
52366
  const settled = modal.contains(held) && held !== modal && held.tagName !== 'IFRAME' && !held.classList.contains('is-tab-content');
52164
52367
  if (settled) return;
52165
52368
  const first = ToolbarKeys.firstControl(modal);
52166
- if (first) first.focus();
52369
+ if (first) first.focus(focusOpts);
52167
52370
  }, 0);
52168
52371
 
52169
52372
  // A dialog whose content is an iframe, the asset picker, is a trap without
@@ -54192,6 +54395,7 @@ class Util$1 {
54192
54395
  * Placeholders substituted by export():
54193
54396
  * [%CONTENT%] the content HTML
54194
54397
  * [%PLUGINS%] runtime plugin registry (see getPluginRegistry)
54398
+ * [%PLUGINFOLDER%] runtime plugin folder (see getPluginFolder)
54195
54399
  * [%PATH%] base URL for runtime/asset references
54196
54400
  */
54197
54401
  getPageTemplate(framework) {
@@ -54229,7 +54433,8 @@ class Util$1 {
54229
54433
  // Registers available plugins (not yet loaded).
54230
54434
  // Scripts and styles are fetched only when the plugin is actually used in content.
54231
54435
  const runtime = new ContentBuilderRuntime({
54232
- plugins: [%PLUGINS%]
54436
+ plugins: [%PLUGINS%],
54437
+ pluginFolder: [%PLUGINFOLDER%]
54233
54438
  });
54234
54439
  runtime.init();
54235
54440
  runtime.renderPageCss('.container');
@@ -54279,7 +54484,8 @@ class Util$1 {
54279
54484
  // Registers available plugins (not yet loaded).
54280
54485
  // Scripts and styles are fetched only when the plugin is actually used in content.
54281
54486
  const runtime = new ContentBuilderRuntime({
54282
- plugins: [%PLUGINS%]
54487
+ plugins: [%PLUGINS%],
54488
+ pluginFolder: [%PLUGINFOLDER%]
54283
54489
  });
54284
54490
  runtime.init();
54285
54491
  </script>
@@ -54332,7 +54538,8 @@ class Util$1 {
54332
54538
  // Registers available plugins (not yet loaded).
54333
54539
  // Scripts and styles are fetched only when the plugin is actually used in content.
54334
54540
  const runtime = new ContentBuilderRuntime({
54335
- plugins: [%PLUGINS%]
54541
+ plugins: [%PLUGINS%],
54542
+ pluginFolder: [%PLUGINFOLDER%]
54336
54543
  });
54337
54544
  runtime.init();
54338
54545
  </script>
@@ -54390,7 +54597,8 @@ class Util$1 {
54390
54597
  // Registers available plugins (not yet loaded).
54391
54598
  // Scripts and styles are fetched only when the plugin is actually used in content.
54392
54599
  const runtime = new ContentBuilderRuntime({
54393
- plugins: [%PLUGINS%]
54600
+ plugins: [%PLUGINS%],
54601
+ pluginFolder: [%PLUGINFOLDER%]
54394
54602
  });
54395
54603
  runtime.init();
54396
54604
  </script>
@@ -54442,7 +54650,8 @@ class Util$1 {
54442
54650
  // Registers available plugins (not yet loaded).
54443
54651
  // Scripts and styles are fetched only when the plugin is actually used in content.
54444
54652
  const runtime = new ContentBuilderRuntime({
54445
- plugins: [%PLUGINS%]
54653
+ plugins: [%PLUGINS%],
54654
+ pluginFolder: [%PLUGINFOLDER%]
54446
54655
  });
54447
54656
  runtime.init();
54448
54657
  </script>
@@ -54496,6 +54705,27 @@ class Util$1 {
54496
54705
  });
54497
54706
  return JSON.stringify(registry, null, 4);
54498
54707
  }
54708
+
54709
+ /*
54710
+ * The runtime's plugin folder (pluginFolder in blank.html), as a JS string.
54711
+ * Plugins used in the content load from it without being registered, so an
54712
+ * exported page keeps them. A URL under the content frame's base becomes a
54713
+ * [%PATH%] token, like the registry's URLs.
54714
+ */
54715
+ getPluginFolder() {
54716
+ const win = this.builder.win;
54717
+ let folder = '';
54718
+ try {
54719
+ folder = win.builderRuntime.settings.pluginFolder || '';
54720
+ } catch (e) {/* no runtime in the content frame */}
54721
+ let base = '';
54722
+ try {
54723
+ const href = win.location.href;
54724
+ base = href.substring(0, href.lastIndexOf('/'));
54725
+ } catch (e) {/* cross-origin content frame */}
54726
+ if (base && folder.startsWith(base + '/')) folder = '[%PATH%]' + folder.substring(base.length);
54727
+ return JSON.stringify(folder);
54728
+ }
54499
54729
  getElementPosition(element) {
54500
54730
  const iframe = this.builder.iframe;
54501
54731
  let top, left;
@@ -54707,6 +54937,46 @@ class Util$1 {
54707
54937
  }
54708
54938
  `;
54709
54939
  }
54940
+
54941
+ // Place a small tool (link/button tool) above its target. When the target sits at the
54942
+ // top edge with no room above, put the tool beside it (right, then left), so it never
54943
+ // lands on the element tool below the element. Below is the last resort.
54944
+ // vTarget (optional, e.g. a selected icon inside a link) sets the vertical position.
54945
+ // The tool must be displayed before this (its size is measured).
54946
+ placeToolAbove(tool, target, vTarget) {
54947
+ const hr = target.getBoundingClientRect();
54948
+ const vr = (vTarget || target).getBoundingClientRect();
54949
+ tool.style.top = vr.top + this.builder.win.pageYOffset - tool.offsetHeight - 3 + 'px';
54950
+ tool.style.left = hr.left + target.offsetWidth * this.builder.opts.zoom - tool.offsetWidth + 'px';
54951
+
54952
+ // Measured, not computed: the tool can carry margins and a scale.
54953
+ const t = tool.getBoundingClientRect();
54954
+ if (t.top >= 0) return;
54955
+ const moveTo = (left, top) => {
54956
+ const c = tool.getBoundingClientRect();
54957
+ tool.style.left = parseFloat(tool.style.left) + left - c.left + 'px';
54958
+ tool.style.top = parseFloat(tool.style.top) + top - c.top + 'px';
54959
+ };
54960
+
54961
+ // Horizontal room, kept clear of the row tool when it sits beside the target.
54962
+ let minX = 0;
54963
+ let maxX = this.builder.doc.documentElement.clientWidth;
54964
+ const row = target.closest('.row');
54965
+ const rowTool = row && row.querySelector(':scope > .is-row-tool');
54966
+ if (rowTool && rowTool.offsetParent) {
54967
+ const r = rowTool.getBoundingClientRect();
54968
+ if (r.left >= hr.right) maxX = Math.min(maxX, r.left);else if (r.right <= hr.left) minX = Math.max(minX, r.right);
54969
+ }
54970
+ const gap = 8;
54971
+ const sideTop = vr.top + vr.height / 2 - t.height / 2;
54972
+ if (hr.right + gap + t.width <= maxX) {
54973
+ moveTo(hr.right + gap, sideTop);
54974
+ } else if (hr.left - gap - t.width >= minX) {
54975
+ moveTo(hr.left - gap - t.width, sideTop);
54976
+ } else {
54977
+ moveTo(t.left, vr.bottom + gap);
54978
+ }
54979
+ }
54710
54980
  }
54711
54981
  class Dom {
54712
54982
  constructor(builder) {
@@ -77568,15 +77838,8 @@ class Hyperlink {
77568
77838
  }
77569
77839
  // /Button Stuff
77570
77840
 
77571
- let left, top;
77572
- top = link.getBoundingClientRect().top + this.builder.win.pageYOffset;
77573
- if (this.builder.activeIcon) top = this.builder.activeIcon.getBoundingClientRect().top + this.builder.win.pageYOffset;
77574
- left = link.getBoundingClientRect().left;
77575
- linkTool.style.display = 'flex';
77576
- const w = linkTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
77577
- const h = linkTool.offsetHeight;
77578
- linkTool.style.top = top - h - 3 + 'px';
77579
- linkTool.style.left = left + link.offsetWidth * this.builder.opts.zoom - w + 'px';
77841
+ linkTool.style.display = 'flex'; //to get the size, the tool must not be hidden (display:none)
77842
+ this.builder.util.placeToolAbove(linkTool, link, this.builder.activeIcon);
77580
77843
  const handleLinkButtonClick = e => {
77581
77844
  const clrPicker = document.querySelector('.pop-picker.active') || document.querySelector('.pickgradientcolor.active');
77582
77845
  let elm = e.target;
@@ -77875,13 +78138,8 @@ class Button {
77875
78138
  // selecting the button, so the pencil is a second route to the same
77876
78139
  // panel and is left out.
77877
78140
  buttonTool.querySelector('.button-edit').style.display = this.builder.opts.onButtonEdit ? 'none' : '';
77878
- let top = activeButton.getBoundingClientRect().top + this.builder.win.pageYOffset;
77879
- let left = activeButton.getBoundingClientRect().left;
77880
- buttonTool.style.display = 'flex';
77881
- const w = buttonTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
77882
- const h = buttonTool.offsetHeight;
77883
- buttonTool.style.top = top - h - 3 + 'px';
77884
- buttonTool.style.left = left + activeButton.offsetWidth * this.builder.opts.zoom - w + 'px';
78141
+ buttonTool.style.display = 'flex'; //to get the size, the tool must not be hidden (display:none)
78142
+ this.builder.util.placeToolAbove(buttonTool, activeButton);
77885
78143
  }
77886
78144
  hideTool() {
77887
78145
  if (this.buttonTool) this.buttonTool.style.display = '';
@@ -79078,7 +79336,10 @@ class Plugin {
79078
79336
  const handlePluginClick = e => {
79079
79337
  const clrPicker = document.querySelector('.pop-picker.active') || document.querySelector('.pickgradientcolor.active');
79080
79338
  let elm = e.target;
79081
- let elmRemoved = !document.contains(elm); // e.g. when deleting a card in card list plugin
79339
+
79340
+ // isConnected, not document.contains(): in iframe mode the click is in
79341
+ // the content document, which this document never contains.
79342
+ let elmRemoved = !elm.isConnected; // e.g. when deleting a card in card list plugin
79082
79343
 
79083
79344
  if (this.builder.doc.activeElement) {
79084
79345
  if (this.builder.doc.activeElement.closest('.is-modal')) {
@@ -79276,7 +79537,9 @@ class Plugin {
79276
79537
  const plugin = e.target.closest('[data-cb-type]');
79277
79538
  if (plugin) {
79278
79539
  this.builder.activePlugin = plugin;
79279
- if (!this.builder.isContentBox) {
79540
+
79541
+ // With a control panel, plugin settings open there instead.
79542
+ if (!this.builder.controlPanel) {
79280
79543
  this.renderTool();
79281
79544
  this.pluginTool.style.display = 'flex';
79282
79545
  let _toolwidth = this.pluginTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
@@ -81552,6 +81815,10 @@ class Element {
81552
81815
  customcode = true;
81553
81816
  subblock = elm.closest('.is-subblock');
81554
81817
  }
81818
+ if (elm.closest('[data-cb-code]')) {
81819
+ // Independent code element: the whole block is the element
81820
+ customcode = true;
81821
+ }
81555
81822
  if (!customcode && !noedit && !_protected || subblock) {
81556
81823
  //previously this is commented: && !noedit && !_protected
81557
81824
 
@@ -103213,6 +103480,136 @@ function coverValue(cover, def) {
103213
103480
  return true;
103214
103481
  }
103215
103482
 
103483
+ /*
103484
+ normalizer.js — the clean-output guarantee.
103485
+
103486
+ Runs after every mutation. It never changes the visible text; it only
103487
+ reduces markup to the minimal equivalent form:
103488
+
103489
+ <strong>a</strong><strong>b</strong> -> <strong>ab</strong>
103490
+ <strong><strong>x</strong></strong> -> <strong>x</strong>
103491
+ <span style="color:red"><span style="font-size:18px">x</span></span>
103492
+ -> one span, two properties
103493
+ <span>x</span> -> x
103494
+ <em></em> -> (removed)
103495
+
103496
+ Because it preserves text content exactly, character-offset selection
103497
+ bookmarks taken before normalization remain valid after it.
103498
+ */
103499
+ const INLINE_TAGS = new Set(['SPAN', 'STRONG', 'B', 'EM', 'I', 'U', 'S', 'STRIKE', 'DEL', 'INS', 'SUB', 'SUP', 'CODE', 'MARK', 'A', 'SMALL', 'BIG', 'FONT', 'ABBR', 'Q', 'CITE', 'DFN', 'KBD', 'SAMP', 'VAR', 'TIME']);
103500
+
103501
+ /* Identity signature: two inline elements with equal signatures are
103502
+ interchangeable and can be merged. */
103503
+ function sig(el) {
103504
+ const attrs = [];
103505
+ for (const a of el.attributes) {
103506
+ attrs.push(a.name === 'style' ? 'style=' + el.style.cssText : a.name + '=' + a.value);
103507
+ }
103508
+ return el.tagName + '|' + attrs.sort().join(';');
103509
+ }
103510
+ function isEmptyInline(el) {
103511
+ if (el.textContent !== '' || el.querySelector(KEEPERS)) return false;
103512
+ // An empty inline that carries a class typically renders its content via CSS
103513
+ // rather than text — an icon font (<i class="bi bi-alarm">, <span
103514
+ // class="material-icons">…</span>, fa-*, glyphicon). It is NOT truly empty,
103515
+ // so never drop it. Bare leftover marks (<em></em>, <i></i>) have no class
103516
+ // and are still removed.
103517
+ if (el.getAttribute('class')) return false;
103518
+ return true;
103519
+ }
103520
+
103521
+ /* Does the page's CSS style this attribute-less span? A design can colour or
103522
+ size "the span in this heading" from a stylesheet (.hero h1 span {...});
103523
+ unwrapping it then changes how the text looks, which this file promises
103524
+ never to do. Compared against the parent for inherited properties and
103525
+ against the initial value for the rest. Without a live view (detached, or
103526
+ a headless test) it is treated as unstyled, as before. */
103527
+ const INHERITED = ['color', 'fontSize', 'fontWeight', 'fontStyle', 'fontFamily', 'letterSpacing', 'textTransform', 'lineHeight'];
103528
+ function styledByCss(el) {
103529
+ const parent = el.parentElement;
103530
+ const win = el.ownerDocument && el.ownerDocument.defaultView;
103531
+ if (!parent || !win || !el.isConnected || typeof win.getComputedStyle !== 'function') return false;
103532
+ const cs = win.getComputedStyle(el);
103533
+ const ps = win.getComputedStyle(parent);
103534
+ for (const p of INHERITED) if (cs[p] !== ps[p]) return true;
103535
+ if (cs.display && cs.display !== 'inline') return true;
103536
+ if (cs.backgroundImage && cs.backgroundImage !== 'none') return true;
103537
+ if (cs.backgroundColor && !/^(transparent|rgba\(0, 0, 0, 0\))$/.test(cs.backgroundColor)) return true;
103538
+ if (cs.textDecorationLine && cs.textDecorationLine !== 'none') return true;
103539
+ return false;
103540
+ }
103541
+
103542
+ /* One bottom-up cleanup pass. Returns true if anything changed. */
103543
+ function pass(scope) {
103544
+ let changed = false;
103545
+ const els = Array.prototype.slice.call(scope.querySelectorAll('*')).reverse();
103546
+ for (const el of els) {
103547
+ if (!el.isConnected && !scope.contains(el)) continue;
103548
+ if (el.closest && el.closest('[contenteditable="false"]')) continue;
103549
+ if (!INLINE_TAGS.has(el.tagName)) continue;
103550
+
103551
+ /* 1. Drop empty inline elements */
103552
+ if (isEmptyInline(el)) {
103553
+ el.remove();
103554
+ changed = true;
103555
+ continue;
103556
+ }
103557
+
103558
+ /* 2. Unwrap attribute-less spans (unless the page's CSS styles them) */
103559
+ if (el.tagName === 'SPAN' && el.attributes.length === 0 && !styledByCss(el)) {
103560
+ unwrap(el);
103561
+ changed = true;
103562
+ continue;
103563
+ }
103564
+
103565
+ /* 3. Unwrap when an identical ancestor already covers this element */
103566
+ let anc = el.parentNode;
103567
+ let redundant = false;
103568
+ while (isEl$1(anc) && anc !== scope && INLINE_TAGS.has(anc.tagName)) {
103569
+ if (sig(anc) === sig(el)) {
103570
+ redundant = true;
103571
+ break;
103572
+ }
103573
+ anc = anc.parentNode;
103574
+ }
103575
+ if (redundant) {
103576
+ unwrap(el);
103577
+ changed = true;
103578
+ continue;
103579
+ }
103580
+
103581
+ /* 4. Merge with an identical previous sibling */
103582
+ const prev = el.previousSibling;
103583
+ if (isEl$1(prev) && sig(prev) === sig(el)) {
103584
+ while (el.firstChild) prev.appendChild(el.firstChild);
103585
+ el.remove();
103586
+ changed = true;
103587
+ continue;
103588
+ }
103589
+
103590
+ /* 5. Collapse span-in-span single-child chains into one span.
103591
+ The inner (more specific) properties win on conflict. */
103592
+ if (el.tagName === 'SPAN' && el.childNodes.length === 1 && isEl$1(el.firstChild) && el.firstChild.tagName === 'SPAN') {
103593
+ const inner = el.firstChild;
103594
+ for (let i = 0; i < inner.style.length; i++) {
103595
+ const prop = inner.style[i];
103596
+ el.style.setProperty(prop, inner.style.getPropertyValue(prop));
103597
+ }
103598
+ inner.classList.forEach(c => el.classList.add(c));
103599
+ if (!el.getAttribute('style')) el.removeAttribute('style');
103600
+ unwrap(inner);
103601
+ changed = true;
103602
+ }
103603
+ }
103604
+ return changed;
103605
+ }
103606
+ function normalize(scope) {
103607
+ let guard = 0;
103608
+ while (pass(scope) && guard++ < 6) {/* repeat until stable */}
103609
+ scope.normalize(); /* merge adjacent text nodes */
103610
+ return scope;
103611
+ }
103612
+
103216
103613
  /*
103217
103614
  formatter.js — inline formatting engine.
103218
103615
 
@@ -103479,7 +103876,9 @@ function strip(el, def) {
103479
103876
  unwrap(el);
103480
103877
  return;
103481
103878
  }
103482
- if (el.tagName === 'SPAN' && el.attributes.length === 0) unwrap(el);
103879
+ /* Left bare: unwrap, unless the page's CSS styles it (a design's
103880
+ ".hero h1 span" colour), which must survive clearing a size from it. */
103881
+ if (el.tagName === 'SPAN' && el.attributes.length === 0 && !styledByCss(el)) unwrap(el);
103483
103882
  }
103484
103883
 
103485
103884
  /* Apply a style across a (non-collapsed) range. */
@@ -103544,115 +103943,6 @@ function removeStyle(root, range, def, opts) {
103544
103943
  return texts;
103545
103944
  }
103546
103945
 
103547
- /*
103548
- normalizer.js — the clean-output guarantee.
103549
-
103550
- Runs after every mutation. It never changes the visible text; it only
103551
- reduces markup to the minimal equivalent form:
103552
-
103553
- <strong>a</strong><strong>b</strong> -> <strong>ab</strong>
103554
- <strong><strong>x</strong></strong> -> <strong>x</strong>
103555
- <span style="color:red"><span style="font-size:18px">x</span></span>
103556
- -> one span, two properties
103557
- <span>x</span> -> x
103558
- <em></em> -> (removed)
103559
-
103560
- Because it preserves text content exactly, character-offset selection
103561
- bookmarks taken before normalization remain valid after it.
103562
- */
103563
- const INLINE_TAGS = new Set(['SPAN', 'STRONG', 'B', 'EM', 'I', 'U', 'S', 'STRIKE', 'DEL', 'INS', 'SUB', 'SUP', 'CODE', 'MARK', 'A', 'SMALL', 'BIG', 'FONT', 'ABBR', 'Q', 'CITE', 'DFN', 'KBD', 'SAMP', 'VAR', 'TIME']);
103564
-
103565
- /* Identity signature: two inline elements with equal signatures are
103566
- interchangeable and can be merged. */
103567
- function sig(el) {
103568
- const attrs = [];
103569
- for (const a of el.attributes) {
103570
- attrs.push(a.name === 'style' ? 'style=' + el.style.cssText : a.name + '=' + a.value);
103571
- }
103572
- return el.tagName + '|' + attrs.sort().join(';');
103573
- }
103574
- function isEmptyInline(el) {
103575
- if (el.textContent !== '' || el.querySelector(KEEPERS)) return false;
103576
- // An empty inline that carries a class typically renders its content via CSS
103577
- // rather than text — an icon font (<i class="bi bi-alarm">, <span
103578
- // class="material-icons">…</span>, fa-*, glyphicon). It is NOT truly empty,
103579
- // so never drop it. Bare leftover marks (<em></em>, <i></i>) have no class
103580
- // and are still removed.
103581
- if (el.getAttribute('class')) return false;
103582
- return true;
103583
- }
103584
-
103585
- /* One bottom-up cleanup pass. Returns true if anything changed. */
103586
- function pass(scope) {
103587
- let changed = false;
103588
- const els = Array.prototype.slice.call(scope.querySelectorAll('*')).reverse();
103589
- for (const el of els) {
103590
- if (!el.isConnected && !scope.contains(el)) continue;
103591
- if (el.closest && el.closest('[contenteditable="false"]')) continue;
103592
- if (!INLINE_TAGS.has(el.tagName)) continue;
103593
-
103594
- /* 1. Drop empty inline elements */
103595
- if (isEmptyInline(el)) {
103596
- el.remove();
103597
- changed = true;
103598
- continue;
103599
- }
103600
-
103601
- /* 2. Unwrap attribute-less spans */
103602
- if (el.tagName === 'SPAN' && el.attributes.length === 0) {
103603
- unwrap(el);
103604
- changed = true;
103605
- continue;
103606
- }
103607
-
103608
- /* 3. Unwrap when an identical ancestor already covers this element */
103609
- let anc = el.parentNode;
103610
- let redundant = false;
103611
- while (isEl$1(anc) && anc !== scope && INLINE_TAGS.has(anc.tagName)) {
103612
- if (sig(anc) === sig(el)) {
103613
- redundant = true;
103614
- break;
103615
- }
103616
- anc = anc.parentNode;
103617
- }
103618
- if (redundant) {
103619
- unwrap(el);
103620
- changed = true;
103621
- continue;
103622
- }
103623
-
103624
- /* 4. Merge with an identical previous sibling */
103625
- const prev = el.previousSibling;
103626
- if (isEl$1(prev) && sig(prev) === sig(el)) {
103627
- while (el.firstChild) prev.appendChild(el.firstChild);
103628
- el.remove();
103629
- changed = true;
103630
- continue;
103631
- }
103632
-
103633
- /* 5. Collapse span-in-span single-child chains into one span.
103634
- The inner (more specific) properties win on conflict. */
103635
- if (el.tagName === 'SPAN' && el.childNodes.length === 1 && isEl$1(el.firstChild) && el.firstChild.tagName === 'SPAN') {
103636
- const inner = el.firstChild;
103637
- for (let i = 0; i < inner.style.length; i++) {
103638
- const prop = inner.style[i];
103639
- el.style.setProperty(prop, inner.style.getPropertyValue(prop));
103640
- }
103641
- inner.classList.forEach(c => el.classList.add(c));
103642
- if (!el.getAttribute('style')) el.removeAttribute('style');
103643
- unwrap(inner);
103644
- changed = true;
103645
- }
103646
- }
103647
- return changed;
103648
- }
103649
- function normalize(scope) {
103650
- let guard = 0;
103651
- while (pass(scope) && guard++ < 6) {/* repeat until stable */}
103652
- scope.normalize(); /* merge adjacent text nodes */
103653
- return scope;
103654
- }
103655
-
103656
103946
  /*
103657
103947
  probe.js — non-mutating state inspection (queryCommandState done right).
103658
103948
 
@@ -144123,11 +144413,14 @@ class ContentBuilder {
144123
144413
  this.shortenOutput = false;
144124
144414
  output = pageTemplate.replace('[%CONTENT%]', html);
144125
144415
 
144126
- // [%PLUGINS%] - runtime plugin registry (must run before [%PATH%],
144127
- // the registry's URLs are emitted as [%PATH%] tokens)
144416
+ // [%PLUGINS%], [%PLUGINFOLDER%] - runtime plugin registry and folder (must run
144417
+ // before [%PATH%], their URLs are emitted as [%PATH%] tokens)
144128
144418
  if (output.indexOf('[%PLUGINS%]') !== -1) {
144129
144419
  output = output.replaceAll('[%PLUGINS%]', this.util.getPluginRegistry());
144130
144420
  }
144421
+ if (output.indexOf('[%PLUGINFOLDER%]') !== -1) {
144422
+ output = output.replaceAll('[%PLUGINFOLDER%]', this.util.getPluginFolder());
144423
+ }
144131
144424
 
144132
144425
  // Custom
144133
144426
  if (options.replace) {
@@ -146867,6 +147160,9 @@ Please obtain a license at: https://innovastudio.com/contentbox`);
146867
147160
  });
146868
147161
  tmp.querySelectorAll('*').forEach(el => {
146869
147162
  el.removeAttribute('style');
147163
+ // Text direction is the destination's too: Word marks every
147164
+ // paragraph and run dir="RTL"/"LTR" in right-to-left documents.
147165
+ el.removeAttribute('dir');
146870
147166
  if (el.classList && el.classList.length) {
146871
147167
  const kept = Array.prototype.filter.call(el.classList, c => keep.has(c));
146872
147168
  if (kept.length) el.setAttribute('class', kept.join(' '));else el.removeAttribute('class');
@@ -151889,6 +152185,7 @@ class Util {
151889
152185
  * [%MAINCSS%] page-level <style>
151890
152186
  * [%SECTIONCSS%] per-section <style>
151891
152187
  * [%PLUGINS%] runtime plugin registry (see getPluginRegistry)
152188
+ * [%PLUGINFOLDER%] runtime plugin folder (see getPluginFolder)
151892
152189
  * [%PATH%] base URL for runtime/asset references
151893
152190
  */
151894
152191
 
@@ -151923,7 +152220,8 @@ class Util {
151923
152220
  // Registers available plugins (not yet loaded).
151924
152221
  // Scripts and styles are fetched only when the plugin is actually used in content.
151925
152222
  const libraryInstance = new ContentBoxRuntime({
151926
- plugins: [%PLUGINS%]
152223
+ plugins: [%PLUGINS%],
152224
+ pluginFolder: [%PLUGINFOLDER%]
151927
152225
  });
151928
152226
  libraryInstance.init();
151929
152227
  </script>
@@ -151962,7 +152260,8 @@ class Util {
151962
152260
  // Registers available plugins (not yet loaded).
151963
152261
  // Scripts and styles are fetched only when the plugin is actually used in content.
151964
152262
  const libraryInstance = new ContentBoxRuntime({
151965
- plugins: [%PLUGINS%]
152263
+ plugins: [%PLUGINS%],
152264
+ pluginFolder: [%PLUGINFOLDER%]
151966
152265
  });
151967
152266
  libraryInstance.init();
151968
152267
  </script>
@@ -151999,7 +152298,8 @@ class Util {
151999
152298
  // Registers available plugins (not yet loaded).
152000
152299
  // Scripts and styles are fetched only when the plugin is actually used in content.
152001
152300
  const libraryInstance = new ContentBoxRuntime({
152002
- plugins: [%PLUGINS%]
152301
+ plugins: [%PLUGINS%],
152302
+ pluginFolder: [%PLUGINFOLDER%]
152003
152303
  });
152004
152304
  libraryInstance.init();
152005
152305
  </script>
@@ -152037,7 +152337,8 @@ class Util {
152037
152337
  // Registers available plugins (not yet loaded).
152038
152338
  // Scripts and styles are fetched only when the plugin is actually used in content.
152039
152339
  const libraryInstance = new ContentBoxRuntime({
152040
- plugins: [%PLUGINS%]
152340
+ plugins: [%PLUGINS%],
152341
+ pluginFolder: [%PLUGINFOLDER%]
152041
152342
  });
152042
152343
  libraryInstance.init();
152043
152344
  </script>
@@ -152102,6 +152403,36 @@ class Util {
152102
152403
  });
152103
152404
  return JSON.stringify(registry, null, 4);
152104
152405
  }
152406
+ /*
152407
+ * The runtime's plugin folder (pluginFolder in blank.html), as a JS string.
152408
+ * Plugins used in the content load from it without being registered, so an
152409
+ * exported page keeps them. A URL under the content frame's base becomes a
152410
+ * [%PATH%] token, like the registry's URLs.
152411
+ */
152412
+
152413
+
152414
+ getPluginFolder() {
152415
+ const win = this.builder.win;
152416
+ let folder = '';
152417
+
152418
+ try {
152419
+ folder = win.builderRuntime.settings.pluginFolder || '';
152420
+ } catch (e) {
152421
+ /* no runtime in the content frame */
152422
+ }
152423
+
152424
+ let base = '';
152425
+
152426
+ try {
152427
+ const href = win.location.href;
152428
+ base = href.substring(0, href.lastIndexOf('/'));
152429
+ } catch (e) {
152430
+ /* cross-origin content frame */
152431
+ }
152432
+
152433
+ if (base && folder.startsWith(base + '/')) folder = '[%PATH%]' + folder.substring(base.length);
152434
+ return JSON.stringify(folder);
152435
+ }
152105
152436
 
152106
152437
  }
152107
152438
 
@@ -166555,10 +166886,6 @@ class IframePanel {
166555
166886
  let screenMode = localStorage.getItem('_screenmode');
166556
166887
 
166557
166888
  if (localStorage.getItem('_screenmode')) {
166558
- if (!this.builder.controlPanel && this.builder.iframeSrc) {
166559
- if (screenMode === 'fullview') screenMode = 'desktop';
166560
- }
166561
-
166562
166889
  this.builder.screenMode = screenMode;
166563
166890
  contentView.classList.add(screenMode);
166564
166891
  } else {
@@ -166646,8 +166973,22 @@ class IframePanel {
166646
166973
  // addBreakpoint) and authored tablet values while the user believed they were
166647
166974
  // authoring desktop. Scaling fullview the same way the device modes do puts the
166648
166975
  // viewport back in the desktop band, so no control needs to know about this.
166976
+ // The topOffset setting in pixels. It is a CSS length ('55px', '3rem', 48), the same
166977
+ // value the topspace CSS uses, so a unit other than px is resolved, not read as px.
166649
166978
 
166650
166979
 
166980
+ topOffsetPx() {
166981
+ const value = this.builder.topOffset;
166982
+ if (value === undefined || value === null || value === '') return 55;
166983
+ if (typeof value === 'number') return value;
166984
+ const probe = document.createElement('div');
166985
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;height:' + value;
166986
+ document.body.appendChild(probe);
166987
+ const px = probe.getBoundingClientRect().height;
166988
+ probe.remove();
166989
+ return px || parseInt(value) || 55;
166990
+ }
166991
+
166651
166992
  applyFullviewScale() {
166652
166993
  const contentView = this.contentView;
166653
166994
  if (!contentView || !this.editPanel) return;
@@ -166683,7 +167024,7 @@ class IframePanel {
166683
167024
 
166684
167025
 
166685
167026
  const hasTopSpace = this.builder.topSpace || document.body.classList.contains('topspace');
166686
- const topOffset = hasTopSpace ? parseInt(this.builder.topOffset) || 55 : 0;
167027
+ const topOffset = hasTopSpace ? this.topOffsetPx() : 0;
166687
167028
  const scale = availableWidth / logicalWidth;
166688
167029
  const availableHeight = this.editPanel.clientHeight - topOffset;
166689
167030
  contentView.style.setProperty('--fullview-top', `${topOffset}px`);
@@ -166758,7 +167099,7 @@ class IframePanel {
166758
167099
 
166759
167100
 
166760
167101
  const hasTopSpace = this.builder.topSpace || document.body.classList.contains('topspace');
166761
- const topOffset = hasTopSpace ? parseInt(this.builder.topOffset) || 55 : 0; // A manual width drag writes an inline width and leaves the mode class alone, so
167102
+ const topOffset = hasTopSpace ? this.topOffsetPx() : 0; // A manual width drag writes an inline width and leaves the mode class alone, so
166762
167103
  // that width is the frame the user asked for. Reading it is safe: it is an input
166763
167104
  // here, never something this method writes.
166764
167105
 
@@ -175921,6 +176262,12 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
175921
176262
  outline-offset: -1px;
175922
176263
  }
175923
176264
 
176265
+ /* A faded-out overlay layer covering visible content, under the pointer.
176266
+ Clicks go through it. See ControlPanel.bindLayerPassThrough. */
176267
+ .is-wrapper [data-overlay-passthrough] {
176268
+ pointer-events: none !important;
176269
+ }
176270
+
175924
176271
  /* Hovering a row of the panel's layer list. Locates the element while
175925
176272
  leaving the selection outline alone. */
175926
176273
  .is-wrapper [data-overlay-hover] {
@@ -175931,13 +176278,20 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
175931
176278
  /* An overlay layer is edited through the control panel, not in place, so
175932
176279
  a text I-beam here would promise a caret that never arrives. Regions
175933
176280
  that ARE directly editable inside an overlay (a plugin's .is-subblock,
175934
- which enableEditableRegions makes contentEditable) keep it. */
175935
- .is-wrapper ${this.overlayEditableSelector},
175936
- .is-wrapper ${this.overlayEditableSelector} * {
176281
+ which enableEditableRegions makes contentEditable) keep it. Tool
176282
+ buttons and .is-container islands (normal in-place content) are left
176283
+ alone. :where() keeps this rule at the lowest specificity so it never
176284
+ overrides another cursor rule. */
176285
+ .is-wrapper :where(${this.overlayEditableSelector},
176286
+ ${this.overlayEditableSelector} *:not(.is-tool, .is-tool *, .is-container, .is-container *)) {
175937
176287
  cursor: default;
175938
176288
  }
175939
- .is-wrapper ${this.overlayEditableSelector} [contenteditable="true"],
175940
- .is-wrapper ${this.overlayEditableSelector} [contenteditable="true"] * {
176289
+ /* cursor is inherited: give the containers back the normal behaviour */
176290
+ .is-wrapper :where(${this.overlayEditableSelector} .is-container) {
176291
+ cursor: auto;
176292
+ }
176293
+ .is-wrapper :where(${this.overlayEditableSelector} [contenteditable="true"]:not(.is-container, .is-container *),
176294
+ ${this.overlayEditableSelector} [contenteditable="true"]:not(.is-container, .is-container *) *) {
175941
176295
  cursor: text;
175942
176296
  }
175943
176297
 
@@ -178086,12 +178440,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
178086
178440
  }
178087
178441
  e.stopImmediatePropagation();
178088
178442
  */
178089
- const answer = window.confirm(out('Do you want to open the link?'));
178090
-
178091
- if (answer) {
178092
- window.open(url, '_blank');
178093
- }
178094
-
178443
+ // Editing: a click selects the link, it never leaves the page
178095
178444
  e.preventDefault();
178096
178445
  }
178097
178446
  }
@@ -178700,15 +179049,70 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
178700
179049
  newArea = doc.body.innerHTML;
178701
179050
  this.editor.saveForUndo();
178702
179051
  const d = new Date();
178703
- newArea = newArea.replace('[%YEAR%]', d.getFullYear());
179052
+ newArea = newArea.replace('[%YEAR%]', d.getFullYear()); // A navigation bar is a section that IS one, not any HTML that mentions the
179053
+ // class (a section's own CSS may style itself relative to a bar above it).
179054
+
179055
+ const isNavbar = el => el.matches('.is-section-navbar, [data-cb-type="nav-menu"]') || !!el.querySelector('[data-cb-type="nav-menu"]'); // The site supplies its own menu above the content (setTopContentProvider,
179056
+ // locked): it is the page's navigation, so a design's own bar is left out
179057
+ // and the rest of the design (a header) goes in first, under the menu.
179058
+
179059
+
179060
+ const lockedTop = Array.from(this.wrapperEl.children).filter(el => el.hasAttribute('data-top-locked'));
179061
+
179062
+ if (lockedTop.length && Array.from(doc.body.children).some(isNavbar)) {
179063
+ const tpl = document.createElement('template');
179064
+ tpl.innerHTML = newArea; // The bar's text colour (the colour its design gives it over the header).
179065
+
179066
+ let barInk = '';
179067
+ Array.from(tpl.content.children).filter(isNavbar).forEach(el => {
179068
+ const m = /--bar-color:\s*var\(--bar-ink,\s*(rgba?\([^)]*\)|#[0-9a-fA-F]{3,8})\s*\)/.exec(el.outerHTML);
179069
+ if (m && !barInk) barInk = m[1];
179070
+ el.remove();
179071
+ }); // The header was made with a bar floating over its top: say so, so the
179072
+ // site's menu can float over it the same way, in the colour the header's
179073
+ // own bar used there.
178704
179074
 
178705
- if (newArea.indexOf('is-section-navbar') !== -1 || newArea.indexOf('data-cb-type="nav-menu"') !== -1) {
179075
+ const header = Array.from(tpl.content.children).find(el => el.classList.contains('is-section'));
179076
+
179077
+ if (header) {
179078
+ header.setAttribute('data-under-bar', '');
179079
+ if (barInk && !header.hasAttribute('data-bar-rest-ink')) header.setAttribute('data-bar-rest-ink', barInk);
179080
+ }
179081
+
179082
+ if (!tpl.content.children.length) {
179083
+ this.editor.util.confirm(out('This site\'s menu is managed in the menu settings.'), () => {}, false, out('OK'), true);
179084
+ return;
179085
+ }
179086
+
179087
+ dom.appendHtml(lockedTop[lockedTop.length - 1], tpl.innerHTML, 'afterend');
179088
+ } else if (Array.from(doc.body.children).some(isNavbar)) {
178706
179089
  // Insert first
178707
179090
  const sectionNavbar = this.wrapperEl.querySelector('.is-section-navbar');
178708
179091
 
178709
179092
  if (sectionNavbar) {
178710
- // let delConf = this.deleteConfirm;
179093
+ // A design that brings its own bar together with other sections (a header):
179094
+ // keep the page's bar and insert the rest, or replace the bar too.
179095
+ const tpl = document.createElement('template');
179096
+ tpl.innerHTML = newArea;
179097
+ const newBar = Array.from(tpl.content.children).find(isNavbar);
179098
+
179099
+ if (newBar && tpl.content.children.length > 1) {
179100
+ this.editor.util.confirm(out('This page already has a navigation bar.'), (keep, dismissed) => {
179101
+ if (dismissed) return; // closed without choosing
179102
+
179103
+ if (keep) {
179104
+ newBar.remove();
179105
+ this.addIdea(tpl.innerHTML);
179106
+ } else {
179107
+ sectionNavbar.parentNode.removeChild(sectionNavbar);
179108
+ this.addIdea(newArea);
179109
+ }
179110
+ }, true, out('Keep current'), true, 'Replace');
179111
+ return;
179112
+ } // let delConf = this.deleteConfirm;
178711
179113
  // this.editor.deleteConfirm = true;
179114
+
179115
+
178712
179116
  this.editor.util.confirm(out('Do you want to replace the current navigation bar?'), ok => {
178713
179117
  if (ok) {
178714
179118
  sectionNavbar.parentNode.removeChild(sectionNavbar);
@@ -178957,7 +179361,10 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
178957
179361
  // }
178958
179362
 
178959
179363
  if (lastNewSection) {
178960
- this.refreshSectionAnim(lastNewSection);
179364
+ // A template can bring several sections; a pinned one among them (not
179365
+ // only the last) needs the full re-render that sets up its pin.
179366
+ const anyPinned = Array.from(newSections).some(s => s.classList.contains('section-pin') || !!s.querySelector('.section-pin'));
179367
+ this.refreshSectionAnim(lastNewSection, anyPinned);
178961
179368
  this.scrollTo(lastNewSection, 100);
178962
179369
  }
178963
179370
 
@@ -179223,15 +179630,19 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
179223
179630
  const btnSectionUp = sectionTool.querySelector('.btn-move-section-up');
179224
179631
  const btnSectionDown = sectionTool.querySelector('.btn-move-section-down');
179225
179632
  const btnSectionRemove = sectionTool.querySelector('.btn-section-remove');
179633
+ const btnSectionEdit = sectionTool.querySelector('.btn-section-edit');
179226
179634
 
179227
179635
  if (box.offsetHeight < 160) {
179636
+ // Short section (e.g. a nav bar): room for two buttons, HTML and Remove
179228
179637
  btnSectionUp.style.display = 'none';
179229
179638
  btnSectionDown.style.display = 'none';
179230
- btnSectionRemove.style.display = 'none';
179639
+ if (btnSectionEdit) btnSectionEdit.style.display = 'none';
179640
+ btnSectionRemove.style.display = '';
179231
179641
  sectionTool.style.top = '17px'; //'calc(50% - 17px)';
179232
179642
  } else {
179233
179643
  btnSectionUp.style.display = '';
179234
179644
  btnSectionDown.style.display = '';
179645
+ if (btnSectionEdit) btnSectionEdit.style.display = '';
179235
179646
  btnSectionRemove.style.display = '';
179236
179647
  }
179237
179648
  } //new box tool
@@ -179914,7 +180325,16 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
179914
180325
 
179915
180326
  this.activeBox = null;
179916
180327
  if (this.editor) this.editor.activeBox = null;
179917
- this.activeSection = null; // Wait for IFRAME loaded
180328
+ this.activeSection = null; // New content: whatever the control panel was showing belongs to the
180329
+ // old page (e.g. after Clear Content), so return it to its empty state.
180330
+
180331
+ if (this.controlPanel && this.controlpanel) this.controlpanel.select(''); // ...and so do the floating tools on the page (the element tool).
180332
+
180333
+ if (this.editor && this.editor.util && this.editor.doc) {
180334
+ this.editor.util.clearActiveElement();
180335
+ this.editor.util.hideControls();
180336
+ } // Wait for IFRAME loaded
180337
+
179918
180338
 
179919
180339
  const wrapper = this.wrapperEl;
179920
180340
 
@@ -180350,9 +180770,10 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
180350
180770
  // html.js attribute sweep: the attributes are ContentBox's, so the
180351
180771
  // cleanup belongs on this side.
180352
180772
 
180353
- clone.querySelectorAll('[data-overlay-select], [data-overlay-hover]').forEach(elm => {
180773
+ clone.querySelectorAll('[data-overlay-select], [data-overlay-hover], [data-overlay-passthrough]').forEach(elm => {
180354
180774
  elm.removeAttribute('data-overlay-select');
180355
180775
  elm.removeAttribute('data-overlay-hover');
180776
+ elm.removeAttribute('data-overlay-passthrough');
180356
180777
  });
180357
180778
  }
180358
180779
 
@@ -180823,11 +181244,15 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
180823
181244
  let mainCss = this.mainCss(true);
180824
181245
  let sectionCss = this.sectionCss(true);
180825
181246
  output = output.replaceAll('[%MAINCSS%]', mainCss);
180826
- output = output.replaceAll('[%SECTIONCSS%]', sectionCss); // [%PLUGINS%] - runtime plugin registry (must run before [%PATH%],
180827
- // the registry's URLs are emitted as [%PATH%] tokens)
181247
+ output = output.replaceAll('[%SECTIONCSS%]', sectionCss); // [%PLUGINS%], [%PLUGINFOLDER%] - runtime plugin registry and folder (must run
181248
+ // before [%PATH%], their URLs are emitted as [%PATH%] tokens)
180828
181249
 
180829
181250
  if (output.indexOf('[%PLUGINS%]') !== -1) {
180830
181251
  output = output.replaceAll('[%PLUGINS%]', this.util.getPluginRegistry());
181252
+ }
181253
+
181254
+ if (output.indexOf('[%PLUGINFOLDER%]') !== -1) {
181255
+ output = output.replaceAll('[%PLUGINFOLDER%]', this.util.getPluginFolder());
180831
181256
  } // Custom
180832
181257
 
180833
181258