@innovastudio/contentbuilder 1.5.221 → 1.5.223
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.
package/package.json
CHANGED
|
@@ -4977,6 +4977,12 @@ i.bi, i.icon {
|
|
|
4977
4977
|
display: inline-flex;
|
|
4978
4978
|
}
|
|
4979
4979
|
|
|
4980
|
+
/* As a flex box an icon ignores Bootstrap Icons' own vertical-align (on its ::before) and sits
|
|
4981
|
+
higher than the text beside it in the editor. Put the same offset on the icon itself. */
|
|
4982
|
+
.is-container i.bi {
|
|
4983
|
+
vertical-align: -0.125em;
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4980
4986
|
/* block tool */
|
|
4981
4987
|
/*
|
|
4982
4988
|
.is-block-tool {
|
|
@@ -5591,6 +5591,10 @@ class ToolbarKeys {
|
|
|
5591
5591
|
*/
|
|
5592
5592
|
_hold(container, control) {
|
|
5593
5593
|
if (!control) return;
|
|
5594
|
+
|
|
5595
|
+
// Where the control was: if the command removes what it sat on, the caret
|
|
5596
|
+
// goes to what took its place
|
|
5597
|
+
const spot = (control.closest('.row, [data-cb-block]') || control).getBoundingClientRect();
|
|
5594
5598
|
setTimeout(() => {
|
|
5595
5599
|
// Not bailing when the container has gone: deleting the last column
|
|
5596
5600
|
// takes the strip with it, and that is exactly when the focus needs
|
|
@@ -5648,8 +5652,10 @@ class ToolbarKeys {
|
|
|
5648
5652
|
// A tick later again: emptying an area makes the editor put its
|
|
5649
5653
|
// own button there, and that has not happened yet.
|
|
5650
5654
|
setTimeout(() => {
|
|
5651
|
-
const somewhere = this._somewhereUsable();
|
|
5652
|
-
if (somewhere) somewhere.focus(
|
|
5655
|
+
const somewhere = this._somewhereUsable(spot);
|
|
5656
|
+
if (somewhere) somewhere.focus({
|
|
5657
|
+
preventScroll: true
|
|
5658
|
+
});
|
|
5653
5659
|
}, 30);
|
|
5654
5660
|
}
|
|
5655
5661
|
}, 0);
|
|
@@ -5720,7 +5726,9 @@ class ToolbarKeys {
|
|
|
5720
5726
|
// the trip to the toolbar after one button. Stay on it, so the next format
|
|
5721
5727
|
// is a key away, and leave deliberately with Escape. Anything that claims
|
|
5722
5728
|
// the focus for itself, a panel or a dialog, is left to do so.
|
|
5729
|
+
// A mouse click (detail > 0) is left alone, so the mouse works as it always did.
|
|
5723
5730
|
tool.addEventListener('click', e => {
|
|
5731
|
+
if (e.detail > 0) return;
|
|
5724
5732
|
const control = this._controlFrom(e, tool, BUTTONS);
|
|
5725
5733
|
if (control) this._hold(tool, control);
|
|
5726
5734
|
});
|
|
@@ -5862,13 +5870,26 @@ class ToolbarKeys {
|
|
|
5862
5870
|
* go dead with the focus stuck on a strip. Falls back through what the editor
|
|
5863
5871
|
* still calls active, then to the first column on the page.
|
|
5864
5872
|
*/
|
|
5865
|
-
currentColumn() {
|
|
5873
|
+
currentColumn(near) {
|
|
5866
5874
|
// It has to be able to hold the focus. A locked column is still the active
|
|
5867
5875
|
// one but cannot take it, and focusing it silently does nothing.
|
|
5868
5876
|
const usable = el => el && el.isConnected && (el.isContentEditable || el.hasAttribute('tabindex')) ? el : null;
|
|
5869
5877
|
const active = usable(this.builder.util.cellSelected()) || usable(this.builder.activeCol);
|
|
5870
5878
|
if (active) return active;
|
|
5871
5879
|
const rows = this.builder.doc.querySelectorAll(`${this.builder.opts.container} > *`);
|
|
5880
|
+
|
|
5881
|
+
// Near a place on screen (where a removed row was): the column that took its
|
|
5882
|
+
// place, or the last one before it
|
|
5883
|
+
if (near) {
|
|
5884
|
+
let before = null;
|
|
5885
|
+
for (const row of rows) {
|
|
5886
|
+
const cell = ToolbarKeys.cells(row).find(usable);
|
|
5887
|
+
if (!cell) continue;
|
|
5888
|
+
if (cell.getBoundingClientRect().top >= near.top - 1) return cell;
|
|
5889
|
+
before = cell;
|
|
5890
|
+
}
|
|
5891
|
+
if (before) return before;
|
|
5892
|
+
}
|
|
5872
5893
|
for (const row of rows) {
|
|
5873
5894
|
const cell = ToolbarKeys.cells(row).find(usable);
|
|
5874
5895
|
if (cell) return cell;
|
|
@@ -5882,8 +5903,8 @@ class ToolbarKeys {
|
|
|
5882
5903
|
* editing away: in both the focus would otherwise land on the body, where no
|
|
5883
5904
|
* key reaches the editor at all.
|
|
5884
5905
|
*/
|
|
5885
|
-
_somewhereUsable() {
|
|
5886
|
-
const column = this.currentColumn();
|
|
5906
|
+
_somewhereUsable(near) {
|
|
5907
|
+
const column = this.currentColumn(near);
|
|
5887
5908
|
if (column) return column;
|
|
5888
5909
|
const doc = this.builder.doc;
|
|
5889
5910
|
|
|
@@ -7056,6 +7077,18 @@ class Util {
|
|
|
7056
7077
|
// A toolbar button that opens a dialog often moves the focus first, so the
|
|
7057
7078
|
// strip fills this in from the button actually used (toolbarkeys.js).
|
|
7058
7079
|
|
|
7080
|
+
// Opened from a control inside the content iframe: this page never saw
|
|
7081
|
+
// that mouse click, so the browser would ring the dialog's first control
|
|
7082
|
+
// as if the keyboard had been used. Carry the opener's own state over.
|
|
7083
|
+
let focusOpts;
|
|
7084
|
+
if (opener && opener.tagName === 'IFRAME') {
|
|
7085
|
+
try {
|
|
7086
|
+
const inner = opener.contentDocument.activeElement;
|
|
7087
|
+
if (!(inner && inner.matches(':focus-visible'))) focusOpts = {
|
|
7088
|
+
focusVisible: false
|
|
7089
|
+
};
|
|
7090
|
+
} catch (e) {/* another origin */}
|
|
7091
|
+
}
|
|
7059
7092
|
if (overflowHidden) document.body.style.overflow = 'hidden';
|
|
7060
7093
|
const handleKeyDown = e => {
|
|
7061
7094
|
if (e.keyCode === 27) {
|
|
@@ -7073,11 +7106,11 @@ class Util {
|
|
|
7073
7106
|
modal.addEventListener('keydown', handleKeyDown);
|
|
7074
7107
|
let content = modal.querySelector('.is-tab-content.active');
|
|
7075
7108
|
if (content) {
|
|
7076
|
-
content.focus();
|
|
7109
|
+
content.focus(focusOpts);
|
|
7077
7110
|
this.builder.tabs.setupTabKey(content);
|
|
7078
7111
|
} else {
|
|
7079
7112
|
this.setupTabKeys(modal);
|
|
7080
|
-
modal.focus();
|
|
7113
|
+
modal.focus(focusOpts);
|
|
7081
7114
|
}
|
|
7082
7115
|
|
|
7083
7116
|
// Land on a control rather than on the dialog itself: focus on the dialog
|
|
@@ -7097,7 +7130,7 @@ class Util {
|
|
|
7097
7130
|
const settled = modal.contains(held) && held !== modal && held.tagName !== 'IFRAME' && !held.classList.contains('is-tab-content');
|
|
7098
7131
|
if (settled) return;
|
|
7099
7132
|
const first = ToolbarKeys.firstControl(modal);
|
|
7100
|
-
if (first) first.focus();
|
|
7133
|
+
if (first) first.focus(focusOpts);
|
|
7101
7134
|
}, 0);
|
|
7102
7135
|
|
|
7103
7136
|
// A dialog whose content is an iframe, the asset picker, is a trap without
|
|
@@ -9126,6 +9159,7 @@ class Util {
|
|
|
9126
9159
|
* Placeholders substituted by export():
|
|
9127
9160
|
* [%CONTENT%] the content HTML
|
|
9128
9161
|
* [%PLUGINS%] runtime plugin registry (see getPluginRegistry)
|
|
9162
|
+
* [%PLUGINFOLDER%] runtime plugin folder (see getPluginFolder)
|
|
9129
9163
|
* [%PATH%] base URL for runtime/asset references
|
|
9130
9164
|
*/
|
|
9131
9165
|
getPageTemplate(framework) {
|
|
@@ -9163,7 +9197,8 @@ class Util {
|
|
|
9163
9197
|
// Registers available plugins (not yet loaded).
|
|
9164
9198
|
// Scripts and styles are fetched only when the plugin is actually used in content.
|
|
9165
9199
|
const runtime = new ContentBuilderRuntime({
|
|
9166
|
-
plugins: [%PLUGINS%]
|
|
9200
|
+
plugins: [%PLUGINS%],
|
|
9201
|
+
pluginFolder: [%PLUGINFOLDER%]
|
|
9167
9202
|
});
|
|
9168
9203
|
runtime.init();
|
|
9169
9204
|
runtime.renderPageCss('.container');
|
|
@@ -9213,7 +9248,8 @@ class Util {
|
|
|
9213
9248
|
// Registers available plugins (not yet loaded).
|
|
9214
9249
|
// Scripts and styles are fetched only when the plugin is actually used in content.
|
|
9215
9250
|
const runtime = new ContentBuilderRuntime({
|
|
9216
|
-
plugins: [%PLUGINS%]
|
|
9251
|
+
plugins: [%PLUGINS%],
|
|
9252
|
+
pluginFolder: [%PLUGINFOLDER%]
|
|
9217
9253
|
});
|
|
9218
9254
|
runtime.init();
|
|
9219
9255
|
</script>
|
|
@@ -9266,7 +9302,8 @@ class Util {
|
|
|
9266
9302
|
// Registers available plugins (not yet loaded).
|
|
9267
9303
|
// Scripts and styles are fetched only when the plugin is actually used in content.
|
|
9268
9304
|
const runtime = new ContentBuilderRuntime({
|
|
9269
|
-
plugins: [%PLUGINS%]
|
|
9305
|
+
plugins: [%PLUGINS%],
|
|
9306
|
+
pluginFolder: [%PLUGINFOLDER%]
|
|
9270
9307
|
});
|
|
9271
9308
|
runtime.init();
|
|
9272
9309
|
</script>
|
|
@@ -9324,7 +9361,8 @@ class Util {
|
|
|
9324
9361
|
// Registers available plugins (not yet loaded).
|
|
9325
9362
|
// Scripts and styles are fetched only when the plugin is actually used in content.
|
|
9326
9363
|
const runtime = new ContentBuilderRuntime({
|
|
9327
|
-
plugins: [%PLUGINS%]
|
|
9364
|
+
plugins: [%PLUGINS%],
|
|
9365
|
+
pluginFolder: [%PLUGINFOLDER%]
|
|
9328
9366
|
});
|
|
9329
9367
|
runtime.init();
|
|
9330
9368
|
</script>
|
|
@@ -9376,7 +9414,8 @@ class Util {
|
|
|
9376
9414
|
// Registers available plugins (not yet loaded).
|
|
9377
9415
|
// Scripts and styles are fetched only when the plugin is actually used in content.
|
|
9378
9416
|
const runtime = new ContentBuilderRuntime({
|
|
9379
|
-
plugins: [%PLUGINS%]
|
|
9417
|
+
plugins: [%PLUGINS%],
|
|
9418
|
+
pluginFolder: [%PLUGINFOLDER%]
|
|
9380
9419
|
});
|
|
9381
9420
|
runtime.init();
|
|
9382
9421
|
</script>
|
|
@@ -9430,6 +9469,27 @@ class Util {
|
|
|
9430
9469
|
});
|
|
9431
9470
|
return JSON.stringify(registry, null, 4);
|
|
9432
9471
|
}
|
|
9472
|
+
|
|
9473
|
+
/*
|
|
9474
|
+
* The runtime's plugin folder (pluginFolder in blank.html), as a JS string.
|
|
9475
|
+
* Plugins used in the content load from it without being registered, so an
|
|
9476
|
+
* exported page keeps them. A URL under the content frame's base becomes a
|
|
9477
|
+
* [%PATH%] token, like the registry's URLs.
|
|
9478
|
+
*/
|
|
9479
|
+
getPluginFolder() {
|
|
9480
|
+
const win = this.builder.win;
|
|
9481
|
+
let folder = '';
|
|
9482
|
+
try {
|
|
9483
|
+
folder = win.builderRuntime.settings.pluginFolder || '';
|
|
9484
|
+
} catch (e) {/* no runtime in the content frame */}
|
|
9485
|
+
let base = '';
|
|
9486
|
+
try {
|
|
9487
|
+
const href = win.location.href;
|
|
9488
|
+
base = href.substring(0, href.lastIndexOf('/'));
|
|
9489
|
+
} catch (e) {/* cross-origin content frame */}
|
|
9490
|
+
if (base && folder.startsWith(base + '/')) folder = '[%PATH%]' + folder.substring(base.length);
|
|
9491
|
+
return JSON.stringify(folder);
|
|
9492
|
+
}
|
|
9433
9493
|
getElementPosition(element) {
|
|
9434
9494
|
const iframe = this.builder.iframe;
|
|
9435
9495
|
let top, left;
|
|
@@ -9641,6 +9701,46 @@ class Util {
|
|
|
9641
9701
|
}
|
|
9642
9702
|
`;
|
|
9643
9703
|
}
|
|
9704
|
+
|
|
9705
|
+
// Place a small tool (link/button tool) above its target. When the target sits at the
|
|
9706
|
+
// top edge with no room above, put the tool beside it (right, then left), so it never
|
|
9707
|
+
// lands on the element tool below the element. Below is the last resort.
|
|
9708
|
+
// vTarget (optional, e.g. a selected icon inside a link) sets the vertical position.
|
|
9709
|
+
// The tool must be displayed before this (its size is measured).
|
|
9710
|
+
placeToolAbove(tool, target, vTarget) {
|
|
9711
|
+
const hr = target.getBoundingClientRect();
|
|
9712
|
+
const vr = (vTarget || target).getBoundingClientRect();
|
|
9713
|
+
tool.style.top = vr.top + this.builder.win.pageYOffset - tool.offsetHeight - 3 + 'px';
|
|
9714
|
+
tool.style.left = hr.left + target.offsetWidth * this.builder.opts.zoom - tool.offsetWidth + 'px';
|
|
9715
|
+
|
|
9716
|
+
// Measured, not computed: the tool can carry margins and a scale.
|
|
9717
|
+
const t = tool.getBoundingClientRect();
|
|
9718
|
+
if (t.top >= 0) return;
|
|
9719
|
+
const moveTo = (left, top) => {
|
|
9720
|
+
const c = tool.getBoundingClientRect();
|
|
9721
|
+
tool.style.left = parseFloat(tool.style.left) + left - c.left + 'px';
|
|
9722
|
+
tool.style.top = parseFloat(tool.style.top) + top - c.top + 'px';
|
|
9723
|
+
};
|
|
9724
|
+
|
|
9725
|
+
// Horizontal room, kept clear of the row tool when it sits beside the target.
|
|
9726
|
+
let minX = 0;
|
|
9727
|
+
let maxX = this.builder.doc.documentElement.clientWidth;
|
|
9728
|
+
const row = target.closest('.row');
|
|
9729
|
+
const rowTool = row && row.querySelector(':scope > .is-row-tool');
|
|
9730
|
+
if (rowTool && rowTool.offsetParent) {
|
|
9731
|
+
const r = rowTool.getBoundingClientRect();
|
|
9732
|
+
if (r.left >= hr.right) maxX = Math.min(maxX, r.left);else if (r.right <= hr.left) minX = Math.max(minX, r.right);
|
|
9733
|
+
}
|
|
9734
|
+
const gap = 8;
|
|
9735
|
+
const sideTop = vr.top + vr.height / 2 - t.height / 2;
|
|
9736
|
+
if (hr.right + gap + t.width <= maxX) {
|
|
9737
|
+
moveTo(hr.right + gap, sideTop);
|
|
9738
|
+
} else if (hr.left - gap - t.width >= minX) {
|
|
9739
|
+
moveTo(hr.left - gap - t.width, sideTop);
|
|
9740
|
+
} else {
|
|
9741
|
+
moveTo(t.left, vr.bottom + gap);
|
|
9742
|
+
}
|
|
9743
|
+
}
|
|
9644
9744
|
}
|
|
9645
9745
|
class Dom {
|
|
9646
9746
|
constructor(builder) {
|
|
@@ -32502,15 +32602,8 @@ class Hyperlink {
|
|
|
32502
32602
|
}
|
|
32503
32603
|
// /Button Stuff
|
|
32504
32604
|
|
|
32505
|
-
|
|
32506
|
-
|
|
32507
|
-
if (this.builder.activeIcon) top = this.builder.activeIcon.getBoundingClientRect().top + this.builder.win.pageYOffset;
|
|
32508
|
-
left = link.getBoundingClientRect().left;
|
|
32509
|
-
linkTool.style.display = 'flex';
|
|
32510
|
-
const w = linkTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
|
|
32511
|
-
const h = linkTool.offsetHeight;
|
|
32512
|
-
linkTool.style.top = top - h - 3 + 'px';
|
|
32513
|
-
linkTool.style.left = left + link.offsetWidth * this.builder.opts.zoom - w + 'px';
|
|
32605
|
+
linkTool.style.display = 'flex'; //to get the size, the tool must not be hidden (display:none)
|
|
32606
|
+
this.builder.util.placeToolAbove(linkTool, link, this.builder.activeIcon);
|
|
32514
32607
|
const handleLinkButtonClick = e => {
|
|
32515
32608
|
const clrPicker = document.querySelector('.pop-picker.active') || document.querySelector('.pickgradientcolor.active');
|
|
32516
32609
|
let elm = e.target;
|
|
@@ -32809,13 +32902,8 @@ class Button {
|
|
|
32809
32902
|
// selecting the button, so the pencil is a second route to the same
|
|
32810
32903
|
// panel and is left out.
|
|
32811
32904
|
buttonTool.querySelector('.button-edit').style.display = this.builder.opts.onButtonEdit ? 'none' : '';
|
|
32812
|
-
|
|
32813
|
-
|
|
32814
|
-
buttonTool.style.display = 'flex';
|
|
32815
|
-
const w = buttonTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
|
|
32816
|
-
const h = buttonTool.offsetHeight;
|
|
32817
|
-
buttonTool.style.top = top - h - 3 + 'px';
|
|
32818
|
-
buttonTool.style.left = left + activeButton.offsetWidth * this.builder.opts.zoom - w + 'px';
|
|
32905
|
+
buttonTool.style.display = 'flex'; //to get the size, the tool must not be hidden (display:none)
|
|
32906
|
+
this.builder.util.placeToolAbove(buttonTool, activeButton);
|
|
32819
32907
|
}
|
|
32820
32908
|
hideTool() {
|
|
32821
32909
|
if (this.buttonTool) this.buttonTool.style.display = '';
|
|
@@ -34012,7 +34100,10 @@ class Plugin {
|
|
|
34012
34100
|
const handlePluginClick = e => {
|
|
34013
34101
|
const clrPicker = document.querySelector('.pop-picker.active') || document.querySelector('.pickgradientcolor.active');
|
|
34014
34102
|
let elm = e.target;
|
|
34015
|
-
|
|
34103
|
+
|
|
34104
|
+
// isConnected, not document.contains(): in iframe mode the click is in
|
|
34105
|
+
// the content document, which this document never contains.
|
|
34106
|
+
let elmRemoved = !elm.isConnected; // e.g. when deleting a card in card list plugin
|
|
34016
34107
|
|
|
34017
34108
|
if (this.builder.doc.activeElement) {
|
|
34018
34109
|
if (this.builder.doc.activeElement.closest('.is-modal')) {
|
|
@@ -34210,7 +34301,9 @@ class Plugin {
|
|
|
34210
34301
|
const plugin = e.target.closest('[data-cb-type]');
|
|
34211
34302
|
if (plugin) {
|
|
34212
34303
|
this.builder.activePlugin = plugin;
|
|
34213
|
-
|
|
34304
|
+
|
|
34305
|
+
// With a control panel, plugin settings open there instead.
|
|
34306
|
+
if (!this.builder.controlPanel) {
|
|
34214
34307
|
this.renderTool();
|
|
34215
34308
|
this.pluginTool.style.display = 'flex';
|
|
34216
34309
|
let _toolwidth = this.pluginTool.offsetWidth; //to get value, element must not hidden (display:none). So set display:flex before this.
|
|
@@ -36486,6 +36579,10 @@ class Element {
|
|
|
36486
36579
|
customcode = true;
|
|
36487
36580
|
subblock = elm.closest('.is-subblock');
|
|
36488
36581
|
}
|
|
36582
|
+
if (elm.closest('[data-cb-code]')) {
|
|
36583
|
+
// Independent code element: the whole block is the element
|
|
36584
|
+
customcode = true;
|
|
36585
|
+
}
|
|
36489
36586
|
if (!customcode && !noedit && !_protected || subblock) {
|
|
36490
36587
|
//previously this is commented: && !noedit && !_protected
|
|
36491
36588
|
|
|
@@ -58147,6 +58244,136 @@ function coverValue(cover, def) {
|
|
|
58147
58244
|
return true;
|
|
58148
58245
|
}
|
|
58149
58246
|
|
|
58247
|
+
/*
|
|
58248
|
+
normalizer.js — the clean-output guarantee.
|
|
58249
|
+
|
|
58250
|
+
Runs after every mutation. It never changes the visible text; it only
|
|
58251
|
+
reduces markup to the minimal equivalent form:
|
|
58252
|
+
|
|
58253
|
+
<strong>a</strong><strong>b</strong> -> <strong>ab</strong>
|
|
58254
|
+
<strong><strong>x</strong></strong> -> <strong>x</strong>
|
|
58255
|
+
<span style="color:red"><span style="font-size:18px">x</span></span>
|
|
58256
|
+
-> one span, two properties
|
|
58257
|
+
<span>x</span> -> x
|
|
58258
|
+
<em></em> -> (removed)
|
|
58259
|
+
|
|
58260
|
+
Because it preserves text content exactly, character-offset selection
|
|
58261
|
+
bookmarks taken before normalization remain valid after it.
|
|
58262
|
+
*/
|
|
58263
|
+
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']);
|
|
58264
|
+
|
|
58265
|
+
/* Identity signature: two inline elements with equal signatures are
|
|
58266
|
+
interchangeable and can be merged. */
|
|
58267
|
+
function sig(el) {
|
|
58268
|
+
const attrs = [];
|
|
58269
|
+
for (const a of el.attributes) {
|
|
58270
|
+
attrs.push(a.name === 'style' ? 'style=' + el.style.cssText : a.name + '=' + a.value);
|
|
58271
|
+
}
|
|
58272
|
+
return el.tagName + '|' + attrs.sort().join(';');
|
|
58273
|
+
}
|
|
58274
|
+
function isEmptyInline(el) {
|
|
58275
|
+
if (el.textContent !== '' || el.querySelector(KEEPERS)) return false;
|
|
58276
|
+
// An empty inline that carries a class typically renders its content via CSS
|
|
58277
|
+
// rather than text — an icon font (<i class="bi bi-alarm">, <span
|
|
58278
|
+
// class="material-icons">…</span>, fa-*, glyphicon). It is NOT truly empty,
|
|
58279
|
+
// so never drop it. Bare leftover marks (<em></em>, <i></i>) have no class
|
|
58280
|
+
// and are still removed.
|
|
58281
|
+
if (el.getAttribute('class')) return false;
|
|
58282
|
+
return true;
|
|
58283
|
+
}
|
|
58284
|
+
|
|
58285
|
+
/* Does the page's CSS style this attribute-less span? A design can colour or
|
|
58286
|
+
size "the span in this heading" from a stylesheet (.hero h1 span {...});
|
|
58287
|
+
unwrapping it then changes how the text looks, which this file promises
|
|
58288
|
+
never to do. Compared against the parent for inherited properties and
|
|
58289
|
+
against the initial value for the rest. Without a live view (detached, or
|
|
58290
|
+
a headless test) it is treated as unstyled, as before. */
|
|
58291
|
+
const INHERITED = ['color', 'fontSize', 'fontWeight', 'fontStyle', 'fontFamily', 'letterSpacing', 'textTransform', 'lineHeight'];
|
|
58292
|
+
function styledByCss(el) {
|
|
58293
|
+
const parent = el.parentElement;
|
|
58294
|
+
const win = el.ownerDocument && el.ownerDocument.defaultView;
|
|
58295
|
+
if (!parent || !win || !el.isConnected || typeof win.getComputedStyle !== 'function') return false;
|
|
58296
|
+
const cs = win.getComputedStyle(el);
|
|
58297
|
+
const ps = win.getComputedStyle(parent);
|
|
58298
|
+
for (const p of INHERITED) if (cs[p] !== ps[p]) return true;
|
|
58299
|
+
if (cs.display && cs.display !== 'inline') return true;
|
|
58300
|
+
if (cs.backgroundImage && cs.backgroundImage !== 'none') return true;
|
|
58301
|
+
if (cs.backgroundColor && !/^(transparent|rgba\(0, 0, 0, 0\))$/.test(cs.backgroundColor)) return true;
|
|
58302
|
+
if (cs.textDecorationLine && cs.textDecorationLine !== 'none') return true;
|
|
58303
|
+
return false;
|
|
58304
|
+
}
|
|
58305
|
+
|
|
58306
|
+
/* One bottom-up cleanup pass. Returns true if anything changed. */
|
|
58307
|
+
function pass(scope) {
|
|
58308
|
+
let changed = false;
|
|
58309
|
+
const els = Array.prototype.slice.call(scope.querySelectorAll('*')).reverse();
|
|
58310
|
+
for (const el of els) {
|
|
58311
|
+
if (!el.isConnected && !scope.contains(el)) continue;
|
|
58312
|
+
if (el.closest && el.closest('[contenteditable="false"]')) continue;
|
|
58313
|
+
if (!INLINE_TAGS.has(el.tagName)) continue;
|
|
58314
|
+
|
|
58315
|
+
/* 1. Drop empty inline elements */
|
|
58316
|
+
if (isEmptyInline(el)) {
|
|
58317
|
+
el.remove();
|
|
58318
|
+
changed = true;
|
|
58319
|
+
continue;
|
|
58320
|
+
}
|
|
58321
|
+
|
|
58322
|
+
/* 2. Unwrap attribute-less spans (unless the page's CSS styles them) */
|
|
58323
|
+
if (el.tagName === 'SPAN' && el.attributes.length === 0 && !styledByCss(el)) {
|
|
58324
|
+
unwrap(el);
|
|
58325
|
+
changed = true;
|
|
58326
|
+
continue;
|
|
58327
|
+
}
|
|
58328
|
+
|
|
58329
|
+
/* 3. Unwrap when an identical ancestor already covers this element */
|
|
58330
|
+
let anc = el.parentNode;
|
|
58331
|
+
let redundant = false;
|
|
58332
|
+
while (isEl$1(anc) && anc !== scope && INLINE_TAGS.has(anc.tagName)) {
|
|
58333
|
+
if (sig(anc) === sig(el)) {
|
|
58334
|
+
redundant = true;
|
|
58335
|
+
break;
|
|
58336
|
+
}
|
|
58337
|
+
anc = anc.parentNode;
|
|
58338
|
+
}
|
|
58339
|
+
if (redundant) {
|
|
58340
|
+
unwrap(el);
|
|
58341
|
+
changed = true;
|
|
58342
|
+
continue;
|
|
58343
|
+
}
|
|
58344
|
+
|
|
58345
|
+
/* 4. Merge with an identical previous sibling */
|
|
58346
|
+
const prev = el.previousSibling;
|
|
58347
|
+
if (isEl$1(prev) && sig(prev) === sig(el)) {
|
|
58348
|
+
while (el.firstChild) prev.appendChild(el.firstChild);
|
|
58349
|
+
el.remove();
|
|
58350
|
+
changed = true;
|
|
58351
|
+
continue;
|
|
58352
|
+
}
|
|
58353
|
+
|
|
58354
|
+
/* 5. Collapse span-in-span single-child chains into one span.
|
|
58355
|
+
The inner (more specific) properties win on conflict. */
|
|
58356
|
+
if (el.tagName === 'SPAN' && el.childNodes.length === 1 && isEl$1(el.firstChild) && el.firstChild.tagName === 'SPAN') {
|
|
58357
|
+
const inner = el.firstChild;
|
|
58358
|
+
for (let i = 0; i < inner.style.length; i++) {
|
|
58359
|
+
const prop = inner.style[i];
|
|
58360
|
+
el.style.setProperty(prop, inner.style.getPropertyValue(prop));
|
|
58361
|
+
}
|
|
58362
|
+
inner.classList.forEach(c => el.classList.add(c));
|
|
58363
|
+
if (!el.getAttribute('style')) el.removeAttribute('style');
|
|
58364
|
+
unwrap(inner);
|
|
58365
|
+
changed = true;
|
|
58366
|
+
}
|
|
58367
|
+
}
|
|
58368
|
+
return changed;
|
|
58369
|
+
}
|
|
58370
|
+
function normalize(scope) {
|
|
58371
|
+
let guard = 0;
|
|
58372
|
+
while (pass(scope) && guard++ < 6) {/* repeat until stable */}
|
|
58373
|
+
scope.normalize(); /* merge adjacent text nodes */
|
|
58374
|
+
return scope;
|
|
58375
|
+
}
|
|
58376
|
+
|
|
58150
58377
|
/*
|
|
58151
58378
|
formatter.js — inline formatting engine.
|
|
58152
58379
|
|
|
@@ -58413,7 +58640,9 @@ function strip(el, def) {
|
|
|
58413
58640
|
unwrap(el);
|
|
58414
58641
|
return;
|
|
58415
58642
|
}
|
|
58416
|
-
|
|
58643
|
+
/* Left bare: unwrap, unless the page's CSS styles it (a design's
|
|
58644
|
+
".hero h1 span" colour), which must survive clearing a size from it. */
|
|
58645
|
+
if (el.tagName === 'SPAN' && el.attributes.length === 0 && !styledByCss(el)) unwrap(el);
|
|
58417
58646
|
}
|
|
58418
58647
|
|
|
58419
58648
|
/* Apply a style across a (non-collapsed) range. */
|
|
@@ -58478,115 +58707,6 @@ function removeStyle(root, range, def, opts) {
|
|
|
58478
58707
|
return texts;
|
|
58479
58708
|
}
|
|
58480
58709
|
|
|
58481
|
-
/*
|
|
58482
|
-
normalizer.js — the clean-output guarantee.
|
|
58483
|
-
|
|
58484
|
-
Runs after every mutation. It never changes the visible text; it only
|
|
58485
|
-
reduces markup to the minimal equivalent form:
|
|
58486
|
-
|
|
58487
|
-
<strong>a</strong><strong>b</strong> -> <strong>ab</strong>
|
|
58488
|
-
<strong><strong>x</strong></strong> -> <strong>x</strong>
|
|
58489
|
-
<span style="color:red"><span style="font-size:18px">x</span></span>
|
|
58490
|
-
-> one span, two properties
|
|
58491
|
-
<span>x</span> -> x
|
|
58492
|
-
<em></em> -> (removed)
|
|
58493
|
-
|
|
58494
|
-
Because it preserves text content exactly, character-offset selection
|
|
58495
|
-
bookmarks taken before normalization remain valid after it.
|
|
58496
|
-
*/
|
|
58497
|
-
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']);
|
|
58498
|
-
|
|
58499
|
-
/* Identity signature: two inline elements with equal signatures are
|
|
58500
|
-
interchangeable and can be merged. */
|
|
58501
|
-
function sig(el) {
|
|
58502
|
-
const attrs = [];
|
|
58503
|
-
for (const a of el.attributes) {
|
|
58504
|
-
attrs.push(a.name === 'style' ? 'style=' + el.style.cssText : a.name + '=' + a.value);
|
|
58505
|
-
}
|
|
58506
|
-
return el.tagName + '|' + attrs.sort().join(';');
|
|
58507
|
-
}
|
|
58508
|
-
function isEmptyInline(el) {
|
|
58509
|
-
if (el.textContent !== '' || el.querySelector(KEEPERS)) return false;
|
|
58510
|
-
// An empty inline that carries a class typically renders its content via CSS
|
|
58511
|
-
// rather than text — an icon font (<i class="bi bi-alarm">, <span
|
|
58512
|
-
// class="material-icons">…</span>, fa-*, glyphicon). It is NOT truly empty,
|
|
58513
|
-
// so never drop it. Bare leftover marks (<em></em>, <i></i>) have no class
|
|
58514
|
-
// and are still removed.
|
|
58515
|
-
if (el.getAttribute('class')) return false;
|
|
58516
|
-
return true;
|
|
58517
|
-
}
|
|
58518
|
-
|
|
58519
|
-
/* One bottom-up cleanup pass. Returns true if anything changed. */
|
|
58520
|
-
function pass(scope) {
|
|
58521
|
-
let changed = false;
|
|
58522
|
-
const els = Array.prototype.slice.call(scope.querySelectorAll('*')).reverse();
|
|
58523
|
-
for (const el of els) {
|
|
58524
|
-
if (!el.isConnected && !scope.contains(el)) continue;
|
|
58525
|
-
if (el.closest && el.closest('[contenteditable="false"]')) continue;
|
|
58526
|
-
if (!INLINE_TAGS.has(el.tagName)) continue;
|
|
58527
|
-
|
|
58528
|
-
/* 1. Drop empty inline elements */
|
|
58529
|
-
if (isEmptyInline(el)) {
|
|
58530
|
-
el.remove();
|
|
58531
|
-
changed = true;
|
|
58532
|
-
continue;
|
|
58533
|
-
}
|
|
58534
|
-
|
|
58535
|
-
/* 2. Unwrap attribute-less spans */
|
|
58536
|
-
if (el.tagName === 'SPAN' && el.attributes.length === 0) {
|
|
58537
|
-
unwrap(el);
|
|
58538
|
-
changed = true;
|
|
58539
|
-
continue;
|
|
58540
|
-
}
|
|
58541
|
-
|
|
58542
|
-
/* 3. Unwrap when an identical ancestor already covers this element */
|
|
58543
|
-
let anc = el.parentNode;
|
|
58544
|
-
let redundant = false;
|
|
58545
|
-
while (isEl$1(anc) && anc !== scope && INLINE_TAGS.has(anc.tagName)) {
|
|
58546
|
-
if (sig(anc) === sig(el)) {
|
|
58547
|
-
redundant = true;
|
|
58548
|
-
break;
|
|
58549
|
-
}
|
|
58550
|
-
anc = anc.parentNode;
|
|
58551
|
-
}
|
|
58552
|
-
if (redundant) {
|
|
58553
|
-
unwrap(el);
|
|
58554
|
-
changed = true;
|
|
58555
|
-
continue;
|
|
58556
|
-
}
|
|
58557
|
-
|
|
58558
|
-
/* 4. Merge with an identical previous sibling */
|
|
58559
|
-
const prev = el.previousSibling;
|
|
58560
|
-
if (isEl$1(prev) && sig(prev) === sig(el)) {
|
|
58561
|
-
while (el.firstChild) prev.appendChild(el.firstChild);
|
|
58562
|
-
el.remove();
|
|
58563
|
-
changed = true;
|
|
58564
|
-
continue;
|
|
58565
|
-
}
|
|
58566
|
-
|
|
58567
|
-
/* 5. Collapse span-in-span single-child chains into one span.
|
|
58568
|
-
The inner (more specific) properties win on conflict. */
|
|
58569
|
-
if (el.tagName === 'SPAN' && el.childNodes.length === 1 && isEl$1(el.firstChild) && el.firstChild.tagName === 'SPAN') {
|
|
58570
|
-
const inner = el.firstChild;
|
|
58571
|
-
for (let i = 0; i < inner.style.length; i++) {
|
|
58572
|
-
const prop = inner.style[i];
|
|
58573
|
-
el.style.setProperty(prop, inner.style.getPropertyValue(prop));
|
|
58574
|
-
}
|
|
58575
|
-
inner.classList.forEach(c => el.classList.add(c));
|
|
58576
|
-
if (!el.getAttribute('style')) el.removeAttribute('style');
|
|
58577
|
-
unwrap(inner);
|
|
58578
|
-
changed = true;
|
|
58579
|
-
}
|
|
58580
|
-
}
|
|
58581
|
-
return changed;
|
|
58582
|
-
}
|
|
58583
|
-
function normalize(scope) {
|
|
58584
|
-
let guard = 0;
|
|
58585
|
-
while (pass(scope) && guard++ < 6) {/* repeat until stable */}
|
|
58586
|
-
scope.normalize(); /* merge adjacent text nodes */
|
|
58587
|
-
return scope;
|
|
58588
|
-
}
|
|
58589
|
-
|
|
58590
58710
|
/*
|
|
58591
58711
|
probe.js — non-mutating state inspection (queryCommandState done right).
|
|
58592
58712
|
|
|
@@ -99057,11 +99177,14 @@ class ContentBuilder {
|
|
|
99057
99177
|
this.shortenOutput = false;
|
|
99058
99178
|
output = pageTemplate.replace('[%CONTENT%]', html);
|
|
99059
99179
|
|
|
99060
|
-
// [%PLUGINS%] - runtime plugin registry (must run
|
|
99061
|
-
//
|
|
99180
|
+
// [%PLUGINS%], [%PLUGINFOLDER%] - runtime plugin registry and folder (must run
|
|
99181
|
+
// before [%PATH%], their URLs are emitted as [%PATH%] tokens)
|
|
99062
99182
|
if (output.indexOf('[%PLUGINS%]') !== -1) {
|
|
99063
99183
|
output = output.replaceAll('[%PLUGINS%]', this.util.getPluginRegistry());
|
|
99064
99184
|
}
|
|
99185
|
+
if (output.indexOf('[%PLUGINFOLDER%]') !== -1) {
|
|
99186
|
+
output = output.replaceAll('[%PLUGINFOLDER%]', this.util.getPluginFolder());
|
|
99187
|
+
}
|
|
99065
99188
|
|
|
99066
99189
|
// Custom
|
|
99067
99190
|
if (options.replace) {
|
|
@@ -101801,6 +101924,9 @@ Please obtain a license at: https://innovastudio.com/contentbox`);
|
|
|
101801
101924
|
});
|
|
101802
101925
|
tmp.querySelectorAll('*').forEach(el => {
|
|
101803
101926
|
el.removeAttribute('style');
|
|
101927
|
+
// Text direction is the destination's too: Word marks every
|
|
101928
|
+
// paragraph and run dir="RTL"/"LTR" in right-to-left documents.
|
|
101929
|
+
el.removeAttribute('dir');
|
|
101804
101930
|
if (el.classList && el.classList.length) {
|
|
101805
101931
|
const kept = Array.prototype.filter.call(el.classList, c => keep.has(c));
|
|
101806
101932
|
if (kept.length) el.setAttribute('class', kept.join(' '));else el.removeAttribute('class');
|