@innovastudio/contentbox 1.6.203 → 1.6.205

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) {
@@ -19454,7 +19517,7 @@ class PanelIcon {
19454
19517
  <button title="${out('Color')}" class="btn-color is-btn-color"></button>
19455
19518
  </div>
19456
19519
 
19457
- <div class="label">${out('Icon Size')}:</div>
19520
+ <div class="label label-iconsize">${out('Icon Size')}:</div>
19458
19521
 
19459
19522
  <div role="group" aria-label="${out('Font Size')}" class="group fontsizes">
19460
19523
  ${fontSizeHtml}
@@ -19517,20 +19580,17 @@ class PanelIcon {
19517
19580
  btnSizes.forEach(btn => {
19518
19581
  btn.addEventListener('click', () => {
19519
19582
  this.builder.editor.saveForUndo();
19520
- const num = btn.getAttribute('data-value');
19521
- let currentFontSize = 1;
19522
- let container = this.getElm();
19583
+ const num = btn.getAttribute('data-value'); // Size the icon itself, as ContentBuilder's own size buttons do for an icon.
19523
19584
 
19524
- if (container) {
19525
- currentFontSize = Number(window.getComputedStyle(container).getPropertyValue('font-size').match(/\d+/)[0]);
19585
+ const icon = this.builder.controlpanel.activeElement;
19586
+ const rte = this.builder.editor.rte;
19587
+
19588
+ if ((this.builder.editor.opts.fontSizeClassValues || []).length > 0) {
19589
+ rte.applyClassFontSize(icon, num);
19590
+ } else {
19591
+ rte.applyInlineFontSize(icon, num);
19526
19592
  }
19527
19593
 
19528
- this.builder.editor.dom.execCommand('font-size', currentFontSize + 1 + 'px', (ok, container) => {
19529
- if (ok) {
19530
- this.builder.editor.rte.applyClassFontSize(container, num);
19531
- container.style.fontSize = '';
19532
- }
19533
- });
19534
19594
  this.getState();
19535
19595
  this.builder.editor.onChange();
19536
19596
  });
@@ -19568,7 +19628,13 @@ class PanelIcon {
19568
19628
  getState() {
19569
19629
  const panel = this.panel;
19570
19630
  let icon = this.builder.controlpanel.activeElement;
19571
- const div = icon.parentNode;
19631
+ const div = icon.parentNode; // An icon in overlay text: its link and alignment belong to the line (the Text panel),
19632
+ // and its size follows the design, like the text's. Only Change icon and Color are shown.
19633
+
19634
+ const inOverlayText = !!this.builder.controlpanel.overlayIcon(icon);
19635
+ panel.querySelectorAll('.btn-link, .label-iconsize, .group.fontsizes, .alignicon').forEach(elm => {
19636
+ elm.style.display = inOverlayText ? 'none' : '';
19637
+ });
19572
19638
  const btnAligns = panel.querySelectorAll('[data-align]');
19573
19639
  btnAligns.forEach(btn => btn.classList.remove('on'));
19574
19640
  if (div.classList.contains('text-left')) panel.querySelector('[data-align="left"]').classList.add('on');
@@ -19619,14 +19685,6 @@ class PanelIcon {
19619
19685
  }
19620
19686
  }
19621
19687
 
19622
- getElm() {
19623
- const selection = this.builder.win.getSelection();
19624
- const anchorNode = selection.anchorNode;
19625
- if (!anchorNode) return false;
19626
- const container = anchorNode.nodeType !== Node.TEXT_NODE && anchorNode.nodeType !== Node.COMMENT_NODE ? anchorNode : anchorNode.parentElement;
19627
- return container;
19628
- }
19629
-
19630
19688
  }
19631
19689
 
19632
19690
  class PanelSvg {
@@ -30004,7 +30062,7 @@ class PanelBox {
30004
30062
  <button title="${out('Color')}" class="btn-color is-btn-color"></button>
30005
30063
  </div>
30006
30064
  </div>
30007
- <div>
30065
+ <div class="div-box-gradient">
30008
30066
  <div class="label mt-3">${out('Gradient')}:</div>
30009
30067
  <div class="group">
30010
30068
  <button title="${out('Gradient')}" class="btn-gradient is-btn-color"></button>
@@ -30074,10 +30132,9 @@ class PanelBox {
30074
30132
 
30075
30133
  </div>
30076
30134
 
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
-
30135
+ <div class="submain div-box-plugins" style="display:none">
30136
+ <div class="label mt-3">${out('In this box')}:</div>
30137
+ <div class="overlay-list mt-1"></div>
30081
30138
  </div>
30082
30139
 
30083
30140
  <button class="accordion-item btn-accordion-content" aria-expanded="false" aria-controls="boxcontent1">
@@ -31134,6 +31191,13 @@ class PanelBox {
31134
31191
 
31135
31192
  static css() {
31136
31193
  return `
31194
+ .cb-navbar-box .boxedit,
31195
+ .cb-navbar-box .div-box-gradient,
31196
+ .cb-navbar-box .box-bg-image,
31197
+ .cb-navbar-box > .accordion-item,
31198
+ .cb-navbar-box > .accordion-content {
31199
+ display: none !important;
31200
+ }
31137
31201
  [data-cb-ui] .cb-contentpos .cb-fieldhead {
31138
31202
  display: flex;
31139
31203
  align-items: baseline;
@@ -31445,7 +31509,10 @@ class PanelBox {
31445
31509
  const overlayBg = activeBox.querySelector('.is-overlay-bg');
31446
31510
  const overlayContent = activeBox.querySelector('.is-overlay-content');
31447
31511
  const container = this.box.contentContainer(activeBox);
31448
- const panel = this.panel;
31512
+ const panel = this.panel; // A navigation bar keeps only its background colour (the bar's colour once the
31513
+ // header has scrolled by): no duplicate, extra box, gradient, image or box tabs
31514
+
31515
+ panel.classList.toggle('cb-navbar-box', !!activeBox.closest('.is-section-navbar'));
31449
31516
  const divEditModule = panel.querySelector('.group.editmodule');
31450
31517
 
31451
31518
  if (overlayContent && overlayContent.getAttribute('data-module')) {
@@ -31789,12 +31856,12 @@ class PanelBox {
31789
31856
  chkAutoLayout.checked = true;
31790
31857
  } else {
31791
31858
  chkAutoLayout.checked = false;
31792
- } // PLUGIN SETTINGS
31859
+ } // PLUGINS
31860
+ // A plugin in the box overlay has its own panel: a click on it opens that.
31861
+ // Here the box lists them, and each can hide box settings it has no use for.
31793
31862
 
31794
31863
 
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)
31864
+ const runtime = this.builder.win.builderRuntime; // Reset (can be changed by applyPluginOptions)
31798
31865
 
31799
31866
  const divBgColor = this.panel.querySelector('.box-bg-color');
31800
31867
  divBgColor.style.display = '';
@@ -31805,39 +31872,55 @@ class PanelBox {
31805
31872
  const divPanelTitle = this.panel.parentNode.querySelector('.part-title');
31806
31873
  divPanelTitle.style.display = '';
31807
31874
  this.divPanelTitle = divPanelTitle;
31875
+ const divPlugins = panel.querySelector('.div-box-plugins');
31876
+ const list = divPlugins.querySelector('.overlay-list');
31877
+ list.innerHTML = ''; // The box's own plugins: not the ones inside its content areas, nor inside another plugin
31808
31878
 
31809
- if (overlayContent && runtime) {
31810
- const currentElement = overlayContent.querySelector('[data-cb-type]');
31879
+ 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
31880
 
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
- }
31881
+ const counts = {},
31882
+ seen = {};
31883
+ plugins.forEach(el => {
31884
+ const t = el.getAttribute('data-cb-type');
31885
+ counts[t] = (counts[t] || 0) + 1;
31886
+ });
31887
+ plugins.forEach(el => {
31888
+ const type = el.getAttribute('data-cb-type');
31889
+ const plugin = runtime.getPlugin(type);
31890
+ if (!plugin) return;
31891
+ seen[type] = (seen[type] || 0) + 1;
31892
+ this.applyPluginOptions(el, plugin);
31893
+ const row = this.panel.ownerDocument.createElement('button');
31894
+ row.type = 'button';
31895
+ row.className = 'ovl-row';
31896
+ const label = this.panel.ownerDocument.createElement('span');
31897
+ label.className = 'ovl-row-text';
31898
+ label.textContent = out(plugin.displayName || plugin.name) + (counts[type] > 1 ? ' ' + seen[type] : '');
31899
+ row.appendChild(label);
31900
+ row.addEventListener('click', () => {
31901
+ this.builder.controlpanel.markOverlayHover(null);
31902
+ this.builder.controlpanel.selectOverlayElement(el);
31903
+ });
31904
+ row.addEventListener('mouseenter', () => this.builder.controlpanel.markOverlayHover(el));
31905
+ row.addEventListener('mouseleave', () => this.builder.controlpanel.markOverlayHover(null));
31906
+ list.appendChild(row);
31907
+ });
31908
+ divPlugins.style.display = list.childElementCount ? '' : 'none';
31909
+ } // True when the plugin element fills (practically) the whole box overlay.
31822
31910
 
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
31911
 
31840
- if (currentElement.querySelector('.is-container')) {
31912
+ pluginCoversBox(element) {
31913
+ const overlay = element.closest('.is-overlay');
31914
+ if (!overlay) return true;
31915
+ const box = overlay.getBoundingClientRect();
31916
+ const el = element.getBoundingClientRect();
31917
+ if (!box.width || !box.height) return true;
31918
+ return el.width >= box.width * 0.98 && el.height >= box.height * 0.98;
31919
+ } // The box settings a plugin in this box has no use for (its contentbox options)
31920
+
31921
+
31922
+ applyPluginOptions(currentElement, plugin) {
31923
+ if (plugin.contentbox && currentElement.querySelector('.is-container')) {
31841
31924
  // if plugin element has editable content
31842
31925
  plugin.contentbox.boxTabText = false;
31843
31926
  plugin.contentbox.boxTabContent = false;
@@ -31860,16 +31943,21 @@ class PanelBox {
31860
31943
 
31861
31944
 
31862
31945
  if (plugin.contentbox) {
31863
- if (plugin.contentbox.bg !== undefined && plugin.contentbox.bg !== null && plugin.contentbox.bg === false) {
31946
+ // A plugin hides the box background (bg: false) because it covers it,
31947
+ // like a full-bleed video. When the plugin only fills part of the box
31948
+ // (a framed video, a card), the background is visible and stays editable.
31949
+ const coversBox = this.pluginCoversBox(currentElement);
31950
+
31951
+ if (coversBox && plugin.contentbox.bg !== undefined && plugin.contentbox.bg !== null && plugin.contentbox.bg === false) {
31864
31952
  this.divBgColor.style.display = 'none';
31865
31953
  this.divBgImage.style.display = 'none';
31866
31954
  }
31867
31955
 
31868
- if (plugin.contentbox.bgColor !== undefined && plugin.contentbox.bgColor !== null && plugin.contentbox.bgColor === false) {
31956
+ if (coversBox && plugin.contentbox.bgColor !== undefined && plugin.contentbox.bgColor !== null && plugin.contentbox.bgColor === false) {
31869
31957
  this.divBgColor.style.display = 'none';
31870
31958
  }
31871
31959
 
31872
- if (plugin.contentbox.bgImage !== undefined && plugin.contentbox.bgImage !== null && plugin.contentbox.bgImage === false) {
31960
+ if (coversBox && plugin.contentbox.bgImage !== undefined && plugin.contentbox.bgImage !== null && plugin.contentbox.bgImage === false) {
31873
31961
  this.divBgImage.style.display = 'none';
31874
31962
  }
31875
31963
 
@@ -31941,111 +32029,6 @@ class PanelBox {
31941
32029
  });
31942
32030
  }
31943
32031
  }
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
32032
  }
32050
32033
 
32051
32034
  getStateTargeted() {
@@ -33713,6 +33696,40 @@ class PanelSection {
33713
33696
 
33714
33697
  </div>
33715
33698
 
33699
+ <button class="accordion-item btn-accordion-bar" aria-expanded="false" aria-controls="sectioncontent5">
33700
+ ${out('Navigation Bar')}
33701
+ <span><svg><use xlink:href="#icon-chevron-down"></use></svg></span>
33702
+ </button>
33703
+ <div class="accordion-content" id="sectioncontent5" aria-hidden="true">
33704
+
33705
+ <div class="subpanel">
33706
+ <div class="barstart-row">
33707
+ <div class="label mt-2">${out('Bar at the start')}:</div>
33708
+ <div role="group" aria-label="${out('Bar at the start')}" class="group sectionbar barstart">
33709
+ <button title="${out('Auto')}" data-barstart="">${out('Auto')}</button>
33710
+ <button title="${out('Light')}" data-barstart="light">${out('Light')}</button>
33711
+ <button title="${out('Dark')}" data-barstart="dark">${out('Dark')}</button>
33712
+ </div>
33713
+ </div>
33714
+
33715
+ <div class="label mt-2">${out('Bar over this section')}:</div>
33716
+ <div role="group" aria-label="${out('Bar over this section')}" class="group sectionbar">
33717
+ <button title="${out('Default')}" data-bar="">${out('Default')}</button>
33718
+ <button title="${out('Light')}" data-bar="light">${out('Light')}</button>
33719
+ <button title="${out('Dark')}" data-bar="dark">${out('Dark')}</button>
33720
+ <button title="${out('Light glass')}" data-bar="light-glass">${out('Light glass')}</button>
33721
+ <button title="${out('Dark glass')}" data-bar="dark-glass">${out('Dark glass')}</button>
33722
+ </div>
33723
+
33724
+ <div class="label mt-3">${out('Text Color')}:</div>
33725
+ <div class="group">
33726
+ <button title="${out('Text Color')}" class="btn-barink is-btn-color"></button>
33727
+ <button title="${out('Clear')}" class="btn-clear btn-barink-clear"><svg><use xlink:href="#icon-eraser"></use></svg></button>
33728
+ </div>
33729
+ </div>
33730
+
33731
+ </div>
33732
+
33716
33733
  <button class="accordion-item" aria-expanded="false" aria-controls="sectioncontent3">
33717
33734
  ${out('Responsive')}
33718
33735
  <span><svg><use xlink:href="#icon-chevron-down"></use></svg></span>
@@ -33904,6 +33921,54 @@ class PanelSection {
33904
33921
  btnScrollPreset.forEach(btn => btn.classList.remove('on'));
33905
33922
  if (s !== '') btn.classList.add('on');
33906
33923
  });
33924
+ }); // Navigation Bar: how the page's navigation bar looks while it passes over this
33925
+ // section. The section only says it (data-bar, data-bar-ink); the menu in the bar
33926
+ // does the work.
33927
+
33928
+ const btnBarLooks = panel.querySelectorAll('[data-bar]');
33929
+ btnBarLooks.forEach(btn => {
33930
+ btn.addEventListener('click', () => {
33931
+ this.builder.editor.saveForUndo();
33932
+ const section = this.builder.activeSection;
33933
+ const s = btn.getAttribute('data-bar');
33934
+ if (s) section.setAttribute('data-bar', s);else section.removeAttribute('data-bar');
33935
+ btnBarLooks.forEach(btn => btn.classList.remove('on'));
33936
+ btn.classList.add('on');
33937
+ this.builder.onChange();
33938
+ });
33939
+ }); // Bar at the start: the bar's text while it rests over the top section, at the top of
33940
+ // the page (data-bar-start). Only the top section has it. Auto: no attribute.
33941
+
33942
+ const btnBarStarts = panel.querySelectorAll('[data-barstart]');
33943
+ btnBarStarts.forEach(btn => {
33944
+ btn.addEventListener('click', () => {
33945
+ this.builder.editor.saveForUndo();
33946
+ const section = this.builder.activeSection;
33947
+ const s = btn.getAttribute('data-barstart');
33948
+ if (s) section.setAttribute('data-bar-start', s);else section.removeAttribute('data-bar-start');
33949
+ btnBarStarts.forEach(btn => btn.classList.remove('on'));
33950
+ btn.classList.add('on');
33951
+ this.builder.onChange();
33952
+ });
33953
+ });
33954
+ const btnBarInk = panel.querySelector('.btn-barink');
33955
+ btnBarInk.addEventListener('click', () => {
33956
+ this.builder.editor.saveForUndo(true); // checkLater = true
33957
+
33958
+ const section = this.builder.activeSection;
33959
+ const colorpicker = this.builder.editor.colorpicker();
33960
+ colorpicker.open(s => {
33961
+ if (s) section.setAttribute('data-bar-ink', s);else section.removeAttribute('data-bar-ink');
33962
+ btnBarInk.style.backgroundColor = s; // preview
33963
+
33964
+ this.builder.onChange();
33965
+ }, btnBarInk.style.backgroundColor, () => {}, btnBarInk, true);
33966
+ });
33967
+ panel.querySelector('.btn-barink-clear').addEventListener('click', () => {
33968
+ this.builder.editor.saveForUndo();
33969
+ this.builder.activeSection.removeAttribute('data-bar-ink');
33970
+ btnBarInk.style.backgroundColor = '';
33971
+ this.builder.onChange();
33907
33972
  }); // Responsive
33908
33973
 
33909
33974
  bindDeviceRow(panel.querySelector('.div-target'), this.builder);
@@ -34142,6 +34207,35 @@ class PanelSection {
34142
34207
  btnGradient.style.backgroundImage = gradient; //preview
34143
34208
  } else {
34144
34209
  btnGradient.style.backgroundImage = '';
34210
+ } // A navigation bar section: no height presets and no section tabs
34211
+
34212
+
34213
+ 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
34214
+ // (one that says barLooks: true)
34215
+
34216
+ const btnAccordionBar = panel.querySelector('.btn-accordion-bar');
34217
+ const runtime = this.builder.win.builderRuntime;
34218
+ const barPlugins = this.builder.wrapperEl ? Array.from(this.builder.wrapperEl.querySelectorAll('.is-section-navbar [data-cb-type]')) : [];
34219
+ const pageBar = !!runtime && barPlugins.some(el => {
34220
+ const plugin = runtime.getPlugin(el.getAttribute('data-cb-type'));
34221
+ return !!(plugin && plugin.barLooks);
34222
+ });
34223
+ const showBar = pageBar && !activeSection.classList.contains('is-section-navbar');
34224
+ btnAccordionBar.classList.toggle('is-bar-off', !showBar);
34225
+
34226
+ if (showBar) {
34227
+ const look = activeSection.getAttribute('data-bar') || '';
34228
+ panel.querySelectorAll('[data-bar]').forEach(btn => btn.classList.toggle('on', btn.getAttribute('data-bar') === look));
34229
+ panel.querySelector('.btn-barink').style.backgroundColor = activeSection.getAttribute('data-bar-ink') || ''; // Bar at the start: the top section only (the first section under the bar)
34230
+
34231
+ const top = Array.from(this.builder.wrapperEl.querySelectorAll('.is-section')).find(s => !s.closest('.is-section-navbar'));
34232
+ const isTop = top === activeSection;
34233
+ panel.querySelector('.barstart-row').style.display = isTop ? '' : 'none';
34234
+
34235
+ if (isTop) {
34236
+ const start = activeSection.getAttribute('data-bar-start') || '';
34237
+ panel.querySelectorAll('[data-barstart]').forEach(btn => btn.classList.toggle('on', btn.getAttribute('data-barstart') === start));
34238
+ }
34145
34239
  }
34146
34240
 
34147
34241
  this.realtimeVisibility(activeSection, true);
@@ -34246,6 +34340,13 @@ class PanelSection {
34246
34340
 
34247
34341
  static css() {
34248
34342
  return `
34343
+ .btn-accordion-bar.is-bar-off,
34344
+ .btn-accordion-bar.is-bar-off + .accordion-content ,
34345
+ .cb-navbar-section .div-section-height,
34346
+ .cb-navbar-section > .accordion-item,
34347
+ .cb-navbar-section > .accordion-content {
34348
+ display: none !important;
34349
+ }
34249
34350
  .cb-sectionanim .cb-anim-note {
34250
34351
  font-size: 11.5px;
34251
34352
  line-height: 1.5;
@@ -43492,6 +43593,18 @@ class ControlPanel {
43492
43593
  // elmTool, whose duplicate/delete/move actions would let a user
43493
43594
  // restructure a generated layer.
43494
43595
  this.builder.editor.inspectedElement = overlayTarget.element;
43596
+ } else if (this.overlayIcon(e.target)) {
43597
+ // An icon in overlay text opens the Icon panel.
43598
+ this.builder.editor.inspectedElement = e.target;
43599
+ } else {
43600
+ // A plugin in the box overlay opens its own panel (Section > Box > the
43601
+ // plugin), like overlay text. Its editable parts and content areas are
43602
+ // edited where they are.
43603
+ const plugin = e.target.closest ? e.target.closest('.is-overlay-content [data-cb-type]') : null;
43604
+
43605
+ if (plugin && !e.target.closest('.is-container') && !e.target.closest('.is-subblock')) {
43606
+ this.builder.editor.inspectedElement = plugin;
43607
+ }
43495
43608
  }
43496
43609
 
43497
43610
  if (old) old.call(this);
@@ -43636,8 +43749,92 @@ class ControlPanel {
43636
43749
  this.builder.editor.controlpanel = this; // This overide ContentBuilder controlpanel object
43637
43750
 
43638
43751
  this.bindEscape();
43752
+ this.bindLayerPassThrough();
43639
43753
  this.showTooltip();
43640
43754
  }
43755
+ /**
43756
+ * Clicks go through overlay layers that are faded out.
43757
+ *
43758
+ * A crossfade keeps every layer stacked at full size and only turns the
43759
+ * hidden ones transparent, so a click on the text you can see lands on an
43760
+ * invisible layer above it: an empty wrapper gets selected, or the caret
43761
+ * goes into text that is not on screen. hitTest() already sees through
43762
+ * these for the overlay panel; this does the same for the browser's own
43763
+ * hit test, which is what places the caret in a .is-container.
43764
+ *
43765
+ * Marks only exist under the pointer and are recomputed on every move and
43766
+ * scroll, so a layer that fades back in is clickable again. A faded layer
43767
+ * is only passed through when something visible lies below it; a stack
43768
+ * scrubbed to a moment where everything is faded stays selectable.
43769
+ * The marker is stripped from the saved HTML in html().
43770
+ */
43771
+
43772
+
43773
+ bindLayerPassThrough() {
43774
+ if (!this.passThroughHandler) {
43775
+ let frame = 0;
43776
+ let x = -1,
43777
+ y = -1;
43778
+
43779
+ const update = () => {
43780
+ frame = 0;
43781
+ this.updateLayerPassThrough(x, y);
43782
+ };
43783
+
43784
+ this.passThroughHandler = e => {
43785
+ if (typeof e.clientX === 'number') {
43786
+ x = e.clientX;
43787
+ y = e.clientY;
43788
+ }
43789
+
43790
+ if (e.type === 'pointerdown') {
43791
+ // a touch has no move before it
43792
+ update();
43793
+ return;
43794
+ }
43795
+
43796
+ if (!frame) frame = this.builder.win.requestAnimationFrame(update);
43797
+ };
43798
+ }
43799
+
43800
+ const types = ['pointermove', 'pointerdown', 'scroll'];
43801
+ if (this.passThroughDoc) types.forEach(type => this.passThroughDoc.removeEventListener(type, this.passThroughHandler, true));
43802
+ this.passThroughDoc = this.builder.doc;
43803
+ if (this.passThroughDoc) types.forEach(type => this.passThroughDoc.addEventListener(type, this.passThroughHandler, true));
43804
+ }
43805
+
43806
+ updateLayerPassThrough(x, y) {
43807
+ const doc = this.builder.doc;
43808
+ if (!doc || !doc.elementsFromPoint) return;
43809
+ doc.querySelectorAll('[data-overlay-passthrough]').forEach(elm => {
43810
+ elm.removeAttribute('data-overlay-passthrough');
43811
+ });
43812
+ if (x < 0) return;
43813
+ const selector = this.builder.overlayEditableSelector;
43814
+ const stack = doc.elementsFromPoint(x, y);
43815
+ stack.forEach((el, i) => {
43816
+ const overlay = el.closest(selector); // Faded counts through the ancestors: text in a layer that is
43817
+ // faded out as a whole is painted itself, yet invisible.
43818
+
43819
+ if (!overlay || el === overlay || this.isVisibleIn(el, overlay)) return;
43820
+ const covered = stack.slice(i + 1).some(below => overlay.contains(below) && below !== overlay && !el.contains(below) && this.isLayerContent(below) && this.isVisibleIn(below, overlay));
43821
+ if (covered) el.setAttribute('data-overlay-passthrough', '');
43822
+ });
43823
+ } // Something a click is meant for: text, a picture, or a media element.
43824
+
43825
+
43826
+ isLayerContent(el) {
43827
+ return this.builder.hasOwnText(el) || el.matches('img,video,iframe,svg,a,button') || this.hasBackgroundImage(el);
43828
+ } // Painted itself and through every ancestor up to the overlay.
43829
+
43830
+
43831
+ isVisibleIn(el, overlay) {
43832
+ for (let node = el; node && node !== overlay; node = node.parentElement) {
43833
+ if (!this.isPainted(node)) return false;
43834
+ }
43835
+
43836
+ return true;
43837
+ }
43641
43838
  /**
43642
43839
  * Escape closes the open dialog (Element, Icons, Shadow, Image Adjust,
43643
43840
  * Group, Block Options, Symbols, Settings).
@@ -43987,6 +44184,8 @@ class ControlPanel {
43987
44184
  if (el.closest('[data-html]')) return null; // code/module block — has its own editor
43988
44185
 
43989
44186
  if (el.closest('[data-cb-type]')) return null; // plugin — panel-plugin owns it
44187
+
44188
+ if (this.overlayIcon(el)) return null; // icon in a text — the Icon panel owns it
43990
44189
  // Text of an overlay layer. Resolve upward: a click can land on a
43991
44190
  // <b>/<span> inside the region, which is not itself a region.
43992
44191
 
@@ -44177,7 +44376,8 @@ class ControlPanel {
44177
44376
  const el = stack[i];
44178
44377
 
44179
44378
  if (match(el)) {
44180
- if (this.isPainted(el)) return el; // Crossfade stacks (three images or three captions at the same
44379
+ const overlay = el.closest(this.builder.overlayEditableSelector);
44380
+ if (overlay ? this.isVisibleIn(el, overlay) : this.isPainted(el)) return el; // Crossfade stacks (three images or three captions at the same
44181
44381
  // spot, cycled by opacity keyframes) put a FULLY TRANSPARENT
44182
44382
  // layer on top for most of the scene. Stacking order would hand
44183
44383
  // back that one every time, so the user edits a layer they
@@ -44226,7 +44426,7 @@ class ControlPanel {
44226
44426
  activeElement = this.resolvedImage;
44227
44427
  }
44228
44428
 
44229
- if (activeElement && this.overlayTarget(activeElement)) {
44429
+ if (activeElement && (this.overlayTarget(activeElement) || this.overlayIcon(activeElement))) {
44230
44430
  this.activeElement = activeElement;
44231
44431
  this.activeColumn = null;
44232
44432
  this.activeRow = null;
@@ -44350,7 +44550,7 @@ class ControlPanel {
44350
44550
  } // Overlay content has no row/column to walk back up to.
44351
44551
 
44352
44552
 
44353
- if (selection === 'element' && (this.overlayTarget(this.activeElement) || this.activeElement.closest('[data-cb-type]') && this.activeElement.closest('.is-overlay-content'))) {
44553
+ if (selection === 'element' && (this.overlayTarget(this.activeElement) || this.overlayIcon(this.activeElement) || this.activeElement.closest('[data-cb-type]') && this.activeElement.closest('.is-overlay-content'))) {
44354
44554
  breadcrumbHtml = `<div>${section} ${sep} ${box} ${sep}</div>`;
44355
44555
  }
44356
44556
 
@@ -44424,6 +44624,8 @@ class ControlPanel {
44424
44624
  sectionHeightAllowed() {
44425
44625
  const section = this.builder.activeSection;
44426
44626
  if (!section || !section.classList.contains('is-box')) return true;
44627
+ if (section.classList.contains('is-section-navbar')) return false; // a navigation bar has no height presets
44628
+
44427
44629
  const el = section.querySelector('.is-overlay-content [data-cb-type]');
44428
44630
  if (!el) return true;
44429
44631
  const runtime = this.builder.win.builderRuntime;
@@ -44601,6 +44803,25 @@ class ControlPanel {
44601
44803
  elm.removeAttribute('data-overlay-select');
44602
44804
  });
44603
44805
  if (el) el.setAttribute('data-overlay-select', '');
44806
+ } // A plugin placed in an overlay (eg. a header bar's Site Menu or Site Logo).
44807
+ // Like an overlay target it has no cell/row outline, so it gets the same mark.
44808
+
44809
+
44810
+ overlayPlugin(el) {
44811
+ if (!el || !el.closest) return null;
44812
+ const plugin = el.closest('[data-cb-type]');
44813
+ if (!plugin || !plugin.closest(this.builder.overlayEditableSelector)) return null;
44814
+ if (plugin.closest('.is-container')) return null;
44815
+ return plugin;
44816
+ } // An icon (<i class="bi ...">) in an overlay, eg. at the start of a chip's text. It opens the
44817
+ // Icon panel, so it can be changed, while a click on the words still opens the Text panel.
44818
+
44819
+
44820
+ overlayIcon(el) {
44821
+ if (!el || !el.matches || !el.matches('i.bi, i.icon') || el.innerHTML !== '') return null;
44822
+ if (!el.closest(this.builder.overlayEditableSelector)) return null;
44823
+ if (el.closest('.is-container, [data-html], [data-cb-type]')) return null;
44824
+ return el;
44604
44825
  } // Is `el` (or an ancestor of it) a button?
44605
44826
  //
44606
44827
  // A <button> always is. An <a> is a button when it lays itself out as a
@@ -44683,7 +44904,16 @@ class ControlPanel {
44683
44904
 
44684
44905
  if (what === 'element' && this.activeElement) {
44685
44906
  const overlayNow = this.overlayTarget(this.activeElement);
44686
- if (overlayNow) marked = overlayNow.element;else if (this.pictureOnlyElement(this.activeElement)) marked = this.activeElement;
44907
+ if (overlayNow) marked = overlayNow.element;else if (this.pictureOnlyElement(this.activeElement)) marked = this.activeElement;else marked = this.overlayPlugin(this.activeElement) || this.overlayIcon(this.activeElement);
44908
+ } // An overlay icon stays the editor's active icon only while it is selected; otherwise
44909
+ // a later icon insert would replace it.
44910
+
44911
+
44912
+ const activeIcon = this.builder.editor.activeIcon;
44913
+
44914
+ if (activeIcon && activeIcon !== marked && this.overlayIcon(activeIcon)) {
44915
+ activeIcon.classList.remove('icon-active');
44916
+ this.builder.editor.activeIcon = null;
44687
44917
  }
44688
44918
 
44689
44919
  this.markOverlaySelection(marked);
@@ -44834,6 +45064,9 @@ class ControlPanel {
44834
45064
  this.panelIframe.classList.add('active');
44835
45065
  this.objPanelIframe.getState();
44836
45066
  } else if (element.tagName.toLowerCase() === 'i' && (element.classList.contains('icon') || element.classList.contains('bi'))) {
45067
+ // In a container ContentBuilder's click makes it the active icon; an overlay
45068
+ // icon gets no such click, so name it here (Change icon replaces this one).
45069
+ if (this.overlayIcon(element)) this.builder.editor.activeIcon = element;
44837
45070
  let titleHtml = out('Icon');
44838
45071
  this.title.innerHTML = titleHtml;
44839
45072
  this.panelIcon.classList.add('active');
@@ -44966,8 +45199,10 @@ class ControlPanel {
44966
45199
  // should animate exposes that as its own setting.
44967
45200
 
44968
45201
 
44969
- const isPlugin = what === 'element' && !!pluginOf(this.activeElement);
44970
- const canMotion = !isPlugin && (what === 'element' || what === 'column' || what === 'row' || what === 'container' || what === 'block' || what === 'box' || what === 'section');
45202
+ const isPlugin = what === 'element' && !!pluginOf(this.activeElement); // An icon in overlay text moves with its line; only the line has Motion.
45203
+
45204
+ const isOverlayIcon = what === 'element' && !!this.overlayIcon(this.activeElement);
45205
+ const canMotion = !isPlugin && !isOverlayIcon && (what === 'element' || what === 'column' || what === 'row' || what === 'container' || what === 'block' || what === 'box' || what === 'section');
44971
45206
 
44972
45207
  if (canMotion) {
44973
45208
  if (this.tab === 'motion') {
@@ -50657,6 +50892,10 @@ class ToolbarKeys {
50657
50892
  */
50658
50893
  _hold(container, control) {
50659
50894
  if (!control) return;
50895
+
50896
+ // Where the control was: if the command removes what it sat on, the caret
50897
+ // goes to what took its place
50898
+ const spot = (control.closest('.row, [data-cb-block]') || control).getBoundingClientRect();
50660
50899
  setTimeout(() => {
50661
50900
  // Not bailing when the container has gone: deleting the last column
50662
50901
  // takes the strip with it, and that is exactly when the focus needs
@@ -50714,8 +50953,10 @@ class ToolbarKeys {
50714
50953
  // A tick later again: emptying an area makes the editor put its
50715
50954
  // own button there, and that has not happened yet.
50716
50955
  setTimeout(() => {
50717
- const somewhere = this._somewhereUsable();
50718
- if (somewhere) somewhere.focus();
50956
+ const somewhere = this._somewhereUsable(spot);
50957
+ if (somewhere) somewhere.focus({
50958
+ preventScroll: true
50959
+ });
50719
50960
  }, 30);
50720
50961
  }
50721
50962
  }, 0);
@@ -50786,7 +51027,9 @@ class ToolbarKeys {
50786
51027
  // the trip to the toolbar after one button. Stay on it, so the next format
50787
51028
  // is a key away, and leave deliberately with Escape. Anything that claims
50788
51029
  // the focus for itself, a panel or a dialog, is left to do so.
51030
+ // A mouse click (detail > 0) is left alone, so the mouse works as it always did.
50789
51031
  tool.addEventListener('click', e => {
51032
+ if (e.detail > 0) return;
50790
51033
  const control = this._controlFrom(e, tool, BUTTONS);
50791
51034
  if (control) this._hold(tool, control);
50792
51035
  });
@@ -50928,13 +51171,26 @@ class ToolbarKeys {
50928
51171
  * go dead with the focus stuck on a strip. Falls back through what the editor
50929
51172
  * still calls active, then to the first column on the page.
50930
51173
  */
50931
- currentColumn() {
51174
+ currentColumn(near) {
50932
51175
  // It has to be able to hold the focus. A locked column is still the active
50933
51176
  // one but cannot take it, and focusing it silently does nothing.
50934
51177
  const usable = el => el && el.isConnected && (el.isContentEditable || el.hasAttribute('tabindex')) ? el : null;
50935
51178
  const active = usable(this.builder.util.cellSelected()) || usable(this.builder.activeCol);
50936
51179
  if (active) return active;
50937
51180
  const rows = this.builder.doc.querySelectorAll(`${this.builder.opts.container} > *`);
51181
+
51182
+ // Near a place on screen (where a removed row was): the column that took its
51183
+ // place, or the last one before it
51184
+ if (near) {
51185
+ let before = null;
51186
+ for (const row of rows) {
51187
+ const cell = ToolbarKeys.cells(row).find(usable);
51188
+ if (!cell) continue;
51189
+ if (cell.getBoundingClientRect().top >= near.top - 1) return cell;
51190
+ before = cell;
51191
+ }
51192
+ if (before) return before;
51193
+ }
50938
51194
  for (const row of rows) {
50939
51195
  const cell = ToolbarKeys.cells(row).find(usable);
50940
51196
  if (cell) return cell;
@@ -50948,8 +51204,8 @@ class ToolbarKeys {
50948
51204
  * editing away: in both the focus would otherwise land on the body, where no
50949
51205
  * key reaches the editor at all.
50950
51206
  */
50951
- _somewhereUsable() {
50952
- const column = this.currentColumn();
51207
+ _somewhereUsable(near) {
51208
+ const column = this.currentColumn(near);
50953
51209
  if (column) return column;
50954
51210
  const doc = this.builder.doc;
50955
51211
 
@@ -52122,6 +52378,18 @@ class Util$1 {
52122
52378
  // A toolbar button that opens a dialog often moves the focus first, so the
52123
52379
  // strip fills this in from the button actually used (toolbarkeys.js).
52124
52380
 
52381
+ // Opened from a control inside the content iframe: this page never saw
52382
+ // that mouse click, so the browser would ring the dialog's first control
52383
+ // as if the keyboard had been used. Carry the opener's own state over.
52384
+ let focusOpts;
52385
+ if (opener && opener.tagName === 'IFRAME') {
52386
+ try {
52387
+ const inner = opener.contentDocument.activeElement;
52388
+ if (!(inner && inner.matches(':focus-visible'))) focusOpts = {
52389
+ focusVisible: false
52390
+ };
52391
+ } catch (e) {/* another origin */}
52392
+ }
52125
52393
  if (overflowHidden) document.body.style.overflow = 'hidden';
52126
52394
  const handleKeyDown = e => {
52127
52395
  if (e.keyCode === 27) {
@@ -52139,11 +52407,11 @@ class Util$1 {
52139
52407
  modal.addEventListener('keydown', handleKeyDown);
52140
52408
  let content = modal.querySelector('.is-tab-content.active');
52141
52409
  if (content) {
52142
- content.focus();
52410
+ content.focus(focusOpts);
52143
52411
  this.builder.tabs.setupTabKey(content);
52144
52412
  } else {
52145
52413
  this.setupTabKeys(modal);
52146
- modal.focus();
52414
+ modal.focus(focusOpts);
52147
52415
  }
52148
52416
 
52149
52417
  // Land on a control rather than on the dialog itself: focus on the dialog
@@ -52163,7 +52431,7 @@ class Util$1 {
52163
52431
  const settled = modal.contains(held) && held !== modal && held.tagName !== 'IFRAME' && !held.classList.contains('is-tab-content');
52164
52432
  if (settled) return;
52165
52433
  const first = ToolbarKeys.firstControl(modal);
52166
- if (first) first.focus();
52434
+ if (first) first.focus(focusOpts);
52167
52435
  }, 0);
52168
52436
 
52169
52437
  // A dialog whose content is an iframe, the asset picker, is a trap without
@@ -54192,6 +54460,7 @@ class Util$1 {
54192
54460
  * Placeholders substituted by export():
54193
54461
  * [%CONTENT%] the content HTML
54194
54462
  * [%PLUGINS%] runtime plugin registry (see getPluginRegistry)
54463
+ * [%PLUGINFOLDER%] runtime plugin folder (see getPluginFolder)
54195
54464
  * [%PATH%] base URL for runtime/asset references
54196
54465
  */
54197
54466
  getPageTemplate(framework) {
@@ -54229,7 +54498,8 @@ class Util$1 {
54229
54498
  // Registers available plugins (not yet loaded).
54230
54499
  // Scripts and styles are fetched only when the plugin is actually used in content.
54231
54500
  const runtime = new ContentBuilderRuntime({
54232
- plugins: [%PLUGINS%]
54501
+ plugins: [%PLUGINS%],
54502
+ pluginFolder: [%PLUGINFOLDER%]
54233
54503
  });
54234
54504
  runtime.init();
54235
54505
  runtime.renderPageCss('.container');
@@ -54279,7 +54549,8 @@ class Util$1 {
54279
54549
  // Registers available plugins (not yet loaded).
54280
54550
  // Scripts and styles are fetched only when the plugin is actually used in content.
54281
54551
  const runtime = new ContentBuilderRuntime({
54282
- plugins: [%PLUGINS%]
54552
+ plugins: [%PLUGINS%],
54553
+ pluginFolder: [%PLUGINFOLDER%]
54283
54554
  });
54284
54555
  runtime.init();
54285
54556
  </script>
@@ -54332,7 +54603,8 @@ class Util$1 {
54332
54603
  // Registers available plugins (not yet loaded).
54333
54604
  // Scripts and styles are fetched only when the plugin is actually used in content.
54334
54605
  const runtime = new ContentBuilderRuntime({
54335
- plugins: [%PLUGINS%]
54606
+ plugins: [%PLUGINS%],
54607
+ pluginFolder: [%PLUGINFOLDER%]
54336
54608
  });
54337
54609
  runtime.init();
54338
54610
  </script>
@@ -54390,7 +54662,8 @@ class Util$1 {
54390
54662
  // Registers available plugins (not yet loaded).
54391
54663
  // Scripts and styles are fetched only when the plugin is actually used in content.
54392
54664
  const runtime = new ContentBuilderRuntime({
54393
- plugins: [%PLUGINS%]
54665
+ plugins: [%PLUGINS%],
54666
+ pluginFolder: [%PLUGINFOLDER%]
54394
54667
  });
54395
54668
  runtime.init();
54396
54669
  </script>
@@ -54442,7 +54715,8 @@ class Util$1 {
54442
54715
  // Registers available plugins (not yet loaded).
54443
54716
  // Scripts and styles are fetched only when the plugin is actually used in content.
54444
54717
  const runtime = new ContentBuilderRuntime({
54445
- plugins: [%PLUGINS%]
54718
+ plugins: [%PLUGINS%],
54719
+ pluginFolder: [%PLUGINFOLDER%]
54446
54720
  });
54447
54721
  runtime.init();
54448
54722
  </script>
@@ -54496,6 +54770,27 @@ class Util$1 {
54496
54770
  });
54497
54771
  return JSON.stringify(registry, null, 4);
54498
54772
  }
54773
+
54774
+ /*
54775
+ * The runtime's plugin folder (pluginFolder in blank.html), as a JS string.
54776
+ * Plugins used in the content load from it without being registered, so an
54777
+ * exported page keeps them. A URL under the content frame's base becomes a
54778
+ * [%PATH%] token, like the registry's URLs.
54779
+ */
54780
+ getPluginFolder() {
54781
+ const win = this.builder.win;
54782
+ let folder = '';
54783
+ try {
54784
+ folder = win.builderRuntime.settings.pluginFolder || '';
54785
+ } catch (e) {/* no runtime in the content frame */}
54786
+ let base = '';
54787
+ try {
54788
+ const href = win.location.href;
54789
+ base = href.substring(0, href.lastIndexOf('/'));
54790
+ } catch (e) {/* cross-origin content frame */}
54791
+ if (base && folder.startsWith(base + '/')) folder = '[%PATH%]' + folder.substring(base.length);
54792
+ return JSON.stringify(folder);
54793
+ }
54499
54794
  getElementPosition(element) {
54500
54795
  const iframe = this.builder.iframe;
54501
54796
  let top, left;
@@ -54707,6 +55002,46 @@ class Util$1 {
54707
55002
  }
54708
55003
  `;
54709
55004
  }
55005
+
55006
+ // Place a small tool (link/button tool) above its target. When the target sits at the
55007
+ // top edge with no room above, put the tool beside it (right, then left), so it never
55008
+ // lands on the element tool below the element. Below is the last resort.
55009
+ // vTarget (optional, e.g. a selected icon inside a link) sets the vertical position.
55010
+ // The tool must be displayed before this (its size is measured).
55011
+ placeToolAbove(tool, target, vTarget) {
55012
+ const hr = target.getBoundingClientRect();
55013
+ const vr = (vTarget || target).getBoundingClientRect();
55014
+ tool.style.top = vr.top + this.builder.win.pageYOffset - tool.offsetHeight - 3 + 'px';
55015
+ tool.style.left = hr.left + target.offsetWidth * this.builder.opts.zoom - tool.offsetWidth + 'px';
55016
+
55017
+ // Measured, not computed: the tool can carry margins and a scale.
55018
+ const t = tool.getBoundingClientRect();
55019
+ if (t.top >= 0) return;
55020
+ const moveTo = (left, top) => {
55021
+ const c = tool.getBoundingClientRect();
55022
+ tool.style.left = parseFloat(tool.style.left) + left - c.left + 'px';
55023
+ tool.style.top = parseFloat(tool.style.top) + top - c.top + 'px';
55024
+ };
55025
+
55026
+ // Horizontal room, kept clear of the row tool when it sits beside the target.
55027
+ let minX = 0;
55028
+ let maxX = this.builder.doc.documentElement.clientWidth;
55029
+ const row = target.closest('.row');
55030
+ const rowTool = row && row.querySelector(':scope > .is-row-tool');
55031
+ if (rowTool && rowTool.offsetParent) {
55032
+ const r = rowTool.getBoundingClientRect();
55033
+ if (r.left >= hr.right) maxX = Math.min(maxX, r.left);else if (r.right <= hr.left) minX = Math.max(minX, r.right);
55034
+ }
55035
+ const gap = 8;
55036
+ const sideTop = vr.top + vr.height / 2 - t.height / 2;
55037
+ if (hr.right + gap + t.width <= maxX) {
55038
+ moveTo(hr.right + gap, sideTop);
55039
+ } else if (hr.left - gap - t.width >= minX) {
55040
+ moveTo(hr.left - gap - t.width, sideTop);
55041
+ } else {
55042
+ moveTo(t.left, vr.bottom + gap);
55043
+ }
55044
+ }
54710
55045
  }
54711
55046
  class Dom {
54712
55047
  constructor(builder) {
@@ -77568,15 +77903,8 @@ class Hyperlink {
77568
77903
  }
77569
77904
  // /Button Stuff
77570
77905
 
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';
77906
+ linkTool.style.display = 'flex'; //to get the size, the tool must not be hidden (display:none)
77907
+ this.builder.util.placeToolAbove(linkTool, link, this.builder.activeIcon);
77580
77908
  const handleLinkButtonClick = e => {
77581
77909
  const clrPicker = document.querySelector('.pop-picker.active') || document.querySelector('.pickgradientcolor.active');
77582
77910
  let elm = e.target;
@@ -77875,13 +78203,8 @@ class Button {
77875
78203
  // selecting the button, so the pencil is a second route to the same
77876
78204
  // panel and is left out.
77877
78205
  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';
78206
+ buttonTool.style.display = 'flex'; //to get the size, the tool must not be hidden (display:none)
78207
+ this.builder.util.placeToolAbove(buttonTool, activeButton);
77885
78208
  }
77886
78209
  hideTool() {
77887
78210
  if (this.buttonTool) this.buttonTool.style.display = '';
@@ -79078,7 +79401,10 @@ class Plugin {
79078
79401
  const handlePluginClick = e => {
79079
79402
  const clrPicker = document.querySelector('.pop-picker.active') || document.querySelector('.pickgradientcolor.active');
79080
79403
  let elm = e.target;
79081
- let elmRemoved = !document.contains(elm); // e.g. when deleting a card in card list plugin
79404
+
79405
+ // isConnected, not document.contains(): in iframe mode the click is in
79406
+ // the content document, which this document never contains.
79407
+ let elmRemoved = !elm.isConnected; // e.g. when deleting a card in card list plugin
79082
79408
 
79083
79409
  if (this.builder.doc.activeElement) {
79084
79410
  if (this.builder.doc.activeElement.closest('.is-modal')) {
@@ -79276,7 +79602,9 @@ class Plugin {
79276
79602
  const plugin = e.target.closest('[data-cb-type]');
79277
79603
  if (plugin) {
79278
79604
  this.builder.activePlugin = plugin;
79279
- if (!this.builder.isContentBox) {
79605
+
79606
+ // With a control panel, plugin settings open there instead.
79607
+ if (!this.builder.controlPanel) {
79280
79608
  this.renderTool();
79281
79609
  this.pluginTool.style.display = 'flex';
79282
79610
  let _toolwidth = this.pluginTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
@@ -81552,6 +81880,10 @@ class Element {
81552
81880
  customcode = true;
81553
81881
  subblock = elm.closest('.is-subblock');
81554
81882
  }
81883
+ if (elm.closest('[data-cb-code]')) {
81884
+ // Independent code element: the whole block is the element
81885
+ customcode = true;
81886
+ }
81555
81887
  if (!customcode && !noedit && !_protected || subblock) {
81556
81888
  //previously this is commented: && !noedit && !_protected
81557
81889
 
@@ -103213,6 +103545,136 @@ function coverValue(cover, def) {
103213
103545
  return true;
103214
103546
  }
103215
103547
 
103548
+ /*
103549
+ normalizer.js — the clean-output guarantee.
103550
+
103551
+ Runs after every mutation. It never changes the visible text; it only
103552
+ reduces markup to the minimal equivalent form:
103553
+
103554
+ <strong>a</strong><strong>b</strong> -> <strong>ab</strong>
103555
+ <strong><strong>x</strong></strong> -> <strong>x</strong>
103556
+ <span style="color:red"><span style="font-size:18px">x</span></span>
103557
+ -> one span, two properties
103558
+ <span>x</span> -> x
103559
+ <em></em> -> (removed)
103560
+
103561
+ Because it preserves text content exactly, character-offset selection
103562
+ bookmarks taken before normalization remain valid after it.
103563
+ */
103564
+ 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']);
103565
+
103566
+ /* Identity signature: two inline elements with equal signatures are
103567
+ interchangeable and can be merged. */
103568
+ function sig(el) {
103569
+ const attrs = [];
103570
+ for (const a of el.attributes) {
103571
+ attrs.push(a.name === 'style' ? 'style=' + el.style.cssText : a.name + '=' + a.value);
103572
+ }
103573
+ return el.tagName + '|' + attrs.sort().join(';');
103574
+ }
103575
+ function isEmptyInline(el) {
103576
+ if (el.textContent !== '' || el.querySelector(KEEPERS)) return false;
103577
+ // An empty inline that carries a class typically renders its content via CSS
103578
+ // rather than text — an icon font (<i class="bi bi-alarm">, <span
103579
+ // class="material-icons">…</span>, fa-*, glyphicon). It is NOT truly empty,
103580
+ // so never drop it. Bare leftover marks (<em></em>, <i></i>) have no class
103581
+ // and are still removed.
103582
+ if (el.getAttribute('class')) return false;
103583
+ return true;
103584
+ }
103585
+
103586
+ /* Does the page's CSS style this attribute-less span? A design can colour or
103587
+ size "the span in this heading" from a stylesheet (.hero h1 span {...});
103588
+ unwrapping it then changes how the text looks, which this file promises
103589
+ never to do. Compared against the parent for inherited properties and
103590
+ against the initial value for the rest. Without a live view (detached, or
103591
+ a headless test) it is treated as unstyled, as before. */
103592
+ const INHERITED = ['color', 'fontSize', 'fontWeight', 'fontStyle', 'fontFamily', 'letterSpacing', 'textTransform', 'lineHeight'];
103593
+ function styledByCss(el) {
103594
+ const parent = el.parentElement;
103595
+ const win = el.ownerDocument && el.ownerDocument.defaultView;
103596
+ if (!parent || !win || !el.isConnected || typeof win.getComputedStyle !== 'function') return false;
103597
+ const cs = win.getComputedStyle(el);
103598
+ const ps = win.getComputedStyle(parent);
103599
+ for (const p of INHERITED) if (cs[p] !== ps[p]) return true;
103600
+ if (cs.display && cs.display !== 'inline') return true;
103601
+ if (cs.backgroundImage && cs.backgroundImage !== 'none') return true;
103602
+ if (cs.backgroundColor && !/^(transparent|rgba\(0, 0, 0, 0\))$/.test(cs.backgroundColor)) return true;
103603
+ if (cs.textDecorationLine && cs.textDecorationLine !== 'none') return true;
103604
+ return false;
103605
+ }
103606
+
103607
+ /* One bottom-up cleanup pass. Returns true if anything changed. */
103608
+ function pass(scope) {
103609
+ let changed = false;
103610
+ const els = Array.prototype.slice.call(scope.querySelectorAll('*')).reverse();
103611
+ for (const el of els) {
103612
+ if (!el.isConnected && !scope.contains(el)) continue;
103613
+ if (el.closest && el.closest('[contenteditable="false"]')) continue;
103614
+ if (!INLINE_TAGS.has(el.tagName)) continue;
103615
+
103616
+ /* 1. Drop empty inline elements */
103617
+ if (isEmptyInline(el)) {
103618
+ el.remove();
103619
+ changed = true;
103620
+ continue;
103621
+ }
103622
+
103623
+ /* 2. Unwrap attribute-less spans (unless the page's CSS styles them) */
103624
+ if (el.tagName === 'SPAN' && el.attributes.length === 0 && !styledByCss(el)) {
103625
+ unwrap(el);
103626
+ changed = true;
103627
+ continue;
103628
+ }
103629
+
103630
+ /* 3. Unwrap when an identical ancestor already covers this element */
103631
+ let anc = el.parentNode;
103632
+ let redundant = false;
103633
+ while (isEl$1(anc) && anc !== scope && INLINE_TAGS.has(anc.tagName)) {
103634
+ if (sig(anc) === sig(el)) {
103635
+ redundant = true;
103636
+ break;
103637
+ }
103638
+ anc = anc.parentNode;
103639
+ }
103640
+ if (redundant) {
103641
+ unwrap(el);
103642
+ changed = true;
103643
+ continue;
103644
+ }
103645
+
103646
+ /* 4. Merge with an identical previous sibling */
103647
+ const prev = el.previousSibling;
103648
+ if (isEl$1(prev) && sig(prev) === sig(el)) {
103649
+ while (el.firstChild) prev.appendChild(el.firstChild);
103650
+ el.remove();
103651
+ changed = true;
103652
+ continue;
103653
+ }
103654
+
103655
+ /* 5. Collapse span-in-span single-child chains into one span.
103656
+ The inner (more specific) properties win on conflict. */
103657
+ if (el.tagName === 'SPAN' && el.childNodes.length === 1 && isEl$1(el.firstChild) && el.firstChild.tagName === 'SPAN') {
103658
+ const inner = el.firstChild;
103659
+ for (let i = 0; i < inner.style.length; i++) {
103660
+ const prop = inner.style[i];
103661
+ el.style.setProperty(prop, inner.style.getPropertyValue(prop));
103662
+ }
103663
+ inner.classList.forEach(c => el.classList.add(c));
103664
+ if (!el.getAttribute('style')) el.removeAttribute('style');
103665
+ unwrap(inner);
103666
+ changed = true;
103667
+ }
103668
+ }
103669
+ return changed;
103670
+ }
103671
+ function normalize(scope) {
103672
+ let guard = 0;
103673
+ while (pass(scope) && guard++ < 6) {/* repeat until stable */}
103674
+ scope.normalize(); /* merge adjacent text nodes */
103675
+ return scope;
103676
+ }
103677
+
103216
103678
  /*
103217
103679
  formatter.js — inline formatting engine.
103218
103680
 
@@ -103479,7 +103941,9 @@ function strip(el, def) {
103479
103941
  unwrap(el);
103480
103942
  return;
103481
103943
  }
103482
- if (el.tagName === 'SPAN' && el.attributes.length === 0) unwrap(el);
103944
+ /* Left bare: unwrap, unless the page's CSS styles it (a design's
103945
+ ".hero h1 span" colour), which must survive clearing a size from it. */
103946
+ if (el.tagName === 'SPAN' && el.attributes.length === 0 && !styledByCss(el)) unwrap(el);
103483
103947
  }
103484
103948
 
103485
103949
  /* Apply a style across a (non-collapsed) range. */
@@ -103544,115 +104008,6 @@ function removeStyle(root, range, def, opts) {
103544
104008
  return texts;
103545
104009
  }
103546
104010
 
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
104011
  /*
103657
104012
  probe.js — non-mutating state inspection (queryCommandState done right).
103658
104013
 
@@ -144123,11 +144478,14 @@ class ContentBuilder {
144123
144478
  this.shortenOutput = false;
144124
144479
  output = pageTemplate.replace('[%CONTENT%]', html);
144125
144480
 
144126
- // [%PLUGINS%] - runtime plugin registry (must run before [%PATH%],
144127
- // the registry's URLs are emitted as [%PATH%] tokens)
144481
+ // [%PLUGINS%], [%PLUGINFOLDER%] - runtime plugin registry and folder (must run
144482
+ // before [%PATH%], their URLs are emitted as [%PATH%] tokens)
144128
144483
  if (output.indexOf('[%PLUGINS%]') !== -1) {
144129
144484
  output = output.replaceAll('[%PLUGINS%]', this.util.getPluginRegistry());
144130
144485
  }
144486
+ if (output.indexOf('[%PLUGINFOLDER%]') !== -1) {
144487
+ output = output.replaceAll('[%PLUGINFOLDER%]', this.util.getPluginFolder());
144488
+ }
144131
144489
 
144132
144490
  // Custom
144133
144491
  if (options.replace) {
@@ -146867,6 +147225,9 @@ Please obtain a license at: https://innovastudio.com/contentbox`);
146867
147225
  });
146868
147226
  tmp.querySelectorAll('*').forEach(el => {
146869
147227
  el.removeAttribute('style');
147228
+ // Text direction is the destination's too: Word marks every
147229
+ // paragraph and run dir="RTL"/"LTR" in right-to-left documents.
147230
+ el.removeAttribute('dir');
146870
147231
  if (el.classList && el.classList.length) {
146871
147232
  const kept = Array.prototype.filter.call(el.classList, c => keep.has(c));
146872
147233
  if (kept.length) el.setAttribute('class', kept.join(' '));else el.removeAttribute('class');
@@ -151889,6 +152250,7 @@ class Util {
151889
152250
  * [%MAINCSS%] page-level <style>
151890
152251
  * [%SECTIONCSS%] per-section <style>
151891
152252
  * [%PLUGINS%] runtime plugin registry (see getPluginRegistry)
152253
+ * [%PLUGINFOLDER%] runtime plugin folder (see getPluginFolder)
151892
152254
  * [%PATH%] base URL for runtime/asset references
151893
152255
  */
151894
152256
 
@@ -151923,7 +152285,8 @@ class Util {
151923
152285
  // Registers available plugins (not yet loaded).
151924
152286
  // Scripts and styles are fetched only when the plugin is actually used in content.
151925
152287
  const libraryInstance = new ContentBoxRuntime({
151926
- plugins: [%PLUGINS%]
152288
+ plugins: [%PLUGINS%],
152289
+ pluginFolder: [%PLUGINFOLDER%]
151927
152290
  });
151928
152291
  libraryInstance.init();
151929
152292
  </script>
@@ -151962,7 +152325,8 @@ class Util {
151962
152325
  // Registers available plugins (not yet loaded).
151963
152326
  // Scripts and styles are fetched only when the plugin is actually used in content.
151964
152327
  const libraryInstance = new ContentBoxRuntime({
151965
- plugins: [%PLUGINS%]
152328
+ plugins: [%PLUGINS%],
152329
+ pluginFolder: [%PLUGINFOLDER%]
151966
152330
  });
151967
152331
  libraryInstance.init();
151968
152332
  </script>
@@ -151999,7 +152363,8 @@ class Util {
151999
152363
  // Registers available plugins (not yet loaded).
152000
152364
  // Scripts and styles are fetched only when the plugin is actually used in content.
152001
152365
  const libraryInstance = new ContentBoxRuntime({
152002
- plugins: [%PLUGINS%]
152366
+ plugins: [%PLUGINS%],
152367
+ pluginFolder: [%PLUGINFOLDER%]
152003
152368
  });
152004
152369
  libraryInstance.init();
152005
152370
  </script>
@@ -152037,7 +152402,8 @@ class Util {
152037
152402
  // Registers available plugins (not yet loaded).
152038
152403
  // Scripts and styles are fetched only when the plugin is actually used in content.
152039
152404
  const libraryInstance = new ContentBoxRuntime({
152040
- plugins: [%PLUGINS%]
152405
+ plugins: [%PLUGINS%],
152406
+ pluginFolder: [%PLUGINFOLDER%]
152041
152407
  });
152042
152408
  libraryInstance.init();
152043
152409
  </script>
@@ -152102,6 +152468,36 @@ class Util {
152102
152468
  });
152103
152469
  return JSON.stringify(registry, null, 4);
152104
152470
  }
152471
+ /*
152472
+ * The runtime's plugin folder (pluginFolder in blank.html), as a JS string.
152473
+ * Plugins used in the content load from it without being registered, so an
152474
+ * exported page keeps them. A URL under the content frame's base becomes a
152475
+ * [%PATH%] token, like the registry's URLs.
152476
+ */
152477
+
152478
+
152479
+ getPluginFolder() {
152480
+ const win = this.builder.win;
152481
+ let folder = '';
152482
+
152483
+ try {
152484
+ folder = win.builderRuntime.settings.pluginFolder || '';
152485
+ } catch (e) {
152486
+ /* no runtime in the content frame */
152487
+ }
152488
+
152489
+ let base = '';
152490
+
152491
+ try {
152492
+ const href = win.location.href;
152493
+ base = href.substring(0, href.lastIndexOf('/'));
152494
+ } catch (e) {
152495
+ /* cross-origin content frame */
152496
+ }
152497
+
152498
+ if (base && folder.startsWith(base + '/')) folder = '[%PATH%]' + folder.substring(base.length);
152499
+ return JSON.stringify(folder);
152500
+ }
152105
152501
 
152106
152502
  }
152107
152503
 
@@ -166555,10 +166951,6 @@ class IframePanel {
166555
166951
  let screenMode = localStorage.getItem('_screenmode');
166556
166952
 
166557
166953
  if (localStorage.getItem('_screenmode')) {
166558
- if (!this.builder.controlPanel && this.builder.iframeSrc) {
166559
- if (screenMode === 'fullview') screenMode = 'desktop';
166560
- }
166561
-
166562
166954
  this.builder.screenMode = screenMode;
166563
166955
  contentView.classList.add(screenMode);
166564
166956
  } else {
@@ -166646,8 +167038,22 @@ class IframePanel {
166646
167038
  // addBreakpoint) and authored tablet values while the user believed they were
166647
167039
  // authoring desktop. Scaling fullview the same way the device modes do puts the
166648
167040
  // viewport back in the desktop band, so no control needs to know about this.
167041
+ // The topOffset setting in pixels. It is a CSS length ('55px', '3rem', 48), the same
167042
+ // value the topspace CSS uses, so a unit other than px is resolved, not read as px.
166649
167043
 
166650
167044
 
167045
+ topOffsetPx() {
167046
+ const value = this.builder.topOffset;
167047
+ if (value === undefined || value === null || value === '') return 55;
167048
+ if (typeof value === 'number') return value;
167049
+ const probe = document.createElement('div');
167050
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;height:' + value;
167051
+ document.body.appendChild(probe);
167052
+ const px = probe.getBoundingClientRect().height;
167053
+ probe.remove();
167054
+ return px || parseInt(value) || 55;
167055
+ }
167056
+
166651
167057
  applyFullviewScale() {
166652
167058
  const contentView = this.contentView;
166653
167059
  if (!contentView || !this.editPanel) return;
@@ -166683,7 +167089,7 @@ class IframePanel {
166683
167089
 
166684
167090
 
166685
167091
  const hasTopSpace = this.builder.topSpace || document.body.classList.contains('topspace');
166686
- const topOffset = hasTopSpace ? parseInt(this.builder.topOffset) || 55 : 0;
167092
+ const topOffset = hasTopSpace ? this.topOffsetPx() : 0;
166687
167093
  const scale = availableWidth / logicalWidth;
166688
167094
  const availableHeight = this.editPanel.clientHeight - topOffset;
166689
167095
  contentView.style.setProperty('--fullview-top', `${topOffset}px`);
@@ -166758,7 +167164,7 @@ class IframePanel {
166758
167164
 
166759
167165
 
166760
167166
  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
167167
+ const topOffset = hasTopSpace ? this.topOffsetPx() : 0; // A manual width drag writes an inline width and leaves the mode class alone, so
166762
167168
  // that width is the frame the user asked for. Reading it is safe: it is an input
166763
167169
  // here, never something this method writes.
166764
167170
 
@@ -175921,6 +176327,12 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
175921
176327
  outline-offset: -1px;
175922
176328
  }
175923
176329
 
176330
+ /* A faded-out overlay layer covering visible content, under the pointer.
176331
+ Clicks go through it. See ControlPanel.bindLayerPassThrough. */
176332
+ .is-wrapper [data-overlay-passthrough] {
176333
+ pointer-events: none !important;
176334
+ }
176335
+
175924
176336
  /* Hovering a row of the panel's layer list. Locates the element while
175925
176337
  leaving the selection outline alone. */
175926
176338
  .is-wrapper [data-overlay-hover] {
@@ -175931,13 +176343,20 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
175931
176343
  /* An overlay layer is edited through the control panel, not in place, so
175932
176344
  a text I-beam here would promise a caret that never arrives. Regions
175933
176345
  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} * {
176346
+ which enableEditableRegions makes contentEditable) keep it. Tool
176347
+ buttons and .is-container islands (normal in-place content) are left
176348
+ alone. :where() keeps this rule at the lowest specificity so it never
176349
+ overrides another cursor rule. */
176350
+ .is-wrapper :where(${this.overlayEditableSelector},
176351
+ ${this.overlayEditableSelector} *:not(.is-tool, .is-tool *, .is-container, .is-container *)) {
175937
176352
  cursor: default;
175938
176353
  }
175939
- .is-wrapper ${this.overlayEditableSelector} [contenteditable="true"],
175940
- .is-wrapper ${this.overlayEditableSelector} [contenteditable="true"] * {
176354
+ /* cursor is inherited: give the containers back the normal behaviour */
176355
+ .is-wrapper :where(${this.overlayEditableSelector} .is-container) {
176356
+ cursor: auto;
176357
+ }
176358
+ .is-wrapper :where(${this.overlayEditableSelector} [contenteditable="true"]:not(.is-container, .is-container *),
176359
+ ${this.overlayEditableSelector} [contenteditable="true"]:not(.is-container, .is-container *) *) {
175941
176360
  cursor: text;
175942
176361
  }
175943
176362
 
@@ -176542,6 +176961,9 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
176542
176961
  }
176543
176962
 
176544
176963
  i.bi, i.icon { display: inline-flex; }
176964
+ /* As a flex box an icon ignores Bootstrap Icons' own vertical-align (on its ::before) and
176965
+ sits higher than the text beside it. Put the same offset on the icon itself. */
176966
+ i.bi { vertical-align: -.125em; }
176545
176967
 
176546
176968
  </style>`); // i.bi, i.icon { display: inline-flex; } => this makes icons more cursor friendly
176547
176969
 
@@ -178086,12 +178508,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
178086
178508
  }
178087
178509
  e.stopImmediatePropagation();
178088
178510
  */
178089
- const answer = window.confirm(out('Do you want to open the link?'));
178090
-
178091
- if (answer) {
178092
- window.open(url, '_blank');
178093
- }
178094
-
178511
+ // Editing: a click selects the link, it never leaves the page
178095
178512
  e.preventDefault();
178096
178513
  }
178097
178514
  }
@@ -178700,15 +179117,70 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
178700
179117
  newArea = doc.body.innerHTML;
178701
179118
  this.editor.saveForUndo();
178702
179119
  const d = new Date();
178703
- newArea = newArea.replace('[%YEAR%]', d.getFullYear());
179120
+ newArea = newArea.replace('[%YEAR%]', d.getFullYear()); // A navigation bar is a section that IS one, not any HTML that mentions the
179121
+ // class (a section's own CSS may style itself relative to a bar above it).
179122
+
179123
+ 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,
179124
+ // locked): it is the page's navigation, so a design's own bar is left out
179125
+ // and the rest of the design (a header) goes in first, under the menu.
179126
+
179127
+
179128
+ const lockedTop = Array.from(this.wrapperEl.children).filter(el => el.hasAttribute('data-top-locked'));
178704
179129
 
178705
- if (newArea.indexOf('is-section-navbar') !== -1 || newArea.indexOf('data-cb-type="nav-menu"') !== -1) {
179130
+ if (lockedTop.length && Array.from(doc.body.children).some(isNavbar)) {
179131
+ const tpl = document.createElement('template');
179132
+ tpl.innerHTML = newArea; // The bar's text colour (the colour its design gives it over the header).
179133
+
179134
+ let barInk = '';
179135
+ Array.from(tpl.content.children).filter(isNavbar).forEach(el => {
179136
+ const m = /--bar-color:\s*var\(--bar-ink,\s*(rgba?\([^)]*\)|#[0-9a-fA-F]{3,8})\s*\)/.exec(el.outerHTML);
179137
+ if (m && !barInk) barInk = m[1];
179138
+ el.remove();
179139
+ }); // The header was made with a bar floating over its top: say so, so the
179140
+ // site's menu can float over it the same way, in the colour the header's
179141
+ // own bar used there.
179142
+
179143
+ const header = Array.from(tpl.content.children).find(el => el.classList.contains('is-section'));
179144
+
179145
+ if (header) {
179146
+ header.setAttribute('data-under-bar', '');
179147
+ if (barInk && !header.hasAttribute('data-bar-rest-ink')) header.setAttribute('data-bar-rest-ink', barInk);
179148
+ }
179149
+
179150
+ if (!tpl.content.children.length) {
179151
+ this.editor.util.confirm(out('This site\'s menu is managed in the menu settings.'), () => {}, false, out('OK'), true);
179152
+ return;
179153
+ }
179154
+
179155
+ dom.appendHtml(lockedTop[lockedTop.length - 1], tpl.innerHTML, 'afterend');
179156
+ } else if (Array.from(doc.body.children).some(isNavbar)) {
178706
179157
  // Insert first
178707
179158
  const sectionNavbar = this.wrapperEl.querySelector('.is-section-navbar');
178708
179159
 
178709
179160
  if (sectionNavbar) {
178710
- // let delConf = this.deleteConfirm;
179161
+ // A design that brings its own bar together with other sections (a header):
179162
+ // keep the page's bar and insert the rest, or replace the bar too.
179163
+ const tpl = document.createElement('template');
179164
+ tpl.innerHTML = newArea;
179165
+ const newBar = Array.from(tpl.content.children).find(isNavbar);
179166
+
179167
+ if (newBar && tpl.content.children.length > 1) {
179168
+ this.editor.util.confirm(out('This page already has a navigation bar.'), (keep, dismissed) => {
179169
+ if (dismissed) return; // closed without choosing
179170
+
179171
+ if (keep) {
179172
+ newBar.remove();
179173
+ this.addIdea(tpl.innerHTML);
179174
+ } else {
179175
+ sectionNavbar.parentNode.removeChild(sectionNavbar);
179176
+ this.addIdea(newArea);
179177
+ }
179178
+ }, true, out('Keep current'), true, 'Replace');
179179
+ return;
179180
+ } // let delConf = this.deleteConfirm;
178711
179181
  // this.editor.deleteConfirm = true;
179182
+
179183
+
178712
179184
  this.editor.util.confirm(out('Do you want to replace the current navigation bar?'), ok => {
178713
179185
  if (ok) {
178714
179186
  sectionNavbar.parentNode.removeChild(sectionNavbar);
@@ -178957,7 +179429,10 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
178957
179429
  // }
178958
179430
 
178959
179431
  if (lastNewSection) {
178960
- this.refreshSectionAnim(lastNewSection);
179432
+ // A template can bring several sections; a pinned one among them (not
179433
+ // only the last) needs the full re-render that sets up its pin.
179434
+ const anyPinned = Array.from(newSections).some(s => s.classList.contains('section-pin') || !!s.querySelector('.section-pin'));
179435
+ this.refreshSectionAnim(lastNewSection, anyPinned);
178961
179436
  this.scrollTo(lastNewSection, 100);
178962
179437
  }
178963
179438
 
@@ -179223,15 +179698,19 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
179223
179698
  const btnSectionUp = sectionTool.querySelector('.btn-move-section-up');
179224
179699
  const btnSectionDown = sectionTool.querySelector('.btn-move-section-down');
179225
179700
  const btnSectionRemove = sectionTool.querySelector('.btn-section-remove');
179701
+ const btnSectionEdit = sectionTool.querySelector('.btn-section-edit');
179226
179702
 
179227
179703
  if (box.offsetHeight < 160) {
179704
+ // Short section (e.g. a nav bar): room for two buttons, HTML and Remove
179228
179705
  btnSectionUp.style.display = 'none';
179229
179706
  btnSectionDown.style.display = 'none';
179230
- btnSectionRemove.style.display = 'none';
179707
+ if (btnSectionEdit) btnSectionEdit.style.display = 'none';
179708
+ btnSectionRemove.style.display = '';
179231
179709
  sectionTool.style.top = '17px'; //'calc(50% - 17px)';
179232
179710
  } else {
179233
179711
  btnSectionUp.style.display = '';
179234
179712
  btnSectionDown.style.display = '';
179713
+ if (btnSectionEdit) btnSectionEdit.style.display = '';
179235
179714
  btnSectionRemove.style.display = '';
179236
179715
  }
179237
179716
  } //new box tool
@@ -179914,7 +180393,16 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
179914
180393
 
179915
180394
  this.activeBox = null;
179916
180395
  if (this.editor) this.editor.activeBox = null;
179917
- this.activeSection = null; // Wait for IFRAME loaded
180396
+ this.activeSection = null; // New content: whatever the control panel was showing belongs to the
180397
+ // old page (e.g. after Clear Content), so return it to its empty state.
180398
+
180399
+ if (this.controlPanel && this.controlpanel) this.controlpanel.select(''); // ...and so do the floating tools on the page (the element tool).
180400
+
180401
+ if (this.editor && this.editor.util && this.editor.doc) {
180402
+ this.editor.util.clearActiveElement();
180403
+ this.editor.util.hideControls();
180404
+ } // Wait for IFRAME loaded
180405
+
179918
180406
 
179919
180407
  const wrapper = this.wrapperEl;
179920
180408
 
@@ -180350,9 +180838,10 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
180350
180838
  // html.js attribute sweep: the attributes are ContentBox's, so the
180351
180839
  // cleanup belongs on this side.
180352
180840
 
180353
- clone.querySelectorAll('[data-overlay-select], [data-overlay-hover]').forEach(elm => {
180841
+ clone.querySelectorAll('[data-overlay-select], [data-overlay-hover], [data-overlay-passthrough]').forEach(elm => {
180354
180842
  elm.removeAttribute('data-overlay-select');
180355
180843
  elm.removeAttribute('data-overlay-hover');
180844
+ elm.removeAttribute('data-overlay-passthrough');
180356
180845
  });
180357
180846
  }
180358
180847
 
@@ -180823,11 +181312,15 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
180823
181312
  let mainCss = this.mainCss(true);
180824
181313
  let sectionCss = this.sectionCss(true);
180825
181314
  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)
181315
+ output = output.replaceAll('[%SECTIONCSS%]', sectionCss); // [%PLUGINS%], [%PLUGINFOLDER%] - runtime plugin registry and folder (must run
181316
+ // before [%PATH%], their URLs are emitted as [%PATH%] tokens)
180828
181317
 
180829
181318
  if (output.indexOf('[%PLUGINS%]') !== -1) {
180830
181319
  output = output.replaceAll('[%PLUGINS%]', this.util.getPluginRegistry());
181320
+ }
181321
+
181322
+ if (output.indexOf('[%PLUGINFOLDER%]') !== -1) {
181323
+ output = output.replaceAll('[%PLUGINFOLDER%]', this.util.getPluginFolder());
180831
181324
  } // Custom
180832
181325
 
180833
181326