@innovastudio/contentbox 1.6.200 → 1.6.201

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.
@@ -11844,7 +11844,9 @@ class PanelText {
11844
11844
  <button title="${out('Subscript')}" data-format="subscript"><span style="font-family:serif;font-size:13px;">x</span><sub style="font-size:10px;margin-bottom:-6px;">2</sub></button>
11845
11845
  </div>
11846
11846
 
11847
- <div class="group flex">
11847
+ <!-- 4 to 6 buttons, depending on the symbols and searchReplace
11848
+ options. Named so the scss can wrap it at six. -->
11849
+ <div class="group flex inserts">
11848
11850
  <button title="${out('Link')}" class="btn-link"><svg style="transform:rotate(45deg)"><use xlink:href="#icon-link"></use></svg></button>
11849
11851
  <button title="${out('Image')}" class="btn-image"><svg><use xlink:href="#icon-image"></use></svg></button>
11850
11852
  <button title="${out('Icon')}" class="btn-icon"><svg><use xlink:href="#icon-mood-smile"></use></svg></button>
@@ -12449,7 +12451,7 @@ class PanelText {
12449
12451
  label: `<h6 style="margin:0;line-height:1.6;font-size:13px;">${out('Heading')} 6</h6>`
12450
12452
  }, {
12451
12453
  value: 'p',
12452
- label: `<p style="margin:2px 0 0;line-height:1.6;font-size:12px;">${out('Paragraph')}</p>`
12454
+ label: `<p style="margin:0;line-height:1.6;font-size:12px;">${out('Paragraph')}</p>`
12453
12455
  }, {
12454
12456
  value: 'div',
12455
12457
  label: '<div style="margin:0;line-height:1.6;font-size:12px;">div</div>'
@@ -17802,6 +17804,926 @@ class PanelPlugin {
17802
17804
 
17803
17805
  }
17804
17806
 
17807
+ /*
17808
+ codesource.js
17809
+ ------------------------------------------------------------------
17810
+ The two pure helpers ContentBox needs for [data-cb-code] blocks. Derived
17811
+ from ContentBuilder's codeelement.js, which also defines a CodeElement
17812
+ editor class — ContentBox never instantiates that, so it is not carried
17813
+ here. Named for what this file is, not for where it came from.
17814
+
17815
+ wrapCodeSource() is a string pre-pass. Call it on any HTML string about to
17816
+ be inserted with createContextualFragment (which executes scripts). It
17817
+ parses with DOMParser -- where scripts can never run -- and moves each
17818
+ [data-cb-code] block's source into an inert <template data-source>, so
17819
+ nothing has run by the time the content is in the DOM.
17820
+ */
17821
+ const ATTR$1 = 'data-cb-code';
17822
+ const SEL$1 = '[' + ATTR$1 + ']';
17823
+ const TPL_SEL$1 = ':scope > template[data-source]';
17824
+ function escapeCodeSource$1(src) {
17825
+ return src.replace(/<\/template>/gi, '<\\/template>');
17826
+ }
17827
+ /*
17828
+ STRING PRE-PASS (inert).
17829
+ Call this on any HTML *string* that is about to be inserted with
17830
+ createContextualFragment (which executes scripts). It parses the string with
17831
+ DOMParser -- where scripts can never run -- and moves each [data-cb-code]
17832
+ block's source into an inert <template data-source>. After insertion nothing
17833
+ has run; init()/render() brings them to life.
17834
+
17835
+ Exported standalone as well, so it can be used where no builder instance
17836
+ exists yet (ContentBox injects content before creating the editor).
17837
+ */
17838
+
17839
+ function wrapCodeSource$1(html) {
17840
+ if (!html) return html;
17841
+ if (html.indexOf(ATTR$1) === -1) return html; // fast path
17842
+
17843
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
17844
+ parsed.querySelectorAll(SEL$1).forEach(block => {
17845
+ // Already wrapped (round-trip within the editor) => skip
17846
+ if (block.querySelector(TPL_SEL$1)) return; // Snippet form (data-src) is already inert => handled by init()
17847
+
17848
+ if (block.hasAttribute('data-src')) return; // The block's innerHTML IS the pristine source (what flatten() wrote)
17849
+
17850
+ const src = block.innerHTML;
17851
+ block.innerHTML = '';
17852
+ const tpl = parsed.createElement('template');
17853
+ tpl.setAttribute('data-source', '');
17854
+ tpl.innerHTML = escapeCodeSource$1(src);
17855
+ block.appendChild(tpl);
17856
+ });
17857
+ return parsed.body.innerHTML;
17858
+ }
17859
+ /*
17860
+ SOURCE MAPPING (live render -> template source).
17861
+ ------------------------------------------------------------------
17862
+ A code element keeps two copies of itself: the <template data-source>,
17863
+ which is what gets saved, and the live render beside it, which is thrown
17864
+ away on every save. An edit written to the render alone therefore
17865
+ disappears the moment the page is saved.
17866
+
17867
+ These helpers find the node in the SOURCE that a live node was built from,
17868
+ so an edit can be written to both.
17869
+
17870
+ The mapping is positional: the render IS the source, parsed. A node's path
17871
+ of child indexes is the same in both -- except for <script> elements, which
17872
+ render() deletes once they have run, and the block's own <template>, which
17873
+ is never part of the render. Both are skipped on each side, so the two
17874
+ walks stay in step.
17875
+
17876
+ A node that the running code created or rewrote has no counterpart, or one
17877
+ holding something else. Those are NOT editable: the next render would
17878
+ overwrite the edit anyway. codeSourceEditable() is that test, and the
17879
+ panels use it to decide what they offer.
17880
+ */
17881
+ // The code element `el` belongs to, or null.
17882
+
17883
+ function codeElementOf(el) {
17884
+ if (!el || !el.closest) return null;
17885
+ return el.closest(SEL$1);
17886
+ } // Element children as the RENDER sees them.
17887
+
17888
+ function renderedChildren(parent) {
17889
+ return Array.prototype.filter.call(parent.children, child => {
17890
+ const tag = child.tagName.toLowerCase();
17891
+ if (tag === 'script') return false; // deleted after running
17892
+
17893
+ if (tag === 'template' && child.hasAttribute('data-source')) return false; // the source itself
17894
+
17895
+ return true;
17896
+ });
17897
+ } // The node in the block's source that this live node came from, or null.
17898
+
17899
+
17900
+ function codeSourceNodeFor(el) {
17901
+ const block = codeElementOf(el);
17902
+ if (!block || el === block) return null;
17903
+ const tpl = block.querySelector(TPL_SEL$1);
17904
+ if (!tpl || !tpl.content) return null; // Path from the block down to `el`.
17905
+
17906
+ const path = [];
17907
+
17908
+ for (let node = el; node && node !== block; node = node.parentElement) {
17909
+ const parent = node.parentElement;
17910
+ if (!parent) return null; // detached
17911
+
17912
+ const index = renderedChildren(parent).indexOf(node);
17913
+ if (index === -1) return null;
17914
+ path.unshift({
17915
+ index: index,
17916
+ tag: node.tagName
17917
+ });
17918
+ }
17919
+
17920
+ if (!path.length) return null; // The same walk through the source.
17921
+
17922
+ let node = tpl.content;
17923
+
17924
+ for (let i = 0; i < path.length; i++) {
17925
+ node = renderedChildren(node)[path[i].index];
17926
+ if (!node) return null;
17927
+ if (node.tagName !== path[i].tag) return null; // the code rebuilt this part
17928
+ }
17929
+
17930
+ return node;
17931
+ } // May an edit to this live node be written back to the source?
17932
+
17933
+ function codeSourceEditable(el) {
17934
+ const block = codeElementOf(el);
17935
+ if (!block) return true; // not a code element's business
17936
+
17937
+ if (el === block) return true; // the block itself is ordinary markup, saved as it is
17938
+
17939
+ const src = codeSourceNodeFor(el);
17940
+ if (!src) return false; // It must still hold what the source says it holds. Where the code rewrote
17941
+ // it (a clock's own text, a lazily swapped image) an edit here would be
17942
+ // undone by the next render, so it is not offered at all.
17943
+
17944
+ if (src.innerHTML !== el.innerHTML) return false;
17945
+ if (el.tagName === 'IMG' && src.getAttribute('src') !== el.getAttribute('src')) return false;
17946
+ return true;
17947
+ }
17948
+ /*
17949
+ Write an edit made to the live node into its source counterpart, so it
17950
+ survives the save. A no-op (returns false) for anything outside a code
17951
+ element, or for a node the code built itself.
17952
+
17953
+ `spec` names what to carry over, and only that:
17954
+ { html: true } the node's content
17955
+ { attrs: ['src','alt'] } those attributes
17956
+ { styles: ['background-image'] } those inline style properties
17957
+ { fx: true } every data-fx-* attribute
17958
+
17959
+ Deliberately never the whole element: a running script and the motion
17960
+ runtime both write to the live node (ids, aria state, inline transforms),
17961
+ and none of that belongs in the saved source.
17962
+ */
17963
+
17964
+ function syncToCodeSource(el, spec) {
17965
+ if (!el || !spec) return false;
17966
+ const src = codeSourceNodeFor(el);
17967
+ if (!src) return false;
17968
+ if (spec.html) src.innerHTML = el.innerHTML;
17969
+ if (spec.attrs) spec.attrs.forEach(name => {
17970
+ const value = el.getAttribute(name);
17971
+ if (value === null) src.removeAttribute(name);else src.setAttribute(name, value);
17972
+ });
17973
+ if (spec.styles) spec.styles.forEach(prop => {
17974
+ src.style.setProperty(prop, el.style.getPropertyValue(prop), el.style.getPropertyPriority(prop));
17975
+ });
17976
+
17977
+ if (spec.fx) {
17978
+ Array.prototype.slice.call(src.attributes).forEach(attr => {
17979
+ if (attr.name.indexOf('data-fx-') === 0) src.removeAttribute(attr.name);
17980
+ });
17981
+ Array.prototype.slice.call(el.attributes).forEach(attr => {
17982
+ if (attr.name.indexOf('data-fx-') === 0) src.setAttribute(attr.name, attr.value);
17983
+ });
17984
+ }
17985
+
17986
+ return true;
17987
+ }
17988
+
17989
+ /*
17990
+ * motion-presets.js — the declarative vocabulary of the Motion panel.
17991
+ *
17992
+ * Everything the Motion UI can apply lives here as DATA, so adding a new
17993
+ * effect is a table edit, never a code edit. Nothing in this file touches
17994
+ * the DOM or the builder.
17995
+ *
17996
+ * Three kinds of thing, because Kinetra genuinely has three:
17997
+ *
17998
+ * 1. RECIPES — canned percent keyframes (data-fx-0 … data-fx-100).
17999
+ * "Fade up", "Zoom in", "Wipe in". These are just a
18000
+ * starting set of keyframes; the Motion dock (phase 2)
18001
+ * edits them freely afterwards.
18002
+ * 2. PRESETS — single-attribute effects that are NOT keyframes:
18003
+ * parallax, horizontal, split, typewriter, draw, marquee.
18004
+ * Each is identified by one "key" attribute.
18005
+ * 3. POINTER — micro-interactions (tilt, magnetic). Not scene-bound
18006
+ * at all; they are per-element toggles.
18007
+ *
18008
+ * Option types:
18009
+ * 'text' — free text input, written verbatim to the attribute
18010
+ * 'select' — dropdown; value written to the attribute
18011
+ * 'toggle' — VALUELESS boolean attribute (present / absent)
18012
+ */
18013
+
18014
+ /* Ease names are NOT listed here. motion-ease.js derives them from kinetra's
18015
+ * exported `easings` table, so there is exactly one source of truth and a copy
18016
+ * can't drift when someone adds or renames a curve. */
18017
+
18018
+ /* ------------------------------------------------------------------
18019
+ * 1. KEYFRAME RECIPES
18020
+ *
18021
+ * `frames` maps percent -> declaration string. Applying a recipe REPLACES
18022
+ * every existing data-fx-N on the element (a recipe is a fresh start, not
18023
+ * an overlay), and sets data-fx-ease.
18024
+ *
18025
+ * Compound-string rule: both ends of a clip-path/box-shadow/etc. must share
18026
+ * the same token count, or Kinetra snaps instead of tweening. Every recipe
18027
+ * below already obeys that — keep it that way when adding new ones.
18028
+ * ------------------------------------------------------------------ */
18029
+ const RECIPES = [{
18030
+ id: 'fade',
18031
+ duration: 0.6,
18032
+ label: 'Fade in',
18033
+ ease: 'easeOut',
18034
+ frames: {
18035
+ 0: 'opacity:0',
18036
+ 100: 'opacity:1'
18037
+ }
18038
+ }, {
18039
+ id: 'fade-up',
18040
+ duration: 0.6,
18041
+ label: 'Fade up',
18042
+ ease: 'smooth',
18043
+ // kinetra's closest match to CSS ease-in-out
18044
+ frames: {
18045
+ 0: 'opacity:0; transform:translateY(70px)',
18046
+ // .is-fadeInUp was 70px
18047
+ 100: 'opacity:1; transform:translateY(0px)'
18048
+ }
18049
+ }, {
18050
+ id: 'fade-down',
18051
+ duration: 0.6,
18052
+ label: 'Fade down',
18053
+ ease: 'smooth',
18054
+ frames: {
18055
+ 0: 'opacity:0; transform:translateY(-70px)',
18056
+ 100: 'opacity:1; transform:translateY(0px)'
18057
+ }
18058
+ }, {
18059
+ id: 'fade-left',
18060
+ duration: 0.6,
18061
+ label: 'From left',
18062
+ ease: 'smooth',
18063
+ frames: {
18064
+ 0: 'opacity:0; transform:translateX(-70px)',
18065
+ 100: 'opacity:1; transform:translateX(0px)'
18066
+ }
18067
+ }, {
18068
+ id: 'fade-right',
18069
+ duration: 0.6,
18070
+ label: 'From right',
18071
+ ease: 'smooth',
18072
+ frames: {
18073
+ 0: 'opacity:0; transform:translateX(70px)',
18074
+ 100: 'opacity:1; transform:translateX(0px)'
18075
+ }
18076
+ }, {
18077
+ id: 'zoom-in',
18078
+ duration: 0.8,
18079
+ label: 'Zoom in',
18080
+ ease: 'smooth',
18081
+ frames: {
18082
+ 0: 'opacity:0; transform:scale(0.8)',
18083
+ // .is-zoomIn was scale(.8)
18084
+ 100: 'opacity:1; transform:scale(1)'
18085
+ }
18086
+ }, {
18087
+ id: 'zoom-out',
18088
+ duration: 0.8,
18089
+ label: 'Zoom out',
18090
+ ease: 'smooth',
18091
+ frames: {
18092
+ 0: 'opacity:0; transform:scale(1.2)',
18093
+ // .is-zoomOut was scale(1.2)
18094
+ 100: 'opacity:1; transform:scale(1)'
18095
+ }
18096
+ }, {
18097
+ id: 'slide-up',
18098
+ duration: 0.6,
18099
+ label: 'Slide up',
18100
+ ease: 'smooth',
18101
+ frames: {
18102
+ 0: 'transform:translateY(70px)',
18103
+ 100: 'transform:translateY(0px)'
18104
+ }
18105
+ }, {
18106
+ id: 'slide-down',
18107
+ duration: 0.6,
18108
+ label: 'Slide down',
18109
+ ease: 'smooth',
18110
+ frames: {
18111
+ 0: 'transform:translateY(-70px)',
18112
+ 100: 'transform:translateY(0px)'
18113
+ }
18114
+ }, {
18115
+ id: 'slide-left',
18116
+ duration: 0.6,
18117
+ label: 'Slide from left',
18118
+ ease: 'smooth',
18119
+ frames: {
18120
+ 0: 'transform:translateX(-70px)',
18121
+ 100: 'transform:translateX(0px)'
18122
+ }
18123
+ }, {
18124
+ id: 'slide-right',
18125
+ duration: 0.6,
18126
+ label: 'Slide from right',
18127
+ ease: 'smooth',
18128
+ frames: {
18129
+ 0: 'transform:translateX(70px)',
18130
+ 100: 'transform:translateX(0px)'
18131
+ }
18132
+ }, {
18133
+ id: 'blur-in',
18134
+ duration: 0.6,
18135
+ label: 'Blur in',
18136
+ ease: 'easeOut',
18137
+ frames: {
18138
+ 0: 'opacity:0; filter:blur(12px)',
18139
+ 100: 'opacity:1; filter:blur(0px)'
18140
+ }
18141
+ }, {
18142
+ id: 'flip-x',
18143
+ label: 'Flip X',
18144
+ duration: 0.8,
18145
+ ease: 'smooth',
18146
+ frames: {
18147
+ 0: 'opacity:0; transform:perspective(2500px) rotateX(100deg)',
18148
+ 100: 'opacity:1; transform:perspective(2500px) rotateX(0deg)'
18149
+ }
18150
+ }, {
18151
+ id: 'flip-y',
18152
+ label: 'Flip Y',
18153
+ duration: 0.8,
18154
+ ease: 'smooth',
18155
+ frames: {
18156
+ 0: 'opacity:0; transform:perspective(2500px) rotateY(100deg)',
18157
+ 100: 'opacity:1; transform:perspective(2500px) rotateY(0deg)'
18158
+ }
18159
+ }, {
18160
+ id: 'pulse',
18161
+ label: 'Pulse',
18162
+ duration: 0.6,
18163
+ ease: 'linear',
18164
+ frames: {
18165
+ 0: 'opacity:0; transform:scale(0.9)',
18166
+ 50: 'opacity:1; transform:scale(1.05)',
18167
+ 100: 'opacity:1; transform:scale(1)'
18168
+ }
18169
+ }, {
18170
+ id: 'bounce-in',
18171
+ label: 'Bounce in',
18172
+ duration: 0.6,
18173
+ ease: 'linear',
18174
+ frames: {
18175
+ 0: 'opacity:0; transform:translateX(0px)',
18176
+ 40: 'opacity:1; transform:translateX(-20px)',
18177
+ 60: 'opacity:1; transform:translateX(0px)',
18178
+ 80: 'opacity:1; transform:translateX(-15px)',
18179
+ 100: 'opacity:1; transform:translateX(0px)'
18180
+ }
18181
+ }, {
18182
+ id: 'wipe-right',
18183
+ duration: 0.6,
18184
+ label: 'Wipe in',
18185
+ ease: 'expoOut',
18186
+ frames: {
18187
+ 0: 'opacity:1; clip-path:inset(0% 100% 0% 0%)',
18188
+ 100: 'opacity:1; clip-path:inset(0% 0% 0% 0%)'
18189
+ }
18190
+ }, {
18191
+ id: 'rise-mask',
18192
+ duration: 0.7,
18193
+ label: 'Rise + mask',
18194
+ ease: 'expoOut',
18195
+ frames: {
18196
+ 0: 'opacity:0; transform:translateY(70px); clip-path:inset(100% 0% 0% 0%)',
18197
+ 100: 'opacity:1; transform:translateY(0px); clip-path:inset(0% 0% 0% 0%)'
18198
+ }
18199
+ }, {
18200
+ id: 'rotate-in',
18201
+ duration: 0.6,
18202
+ label: 'Rotate in',
18203
+ ease: 'backOut',
18204
+ frames: {
18205
+ 0: 'opacity:0; transform:rotate(-8deg) scale(0.94)',
18206
+ 100: 'opacity:1; transform:rotate(0deg) scale(1)'
18207
+ }
18208
+ }, {
18209
+ id: 'drift',
18210
+ duration: 0.6,
18211
+ label: 'Drift through',
18212
+ ease: 'linear',
18213
+ noCascade: true,
18214
+ // a through-motion, not an entrance — pointless in a cascade
18215
+ frames: {
18216
+ 0: 'transform:translateY(60px)',
18217
+ 100: 'transform:translateY(-60px)'
18218
+ }
18219
+ }, {
18220
+ id: 'hold-scale',
18221
+ duration: 0.6,
18222
+ label: 'Scale back + hold',
18223
+ ease: 'linear',
18224
+ note: 'Card-stack layer: shrinks as the next layer covers it.',
18225
+ frames: {
18226
+ 0: 'transform:scale(1)',
18227
+ 50: 'transform:scale(0.9)',
18228
+ 100: 'transform:scale(0.9)'
18229
+ }
18230
+ }];
18231
+ /* ------------------------------------------------------------------
18232
+ * 2. SCROLL / TEXT PRESETS (single-attribute effects)
18233
+ * ------------------------------------------------------------------ */
18234
+
18235
+ const PRESETS = [{
18236
+ id: 'parallax',
18237
+ label: 'Parallax',
18238
+ group: 'scroll',
18239
+ key: 'data-fx-parallax',
18240
+ defaults: {
18241
+ 'data-fx-parallax': '120'
18242
+ },
18243
+ hint: 'Drifts as it scrolls. Works in any section.',
18244
+ options: [{
18245
+ attr: 'data-fx-parallax',
18246
+ label: 'Travel',
18247
+ type: 'text',
18248
+ placeholder: '120 (px) or 20%'
18249
+ }]
18250
+ }, {
18251
+ id: 'horizontal',
18252
+ label: 'Horizontal track',
18253
+ group: 'scroll',
18254
+ key: 'data-fx-horizontal',
18255
+ pinnedOnly: true,
18256
+ defaults: {
18257
+ 'data-fx-horizontal': ''
18258
+ },
18259
+ hint: 'Vertical scroll drives a sideways track. Needs a pinned scene, and children with flex:0 0 auto.',
18260
+ options: []
18261
+ }, {
18262
+ id: 'split',
18263
+ label: 'Split text',
18264
+ group: 'text',
18265
+ key: 'data-fx-split',
18266
+ defaults: {
18267
+ 'data-fx-split': 'words'
18268
+ },
18269
+ hint: 'Staggered word/char entrance. Formatting inside the element is preserved.',
18270
+ options: [{
18271
+ attr: 'data-fx-split',
18272
+ label: 'Unit',
18273
+ type: 'select',
18274
+ choices: [['words', 'Words'], ['chars', 'Characters']]
18275
+ }, {
18276
+ attr: 'data-fx-split-mask',
18277
+ label: 'Mask reveal',
18278
+ type: 'toggle'
18279
+ }, {
18280
+ attr: 'data-fx-split-stagger',
18281
+ label: 'Stagger',
18282
+ type: 'text',
18283
+ placeholder: '0.08 (sec)'
18284
+ }, {
18285
+ attr: 'data-fx-split-y',
18286
+ label: 'Travel',
18287
+ type: 'text',
18288
+ placeholder: '115% or 18px'
18289
+ }, {
18290
+ attr: 'data-fx-split-scrub',
18291
+ label: 'Scrub through scene',
18292
+ type: 'toggle',
18293
+ hint: 'Best in a pinned scene — units brighten in reading order as you scroll.'
18294
+ }, {
18295
+ attr: 'data-fx-split-from',
18296
+ label: 'Dim opacity',
18297
+ type: 'text',
18298
+ placeholder: '0.12',
18299
+ requires: 'data-fx-split-scrub'
18300
+ }]
18301
+ }, {
18302
+ id: 'typewriter',
18303
+ label: 'Typewriter',
18304
+ group: 'text',
18305
+ key: 'data-fx-typewriter',
18306
+ defaults: {
18307
+ 'data-fx-typewriter': ''
18308
+ },
18309
+ hint: 'The scrollbar types the text. Plain text only.',
18310
+ options: [{
18311
+ attr: 'data-fx-typewriter-until',
18312
+ label: 'Done at',
18313
+ type: 'text',
18314
+ placeholder: '0.85 (progress)'
18315
+ }]
18316
+ }, {
18317
+ id: 'marquee',
18318
+ label: 'Marquee',
18319
+ group: 'text',
18320
+ key: 'data-fx-marquee',
18321
+ defaults: {
18322
+ 'data-fx-marquee': ''
18323
+ },
18324
+ hint: 'Drifting band. Content should be a repeating strip.',
18325
+ options: [{
18326
+ attr: 'data-fx-marquee-speed',
18327
+ label: 'Speed',
18328
+ type: 'text',
18329
+ placeholder: '90 (px/sec)'
18330
+ }, {
18331
+ attr: 'data-fx-marquee-lean',
18332
+ label: 'Lean with velocity',
18333
+ type: 'toggle'
18334
+ }]
18335
+ }, {
18336
+ id: 'path',
18337
+ label: 'Motion path',
18338
+ group: 'scroll',
18339
+ key: 'data-fx-path',
18340
+ defaults: {
18341
+ 'data-fx-path': '-200,60; 0,-80; 200,60'
18342
+ },
18343
+ hint: 'Travels a smooth curve through waypoints. Edit the stops in the Motion dock; px offsets from the element\'s resting position.',
18344
+ options: [{
18345
+ attr: 'data-fx-path',
18346
+ label: 'Waypoints',
18347
+ type: 'text',
18348
+ placeholder: 'x,y; x,y @tx,ty; …'
18349
+ }, {
18350
+ attr: 'data-fx-path-dwell',
18351
+ label: 'Dwell',
18352
+ type: 'text',
18353
+ placeholder: '0.78 (0 = constant, 1 = stops)'
18354
+ }, {
18355
+ attr: 'data-fx-path-curve',
18356
+ label: 'Curve',
18357
+ type: 'text',
18358
+ placeholder: '1 (0 = straight lines)'
18359
+ }, {
18360
+ attr: 'data-fx-path-rotate',
18361
+ label: 'Auto-rotate along path',
18362
+ type: 'toggle'
18363
+ }, {
18364
+ attr: 'data-fx-path-rotate',
18365
+ label: 'Rotate offset',
18366
+ type: 'text',
18367
+ placeholder: '0 (deg; art faces right 0, up 90)',
18368
+ requires: 'data-fx-path-rotate'
18369
+ }]
18370
+ }, {
18371
+ id: 'draw',
18372
+ label: 'Draw SVG',
18373
+ group: 'scroll',
18374
+ key: 'data-fx-draw',
18375
+ defaults: {
18376
+ 'data-fx-draw': ''
18377
+ },
18378
+ tags: ['svg', 'path'],
18379
+ hint: 'Paths draw themselves as the scene scrubs. Put on an <svg> or a <path>.',
18380
+ options: []
18381
+ }];
18382
+ /* ------------------------------------------------------------------
18383
+ * 3. POINTER MICRO-INTERACTIONS
18384
+ * ------------------------------------------------------------------ */
18385
+
18386
+ const POINTER = [{
18387
+ id: 'tilt',
18388
+ label: 'Tilt',
18389
+ key: 'data-fx-tilt',
18390
+ defaults: {
18391
+ 'data-fx-tilt': ''
18392
+ },
18393
+ options: [{
18394
+ attr: 'data-fx-tilt-max',
18395
+ label: 'Max angle',
18396
+ type: 'text',
18397
+ placeholder: '14 (deg)'
18398
+ }]
18399
+ }, {
18400
+ id: 'magnetic',
18401
+ label: 'Magnetic',
18402
+ key: 'data-fx-magnetic',
18403
+ defaults: {
18404
+ 'data-fx-magnetic': ''
18405
+ },
18406
+ options: [{
18407
+ attr: 'data-fx-magnetic-strength',
18408
+ label: 'Strength',
18409
+ type: 'text',
18410
+ placeholder: '0.35'
18411
+ }]
18412
+ }];
18413
+ /* ------------------------------------------------------------------
18414
+ * Helpers — every consumer of this table should go through these, so the
18415
+ * "one system per element" and "attributes only" rules stay in one place.
18416
+ * ------------------------------------------------------------------ */
18417
+
18418
+ /* High-traffic legacy attributes. Mirrors LEGACY_PROBE in contentbox-effects.js:
18419
+ * an element carrying any of these is owned by the old timeline and must not
18420
+ * receive data-fx-*. */
18421
+
18422
+ const LEGACY_PROBE = ['data-bottom-top', 'data-center', 'data-center-top', 'data-center-bottom', 'data-top', 'data-top-bottom', 'data-bottom', 'data-in', 'data-cen', 'data-out', 'data-t', 'data-t-100', 'data-t-200', 'data-t-300', 'data-t-400', 'data-t-500', 'data-t-600', 'data-t-700', 'data-t-800', 'data-t-900', 'data-t-1000', 'data-hidden-onstart', 'data-smooth-scrolling'];
18423
+ /* THE SECOND LEGACY SYSTEM.
18424
+ *
18425
+ * animatescroll.js's "On View" wizard never wrote attributes at all — it wrote
18426
+ * CSS CLASSES, toggled at runtime by inViewSetup()'s IntersectionObserver. So
18427
+ * an element can be legacy-animated while carrying no legacy attribute
18428
+ * whatsoever, which means contentbox-effects' hasLegacyAttrs() guard sails
18429
+ * straight past it. An element with both class="is-animated is-fadeInUp" and
18430
+ * data-fx-0 would get animated TWICE, by two systems both writing transform
18431
+ * and opacity.
18432
+ *
18433
+ * This list is lifted verbatim from animatescroll.js's cleanupBasic(). */
18434
+
18435
+ const LEGACY_ONVIEW_CLASSES = ['is-animated', 'is-scale-animated', 'is-inview', 'once', 'is-fadeIn', 'is-fadeInUp', 'is-fadeInDown', 'is-fadeInLeft', 'is-fadeInRight', 'is-zoomIn', 'is-zoomOut', 'is-slideInUp', 'is-slideInDown', 'is-slideInLeft', 'is-slideInRight', 'is-flipInX', 'is-flipInY', 'is-pulse', 'is-bounceIn'];
18436
+ function hasLegacyOnView(el) {
18437
+ if (!el || !el.classList) return false;
18438
+ return el.classList.contains('is-animated') || el.classList.contains('is-scale-animated');
18439
+ }
18440
+ /** Strip every trace of the class-based system from one element. */
18441
+
18442
+ function stripLegacyOnView(el) {
18443
+ if (!el || !el.classList) return false;
18444
+ let touched = false;
18445
+ LEGACY_ONVIEW_CLASSES.forEach(c => {
18446
+ if (el.classList.contains(c)) {
18447
+ el.classList.remove(c);
18448
+ touched = true;
18449
+ }
18450
+ }); // delay-0ms … delay-2000ms
18451
+
18452
+ Array.from(el.classList).forEach(c => {
18453
+ if (/^delay-\d+ms$/.test(c)) {
18454
+ el.classList.remove(c);
18455
+ touched = true;
18456
+ }
18457
+ });
18458
+ if (el.getAttribute('class') === '') el.removeAttribute('class');
18459
+ return touched;
18460
+ }
18461
+ /** Any element the OLD systems own — attributes (skrollr) OR classes (on-view). */
18462
+
18463
+ function hasLegacyMotion(el) {
18464
+ if (!el || !el.hasAttribute) return false;
18465
+ if (hasLegacyOnView(el)) return true;
18466
+ if (LEGACY_PROBE.some(a => el.hasAttribute(a))) return true; // catch the whole data-t-N / data-xs-t-N / data-sm-t-N family
18467
+
18468
+ for (const attr of el.attributes) {
18469
+ if (/^data-(xs-|sm-)?t(-\d+)?$/.test(attr.name)) return true;
18470
+ }
18471
+
18472
+ return false;
18473
+ }
18474
+ /** Every data-fx-* attribute name currently on the element. */
18475
+
18476
+ function fxAttrs(el) {
18477
+ if (!el || !el.attributes) return [];
18478
+ return Array.from(el.attributes).map(a => a.name).filter(n => n.startsWith('data-fx-'));
18479
+ }
18480
+ function hasMotion(el) {
18481
+ return fxAttrs(el).some(n => n !== MUTE_ATTR);
18482
+ }
18483
+ /* ---- mute ----
18484
+ *
18485
+ * "Off" is an ATTRIBUTE, not editor memory. The tempting alternative — stash
18486
+ * the data-fx-* attributes in JS, strip them from the DOM, restore on unmute —
18487
+ * loses the user's animation the first time the editor autosaves while muted,
18488
+ * because builder.html() serializes the DOM and the attributes aren't in it.
18489
+ *
18490
+ * As an attribute it survives save, undo, reload and hand-off; it's visible in
18491
+ * the HTML so nobody is ever confused about why an animation isn't running;
18492
+ * and it stops being a debug toggle and becomes a real feature ("turn this off
18493
+ * but keep the setup"). It costs a few dead bytes in the published page.
18494
+ *
18495
+ * contentbox-effects._eligible() is the single gate every feature passes
18496
+ * through, so one check there disables keyframes, split, typewriter, parallax,
18497
+ * horizontal, draw, marquee, tilt and magnetic at once. On a .is-section it
18498
+ * mutes the whole scene. */
18499
+
18500
+ const MUTE_ATTR = 'data-fx-off';
18501
+ /* ---- scene attributes ----
18502
+ *
18503
+ * These live on the .is-section and describe the SCENE, not any element's own
18504
+ * motion — so clearing an element must never touch them (a section is both).
18505
+ *
18506
+ * This list used to be copy-pasted into motion.js, panel-motion.js and
18507
+ * motion-cascade.js. Then data-fx-duration and data-fx-once were added and only
18508
+ * one copy learned about them, so "remove track" on a section quietly deleted
18509
+ * the cascade's timing. One list, one place.
18510
+ *
18511
+ * ADDING A NEW SCENE ATTRIBUTE? It goes here, and in animation-framwork-ai.md.
18512
+ */
18513
+
18514
+ const SCENE_ATTRS = ['data-fx-length', // pinned: how much scroll the pin eats (vh)
18515
+ 'data-fx-scrub', // seconds of catch-up lag, or "off" for time-driven
18516
+ 'data-fx-start', // where progress 0 is
18517
+ 'data-fx-end', // where progress 100 is
18518
+ 'data-fx-duration', // seconds — the scene's own clock, when not scrubbing
18519
+ 'data-fx-once', // don't replay on re-entry
18520
+ 'data-fx-emit'];
18521
+ /* ---- what is NOT the user's content ----
18522
+ *
18523
+ * ContentBox injects its toolbars INTO the row, as siblings of the column:
18524
+ *
18525
+ * div.row
18526
+ * ├ div.column ← real content
18527
+ * ├ div.is-tool.is-row-tool ← editor UI
18528
+ * ├ div.is-tool.is-col-tool ← editor UI
18529
+ * └ div.is-rowadd-tool ← editor UI
18530
+ *
18531
+ * So anything walking row.children sees toolbars as columns. They aren't empty
18532
+ * — they hold buttons — so they pass every "does this cell have content?" test,
18533
+ * and the cascade cheerfully animates the editor's own move/delete buttons.
18534
+ *
18535
+ * Kinetra also generates nodes (split-text spans, pinned-section clones). Those
18536
+ * aren't content either.
18537
+ *
18538
+ * NOTE the deliberate absence of `.column` / `.row` here: those class names are
18539
+ * user-configurable, so structure is read positionally and only the chrome is
18540
+ * named. */
18541
+
18542
+ const EDITOR_CHROME_SELECTOR = ['.is-tool', // row + column toolbars (contentbuilder's controlSelector)
18543
+ '.is-rowadd-tool', '.is-section-tool', '.is-box-tool', '.is-section-info'].join(',');
18544
+ /* NOT `.is-overlay-bg`. It is authored markup saved with the page, and it is
18545
+ * where the parallax of most section templates lives — and where converting a
18546
+ * legacy section writes its keyframes. Excluding it hid a section's most
18547
+ * common animation from the timeline dock, which lists a scene's animated
18548
+ * elements. It stays out of the ancestor picker and out of the cascade, but
18549
+ * that is each of their own rule, not this one. */
18550
+
18551
+ const NOT_CONTENT_SELECTOR = [EDITOR_CHROME_SELECTOR, '.kinetra-word', '.kinetra-char', '.kinetra-mask', '[data-section-clone]', '[data-fx-mirrors]'].join(',');
18552
+ /** Editor chrome or Kinetra-generated — never animate it, never list it. */
18553
+
18554
+ function isNotContent(el) {
18555
+ if (!el || el.nodeType !== 1 || !el.closest) return true;
18556
+ return !!el.closest(NOT_CONTENT_SELECTOR); // closest → catches buttons *inside* a toolbar
18557
+ }
18558
+ function isMuted(el) {
18559
+ return !!(el && el.hasAttribute && el.hasAttribute(MUTE_ATTR));
18560
+ }
18561
+ /** Percent keyframes on the element, sorted: [{ pct, value }] */
18562
+
18563
+ function keyframesOf(el) {
18564
+ if (!el || !el.attributes) return [];
18565
+ const out = [];
18566
+
18567
+ for (const a of el.attributes) {
18568
+ const m = /^data-fx-(\d{1,3})$/.exec(a.name);
18569
+ if (!m) continue;
18570
+ const pct = parseInt(m[1], 10);
18571
+ if (pct < 0 || pct > 100) continue;
18572
+ out.push({
18573
+ pct,
18574
+ value: a.value
18575
+ });
18576
+ }
18577
+
18578
+ return out.sort((x, y) => x.pct - y.pct);
18579
+ }
18580
+ /** Which preset (if any) is applied. Returns the preset object or null. */
18581
+
18582
+ function activePreset(el, table) {
18583
+ if (!el || !el.hasAttribute) return null;
18584
+ return (table || PRESETS).find(p => el.hasAttribute(p.key)) || null;
18585
+ }
18586
+ /** Remove every data-fx-N keyframe (leaves presets and ease alone). */
18587
+
18588
+ function clearKeyframes(el) {
18589
+ for (const name of fxAttrs(el)) {
18590
+ if (/^data-fx-\d{1,3}$/.test(name)) el.removeAttribute(name);
18591
+ }
18592
+ }
18593
+ /** Remove one preset and all of its option attributes. */
18594
+
18595
+ function clearPreset(el, preset) {
18596
+ el.removeAttribute(preset.key);
18597
+ (preset.options || []).forEach(o => {
18598
+ if (o.attr !== preset.key) el.removeAttribute(o.attr);
18599
+ });
18600
+ }
18601
+ /** Apply a keyframe recipe: replaces all existing keyframes. */
18602
+
18603
+ function applyRecipe(el, recipe) {
18604
+ clearKeyframes(el);
18605
+ Object.keys(recipe.frames).forEach(pct => {
18606
+ el.setAttribute('data-fx-' + pct, recipe.frames[pct]);
18607
+ });
18608
+ if (recipe.ease) el.setAttribute('data-fx-ease', recipe.ease);else el.removeAttribute('data-fx-ease');
18609
+ }
18610
+ /** Best-effort: which recipe do the current keyframes look like? */
18611
+
18612
+ function matchRecipe(el) {
18613
+ const frames = keyframesOf(el);
18614
+ if (!frames.length) return null;
18615
+
18616
+ const norm = s => s.replace(/\s+/g, '').replace(/;$/, '').toLowerCase();
18617
+
18618
+ return RECIPES.find(r => {
18619
+ const keys = Object.keys(r.frames);
18620
+ if (keys.length !== frames.length) return false;
18621
+ return keys.every(pct => {
18622
+ const f = frames.find(x => x.pct === parseInt(pct, 10));
18623
+ return f && norm(f.value) === norm(r.frames[pct]);
18624
+ });
18625
+ }) || null;
18626
+ }
18627
+ /* ---- Scene (section) helpers ---- */
18628
+
18629
+ function isPinned(section) {
18630
+ return !!(section && section.classList.contains('section-pin'));
18631
+ }
18632
+ const SCENE_DEFAULTS = {
18633
+ length: '320',
18634
+ // vh, pinned only
18635
+ scrub: '0.25',
18636
+ // seconds of catch-up lag
18637
+ duration: '1',
18638
+ // seconds — only read when NOT scrubbing
18639
+ start: 'top bottom',
18640
+ // end: 'bottom top',
18641
+ end: 'bottom bottom'
18642
+ };
18643
+ /* The scene window, as a choice rather than a string.
18644
+ *
18645
+ * There are only two answers that matter:
18646
+ * - fully in view — the entrance is the scene. Right for ~everything.
18647
+ * - leaves the screen — keep moving the whole time the section is on screen.
18648
+ * Right for drifting, parallax-like motion.
18649
+ * Anything else is a fine-tune, and stays available as raw edge strings.
18650
+ *
18651
+ * NOTE: the window is IGNORED when the scene is time-driven (scrub="off") —
18652
+ * an IntersectionObserver does the triggering there. */
18653
+
18654
+ const SCENE_WINDOWS = [{
18655
+ id: 'inview',
18656
+ label: 'the section is fully in view',
18657
+ start: 'top bottom',
18658
+ end: 'bottom bottom'
18659
+ }, {
18660
+ id: 'offscreen',
18661
+ label: 'the section leaves the screen',
18662
+ start: 'top bottom',
18663
+ end: 'bottom top'
18664
+ }];
18665
+ function windowOf(section) {
18666
+ if (!section) return 'inview';
18667
+ const s = (section.getAttribute('data-fx-start') || '').trim();
18668
+ const e = (section.getAttribute('data-fx-end') || '').trim();
18669
+ if (!s && !e) return 'inview'; // no attributes = defaults
18670
+
18671
+ const m = SCENE_WINDOWS.find(w => (s === '' || s === w.start) && e === w.end);
18672
+ return m ? m.id : 'custom';
18673
+ }
18674
+ function setWindow(section, id) {
18675
+ if (!section) return;
18676
+
18677
+ if (id === 'inview') {
18678
+ // the default — write nothing
18679
+ section.removeAttribute('data-fx-start');
18680
+ section.removeAttribute('data-fx-end');
18681
+ return;
18682
+ }
18683
+
18684
+ const w = SCENE_WINDOWS.find(x => x.id === id);
18685
+ if (!w) return; // 'custom' — leave the strings alone
18686
+
18687
+ section.removeAttribute('data-fx-start'); // start default is already right
18688
+
18689
+ section.setAttribute('data-fx-end', w.end);
18690
+ }
18691
+ /* Which clock drives the scene. `data-fx-scrub="off"` is the switch:
18692
+ * the scrollbar stops being the clock and the scene plays itself. */
18693
+
18694
+ function triggerOf(section) {
18695
+ if (!section) return 'scroll';
18696
+ const v = (section.getAttribute('data-fx-scrub') || '').trim().toLowerCase();
18697
+ return v === 'off' || v === 'none' || v === 'false' ? 'enter' : 'scroll';
18698
+ }
18699
+ /* The trigger a scene gets when nobody has chosen one.
18700
+ *
18701
+ * Note what this is NOT: it does not reinterpret a missing data-fx-scrub.
18702
+ * The runtime's fallback for that attribute is scrub 0.25 (contentbox-effects
18703
+ * sceneOptsFor), so a saved page with no trigger on it is scroll-driven and
18704
+ * will stay that way. This is the value the editor WRITES the first time it
18705
+ * gives a section motion — content made from now on plays on enter, content
18706
+ * already out there keeps whatever it has. */
18707
+
18708
+ const DEFAULT_TRIGGER = 'enter';
18709
+ /* Is anything in this section actually animated?
18710
+ *
18711
+ * Scene attributes don't count — data-fx-duration on a bare section describes
18712
+ * a clock nothing is running on. What counts is an element's own motion:
18713
+ * keyframes or a preset, on the section itself or anywhere inside it. */
18714
+
18715
+ function sceneHasMotion(section) {
18716
+ if (!section) return false;
18717
+ const ownMotion = fxAttrs(section).some(n => n !== MUTE_ATTR && SCENE_ATTRS.indexOf(n) === -1);
18718
+ if (ownMotion) return true;
18719
+ return Array.from(section.querySelectorAll('*')).some(el => hasMotion(el));
18720
+ }
18721
+
18722
+ // block's source for an anchor.
18723
+
18724
+ const LINK_ATTRS = {
18725
+ attrs: ['href', 'title', 'target']
18726
+ }; // Panel for elements that live in a BOX OVERLAY but outside div.is-container:
17805
18727
  //
17806
18728
  // .is-section.is-box > .is-overlay > .is-overlay-content > ... > <img>
17807
18729
  //
@@ -17848,7 +18770,28 @@ class PanelOverlay {
17848
18770
 
17849
18771
  <div class="inp-text" role="textbox"></div>
17850
18772
 
17851
- <div class="label-note mt-4">${out('This text is part of an animated layer. Change the wording here; its movement lives on the Motion tab above.')}</div>
18773
+ <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>
18774
+
18775
+ <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>
18776
+
18777
+ </div>
18778
+
18779
+ <div class="div-overlay-link" style="display:none">
18780
+
18781
+ <label class="label">
18782
+ <div>${out('Link')}:</div>
18783
+ <input type="text" class="inp-href" id="_inp_ovl_href_${this.random()}">
18784
+ </label>
18785
+
18786
+ <label class="label mt-2">
18787
+ <div>${out('Title')}:</div>
18788
+ <input type="text" class="inp-linktitle" id="_inp_ovl_linktitle_${this.random()}">
18789
+ </label>
18790
+
18791
+ <label class="label checkbox mt-3">
18792
+ <input type="checkbox" class="chk-newwindow" id="_chk_ovl_newwindow_${this.random()}">
18793
+ <span>${out('Open new window')}</span>
18794
+ </label>
17852
18795
 
17853
18796
  </div>
17854
18797
 
@@ -17873,7 +18816,7 @@ class PanelOverlay {
17873
18816
  </div>
17874
18817
 
17875
18818
  <label class="label mt-2 label-title">
17876
- <div>${out('Title')}:</div>
18819
+ <div>${out('Alt')}:</div>
17877
18820
  <input type="text" class="inp-title" id="_inp_ovl_title_${this.random()}">
17878
18821
  </label>
17879
18822
 
@@ -17881,6 +18824,15 @@ class PanelOverlay {
17881
18824
 
17882
18825
  </div>
17883
18826
 
18827
+ <div class="div-overlay-code mt-4" style="display:none">
18828
+ <div class="group single flex" style="width:100%">
18829
+ <button title="${out('Edit Code')}" class="btn-editcode">
18830
+ <svg><use xlink:href="#icon-viewcode"></use></svg>
18831
+ <span>${out('Edit Code')}</span>
18832
+ </button>
18833
+ </div>
18834
+ </div>
18835
+
17884
18836
  <div class="div-overlay-list" style="display:none">
17885
18837
  <div class="label-note list-caption">${out('Everything else in this layer')}:</div>
17886
18838
  <div class="overlay-list"></div>
@@ -17926,13 +18878,70 @@ class PanelOverlay {
17926
18878
  }, btnOpenAsset);
17927
18879
  }); // Show/hide button
17928
18880
 
17929
- if (!(this.builder.onImageSelectClick || this.builder.imageSelect)) btnOpenAsset.style.display = 'none';
18881
+ if (!(this.builder.onImageSelectClick || this.builder.imageSelect)) btnOpenAsset.style.display = 'none'; // --- Link ---
18882
+ //
18883
+ // These fields only ever EDIT an anchor that is already in the layer;
18884
+ // nothing here creates one. Wrapping a layer's text or image in a link
18885
+ // restructures the design, which is the same reason the floating image
18886
+ // tool is reduced for overlay images (see controlpanel.js getActive).
18887
+
18888
+ const inpHref = panel.querySelector('.inp-href');
18889
+ inpHref.addEventListener('input', () => {
18890
+ const link = this.link();
18891
+ if (!link) return;
18892
+ this.builder.editor.saveForUndo();
18893
+ const url = inpHref.value.trim();
18894
+ if (url) link.setAttribute('href', url);else link.removeAttribute('href'); // emptied: no link, but the element stays as it is
18895
+
18896
+ syncToCodeSource(link, LINK_ATTRS);
18897
+ this.builder.editor.onChange();
18898
+ });
18899
+ const inpLinkTitle = panel.querySelector('.inp-linktitle');
18900
+ inpLinkTitle.addEventListener('input', () => {
18901
+ const link = this.link();
18902
+ if (!link) return;
18903
+ this.builder.editor.saveForUndo();
18904
+ const title = inpLinkTitle.value;
18905
+ if (title) link.setAttribute('title', title);else link.removeAttribute('title');
18906
+ syncToCodeSource(link, LINK_ATTRS);
18907
+ this.builder.editor.onChange();
18908
+ });
18909
+ const chkNewWindow = panel.querySelector('.chk-newwindow');
18910
+ chkNewWindow.addEventListener('change', () => {
18911
+ const link = this.link();
18912
+ if (!link) return;
18913
+ this.builder.editor.saveForUndo(); // target alone, as the editor's own link dialog writes it
18914
+ // (elementhyperlink.js).
18915
+
18916
+ if (chkNewWindow.checked) link.setAttribute('target', '_blank');else link.removeAttribute('target');
18917
+ syncToCodeSource(link, LINK_ATTRS);
18918
+ this.builder.editor.onChange();
18919
+ }); // The layer is a code block: its markup is edited here, its code there.
18920
+ // Without this the code is out of reach while one of its elements is
18921
+ // selected, since the panel showing is this one, not the Code panel.
18922
+
18923
+ const btnEditCode = panel.querySelector('.btn-editcode');
18924
+ btnEditCode.addEventListener('click', () => {
18925
+ const el = this.target();
18926
+ const block = el ? el.closest('[data-cb-code]') : null;
18927
+ const codeElement = this.builder.editor.codeElement;
18928
+ if (!block || !codeElement) return; // Applying the dialog rebuilds the block from its source, replacing
18929
+ // the element this panel is on. Select the block first, so what the
18930
+ // panel shows is still on the page when the dialog closes.
18931
+
18932
+ this.builder.controlpanel.selectOverlayElement(block);
18933
+ codeElement.activeBlock = block;
18934
+ codeElement.edit();
18935
+ });
17930
18936
  const inpTitle = panel.querySelector('.inp-title');
17931
18937
  inpTitle.addEventListener('input', () => {
17932
18938
  this.builder.editor.saveForUndo();
17933
18939
  let img = this.target();
17934
18940
  if (!img) return;
17935
18941
  img.setAttribute('alt', inpTitle.value);
18942
+ syncToCodeSource(img, {
18943
+ attrs: ['alt']
18944
+ });
17936
18945
  this.builder.editor.onChange();
17937
18946
  }); // --- Text ---
17938
18947
 
@@ -18012,7 +19021,14 @@ class PanelOverlay {
18012
19021
  const html = this.unwrapBlock(this.ed.getHTML());
18013
19022
  if (html === el.innerHTML) return;
18014
19023
  this.builder.editor.saveForUndo();
18015
- el.innerHTML = html;
19024
+ el.innerHTML = html; // An element inside a code block lives in the block's RENDER, which is
19025
+ // thrown away on save; the wording has to reach the block's source too.
19026
+ // No-op everywhere else. (Elements the code built itself never get
19027
+ // here: isOverlayTextElement refuses them.)
19028
+
19029
+ syncToCodeSource(el, {
19030
+ html: true
19031
+ });
18016
19032
  this.refreshRow(el);
18017
19033
  this.builder.editor.onChange();
18018
19034
  }
@@ -18026,6 +19042,41 @@ class PanelOverlay {
18026
19042
  }
18027
19043
 
18028
19044
  return html;
19045
+ } // The anchor the current selection belongs to, or the single one inside it.
19046
+ //
19047
+ // Null where there is none, and null where the region holds SEVERAL: which
19048
+ // of them the fields would then be editing is a guess, and a wrong guess
19049
+ // rewrites a link the user was not looking at.
19050
+
19051
+
19052
+ linkFor(el) {
19053
+ if (!el || !el.closest) return null;
19054
+ const scope = el.closest(this.builder.overlayEditableSelector);
19055
+ if (!scope) return null;
19056
+ let link = el.closest('a');
19057
+ if (link && !scope.contains(link)) link = null;
19058
+
19059
+ if (!link) {
19060
+ const inside = el.querySelectorAll('a');
19061
+ if (inside.length === 1) link = inside[0];
19062
+ }
19063
+
19064
+ if (!link) return null; // Inside a code block, only an anchor still verbatim in the block's
19065
+ // source can be edited — the same rule as the wording.
19066
+
19067
+ if (!codeSourceEditable(link)) return null;
19068
+ return link;
19069
+ }
19070
+
19071
+ link() {
19072
+ return this.linkFor(this.target());
19073
+ }
19074
+
19075
+ getStateLink(link) {
19076
+ const panel = this.panel;
19077
+ panel.querySelector('.inp-href').value = link.getAttribute('href') || '';
19078
+ panel.querySelector('.inp-linktitle').value = link.getAttribute('title') || '';
19079
+ panel.querySelector('.chk-newwindow').checked = link.getAttribute('target') === '_blank';
18029
19080
  } // The element this panel is currently editing. ControlPanel keeps it in
18030
19081
  // activeElement, the same as every other panel.
18031
19082
 
@@ -18057,8 +19108,13 @@ class PanelOverlay {
18057
19108
  // selection lost (eg. the image got wrapped in a link)
18058
19109
  this.builder.controlpanel.select('');
18059
19110
  return;
18060
- }
19111
+ } // The floating tool writes to the live element and knows nothing
19112
+ // about code blocks; carry the change into the source as well.
19113
+
18061
19114
 
19115
+ syncToCodeSource(img, {
19116
+ attrs: ['src', 'alt']
19117
+ });
18062
19118
  this.getState();
18063
19119
  };
18064
19120
 
@@ -18070,6 +19126,10 @@ class PanelOverlay {
18070
19126
  return;
18071
19127
  }
18072
19128
 
19129
+ const el = this.target();
19130
+ if (el) syncToCodeSource(el, {
19131
+ attrs: ['src', 'alt']
19132
+ });
18073
19133
  this.getState();
18074
19134
  };
18075
19135
  } // `target` is the {type, element} descriptor from ControlPanel.overlayTarget().
@@ -18091,19 +19151,30 @@ class PanelOverlay {
18091
19151
  const hasText = target ? !!target.hasText : type === 'text';
18092
19152
  const hasSource = target ? !!target.hasSource : type !== 'text';
18093
19153
  const isImg = el.tagName.toLowerCase() === 'img';
18094
- this.sourceKind = isImg ? 'image' : 'background';
19154
+ this.sourceKind = isImg ? 'image' : 'background'; // A link is an ASPECT too, and an independent one: a heading can be a
19155
+ // link, so can an image, and so can an icon with neither text nor
19156
+ // picture in it (which is why the decoration note steps aside for it).
19157
+
19158
+ const link = this.linkFor(el);
18095
19159
  const divText = this.panel.querySelector('.div-overlay-text');
19160
+ const divLink = this.panel.querySelector('.div-overlay-link');
18096
19161
  const divImage = this.panel.querySelector('.div-overlay-image');
18097
19162
  const divDeco = this.panel.querySelector('.div-overlay-decoration');
18098
19163
  divText.style.display = hasText ? '' : 'none';
19164
+ divLink.style.display = link ? '' : 'none';
18099
19165
  divImage.style.display = hasSource ? '' : 'none';
18100
- if (divDeco) divDeco.style.display = !hasText && !hasSource ? '' : 'none';
19166
+ if (divDeco) divDeco.style.display = !hasText && !hasSource && !link ? '' : 'none';
18101
19167
  /* [FX-PATH] */
18102
- // Both on screen at once (an element painting its image inside its own
18103
- // letterforms): the second section needs separating from the first.
18104
19168
 
18105
- divImage.classList.toggle('stacked', hasText && hasSource);
19169
+ if (link) this.getStateLink(link); // More than one section on screen at once (an element painting its image
19170
+ // inside its own letterforms; a heading that is also a link): each one
19171
+ // after the first needs separating from what precedes it.
19172
+
19173
+ divLink.classList.toggle('stacked', !!link && hasText);
19174
+ divImage.classList.toggle('stacked', hasSource && (hasText || !!link));
18106
19175
  this.panel.querySelector('.label-title').style.display = isImg ? '' : 'none';
19176
+ const divCode = this.panel.querySelector('.div-overlay-code');
19177
+ divCode.style.display = this.builder.editor.codeElement && el.closest('[data-cb-code]') ? '' : 'none';
18107
19178
  if (hasText) this.getStateText(el);
18108
19179
  if (hasSource) this.getStateSource(el);
18109
19180
  this.renderList(el);
@@ -18219,11 +19290,28 @@ class PanelOverlay {
18219
19290
  if (!value || value === 'none') return '';
18220
19291
  const found = /url\((['"]?)(.*?)\1\)/.exec(value);
18221
19292
  return found ? found[2] : '';
19293
+ } // Does this layer move on the Motion tab? Only then does the note that
19294
+ // points at it say anything true: a layer can be a static caption, or a
19295
+ // code block, or animate from CSS keyframes the tab has no part in.
19296
+ // Resolved up the layer, since the keyframes are often on a wrapper.
19297
+
19298
+
19299
+ layerHasMotion(el) {
19300
+ const stop = el.closest(this.builder.overlayEditableSelector);
19301
+
19302
+ for (let node = el; node && node !== stop; node = node.parentElement) {
19303
+ if (hasMotion(node)) return true;
19304
+ }
19305
+
19306
+ return false;
18222
19307
  }
18223
19308
 
18224
19309
  getStateText(el) {
18225
19310
  const panel = this.panel;
18226
- if (!el) return; // Load the element's content into the field. Guarded so the write-back
19311
+ if (!el) return;
19312
+ const animated = this.layerHasMotion(el);
19313
+ panel.querySelector('.note-motion').style.display = animated ? '' : 'none';
19314
+ panel.querySelector('.note-plain').style.display = animated ? 'none' : ''; // Load the element's content into the field. Guarded so the write-back
18227
19315
  // does not fire on our own load, and skipped while the user is typing in
18228
19316
  // it (which would move their caret to the end on every keystroke).
18229
19317
 
@@ -18314,6 +19402,9 @@ class PanelOverlay {
18314
19402
  el.style.backgroundImage = `url("${src}")`;
18315
19403
  }
18316
19404
 
19405
+ syncToCodeSource(el, {
19406
+ styles: ['background-image']
19407
+ });
18317
19408
  this.refreshRow(el);
18318
19409
  return;
18319
19410
  }
@@ -18322,6 +19413,9 @@ class PanelOverlay {
18322
19413
  this.builder.editor.element.image.repositionImageTool();
18323
19414
  });
18324
19415
  el.setAttribute('src', src);
19416
+ syncToCodeSource(el, {
19417
+ attrs: ['src']
19418
+ });
18325
19419
  this.refreshRow(el);
18326
19420
  }
18327
19421
 
@@ -25309,739 +26403,6 @@ js$1.exports;
25309
26403
  var jsExports = js$1.exports;
25310
26404
  var JsBeautify$1 = /*@__PURE__*/getDefaultExportFromCjs(jsExports);
25311
26405
 
25312
- /*
25313
- * motion-presets.js — the declarative vocabulary of the Motion panel.
25314
- *
25315
- * Everything the Motion UI can apply lives here as DATA, so adding a new
25316
- * effect is a table edit, never a code edit. Nothing in this file touches
25317
- * the DOM or the builder.
25318
- *
25319
- * Three kinds of thing, because Kinetra genuinely has three:
25320
- *
25321
- * 1. RECIPES — canned percent keyframes (data-fx-0 … data-fx-100).
25322
- * "Fade up", "Zoom in", "Wipe in". These are just a
25323
- * starting set of keyframes; the Motion dock (phase 2)
25324
- * edits them freely afterwards.
25325
- * 2. PRESETS — single-attribute effects that are NOT keyframes:
25326
- * parallax, horizontal, split, typewriter, draw, marquee.
25327
- * Each is identified by one "key" attribute.
25328
- * 3. POINTER — micro-interactions (tilt, magnetic). Not scene-bound
25329
- * at all; they are per-element toggles.
25330
- *
25331
- * Option types:
25332
- * 'text' — free text input, written verbatim to the attribute
25333
- * 'select' — dropdown; value written to the attribute
25334
- * 'toggle' — VALUELESS boolean attribute (present / absent)
25335
- */
25336
-
25337
- /* Ease names are NOT listed here. motion-ease.js derives them from kinetra's
25338
- * exported `easings` table, so there is exactly one source of truth and a copy
25339
- * can't drift when someone adds or renames a curve. */
25340
-
25341
- /* ------------------------------------------------------------------
25342
- * 1. KEYFRAME RECIPES
25343
- *
25344
- * `frames` maps percent -> declaration string. Applying a recipe REPLACES
25345
- * every existing data-fx-N on the element (a recipe is a fresh start, not
25346
- * an overlay), and sets data-fx-ease.
25347
- *
25348
- * Compound-string rule: both ends of a clip-path/box-shadow/etc. must share
25349
- * the same token count, or Kinetra snaps instead of tweening. Every recipe
25350
- * below already obeys that — keep it that way when adding new ones.
25351
- * ------------------------------------------------------------------ */
25352
- const RECIPES = [{
25353
- id: 'fade',
25354
- duration: 0.6,
25355
- label: 'Fade in',
25356
- ease: 'easeOut',
25357
- frames: {
25358
- 0: 'opacity:0',
25359
- 100: 'opacity:1'
25360
- }
25361
- }, {
25362
- id: 'fade-up',
25363
- duration: 0.6,
25364
- label: 'Fade up',
25365
- ease: 'smooth',
25366
- // kinetra's closest match to CSS ease-in-out
25367
- frames: {
25368
- 0: 'opacity:0; transform:translateY(70px)',
25369
- // .is-fadeInUp was 70px
25370
- 100: 'opacity:1; transform:translateY(0px)'
25371
- }
25372
- }, {
25373
- id: 'fade-down',
25374
- duration: 0.6,
25375
- label: 'Fade down',
25376
- ease: 'smooth',
25377
- frames: {
25378
- 0: 'opacity:0; transform:translateY(-70px)',
25379
- 100: 'opacity:1; transform:translateY(0px)'
25380
- }
25381
- }, {
25382
- id: 'fade-left',
25383
- duration: 0.6,
25384
- label: 'From left',
25385
- ease: 'smooth',
25386
- frames: {
25387
- 0: 'opacity:0; transform:translateX(-70px)',
25388
- 100: 'opacity:1; transform:translateX(0px)'
25389
- }
25390
- }, {
25391
- id: 'fade-right',
25392
- duration: 0.6,
25393
- label: 'From right',
25394
- ease: 'smooth',
25395
- frames: {
25396
- 0: 'opacity:0; transform:translateX(70px)',
25397
- 100: 'opacity:1; transform:translateX(0px)'
25398
- }
25399
- }, {
25400
- id: 'zoom-in',
25401
- duration: 0.8,
25402
- label: 'Zoom in',
25403
- ease: 'smooth',
25404
- frames: {
25405
- 0: 'opacity:0; transform:scale(0.8)',
25406
- // .is-zoomIn was scale(.8)
25407
- 100: 'opacity:1; transform:scale(1)'
25408
- }
25409
- }, {
25410
- id: 'zoom-out',
25411
- duration: 0.8,
25412
- label: 'Zoom out',
25413
- ease: 'smooth',
25414
- frames: {
25415
- 0: 'opacity:0; transform:scale(1.2)',
25416
- // .is-zoomOut was scale(1.2)
25417
- 100: 'opacity:1; transform:scale(1)'
25418
- }
25419
- }, {
25420
- id: 'slide-up',
25421
- duration: 0.6,
25422
- label: 'Slide up',
25423
- ease: 'smooth',
25424
- frames: {
25425
- 0: 'transform:translateY(70px)',
25426
- 100: 'transform:translateY(0px)'
25427
- }
25428
- }, {
25429
- id: 'slide-down',
25430
- duration: 0.6,
25431
- label: 'Slide down',
25432
- ease: 'smooth',
25433
- frames: {
25434
- 0: 'transform:translateY(-70px)',
25435
- 100: 'transform:translateY(0px)'
25436
- }
25437
- }, {
25438
- id: 'slide-left',
25439
- duration: 0.6,
25440
- label: 'Slide from left',
25441
- ease: 'smooth',
25442
- frames: {
25443
- 0: 'transform:translateX(-70px)',
25444
- 100: 'transform:translateX(0px)'
25445
- }
25446
- }, {
25447
- id: 'slide-right',
25448
- duration: 0.6,
25449
- label: 'Slide from right',
25450
- ease: 'smooth',
25451
- frames: {
25452
- 0: 'transform:translateX(70px)',
25453
- 100: 'transform:translateX(0px)'
25454
- }
25455
- }, {
25456
- id: 'blur-in',
25457
- duration: 0.6,
25458
- label: 'Blur in',
25459
- ease: 'easeOut',
25460
- frames: {
25461
- 0: 'opacity:0; filter:blur(12px)',
25462
- 100: 'opacity:1; filter:blur(0px)'
25463
- }
25464
- }, {
25465
- id: 'flip-x',
25466
- label: 'Flip X',
25467
- duration: 0.8,
25468
- ease: 'smooth',
25469
- frames: {
25470
- 0: 'opacity:0; transform:perspective(2500px) rotateX(100deg)',
25471
- 100: 'opacity:1; transform:perspective(2500px) rotateX(0deg)'
25472
- }
25473
- }, {
25474
- id: 'flip-y',
25475
- label: 'Flip Y',
25476
- duration: 0.8,
25477
- ease: 'smooth',
25478
- frames: {
25479
- 0: 'opacity:0; transform:perspective(2500px) rotateY(100deg)',
25480
- 100: 'opacity:1; transform:perspective(2500px) rotateY(0deg)'
25481
- }
25482
- }, {
25483
- id: 'pulse',
25484
- label: 'Pulse',
25485
- duration: 0.6,
25486
- ease: 'linear',
25487
- frames: {
25488
- 0: 'opacity:0; transform:scale(0.9)',
25489
- 50: 'opacity:1; transform:scale(1.05)',
25490
- 100: 'opacity:1; transform:scale(1)'
25491
- }
25492
- }, {
25493
- id: 'bounce-in',
25494
- label: 'Bounce in',
25495
- duration: 0.6,
25496
- ease: 'linear',
25497
- frames: {
25498
- 0: 'opacity:0; transform:translateX(0px)',
25499
- 40: 'opacity:1; transform:translateX(-20px)',
25500
- 60: 'opacity:1; transform:translateX(0px)',
25501
- 80: 'opacity:1; transform:translateX(-15px)',
25502
- 100: 'opacity:1; transform:translateX(0px)'
25503
- }
25504
- }, {
25505
- id: 'wipe-right',
25506
- duration: 0.6,
25507
- label: 'Wipe in',
25508
- ease: 'expoOut',
25509
- frames: {
25510
- 0: 'opacity:1; clip-path:inset(0% 100% 0% 0%)',
25511
- 100: 'opacity:1; clip-path:inset(0% 0% 0% 0%)'
25512
- }
25513
- }, {
25514
- id: 'rise-mask',
25515
- duration: 0.7,
25516
- label: 'Rise + mask',
25517
- ease: 'expoOut',
25518
- frames: {
25519
- 0: 'opacity:0; transform:translateY(70px); clip-path:inset(100% 0% 0% 0%)',
25520
- 100: 'opacity:1; transform:translateY(0px); clip-path:inset(0% 0% 0% 0%)'
25521
- }
25522
- }, {
25523
- id: 'rotate-in',
25524
- duration: 0.6,
25525
- label: 'Rotate in',
25526
- ease: 'backOut',
25527
- frames: {
25528
- 0: 'opacity:0; transform:rotate(-8deg) scale(0.94)',
25529
- 100: 'opacity:1; transform:rotate(0deg) scale(1)'
25530
- }
25531
- }, {
25532
- id: 'drift',
25533
- duration: 0.6,
25534
- label: 'Drift through',
25535
- ease: 'linear',
25536
- noCascade: true,
25537
- // a through-motion, not an entrance — pointless in a cascade
25538
- frames: {
25539
- 0: 'transform:translateY(60px)',
25540
- 100: 'transform:translateY(-60px)'
25541
- }
25542
- }, {
25543
- id: 'hold-scale',
25544
- duration: 0.6,
25545
- label: 'Scale back + hold',
25546
- ease: 'linear',
25547
- note: 'Card-stack layer: shrinks as the next layer covers it.',
25548
- frames: {
25549
- 0: 'transform:scale(1)',
25550
- 50: 'transform:scale(0.9)',
25551
- 100: 'transform:scale(0.9)'
25552
- }
25553
- }];
25554
- /* ------------------------------------------------------------------
25555
- * 2. SCROLL / TEXT PRESETS (single-attribute effects)
25556
- * ------------------------------------------------------------------ */
25557
-
25558
- const PRESETS = [{
25559
- id: 'parallax',
25560
- label: 'Parallax',
25561
- group: 'scroll',
25562
- key: 'data-fx-parallax',
25563
- defaults: {
25564
- 'data-fx-parallax': '120'
25565
- },
25566
- hint: 'Drifts as it scrolls. Works in any section.',
25567
- options: [{
25568
- attr: 'data-fx-parallax',
25569
- label: 'Travel',
25570
- type: 'text',
25571
- placeholder: '120 (px) or 20%'
25572
- }]
25573
- }, {
25574
- id: 'horizontal',
25575
- label: 'Horizontal track',
25576
- group: 'scroll',
25577
- key: 'data-fx-horizontal',
25578
- pinnedOnly: true,
25579
- defaults: {
25580
- 'data-fx-horizontal': ''
25581
- },
25582
- hint: 'Vertical scroll drives a sideways track. Needs a pinned scene, and children with flex:0 0 auto.',
25583
- options: []
25584
- }, {
25585
- id: 'split',
25586
- label: 'Split text',
25587
- group: 'text',
25588
- key: 'data-fx-split',
25589
- defaults: {
25590
- 'data-fx-split': 'words'
25591
- },
25592
- hint: 'Staggered word/char entrance. Formatting inside the element is preserved.',
25593
- options: [{
25594
- attr: 'data-fx-split',
25595
- label: 'Unit',
25596
- type: 'select',
25597
- choices: [['words', 'Words'], ['chars', 'Characters']]
25598
- }, {
25599
- attr: 'data-fx-split-mask',
25600
- label: 'Mask reveal',
25601
- type: 'toggle'
25602
- }, {
25603
- attr: 'data-fx-split-stagger',
25604
- label: 'Stagger',
25605
- type: 'text',
25606
- placeholder: '0.08 (sec)'
25607
- }, {
25608
- attr: 'data-fx-split-y',
25609
- label: 'Travel',
25610
- type: 'text',
25611
- placeholder: '115% or 18px'
25612
- }, {
25613
- attr: 'data-fx-split-scrub',
25614
- label: 'Scrub through scene',
25615
- type: 'toggle',
25616
- hint: 'Best in a pinned scene — units brighten in reading order as you scroll.'
25617
- }, {
25618
- attr: 'data-fx-split-from',
25619
- label: 'Dim opacity',
25620
- type: 'text',
25621
- placeholder: '0.12',
25622
- requires: 'data-fx-split-scrub'
25623
- }]
25624
- }, {
25625
- id: 'typewriter',
25626
- label: 'Typewriter',
25627
- group: 'text',
25628
- key: 'data-fx-typewriter',
25629
- defaults: {
25630
- 'data-fx-typewriter': ''
25631
- },
25632
- hint: 'The scrollbar types the text. Plain text only.',
25633
- options: [{
25634
- attr: 'data-fx-typewriter-until',
25635
- label: 'Done at',
25636
- type: 'text',
25637
- placeholder: '0.85 (progress)'
25638
- }]
25639
- }, {
25640
- id: 'marquee',
25641
- label: 'Marquee',
25642
- group: 'text',
25643
- key: 'data-fx-marquee',
25644
- defaults: {
25645
- 'data-fx-marquee': ''
25646
- },
25647
- hint: 'Drifting band. Content should be a repeating strip.',
25648
- options: [{
25649
- attr: 'data-fx-marquee-speed',
25650
- label: 'Speed',
25651
- type: 'text',
25652
- placeholder: '90 (px/sec)'
25653
- }, {
25654
- attr: 'data-fx-marquee-lean',
25655
- label: 'Lean with velocity',
25656
- type: 'toggle'
25657
- }]
25658
- }, {
25659
- id: 'path',
25660
- label: 'Motion path',
25661
- group: 'scroll',
25662
- key: 'data-fx-path',
25663
- defaults: {
25664
- 'data-fx-path': '-200,60; 0,-80; 200,60'
25665
- },
25666
- hint: 'Travels a smooth curve through waypoints. Edit the stops in the Motion dock; px offsets from the element\'s resting position.',
25667
- options: [{
25668
- attr: 'data-fx-path',
25669
- label: 'Waypoints',
25670
- type: 'text',
25671
- placeholder: 'x,y; x,y @tx,ty; …'
25672
- }, {
25673
- attr: 'data-fx-path-dwell',
25674
- label: 'Dwell',
25675
- type: 'text',
25676
- placeholder: '0.78 (0 = constant, 1 = stops)'
25677
- }, {
25678
- attr: 'data-fx-path-curve',
25679
- label: 'Curve',
25680
- type: 'text',
25681
- placeholder: '1 (0 = straight lines)'
25682
- }, {
25683
- attr: 'data-fx-path-rotate',
25684
- label: 'Auto-rotate along path',
25685
- type: 'toggle'
25686
- }, {
25687
- attr: 'data-fx-path-rotate',
25688
- label: 'Rotate offset',
25689
- type: 'text',
25690
- placeholder: '0 (deg; art faces right 0, up 90)',
25691
- requires: 'data-fx-path-rotate'
25692
- }]
25693
- }, {
25694
- id: 'draw',
25695
- label: 'Draw SVG',
25696
- group: 'scroll',
25697
- key: 'data-fx-draw',
25698
- defaults: {
25699
- 'data-fx-draw': ''
25700
- },
25701
- tags: ['svg', 'path'],
25702
- hint: 'Paths draw themselves as the scene scrubs. Put on an <svg> or a <path>.',
25703
- options: []
25704
- }];
25705
- /* ------------------------------------------------------------------
25706
- * 3. POINTER MICRO-INTERACTIONS
25707
- * ------------------------------------------------------------------ */
25708
-
25709
- const POINTER = [{
25710
- id: 'tilt',
25711
- label: 'Tilt',
25712
- key: 'data-fx-tilt',
25713
- defaults: {
25714
- 'data-fx-tilt': ''
25715
- },
25716
- options: [{
25717
- attr: 'data-fx-tilt-max',
25718
- label: 'Max angle',
25719
- type: 'text',
25720
- placeholder: '14 (deg)'
25721
- }]
25722
- }, {
25723
- id: 'magnetic',
25724
- label: 'Magnetic',
25725
- key: 'data-fx-magnetic',
25726
- defaults: {
25727
- 'data-fx-magnetic': ''
25728
- },
25729
- options: [{
25730
- attr: 'data-fx-magnetic-strength',
25731
- label: 'Strength',
25732
- type: 'text',
25733
- placeholder: '0.35'
25734
- }]
25735
- }];
25736
- /* ------------------------------------------------------------------
25737
- * Helpers — every consumer of this table should go through these, so the
25738
- * "one system per element" and "attributes only" rules stay in one place.
25739
- * ------------------------------------------------------------------ */
25740
-
25741
- /* High-traffic legacy attributes. Mirrors LEGACY_PROBE in contentbox-effects.js:
25742
- * an element carrying any of these is owned by the old timeline and must not
25743
- * receive data-fx-*. */
25744
-
25745
- const LEGACY_PROBE = ['data-bottom-top', 'data-center', 'data-center-top', 'data-center-bottom', 'data-top', 'data-top-bottom', 'data-bottom', 'data-in', 'data-cen', 'data-out', 'data-t', 'data-t-100', 'data-t-200', 'data-t-300', 'data-t-400', 'data-t-500', 'data-t-600', 'data-t-700', 'data-t-800', 'data-t-900', 'data-t-1000', 'data-hidden-onstart', 'data-smooth-scrolling'];
25746
- /* THE SECOND LEGACY SYSTEM.
25747
- *
25748
- * animatescroll.js's "On View" wizard never wrote attributes at all — it wrote
25749
- * CSS CLASSES, toggled at runtime by inViewSetup()'s IntersectionObserver. So
25750
- * an element can be legacy-animated while carrying no legacy attribute
25751
- * whatsoever, which means contentbox-effects' hasLegacyAttrs() guard sails
25752
- * straight past it. An element with both class="is-animated is-fadeInUp" and
25753
- * data-fx-0 would get animated TWICE, by two systems both writing transform
25754
- * and opacity.
25755
- *
25756
- * This list is lifted verbatim from animatescroll.js's cleanupBasic(). */
25757
-
25758
- const LEGACY_ONVIEW_CLASSES = ['is-animated', 'is-scale-animated', 'is-inview', 'once', 'is-fadeIn', 'is-fadeInUp', 'is-fadeInDown', 'is-fadeInLeft', 'is-fadeInRight', 'is-zoomIn', 'is-zoomOut', 'is-slideInUp', 'is-slideInDown', 'is-slideInLeft', 'is-slideInRight', 'is-flipInX', 'is-flipInY', 'is-pulse', 'is-bounceIn'];
25759
- function hasLegacyOnView(el) {
25760
- if (!el || !el.classList) return false;
25761
- return el.classList.contains('is-animated') || el.classList.contains('is-scale-animated');
25762
- }
25763
- /** Strip every trace of the class-based system from one element. */
25764
-
25765
- function stripLegacyOnView(el) {
25766
- if (!el || !el.classList) return false;
25767
- let touched = false;
25768
- LEGACY_ONVIEW_CLASSES.forEach(c => {
25769
- if (el.classList.contains(c)) {
25770
- el.classList.remove(c);
25771
- touched = true;
25772
- }
25773
- }); // delay-0ms … delay-2000ms
25774
-
25775
- Array.from(el.classList).forEach(c => {
25776
- if (/^delay-\d+ms$/.test(c)) {
25777
- el.classList.remove(c);
25778
- touched = true;
25779
- }
25780
- });
25781
- if (el.getAttribute('class') === '') el.removeAttribute('class');
25782
- return touched;
25783
- }
25784
- /** Any element the OLD systems own — attributes (skrollr) OR classes (on-view). */
25785
-
25786
- function hasLegacyMotion(el) {
25787
- if (!el || !el.hasAttribute) return false;
25788
- if (hasLegacyOnView(el)) return true;
25789
- if (LEGACY_PROBE.some(a => el.hasAttribute(a))) return true; // catch the whole data-t-N / data-xs-t-N / data-sm-t-N family
25790
-
25791
- for (const attr of el.attributes) {
25792
- if (/^data-(xs-|sm-)?t(-\d+)?$/.test(attr.name)) return true;
25793
- }
25794
-
25795
- return false;
25796
- }
25797
- /** Every data-fx-* attribute name currently on the element. */
25798
-
25799
- function fxAttrs(el) {
25800
- if (!el || !el.attributes) return [];
25801
- return Array.from(el.attributes).map(a => a.name).filter(n => n.startsWith('data-fx-'));
25802
- }
25803
- function hasMotion(el) {
25804
- return fxAttrs(el).some(n => n !== MUTE_ATTR);
25805
- }
25806
- /* ---- mute ----
25807
- *
25808
- * "Off" is an ATTRIBUTE, not editor memory. The tempting alternative — stash
25809
- * the data-fx-* attributes in JS, strip them from the DOM, restore on unmute —
25810
- * loses the user's animation the first time the editor autosaves while muted,
25811
- * because builder.html() serializes the DOM and the attributes aren't in it.
25812
- *
25813
- * As an attribute it survives save, undo, reload and hand-off; it's visible in
25814
- * the HTML so nobody is ever confused about why an animation isn't running;
25815
- * and it stops being a debug toggle and becomes a real feature ("turn this off
25816
- * but keep the setup"). It costs a few dead bytes in the published page.
25817
- *
25818
- * contentbox-effects._eligible() is the single gate every feature passes
25819
- * through, so one check there disables keyframes, split, typewriter, parallax,
25820
- * horizontal, draw, marquee, tilt and magnetic at once. On a .is-section it
25821
- * mutes the whole scene. */
25822
-
25823
- const MUTE_ATTR = 'data-fx-off';
25824
- /* ---- scene attributes ----
25825
- *
25826
- * These live on the .is-section and describe the SCENE, not any element's own
25827
- * motion — so clearing an element must never touch them (a section is both).
25828
- *
25829
- * This list used to be copy-pasted into motion.js, panel-motion.js and
25830
- * motion-cascade.js. Then data-fx-duration and data-fx-once were added and only
25831
- * one copy learned about them, so "remove track" on a section quietly deleted
25832
- * the cascade's timing. One list, one place.
25833
- *
25834
- * ADDING A NEW SCENE ATTRIBUTE? It goes here, and in animation-framwork-ai.md.
25835
- */
25836
-
25837
- const SCENE_ATTRS = ['data-fx-length', // pinned: how much scroll the pin eats (vh)
25838
- 'data-fx-scrub', // seconds of catch-up lag, or "off" for time-driven
25839
- 'data-fx-start', // where progress 0 is
25840
- 'data-fx-end', // where progress 100 is
25841
- 'data-fx-duration', // seconds — the scene's own clock, when not scrubbing
25842
- 'data-fx-once', // don't replay on re-entry
25843
- 'data-fx-emit'];
25844
- /* ---- what is NOT the user's content ----
25845
- *
25846
- * ContentBox injects its toolbars INTO the row, as siblings of the column:
25847
- *
25848
- * div.row
25849
- * ├ div.column ← real content
25850
- * ├ div.is-tool.is-row-tool ← editor UI
25851
- * ├ div.is-tool.is-col-tool ← editor UI
25852
- * └ div.is-rowadd-tool ← editor UI
25853
- *
25854
- * So anything walking row.children sees toolbars as columns. They aren't empty
25855
- * — they hold buttons — so they pass every "does this cell have content?" test,
25856
- * and the cascade cheerfully animates the editor's own move/delete buttons.
25857
- *
25858
- * Kinetra also generates nodes (split-text spans, pinned-section clones). Those
25859
- * aren't content either.
25860
- *
25861
- * NOTE the deliberate absence of `.column` / `.row` here: those class names are
25862
- * user-configurable, so structure is read positionally and only the chrome is
25863
- * named. */
25864
-
25865
- const EDITOR_CHROME_SELECTOR = ['.is-tool', // row + column toolbars (contentbuilder's controlSelector)
25866
- '.is-rowadd-tool', '.is-section-tool', '.is-box-tool', '.is-section-info'].join(',');
25867
- /* NOT `.is-overlay-bg`. It is authored markup saved with the page, and it is
25868
- * where the parallax of most section templates lives — and where converting a
25869
- * legacy section writes its keyframes. Excluding it hid a section's most
25870
- * common animation from the timeline dock, which lists a scene's animated
25871
- * elements. It stays out of the ancestor picker and out of the cascade, but
25872
- * that is each of their own rule, not this one. */
25873
-
25874
- const NOT_CONTENT_SELECTOR = [EDITOR_CHROME_SELECTOR, '.kinetra-word', '.kinetra-char', '.kinetra-mask', '[data-section-clone]', '[data-fx-mirrors]'].join(',');
25875
- /** Editor chrome or Kinetra-generated — never animate it, never list it. */
25876
-
25877
- function isNotContent(el) {
25878
- if (!el || el.nodeType !== 1 || !el.closest) return true;
25879
- return !!el.closest(NOT_CONTENT_SELECTOR); // closest → catches buttons *inside* a toolbar
25880
- }
25881
- function isMuted(el) {
25882
- return !!(el && el.hasAttribute && el.hasAttribute(MUTE_ATTR));
25883
- }
25884
- /** Percent keyframes on the element, sorted: [{ pct, value }] */
25885
-
25886
- function keyframesOf(el) {
25887
- if (!el || !el.attributes) return [];
25888
- const out = [];
25889
-
25890
- for (const a of el.attributes) {
25891
- const m = /^data-fx-(\d{1,3})$/.exec(a.name);
25892
- if (!m) continue;
25893
- const pct = parseInt(m[1], 10);
25894
- if (pct < 0 || pct > 100) continue;
25895
- out.push({
25896
- pct,
25897
- value: a.value
25898
- });
25899
- }
25900
-
25901
- return out.sort((x, y) => x.pct - y.pct);
25902
- }
25903
- /** Which preset (if any) is applied. Returns the preset object or null. */
25904
-
25905
- function activePreset(el, table) {
25906
- if (!el || !el.hasAttribute) return null;
25907
- return (table || PRESETS).find(p => el.hasAttribute(p.key)) || null;
25908
- }
25909
- /** Remove every data-fx-N keyframe (leaves presets and ease alone). */
25910
-
25911
- function clearKeyframes(el) {
25912
- for (const name of fxAttrs(el)) {
25913
- if (/^data-fx-\d{1,3}$/.test(name)) el.removeAttribute(name);
25914
- }
25915
- }
25916
- /** Remove one preset and all of its option attributes. */
25917
-
25918
- function clearPreset(el, preset) {
25919
- el.removeAttribute(preset.key);
25920
- (preset.options || []).forEach(o => {
25921
- if (o.attr !== preset.key) el.removeAttribute(o.attr);
25922
- });
25923
- }
25924
- /** Apply a keyframe recipe: replaces all existing keyframes. */
25925
-
25926
- function applyRecipe(el, recipe) {
25927
- clearKeyframes(el);
25928
- Object.keys(recipe.frames).forEach(pct => {
25929
- el.setAttribute('data-fx-' + pct, recipe.frames[pct]);
25930
- });
25931
- if (recipe.ease) el.setAttribute('data-fx-ease', recipe.ease);else el.removeAttribute('data-fx-ease');
25932
- }
25933
- /** Best-effort: which recipe do the current keyframes look like? */
25934
-
25935
- function matchRecipe(el) {
25936
- const frames = keyframesOf(el);
25937
- if (!frames.length) return null;
25938
-
25939
- const norm = s => s.replace(/\s+/g, '').replace(/;$/, '').toLowerCase();
25940
-
25941
- return RECIPES.find(r => {
25942
- const keys = Object.keys(r.frames);
25943
- if (keys.length !== frames.length) return false;
25944
- return keys.every(pct => {
25945
- const f = frames.find(x => x.pct === parseInt(pct, 10));
25946
- return f && norm(f.value) === norm(r.frames[pct]);
25947
- });
25948
- }) || null;
25949
- }
25950
- /* ---- Scene (section) helpers ---- */
25951
-
25952
- function isPinned(section) {
25953
- return !!(section && section.classList.contains('section-pin'));
25954
- }
25955
- const SCENE_DEFAULTS = {
25956
- length: '320',
25957
- // vh, pinned only
25958
- scrub: '0.25',
25959
- // seconds of catch-up lag
25960
- duration: '1',
25961
- // seconds — only read when NOT scrubbing
25962
- start: 'top bottom',
25963
- // end: 'bottom top',
25964
- end: 'bottom bottom'
25965
- };
25966
- /* The scene window, as a choice rather than a string.
25967
- *
25968
- * There are only two answers that matter:
25969
- * - fully in view — the entrance is the scene. Right for ~everything.
25970
- * - leaves the screen — keep moving the whole time the section is on screen.
25971
- * Right for drifting, parallax-like motion.
25972
- * Anything else is a fine-tune, and stays available as raw edge strings.
25973
- *
25974
- * NOTE: the window is IGNORED when the scene is time-driven (scrub="off") —
25975
- * an IntersectionObserver does the triggering there. */
25976
-
25977
- const SCENE_WINDOWS = [{
25978
- id: 'inview',
25979
- label: 'the section is fully in view',
25980
- start: 'top bottom',
25981
- end: 'bottom bottom'
25982
- }, {
25983
- id: 'offscreen',
25984
- label: 'the section leaves the screen',
25985
- start: 'top bottom',
25986
- end: 'bottom top'
25987
- }];
25988
- function windowOf(section) {
25989
- if (!section) return 'inview';
25990
- const s = (section.getAttribute('data-fx-start') || '').trim();
25991
- const e = (section.getAttribute('data-fx-end') || '').trim();
25992
- if (!s && !e) return 'inview'; // no attributes = defaults
25993
-
25994
- const m = SCENE_WINDOWS.find(w => (s === '' || s === w.start) && e === w.end);
25995
- return m ? m.id : 'custom';
25996
- }
25997
- function setWindow(section, id) {
25998
- if (!section) return;
25999
-
26000
- if (id === 'inview') {
26001
- // the default — write nothing
26002
- section.removeAttribute('data-fx-start');
26003
- section.removeAttribute('data-fx-end');
26004
- return;
26005
- }
26006
-
26007
- const w = SCENE_WINDOWS.find(x => x.id === id);
26008
- if (!w) return; // 'custom' — leave the strings alone
26009
-
26010
- section.removeAttribute('data-fx-start'); // start default is already right
26011
-
26012
- section.setAttribute('data-fx-end', w.end);
26013
- }
26014
- /* Which clock drives the scene. `data-fx-scrub="off"` is the switch:
26015
- * the scrollbar stops being the clock and the scene plays itself. */
26016
-
26017
- function triggerOf(section) {
26018
- if (!section) return 'scroll';
26019
- const v = (section.getAttribute('data-fx-scrub') || '').trim().toLowerCase();
26020
- return v === 'off' || v === 'none' || v === 'false' ? 'enter' : 'scroll';
26021
- }
26022
- /* The trigger a scene gets when nobody has chosen one.
26023
- *
26024
- * Note what this is NOT: it does not reinterpret a missing data-fx-scrub.
26025
- * The runtime's fallback for that attribute is scrub 0.25 (contentbox-effects
26026
- * sceneOptsFor), so a saved page with no trigger on it is scroll-driven and
26027
- * will stay that way. This is the value the editor WRITES the first time it
26028
- * gives a section motion — content made from now on plays on enter, content
26029
- * already out there keeps whatever it has. */
26030
-
26031
- const DEFAULT_TRIGGER = 'enter';
26032
- /* Is anything in this section actually animated?
26033
- *
26034
- * Scene attributes don't count — data-fx-duration on a bare section describes
26035
- * a clock nothing is running on. What counts is an element's own motion:
26036
- * keyframes or a preset, on the section itself or anywhere inside it. */
26037
-
26038
- function sceneHasMotion(section) {
26039
- if (!section) return false;
26040
- const ownMotion = fxAttrs(section).some(n => n !== MUTE_ATTR && SCENE_ATTRS.indexOf(n) === -1);
26041
- if (ownMotion) return true;
26042
- return Array.from(section.querySelectorAll('*')).some(el => hasMotion(el));
26043
- }
26044
-
26045
26406
  /*
26046
26407
  * section-anim.js — the Section panel's ready-made "Animation" presets,
26047
26408
  * expressed as Motion (data-fx-*) instead of the legacy skrollr attributes.
@@ -33184,6 +33545,11 @@ class PanelSection {
33184
33545
  <button title="${out('Duplicate')}" class="btn-duplicate">
33185
33546
  <svg><use xlink:href="#icon-copy"></use></svg>
33186
33547
  </button>
33548
+ ${builder.sectionHtmlButton ? `
33549
+ <button title="${out('Section HTML')}" class="btn-html">
33550
+ <svg><use xlink:href="#icon-code"></use></svg>
33551
+ </button>
33552
+ ` : ''}
33187
33553
  <button title="${out('Delete')}" class="btn-delete">
33188
33554
  <svg><use xlink:href="#icon-trash"></use></svg>
33189
33555
  </button>
@@ -33385,6 +33751,10 @@ class PanelSection {
33385
33751
  btnDuplicate.addEventListener('click', () => {
33386
33752
  this.section.sectionDuplicate();
33387
33753
  });
33754
+ const btnHtml = panel.querySelector('.btn-html');
33755
+ if (btnHtml) btnHtml.addEventListener('click', () => {
33756
+ this.builder.openSectionHtml();
33757
+ });
33388
33758
  const btnDelete = panel.querySelector('.btn-delete');
33389
33759
  btnDelete.addEventListener('click', () => {
33390
33760
  this.section.sectionDelete();
@@ -35920,6 +36290,14 @@ class PanelMotion {
35920
36290
  const builder = this.builder;
35921
36291
  builder.editor.saveForUndo();
35922
36292
  mutate();
36293
+ /* An element inside a code block lives in the block's RENDER, which is
36294
+ * discarded on save. Carry its motion into the block's source so the
36295
+ * animation survives. No-op for every other element. */
36296
+
36297
+ const inCode = this.target();
36298
+ if (inCode) syncToCodeSource(inCode, {
36299
+ fx: true
36300
+ });
35923
36301
  this.refreshRuntime(rerender);
35924
36302
  builder.onChange();
35925
36303
  const dock = builder.motion;
@@ -42340,47 +42718,100 @@ function effectiveFullviewWidth(preferred, availableWidth) {
42340
42718
  }
42341
42719
 
42342
42720
  class Settings {
42343
- constructor(dialog, builder) {
42721
+ constructor(builder) {
42344
42722
  this.builder = builder;
42345
- this.dialog = dialog;
42346
- let html = `
42347
- <button class="btn-back" tabindex="-1" title="${out('Close')}"><svg class="is-icon-flex"><use xlink:href="#icon-arrow-back"></use></svg></button>
42348
-
42349
- <div class="submain">
42723
+ } // Built on first open, not with the control panel: the dialog lives in the
42724
+ // editor's UI root, and the editor is created after the panel.
42350
42725
 
42351
- <div class="label" style="font-size:18px">${out('Settings')}</div>
42352
42726
 
42353
- <div class="label">${out('Paste')}:</div>
42354
- <select class="inp-paste">
42355
- <option value="auto">${out('Auto')}</option>
42356
- <option value="html-without-styles">${out('HTML (without styles)')}</option>
42357
- <option value="html">${out('HTML (with styles)')}</option>
42358
- <option value="text">${out('Text only')}</option>
42359
- </select>
42360
-
42361
- <div class="label">${out('HTML View')}:</div>
42362
- <select class="inp-htmlview">
42363
- <option value="shorten">${out('Shorten HTML')}</option>
42364
- <option value="actual">${out('Actual')}</option>
42365
- </select>
42727
+ render() {
42728
+ const builder = this.builder; // A settings dialog in the editor's own dialog world: the shell, the
42729
+ // title bar and every control below are the components the Preferences
42730
+ // and Column/Row dialogs use, so this reads as one of them rather than
42731
+ // a side panel that happens to float. See the editor's
42732
+ // docs/settings-ui-guide.md.
42366
42733
 
42367
- <div class="label editing-width-label">${out('Editing Width')}:</div>
42368
- <div class="group desktopwidth">
42369
- <button type="button" data-width="auto">${out('Auto')}</button>
42370
- ${FULLVIEW_WIDTHS.map(w => `<button type="button" data-width="${w}">${w}</button>`).join('')}
42734
+ const html = `
42735
+ <div class="is-modal cbxsettings" tabindex="-1" role="dialog" aria-modal="true" aria-hidden="true">
42736
+ <div class="is-modal-content">
42737
+ <div class="is-modal-bar is-draggable">
42738
+ ${out('Settings')}
42739
+ <button type="button" class="is-modal-close" title="${out('Close')}" tabindex="-1">&#10005;</button>
42371
42740
  </div>
42372
42741
 
42373
- ${this.builder.themes && this.builder.themes.length > 0 ? `
42374
- <div class="label">${out('Theme')}:</div>
42375
- <div class="group">
42376
- <button type="button" title="${out('Light')}" data-theme="light" style="background:#fff;border:rgb(132 132 132 / 16%) 1px solid;"></button>
42377
- <button type="button" title="${out('Dark')}" data-theme="dark" style="background:#000;border:rgba(0,0,0,0.15) 1px solid;"></button>
42742
+ <div style="padding:0;margin-top:35px;">
42743
+ <div class="cbx-set-body">
42744
+
42745
+ <div class="cbx-set-row2">
42746
+ <div class="cb-field">
42747
+ <span class="cb-grouplabel">${out('Paste')}</span>
42748
+ <div class="cb-sel">
42749
+ <select class="inp-paste">
42750
+ <option value="auto">${out('Auto')}</option>
42751
+ <option value="html-without-styles">${out('HTML (without styles)')}</option>
42752
+ <option value="html">${out('HTML (with styles)')}</option>
42753
+ <option value="text">${out('Text only')}</option>
42754
+ </select>
42755
+ </div>
42756
+ </div>
42757
+
42758
+ <div class="cb-field">
42759
+ <span class="cb-grouplabel">${out('HTML View')}</span>
42760
+ <div class="cb-sel">
42761
+ <select class="inp-htmlview">
42762
+ <option value="shorten">${out('Shorten HTML')}</option>
42763
+ <option value="actual">${out('Actual')}</option>
42764
+ </select>
42765
+ </div>
42766
+ </div>
42767
+ </div>
42768
+
42769
+ <div class="cb-field editing-width-field">
42770
+ <span class="cb-grouplabel editing-width-label">${out('Editing Width')}</span>
42771
+ <div class="cb-seg desktopwidth" role="group" aria-label="${out('Editing Width')}">
42772
+ <button type="button" data-width="auto">${out('Auto')}</button>
42773
+ ${FULLVIEW_WIDTHS.map(w => `<button type="button" data-width="${w}">${w}</button>`).join('')}
42774
+ </div>
42775
+ </div>
42776
+
42777
+ <div class="cbx-set-sep"></div>
42778
+
42779
+ <div class="cb-switchrow">
42780
+ <span class="lbl">${out('Quick Section Controls')}</span>
42781
+ <label class="switch"><input type="checkbox" class="inp-sectionquicktool"><span class="slider"></span></label>
42782
+ </div>
42783
+ <div class="cb-switchrow">
42784
+ <span class="lbl">${out('Section Tools')}</span>
42785
+ <label class="switch"><input type="checkbox" class="inp-sectiontool"><span class="slider"></span></label>
42786
+ </div>
42787
+ <div class="cb-switchrow">
42788
+ <span class="lbl">${out('Box Tools')}</span>
42789
+ <label class="switch"><input type="checkbox" class="inp-boxtool"><span class="slider"></span></label>
42790
+ </div>
42791
+
42792
+ ${this.builder.themes && this.builder.themes.length > 0 ? `
42793
+ <div class="cbx-set-sep"></div>
42794
+
42795
+ <div class="cb-field">
42796
+ <span class="cb-grouplabel">${out('Theme')}</span>
42797
+ <div class="cbx-themes">
42798
+ <button type="button" title="${out('Light')}" data-theme="light" style="background:#fff;border:rgb(132 132 132 / 16%) 1px solid;"></button>
42799
+ <button type="button" title="${out('Dark')}" data-theme="dark" style="background:#000;border:rgba(0,0,0,0.15) 1px solid;"></button>
42800
+ </div>
42801
+ </div>
42802
+ ` : ''}
42803
+
42804
+ </div>
42378
42805
  </div>
42379
- ` : ''}
42380
-
42381
42806
  </div>
42382
- `;
42383
- dialog.insertAdjacentHTML('beforeend', html);
42807
+ </div>
42808
+ `; // In the editor's UI root, not ContentBox's: that is where the dialog
42809
+ // components live ([data-cb-ui]).
42810
+
42811
+ const stuff = builder.editor.builderStuff;
42812
+ stuff.insertAdjacentHTML('beforeend', html);
42813
+ const dialog = stuff.querySelector('.is-modal.cbxsettings');
42814
+ this.modal = dialog;
42384
42815
  this.dialog = dialog;
42385
42816
  const inpPaste = dialog.querySelector('.inp-paste');
42386
42817
  inpPaste.addEventListener('change', () => {
@@ -42399,6 +42830,27 @@ class Settings {
42399
42830
  }
42400
42831
 
42401
42832
  localStorage.setItem('_htmlview', htmlView);
42833
+ }); // The tools on the page. Everything they do is on the control panel too,
42834
+ // so this is a preference, not a capability: it is remembered per user
42835
+ // and the page is re-rendered at once so the choice is visible.
42836
+
42837
+ const inpSectionQuickTool = dialog.querySelector('.inp-sectionquicktool');
42838
+ inpSectionQuickTool.addEventListener('change', () => {
42839
+ this.builder.sectionQuickTool = inpSectionQuickTool.checked;
42840
+ localStorage.setItem('_sectionquicktool', inpSectionQuickTool.checked ? '1' : '0');
42841
+ this.builder.controlpanel.refreshSectionQuickTool();
42842
+ });
42843
+ const inpSectionTool = dialog.querySelector('.inp-sectiontool');
42844
+ inpSectionTool.addEventListener('change', () => {
42845
+ this.builder.sectionTool = inpSectionTool.checked;
42846
+ localStorage.setItem('_sectiontool', inpSectionTool.checked ? '1' : '0');
42847
+ this.builder.refreshPageTools();
42848
+ });
42849
+ const inpBoxTool = dialog.querySelector('.inp-boxtool');
42850
+ inpBoxTool.addEventListener('change', () => {
42851
+ this.builder.boxTool = inpBoxTool.checked;
42852
+ localStorage.setItem('_boxtool', inpBoxTool.checked ? '1' : '0');
42853
+ this.builder.refreshPageTools();
42402
42854
  });
42403
42855
  const btnTheme = dialog.querySelectorAll('[data-theme]');
42404
42856
  btnTheme.forEach(btn => {
@@ -42441,11 +42893,7 @@ class Settings {
42441
42893
  // it is actually on screen.
42442
42894
 
42443
42895
  window.addEventListener('resize', () => {
42444
- if (this.dialog.style.display) this.renderDesktopWidth();
42445
- });
42446
- const btnBack = dialog.querySelector('.btn-back');
42447
- btnBack.addEventListener('click', () => {
42448
- this.close();
42896
+ if (this.modal.classList.contains('active')) this.renderDesktopWidth();
42449
42897
  });
42450
42898
  }
42451
42899
 
@@ -42477,6 +42925,9 @@ class Settings {
42477
42925
  inpHtmlView.value = 'actual';
42478
42926
  }
42479
42927
 
42928
+ dialog.querySelector('.inp-sectionquicktool').checked = this.builder.sectionQuickTool !== false;
42929
+ dialog.querySelector('.inp-sectiontool').checked = this.builder.sectionTool !== false;
42930
+ dialog.querySelector('.inp-boxtool').checked = this.builder.boxTool !== false;
42480
42931
  this.renderDesktopWidth();
42481
42932
  }
42482
42933
  /**
@@ -42499,8 +42950,8 @@ class Settings {
42499
42950
 
42500
42951
  renderDesktopWidth() {
42501
42952
  const dialog = this.dialog;
42502
- const label = dialog.querySelector('.editing-width-label');
42503
- const group = dialog.querySelector('.group.desktopwidth');
42953
+ const label = dialog.querySelector('.editing-width-field');
42954
+ const group = dialog.querySelector('.desktopwidth');
42504
42955
  if (!group) return; // The workspace, not the window. In fullview #editPanel is inset by the left rail
42505
42956
  // and by the control panel this dialog sits in, so a 1290px window leaves about
42506
42957
  // 939px here, and the difference decides whether any of this is worth showing.
@@ -42514,8 +42965,8 @@ class Settings {
42514
42965
 
42515
42966
  const selectable = FULLVIEW_WIDTHS.some(width => width > available && fullviewWidthFits(width, available));
42516
42967
  const show = this.builder.scaleFullview && selectable;
42517
- label.style.display = show ? '' : 'none';
42518
- group.style.display = show ? '' : 'none';
42968
+ label.style.display = show ? '' : 'none'; // the whole field: label + segments
42969
+
42519
42970
  if (!show) return; // Auto is the state with nothing stored, so storage is what decides it, not the
42520
42971
  // resolved width: under Auto on a wide workspace the width resolves to the
42521
42972
  // fullviewLogicalWidth default, and marking that too would light up two buttons
@@ -42558,12 +43009,20 @@ class Settings {
42558
43009
  }
42559
43010
 
42560
43011
  open() {
42561
- this.dialog.style.display = 'flex';
42562
- this.getState();
43012
+ if (!this.modal) this.render();
43013
+ this.getState(); // Dragging is wired on first open: the shell is rendered with the panel,
43014
+ // long before the editor is ready to take a selector.
43015
+
43016
+ if (!this.draggable) {
43017
+ this.draggable = true;
43018
+ this.builder.editor.draggable('.is-modal.cbxsettings .is-draggable');
43019
+ }
43020
+
43021
+ this.builder.editor.util.showModal(this.modal);
42563
43022
  }
42564
43023
 
42565
43024
  close() {
42566
- this.dialog.style.display = '';
43025
+ if (this.modal) this.builder.editor.util.hideModal(this.modal);
42567
43026
  }
42568
43027
 
42569
43028
  }
@@ -42679,6 +43138,28 @@ class ControlPanel {
42679
43138
 
42680
43139
  <div class="panelnav">
42681
43140
  <div class="part-breadcrumb"></div>
43141
+
43142
+ <!-- Quick access to the section's own actions, right under the
43143
+ Section crumb. A click in the page selects the Box, so these
43144
+ would otherwise cost a trip to the Section panel first.
43145
+ Deliberately smaller than the panel's own controls: it reads
43146
+ as a shortcut belonging to the crumb above it, not as part of
43147
+ the panel below. Shown only when Section is the only crumb (a
43148
+ Box is selected); deeper selections have their own arrows and
43149
+ these would read as "move this element". -->
43150
+ <div class="part-sectionmove group" role="group" aria-label="${out('Section')}" style="display:none">
43151
+ <button title="${out('Top')}" data-sectionmove="top"><svg><use xlink:href="#icon-arrow-top"></use></svg></button>
43152
+ <button title="${out('Up')}" data-sectionmove="up"><svg><use xlink:href="#icon-arrow-up"></use></svg></button>
43153
+ <button title="${out('Down')}" data-sectionmove="down"><svg><use xlink:href="#icon-arrow-down"></use></svg></button>
43154
+ <button title="${out('Bottom')}" data-sectionmove="bottom"><svg><use xlink:href="#icon-arrow-bottom"></use></svg></button>
43155
+ <button class="btn-sectionmore" title="${out('Height')}" aria-haspopup="true" aria-expanded="false"><svg><use xlink:href="#icon-dots"></use></svg></button>
43156
+ <button class="btn-sectiondup" title="${out('Duplicate')}"><svg><use xlink:href="#icon-copy"></use></svg></button>
43157
+ ${this.builder.sectionHtmlButton ? `
43158
+ <button class="btn-sectionhtml" title="${out('Section HTML')}"><svg><use xlink:href="#icon-code"></use></svg></button>
43159
+ ` : ''}
43160
+ <button class="btn-sectiondel" title="${out('Delete')}"><svg><use xlink:href="#icon-trash"></use></svg></button>
43161
+ </div>
43162
+
42682
43163
  <h3 class="part-title"></h3>
42683
43164
  <div class="part-empty">${out('No Selection.')}</div>
42684
43165
  <div class="part-tabs" style="display:none">
@@ -42710,12 +43191,23 @@ class ControlPanel {
42710
43191
  <div class="panel-dialog icons"></div>
42711
43192
  <div class="panel-dialog blocks"></div>
42712
43193
  <div class="panel-dialog imageadjust"></div>
42713
- <div class="panel-dialog settings"></div>
42714
43194
  <div class="panel-dialog group"></div>
42715
43195
  <div class="panel-dialog blockoptions"></div>
42716
43196
  <div class="panel-dialog shadow"></div>
42717
43197
  <div class="panel-dialog symbols"></div>
42718
43198
 
43199
+ <!-- Section height, the one remaining section control used often
43200
+ enough to be worth reaching from the Box selection. A pop
43201
+ rather than four more icons: the presets are a grid, and the
43202
+ strip has to stay small to keep reading as a shortcut. -->
43203
+ <div class="panel-pop heightpop" tabIndex="-1" aria-hidden="true">
43204
+ <div class="label">${out('Height')}:</div>
43205
+ <div class="group sectionheight">
43206
+ ${[10, 15, 20, 25, 30, 40, 50, 60, 70, 75, 80, 85, 90, 100].map(h => `<button title="${h}%" data-sectionheight="${h}">${h}%</button>`).join('')}
43207
+ <button title="${out('Clear')}" class="btn-clear" data-sectionheight="0"><svg><use xlink:href="#icon-eraser"></use></svg></button>
43208
+ </div>
43209
+ </div>
43210
+
42719
43211
  <div class="panel-pop more" tabIndex="-1" aria-hidden="true">
42720
43212
  <!--<button class="btn-preferences" title="${out('Preferences')}">${out('Preferences')}</button>-->
42721
43213
  <button class="btn-settings" title="${out('Settings')}">${out('Settings')}</button>
@@ -42725,14 +43217,6 @@ class ControlPanel {
42725
43217
  <div class="plugins"></div>
42726
43218
  </div>
42727
43219
 
42728
- <div class="is-modal modal-settings" aria-hidden="true">
42729
- <div class="modal-content">
42730
- <div style="padding:30px">
42731
- <p>Modal content goes here.</p>
42732
- </div>
42733
- </div>
42734
- </div>
42735
-
42736
43220
  </div>
42737
43221
 
42738
43222
  `;
@@ -42770,14 +43254,64 @@ class ControlPanel {
42770
43254
  this.showPanel(this.what);
42771
43255
  });
42772
43256
  });
42773
- this.breadcrumb = controlPanel.querySelector('.part-breadcrumb');
43257
+ this.breadcrumb = controlPanel.querySelector('.part-breadcrumb'); // builder.activeSection is kept current whenever a box is activated, and
43258
+ // every action below reads it, so these need no selection of their own
43259
+ // and leave the open panel untouched.
43260
+
43261
+ this.sectionMoveBar = controlPanel.querySelector('.part-sectionmove');
43262
+ this.sectionMoveBar.querySelectorAll('[data-sectionmove]').forEach(btn => {
43263
+ btn.addEventListener('click', () => {
43264
+ this.builder.section.sectionMove(btn.getAttribute('data-sectionmove'));
43265
+ });
43266
+ });
43267
+ this.sectionMoveBar.querySelector('.btn-sectiondup').addEventListener('click', () => {
43268
+ this.builder.section.sectionDuplicate();
43269
+ });
43270
+ const btnSectionHtml = this.sectionMoveBar.querySelector('.btn-sectionhtml');
43271
+ if (btnSectionHtml) btnSectionHtml.addEventListener('click', () => {
43272
+ this.builder.openSectionHtml();
43273
+ });
43274
+ this.heightPop = controlPanel.querySelector('.panel-pop.heightpop');
43275
+ this.btnSectionMore = this.sectionMoveBar.querySelector('.btn-sectionmore');
43276
+ this.btnSectionMore.addEventListener('click', () => {
43277
+ this.markSectionHeight();
43278
+ this.showPop(this.heightPop, () => {
43279
+ this.btnSectionMore.setAttribute('aria-expanded', false);
43280
+ this.btnSectionMore.focus();
43281
+ }, this.btnSectionMore); // showPop right-aligns the pop to its button, which is how the
43282
+ // toolbar's own pops sit. This one belongs to the strip, so it hangs
43283
+ // under the whole strip and lines up with its left edge, pulled back
43284
+ // in only if the window is narrow enough to push it off screen.
43285
+
43286
+ const r = this.sectionMoveBar.getBoundingClientRect();
43287
+ this.heightPop.style.top = `${r.bottom + 5}px`;
43288
+ this.heightPop.style.left = `${Math.max(8, Math.min(r.left, window.innerWidth - this.heightPop.offsetWidth - 8))}px`;
43289
+ this.btnSectionMore.setAttribute('aria-expanded', true);
43290
+ });
43291
+ this.heightPop.querySelectorAll('[data-sectionheight]').forEach(btn => {
43292
+ btn.addEventListener('click', () => {
43293
+ this.builder.section.setSectionHeight(btn.getAttribute('data-sectionheight'));
43294
+ this.markSectionHeight(btn.getAttribute('data-sectionheight')); // Left open: picking a height is a try-and-look action, and the
43295
+ // next try should not cost another trip to the button.
43296
+ });
43297
+ }); // Same confirm as the section tool's trash: this is the same action at
43298
+ // the same scale, and the whole section goes with one small click.
43299
+
43300
+ this.sectionMoveBar.querySelector('.btn-sectiondel').addEventListener('click', () => {
43301
+ if (this.builder.settings.deleteConfirm) {
43302
+ this.builder.editor.util.showModal(this.builder.builderStuff.querySelector('.is-modal.delsectionconfirm'));
43303
+ return;
43304
+ }
43305
+
43306
+ this.builder.section.sectionDelete();
43307
+ if (this.builder.onSelectChange) this.builder.onSelectChange();
43308
+ });
42774
43309
  this.title = controlPanel.querySelector('.part-title');
42775
43310
  this.empty = controlPanel.querySelector('.part-empty');
42776
43311
  this.dialogIcons = controlPanel.querySelector('.panel-dialog.icons'); // this.dialogBlocks = controlPanel.querySelector('.panel-dialog.blocks');
42777
43312
 
42778
43313
  this.dialogShadow = controlPanel.querySelector('.panel-dialog.shadow');
42779
43314
  this.dialogImageAdjust = controlPanel.querySelector('.panel-dialog.imageadjust');
42780
- this.dialogSettings = controlPanel.querySelector('.panel-dialog.settings');
42781
43315
  this.dialogGroup = controlPanel.querySelector('.panel-dialog.group');
42782
43316
  this.dialogBlockOptions = controlPanel.querySelector('.panel-dialog.blockoptions');
42783
43317
  this.dialogSymbols = controlPanel.querySelector('.panel-dialog.symbols');
@@ -42785,8 +43319,11 @@ class ControlPanel {
42785
43319
  this.objDialogIcons = new Icons(this.dialogIcons, this.builder); // this.objDialogBlocks = new Blocks(this.dialogBlocks, this.builder);
42786
43320
 
42787
43321
  this.objDialogShadow = new Shadow(this.dialogShadow, this.builder);
42788
- this.objDialogImageAdjust = new ImageAdjust(this.dialogImageAdjust, this.builder);
42789
- this.objDialogSettings = new Settings(this.dialogSettings, this.builder);
43322
+ this.objDialogImageAdjust = new ImageAdjust(this.dialogImageAdjust, this.builder); // Settings is a facility, not a selection level: it has nothing to do with
43323
+ // what is selected, so it builds its own dialog in the editor's UI root
43324
+ // and opens over everything.
43325
+
43326
+ this.objDialogSettings = new Settings(this.builder);
42790
43327
  this.objDialogGroup = new Group(this.dialogGroup, this.builder);
42791
43328
  this.objDialogBlockOptions = new BlockOptions(this.dialogBlockOptions, this.builder);
42792
43329
  this.objPanelText = new PanelText(this.panelText, this.builder); // comment/disable if using ContentBuilder's controlpanel
@@ -42945,9 +43482,7 @@ class ControlPanel {
42945
43482
 
42946
43483
  const btnSettings = controlPanel.querySelector('.btn-settings');
42947
43484
  btnSettings.addEventListener('click', () => {
42948
- this.objDialogSettings.open(); // const modal = document.querySelector('.modal-settings');
42949
- // this.modal.showModal(modal);
42950
-
43485
+ this.objDialogSettings.open();
42951
43486
  this.hidePop(this.more);
42952
43487
  }); // Accordion // Move to editorReady if using ContentBuilder's controlpanel
42953
43488
 
@@ -43178,7 +43713,9 @@ class ControlPanel {
43178
43713
  return;
43179
43714
  }
43180
43715
 
43181
- if (isOpen(this.dialogSettings)) {
43716
+ const settingsModal = this.objDialogSettings.modal;
43717
+
43718
+ if (settingsModal && settingsModal.classList.contains('active')) {
43182
43719
  this.objDialogSettings.close();
43183
43720
  return;
43184
43721
  } // The element dock last: the pickers above are transient, opened for one
@@ -43394,7 +43931,11 @@ class ControlPanel {
43394
43931
 
43395
43932
  const target = textElement || el;
43396
43933
  const hasText = !!textElement;
43397
- const hasSource = this.builder.isOverlayImageElement(target);
43934
+ const hasSource = this.builder.isOverlayImageElement(target); // A link is an aspect of its own: a heading can be one, so can an
43935
+ // image, and so can an icon that is neither. panel-overlay.js owns the
43936
+ // rule (which anchor, and whether it can be saved); this only names it.
43937
+
43938
+ const linkOf = node => !!(this.objPanelOverlay && this.objPanelOverlay.linkFor(node));
43398
43939
 
43399
43940
  if (!hasText && !hasSource) {
43400
43941
  // [FX-PATH] Decoration aspect. Ornament layers — a cloud, a
@@ -43409,7 +43950,8 @@ class ControlPanel {
43409
43950
  type: 'decoration',
43410
43951
  element: deco,
43411
43952
  hasText: false,
43412
- hasSource: false
43953
+ hasSource: false,
43954
+ hasLink: linkOf(deco)
43413
43955
  };
43414
43956
  }
43415
43957
 
@@ -43419,7 +43961,8 @@ class ControlPanel {
43419
43961
  type: type,
43420
43962
  element: target,
43421
43963
  hasText: hasText,
43422
- hasSource: hasSource
43964
+ hasSource: hasSource,
43965
+ hasLink: linkOf(target)
43423
43966
  };
43424
43967
  } // [FX-PATH] Which element a decoration click selects. Path-first, matching
43425
43968
  // the Motion picker: a compound object (3D die tumbling inside a flying
@@ -43765,6 +44308,7 @@ class ControlPanel {
43765
44308
  }
43766
44309
 
43767
44310
  this.breadcrumb.innerHTML = breadcrumbHtml;
44311
+ this.showSectionQuickTool(selection);
43768
44312
  const links = this.breadcrumb.querySelectorAll('a');
43769
44313
  links.forEach(link => {
43770
44314
  link.addEventListener('click', e => {
@@ -43775,6 +44319,51 @@ class ControlPanel {
43775
44319
  });
43776
44320
  });
43777
44321
  }
44322
+ /**
44323
+ * The quick section controls, shown only when Section is the only crumb (a
44324
+ * Box is selected). Deeper selections have their own arrows and these would
44325
+ * read as "move this element".
44326
+ */
44327
+
44328
+
44329
+ showSectionQuickTool(selection) {
44330
+ this.quickToolSelection = selection;
44331
+ const show = selection === 'box' && this.builder.sectionQuickTool !== false;
44332
+ this.sectionMoveBar.style.display = show ? '' : 'none';
44333
+ this.btnSectionMore.style.display = show && this.sectionHeightAllowed() ? '' : 'none';
44334
+ this.hidePop(this.heightPop);
44335
+ }
44336
+ /** Re-apply the switch in Settings to the selection already on screen. */
44337
+
44338
+
44339
+ refreshSectionQuickTool() {
44340
+ this.showSectionQuickTool(this.quickToolSelection);
44341
+ } // Light up the height the section is on. `value` is passed straight after a
44342
+ // click so the state is right even though the class lands on a timeout.
44343
+
44344
+
44345
+ markSectionHeight(value) {
44346
+ const section = this.builder.activeSection;
44347
+ if (!section) return;
44348
+ this.heightPop.querySelectorAll('[data-sectionheight]').forEach(btn => {
44349
+ const h = btn.getAttribute('data-sectionheight');
44350
+ const on = value !== undefined ? h === value && h !== '0' : section.classList.contains('is-section-' + h);
44351
+ btn.classList.toggle('on', on);
44352
+ });
44353
+ } // A plugin that owns its whole section can refuse the height presets
44354
+ // (contentbox.sectionHeight === false), exactly as the Section panel reads
44355
+ // it. Then the pop has nothing to show and the button goes with it.
44356
+
44357
+
44358
+ sectionHeightAllowed() {
44359
+ const section = this.builder.activeSection;
44360
+ if (!section || !section.classList.contains('is-box')) return true;
44361
+ const el = section.querySelector('.is-overlay-content [data-cb-type]');
44362
+ if (!el) return true;
44363
+ const runtime = this.builder.win.builderRuntime;
44364
+ const plugin = runtime && runtime.getPlugin(el.getAttribute('data-cb-type'));
44365
+ return !(plugin && plugin.contentbox && plugin.contentbox.sectionHeight === false);
44366
+ }
43778
44367
 
43779
44368
  animSelection(elm) {
43780
44369
  elm.classList.add('selection-active');
@@ -43796,6 +44385,7 @@ class ControlPanel {
43796
44385
  if (this.builder.contentSize) this.builder.contentSize.hide();
43797
44386
  this.markOverlaySelection(null);
43798
44387
  this.breadcrumb.innerHTML = '';
44388
+ this.showSectionQuickTool('');
43799
44389
  this.title.innerHTML = '';
43800
44390
  this.title.style.display = 'none';
43801
44391
  this.empty.style.display = '';
@@ -44055,9 +44645,15 @@ class ControlPanel {
44055
44645
  const overlayTarget = this.overlayTarget(element); // Resolved once, here, not inside the branch below: it reads
44056
44646
  // computed style, and the dispatch would otherwise call it twice.
44057
44647
 
44058
- const buttonNow = this.buttonTarget(element);
44648
+ const buttonNow = this.buttonTarget(element); // A code element sitting in an overlay is not a decorative layer:
44649
+ // it has an editor of its own. Without this it would land on the
44650
+ // Decoration note, which is the one place its code cannot be
44651
+ // reached from. Only the block ITSELF — a layer that merely
44652
+ // contains one still belongs to the overlay panel.
44059
44653
 
44060
- if (overlayTarget) {
44654
+ const overlayCodeBlock = overlayTarget && overlayTarget.element.matches('[data-cb-code]') ? overlayTarget.element : null;
44655
+
44656
+ if (overlayTarget && !overlayCodeBlock) {
44061
44657
  // Overlay layer content (outside .is-container) — reduced panel
44062
44658
  // Name every aspect: titling an element 'Text' while the panel
44063
44659
  // leads with an image source reads as the wrong panel.
@@ -44069,6 +44665,7 @@ class ControlPanel {
44069
44665
  // backdrop setting that is not what they clicked.
44070
44666
  const parts = [];
44071
44667
  if (overlayTarget.hasText) parts.push(out('Text'));
44668
+ if (overlayTarget.hasLink) parts.push(out('Link'));
44072
44669
  if (overlayTarget.hasSource) parts.push(out('Image'));
44073
44670
  if (!parts.length) parts.push(out('Decoration')); // [FX-PATH]
44074
44671
 
@@ -61382,9 +61979,9 @@ class UndoRedo {
61382
61979
  [data-html] and [data-html] logic never touches [data-cb-code].
61383
61980
  */
61384
61981
 
61385
- const ATTR$1 = 'data-cb-code';
61386
- const SEL$1 = '[' + ATTR$1 + ']';
61387
- const TPL_SEL$1 = ':scope > template[data-source]';
61982
+ const ATTR = 'data-cb-code';
61983
+ const SEL = '[' + ATTR + ']';
61984
+ const TPL_SEL = ':scope > template[data-source]';
61388
61985
 
61389
61986
  /*
61390
61987
  Prevent a literal </template> inside user code from prematurely closing the
@@ -61392,7 +61989,7 @@ const TPL_SEL$1 = ':scope > template[data-source]';
61392
61989
  (save -> load). Inside a JS string, <\/template> is identical to </template>
61393
61990
  at runtime. (Same convention as the classic <\/script>.)
61394
61991
  */
61395
- function escapeCodeSource$1(src) {
61992
+ function escapeCodeSource(src) {
61396
61993
  return src.replace(/<\/template>/gi, '<\\/template>');
61397
61994
  }
61398
61995
 
@@ -61407,14 +62004,14 @@ function escapeCodeSource$1(src) {
61407
62004
  Exported standalone as well, so it can be used where no builder instance
61408
62005
  exists yet (ContentBox injects content before creating the editor).
61409
62006
  */
61410
- function wrapCodeSource$1(html) {
62007
+ function wrapCodeSource(html) {
61411
62008
  if (!html) return html;
61412
- if (html.indexOf(ATTR$1) === -1) return html; // fast path
62009
+ if (html.indexOf(ATTR) === -1) return html; // fast path
61413
62010
 
61414
62011
  const parsed = new DOMParser().parseFromString(html, 'text/html');
61415
- parsed.querySelectorAll(SEL$1).forEach(block => {
62012
+ parsed.querySelectorAll(SEL).forEach(block => {
61416
62013
  // Already wrapped (round-trip within the editor) => skip
61417
- if (block.querySelector(TPL_SEL$1)) return;
62014
+ if (block.querySelector(TPL_SEL)) return;
61418
62015
 
61419
62016
  // Snippet form (data-src) is already inert => handled by init()
61420
62017
  if (block.hasAttribute('data-src')) return;
@@ -61424,7 +62021,7 @@ function wrapCodeSource$1(html) {
61424
62021
  block.innerHTML = '';
61425
62022
  const tpl = parsed.createElement('template');
61426
62023
  tpl.setAttribute('data-source', '');
61427
- tpl.innerHTML = escapeCodeSource$1(src);
62024
+ tpl.innerHTML = escapeCodeSource(src);
61428
62025
  block.appendChild(tpl);
61429
62026
  });
61430
62027
  return parsed.body.innerHTML;
@@ -61443,7 +62040,7 @@ class CodeElement {
61443
62040
  // hideControls), and so it also works in non-editable columns.
61444
62041
  this.doDocumentClick = e => {
61445
62042
  if (!e.target || !e.target.closest) return;
61446
- const block = e.target.closest(SEL$1);
62043
+ const block = e.target.closest(SEL);
61447
62044
  if (block && this.inContentArea(block)) {
61448
62045
  this.activeBlock = block;
61449
62046
  // ContentBox drives editing from its side panel (same as
@@ -61485,7 +62082,7 @@ class CodeElement {
61485
62082
  return this.builder.util.makeId();
61486
62083
  }
61487
62084
  getTemplate(block) {
61488
- return block.querySelector(TPL_SEL$1);
62085
+ return block.querySelector(TPL_SEL);
61489
62086
  }
61490
62087
 
61491
62088
  /*
@@ -61512,7 +62109,7 @@ class CodeElement {
61512
62109
  return this.contentRoots().some(root => root.contains(block));
61513
62110
  }
61514
62111
  escapeSource(src) {
61515
- return escapeCodeSource$1(src);
62112
+ return escapeCodeSource(src);
61516
62113
  }
61517
62114
 
61518
62115
  /*
@@ -61528,7 +62125,7 @@ class CodeElement {
61528
62125
  return ids;
61529
62126
  }
61530
62127
  wrapSource(html) {
61531
- return wrapCodeSource$1(html);
62128
+ return wrapCodeSource(html);
61532
62129
  }
61533
62130
 
61534
62131
  /*
@@ -61584,7 +62181,7 @@ class CodeElement {
61584
62181
  rerenderAll(root) {
61585
62182
  const scope = root || this.builder.doc;
61586
62183
  if (!scope || !scope.querySelectorAll) return;
61587
- scope.querySelectorAll(SEL$1).forEach(block => this.render(block));
62184
+ scope.querySelectorAll(SEL).forEach(block => this.render(block));
61588
62185
  }
61589
62186
 
61590
62187
  /*
@@ -61617,7 +62214,7 @@ class CodeElement {
61617
62214
  }
61618
62215
 
61619
62216
  // 2) Earlier code element templates (not part of live DOM queries)
61620
- const otherTpls = roots[r].querySelectorAll(SEL$1 + ' > template[data-source]');
62217
+ const otherTpls = roots[r].querySelectorAll(SEL + ' > template[data-source]');
61621
62218
  for (let i = 0; i < otherTpls.length; i++) {
61622
62219
  const otherTpl = otherTpls[i];
61623
62220
  if (otherTpl === tpl) continue;
@@ -61659,7 +62256,7 @@ class CodeElement {
61659
62256
  */
61660
62257
  init(container) {
61661
62258
  if (!container || !container.querySelectorAll) return;
61662
- const blocks = container.querySelectorAll(SEL$1);
62259
+ const blocks = container.querySelectorAll(SEL);
61663
62260
  if (blocks.length === 0) return;
61664
62261
  this.ensureCss();
61665
62262
  blocks.forEach(block => {
@@ -61840,7 +62437,7 @@ class CodeElement {
61840
62437
  markup filled the section on the page.
61841
62438
  */
61842
62439
  style.textContent = `
61843
- [${ATTR$1}] { min-height: 30px; -webkit-user-select: none; user-select: none; }
62440
+ [${ATTR}] { min-height: 30px; -webkit-user-select: none; user-select: none; }
61844
62441
  `;
61845
62442
  doc.head.appendChild(style);
61846
62443
  }
@@ -73966,7 +74563,7 @@ class Image$1 {
73966
74563
  <label class="label-checkbox">
73967
74564
  <input class="input-newwindow" id="__input_newwindow2" type="checkbox" /> ${util.out('Open new window')}
73968
74565
  </label>
73969
- <label class="label-checkbox" id="lblImageLinkOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
74566
+ <label class="label-checkbox mt-1" id="lblImageLinkOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
73970
74567
  <input class="input-openlightbox" id="__input_openlightbox2" type="checkbox" /> ${util.out('Open in a lightbox (for image, video or Youtube)')}
73971
74568
  </label>
73972
74569
  </div>
@@ -90676,7 +91273,16 @@ class ColorPickerKelir {
90676
91273
  }
90677
91274
 
90678
91275
  // Close the picker on scroll — it's positioned once, so scrolling detaches it.
90679
- const hideOnScroll = () => {
91276
+ //
91277
+ // The page moving under the picker is the only scroll that counts. A text
91278
+ // input scrolls its own content whenever the caret passes the edge — on
91279
+ // paste, on Delete, on Cmd+Left/Right — and that is a scroll event too,
91280
+ // which a capture listener on window receives even though it does not
91281
+ // bubble. Without this guard the picker closed while its own hex field
91282
+ // was being edited.
91283
+ const hideOnScroll = e => {
91284
+ const target = e && e.target;
91285
+ if (target && target.nodeType === 1 && popPicker.contains(target)) return;
90680
91286
  this.builder.util.hidePop(popPicker);
90681
91287
  window.removeEventListener('scroll', hideOnScroll, true);
90682
91288
  if (this.builder.win && this.builder.win !== window) this.builder.win.removeEventListener('scroll', hideOnScroll, true);
@@ -137250,6 +137856,90 @@ class ContentBuilder {
137250
137856
  openFilePicker(type, callback) {
137251
137857
  this.openAssetSelect(type, callback);
137252
137858
  }
137859
+
137860
+ /**
137861
+ * Is an asset manager reachable for this type of file? Mirrors the branches
137862
+ * openAsset() takes: either a picker page (filePicker / imageSelect / ...)
137863
+ * or the host's own onXSelectClick handler.
137864
+ */
137865
+ hasFilePicker(type) {
137866
+ if (type === 'media') return !!(this.onMediaSelectClick || this.onImageSelectClick || this.mediaSelect || this.imageSelect);
137867
+ if (type === 'video') return !!(this.onVideoSelectClick || this.videoSelect);
137868
+ if (type === 'audio') return !!(this.onAudioSelectClick || this.audioSelect);
137869
+ if (type === 'all') return !!(this.onFileSelectClick || this.fileSelect);
137870
+ return !!(this.onImageSelectClick || this.imageSelect);
137871
+ }
137872
+
137873
+ /**
137874
+ * Upload a file from the user's computer and hand its URL to `callback`.
137875
+ *
137876
+ * The per-type upload handler always exists — a builder configured without
137877
+ * one gets the data-URL fallback — so this is available whatever the host
137878
+ * set up. The URL comes back through returnUrl(), the same path the image
137879
+ * tool's upload button uses.
137880
+ */
137881
+ openFileUpload(type, callback) {
137882
+ const accept = type === 'video' ? 'video/*' : type === 'audio' ? 'audio/*' : type === 'media' ? 'image/*,video/*' : type === 'all' ? '*' : 'image/*';
137883
+ const handler = type === 'video' ? this.onVideoUpload : type === 'audio' ? this.onAudioUpload : type === 'media' ? this.onMediaUpload : type === 'all' ? this.onFileUpload : this.onImageUpload;
137884
+ if (!handler) return;
137885
+ const inpFile = document.createElement('input');
137886
+ inpFile.type = 'file';
137887
+ inpFile.accept = accept;
137888
+ inpFile.style.display = 'none';
137889
+ document.body.appendChild(inpFile);
137890
+ inpFile.addEventListener('change', async e => {
137891
+ if (!e.target.files || !e.target.files.length) {
137892
+ inpFile.remove();
137893
+ return;
137894
+ }
137895
+ this.onAssetUpload = url => {
137896
+ if (callback) callback(url);
137897
+ };
137898
+ await handler(e);
137899
+ inpFile.remove();
137900
+ });
137901
+ inpFile.click();
137902
+ }
137903
+
137904
+ /**
137905
+ * The file controls for a URL field in a plugin's settings: select from the
137906
+ * asset manager, and upload from the computer.
137907
+ *
137908
+ * Returns the buttons to append next to the input, in the order and with the
137909
+ * icons the editor's own image dialog uses — select first, upload last, and
137910
+ * select only where an asset manager is configured. A plugin appends what it
137911
+ * gets rather than deciding, so a builder without an asset manager never
137912
+ * shows a button that cannot open one.
137913
+ *
137914
+ * const [ ...buttons ] = builder.createFileButtons('media', (url) => {...});
137915
+ * row.append(input, ...buttons);
137916
+ *
137917
+ * `type` may be a function for a field whose kind follows another control
137918
+ * (a Media Type select, say) — it is then read on each click.
137919
+ */
137920
+ createFileButtons(type, callback) {
137921
+ const out = s => this.util.out(s);
137922
+ const fileType = () => typeof type === 'function' ? type() : type;
137923
+ const button = (label, icon, onClick) => {
137924
+ const btn = document.createElement('button');
137925
+ btn.type = 'button';
137926
+ btn.className = 'cbx-iconbtn';
137927
+ btn.title = label;
137928
+ btn.setAttribute('aria-label', label);
137929
+ btn.innerHTML = `<svg aria-hidden="true"><use xlink:href="#${icon}"></use></svg>`;
137930
+ btn.addEventListener('click', e => {
137931
+ e.preventDefault();
137932
+ onClick(btn);
137933
+ });
137934
+ return btn;
137935
+ };
137936
+ const buttons = [];
137937
+ if (this.hasFilePicker(fileType())) {
137938
+ buttons.push(button(out('Select'), 'icon-folder', btn => this.openFilePicker(fileType(), callback, btn)));
137939
+ }
137940
+ buttons.push(button(out('Upload'), 'icon-upload', () => this.openFileUpload(fileType(), callback)));
137941
+ return buttons;
137942
+ }
137253
137943
  openAssetSelect(targetAssetType, callback, defaultValue) {
137254
137944
  const inpUrl = document.createElement('input');
137255
137945
 
@@ -141372,59 +142062,6 @@ Please obtain a license at: https://innovastudio.com/contentbox`);
141372
142062
  }
141373
142063
  }
141374
142064
 
141375
- /*
141376
- codesource.js
141377
- ------------------------------------------------------------------
141378
- The two pure helpers ContentBox needs for [data-cb-code] blocks. Derived
141379
- from ContentBuilder's codeelement.js, which also defines a CodeElement
141380
- editor class — ContentBox never instantiates that, so it is not carried
141381
- here. Named for what this file is, not for where it came from.
141382
-
141383
- wrapCodeSource() is a string pre-pass. Call it on any HTML string about to
141384
- be inserted with createContextualFragment (which executes scripts). It
141385
- parses with DOMParser -- where scripts can never run -- and moves each
141386
- [data-cb-code] block's source into an inert <template data-source>, so
141387
- nothing has run by the time the content is in the DOM.
141388
- */
141389
- const ATTR = 'data-cb-code';
141390
- const SEL = '[' + ATTR + ']';
141391
- const TPL_SEL = ':scope > template[data-source]';
141392
- function escapeCodeSource(src) {
141393
- return src.replace(/<\/template>/gi, '<\\/template>');
141394
- }
141395
- /*
141396
- STRING PRE-PASS (inert).
141397
- Call this on any HTML *string* that is about to be inserted with
141398
- createContextualFragment (which executes scripts). It parses the string with
141399
- DOMParser -- where scripts can never run -- and moves each [data-cb-code]
141400
- block's source into an inert <template data-source>. After insertion nothing
141401
- has run; init()/render() brings them to life.
141402
-
141403
- Exported standalone as well, so it can be used where no builder instance
141404
- exists yet (ContentBox injects content before creating the editor).
141405
- */
141406
-
141407
- function wrapCodeSource(html) {
141408
- if (!html) return html;
141409
- if (html.indexOf(ATTR) === -1) return html; // fast path
141410
-
141411
- const parsed = new DOMParser().parseFromString(html, 'text/html');
141412
- parsed.querySelectorAll(SEL).forEach(block => {
141413
- // Already wrapped (round-trip within the editor) => skip
141414
- if (block.querySelector(TPL_SEL)) return; // Snippet form (data-src) is already inert => handled by init()
141415
-
141416
- if (block.hasAttribute('data-src')) return; // The block's innerHTML IS the pristine source (what flatten() wrote)
141417
-
141418
- const src = block.innerHTML;
141419
- block.innerHTML = '';
141420
- const tpl = parsed.createElement('template');
141421
- tpl.setAttribute('data-source', '');
141422
- tpl.innerHTML = escapeCodeSource(src);
141423
- block.appendChild(tpl);
141424
- });
141425
- return parsed.body.innerHTML;
141426
- }
141427
-
141428
142065
  class Draggable {
141429
142066
  constructor(opts = {}) {
141430
142067
  this.opts = opts;
@@ -164393,6 +165030,12 @@ class ContentBox {
164393
165030
  // HTML button on left sidebar
164394
165031
  sectionHtmlButton: true,
164395
165032
  // HTML button on section tool (edits the selected section only)
165033
+ sectionQuickTool: false,
165034
+ // Quick section controls under the Section breadcrumb of the panel. Off by default so nothing changes for people who know the panel; users can switch them on in Settings.
165035
+ sectionTool: true,
165036
+ // Section tools on the page (move, HTML, settings, remove). Control panel only; users can switch these off in Settings.
165037
+ boxTool: true,
165038
+ // Box tools on the page (background upload & select, box & module settings). Control panel only; users can switch these off in Settings.
164396
165039
  undoRedoButtons: true,
164397
165040
  // Undo & redo buttons on control panel
164398
165041
  contentSizeSlider: true,
@@ -167804,7 +168447,12 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
167804
168447
  localStorage.removeItem('_zoom'); // Reset zoom
167805
168448
 
167806
168449
  localStorage.removeItem('_livepreview');
167807
- localStorage.removeItem('_disableanimation');
168450
+ localStorage.removeItem('_disableanimation'); // The user's own pick in Settings stands in front of the shipped
168451
+ // default, like _fullviewwidth does.
168452
+
168453
+ if (localStorage.getItem('_sectionquicktool') !== null) this.sectionQuickTool = localStorage.getItem('_sectionquicktool') === '1';
168454
+ if (localStorage.getItem('_sectiontool') !== null) this.sectionTool = localStorage.getItem('_sectiontool') === '1';
168455
+ if (localStorage.getItem('_boxtool') !== null) this.boxTool = localStorage.getItem('_boxtool') === '1';
167808
168456
  }
167809
168457
 
167810
168458
  if (this.uploadFile) {
@@ -168060,7 +168708,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
168060
168708
  // scripts can run ([data-cb-code]); codeElement.init() then
168061
168709
  // renders them (called from applyBehaviorOn).
168062
168710
 
168063
- html = wrapCodeSource(html); // Render html content
168711
+ html = wrapCodeSource$1(html); // Render html content
168064
168712
 
168065
168713
  let range = document.createRange();
168066
168714
  this.wrapperEl.innerHTML = '';
@@ -170845,6 +171493,18 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
170845
171493
  this.editor.openAssetSelect(type, callback);
170846
171494
  }
170847
171495
 
171496
+ hasFilePicker(type) {
171497
+ return this.editor.hasFilePicker(type);
171498
+ }
171499
+
171500
+ openFileUpload(type, callback) {
171501
+ this.editor.openFileUpload(type, callback);
171502
+ }
171503
+
171504
+ createFileButtons(type, callback) {
171505
+ return this.editor.createFileButtons(type, callback);
171506
+ }
171507
+
170848
171508
  openColorPicker(currentColor, callback, btn) {
170849
171509
  this.editor.openColorPicker(currentColor, callback, btn);
170850
171510
  }
@@ -171003,7 +171663,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
171003
171663
  sectionSetup(section) {
171004
171664
  let tool = section.querySelector('.is-section-tool');
171005
171665
  if (tool) tool.parentNode.removeChild(tool);
171006
- dom.appendHtml(section, `<div class="is-section-tool">
171666
+ if (this.sectionToolEnabled) dom.appendHtml(section, `<div class="is-section-tool">
171007
171667
  <button type="button" class="btn-move-section-up" tabindex="-1" data-title="${out('Move Up')}" title="${out('Move Up')}">
171008
171668
  <svg class="is-icon-flex" style="transform:rotate(180deg)"><use xlink:href="#icon-arrow-down2"></use></svg>
171009
171669
  </button>
@@ -171029,7 +171689,8 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
171029
171689
  dom.appendHtml(section, '<div class="is-section-info">' + (sectionId ? `<div>#${sectionId}</div>` : '') + '</div>'); // this.editor.setTooltip(section);
171030
171690
  // Prepare for tooltip
171031
171691
 
171032
- let elms = section.querySelector('.is-section-tool').querySelectorAll('[title]');
171692
+ const sectionToolEl = section.querySelector('.is-section-tool');
171693
+ let elms = sectionToolEl ? sectionToolEl.querySelectorAll('[title]') : [];
171033
171694
  Array.prototype.forEach.call(elms, elm => {
171034
171695
  elm.setAttribute('data-title', elm.getAttribute('title'));
171035
171696
 
@@ -171040,15 +171701,15 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
171040
171701
  }
171041
171702
  });
171042
171703
  const btnSectionUp = section.querySelector('.btn-move-section-up');
171043
- btnSectionUp.addEventListener('click', () => {
171704
+ if (btnSectionUp) btnSectionUp.addEventListener('click', () => {
171044
171705
  this.section.sectionMove('up');
171045
171706
  });
171046
171707
  const btnSectionDown = section.querySelector('.btn-move-section-down');
171047
- btnSectionDown.addEventListener('click', () => {
171708
+ if (btnSectionDown) btnSectionDown.addEventListener('click', () => {
171048
171709
  this.section.sectionMove('down');
171049
171710
  });
171050
171711
  const btnSectionEdit = section.querySelector('.btn-section-edit');
171051
- btnSectionEdit.addEventListener('click', () => {
171712
+ if (btnSectionEdit) btnSectionEdit.addEventListener('click', () => {
171052
171713
  if (this.controlPanel) {
171053
171714
  this.controlpanel.open();
171054
171715
  this.controlpanel.select('section', true);
@@ -171062,7 +171723,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
171062
171723
  this.openSectionHtml(section);
171063
171724
  });
171064
171725
  const btnSectionRemove = section.querySelector('.btn-section-remove');
171065
- btnSectionRemove.addEventListener('click', () => {
171726
+ if (btnSectionRemove) btnSectionRemove.addEventListener('click', () => {
171066
171727
  const modal = document.querySelector('.is-modal.delsectionconfirm');
171067
171728
 
171068
171729
  if (this.settings.deleteConfirm) {
@@ -171221,6 +171882,41 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
171221
171882
  set activePlugin(el) {
171222
171883
  if (this.editor) this.editor.activePlugin = el;
171223
171884
  }
171885
+ /**
171886
+ * The page's own section and box tools, which the user can switch off in
171887
+ * Settings (sectionTool / boxTool).
171888
+ *
171889
+ * Only with the control panel: everything those tools do is on the panel
171890
+ * (the strip under the Section crumb, and the Box panel), so switching them
171891
+ * off costs nothing there. In legacy modal mode they are the only way in, so
171892
+ * the option is ignored and the tools always render.
171893
+ */
171894
+
171895
+
171896
+ get sectionToolEnabled() {
171897
+ return this.controlPanel ? this.sectionTool !== false : true;
171898
+ }
171899
+
171900
+ get boxToolEnabled() {
171901
+ return this.controlPanel ? this.boxTool !== false : true;
171902
+ }
171903
+ /** Re-run the section/box setup so the tools appear or disappear right away. */
171904
+
171905
+
171906
+ refreshPageTools() {
171907
+ if (!this.wrapperEl) return;
171908
+ this.wrapperEl.querySelectorAll('.is-section').forEach(section => {
171909
+ this.sectionSetup(section);
171910
+
171911
+ if (dom.hasClass(section, 'is-box')) {
171912
+ this.boxSetup(section);
171913
+ } else {
171914
+ section.querySelectorAll('.is-box').forEach(box => {
171915
+ this.boxSetup(box);
171916
+ });
171917
+ }
171918
+ });
171919
+ }
171224
171920
 
171225
171921
  boxSetup(box) {
171226
171922
  let tool = box.querySelector('.is-box-tool');
@@ -171240,7 +171936,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
171240
171936
  '</div>');
171241
171937
  */
171242
171938
 
171243
- dom.appendHtml(box, '<div class="is-box-tool" data-tool="1">' + '<button type="button" class="is-cover-upload" tabindex="-1" data-tooltip-top data-title="' + out('Upload Background') + '" title="' + out('Upload Background') + '" style="background:rgb(78 170 57)"><svg class="is-icon-flex" style="position: absolute;top: 8px;left: 8px;fill:rgb(255,255,255);"><use xlink:href="#ion-image"></use></svg></button>' + '<button type="button" class="is-select-image" tabindex="-1" data-tooltip-top data-title="' + out('Select Background') + '" title="' + out('Select Background') + '" style="background:rgb(251 173 27)"><svg class="is-icon-flex" style="color:#fff;width:15px;height:15px;"><use style="color: #fff;" xlink:href="#icon-folder2"></use></svg></button>' + '<button type="button" class="is-box-edit" tabindex="-1" data-tooltip-top data-title="' + out('Box Settings') + '" title="' + out('Box Settings') + '"><svg class="is-icon-flex"><use xlink:href="#ion-wrench"></use></svg></button>' + '<button type="button" class="is-module-edit" tabindex="-1" data-tooltip-top data-title="' + out('Module Settings') + '" title="' + out('Module Settings') + '" style="background:rgb(135 116 215)"><svg class="is-icon-flex"><use xlink:href="#ion-ios-gear"></use></svg></button>' + '</div>'); // Prepare for tooltip
171939
+ if (this.boxToolEnabled) dom.appendHtml(box, '<div class="is-box-tool" data-tool="1">' + '<button type="button" class="is-cover-upload" tabindex="-1" data-tooltip-top data-title="' + out('Upload Background') + '" title="' + out('Upload Background') + '" style="background:rgb(78 170 57)"><svg class="is-icon-flex" style="position: absolute;top: 8px;left: 8px;fill:rgb(255,255,255);"><use xlink:href="#ion-image"></use></svg></button>' + '<button type="button" class="is-select-image" tabindex="-1" data-tooltip-top data-title="' + out('Select Background') + '" title="' + out('Select Background') + '" style="background:rgb(251 173 27)"><svg class="is-icon-flex" style="color:#fff;width:15px;height:15px;"><use style="color: #fff;" xlink:href="#icon-folder2"></use></svg></button>' + '<button type="button" class="is-box-edit" tabindex="-1" data-tooltip-top data-title="' + out('Box Settings') + '" title="' + out('Box Settings') + '"><svg class="is-icon-flex"><use xlink:href="#ion-wrench"></use></svg></button>' + '<button type="button" class="is-module-edit" tabindex="-1" data-tooltip-top data-title="' + out('Module Settings') + '" title="' + out('Module Settings') + '" style="background:rgb(135 116 215)"><svg class="is-icon-flex"><use xlink:href="#ion-ios-gear"></use></svg></button>' + '</div>'); // Prepare for tooltip
171244
171940
 
171245
171941
  tool = box.querySelector('.is-box-tool');
171246
171942
 
@@ -171499,7 +172195,14 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
171499
172195
 
171500
172196
  if (el.closest('[data-cb-type]')) return false; // plugin — its own editable regions
171501
172197
 
171502
- if (el.closest('script, style, svg, noscript')) return false;
172198
+ if (el.closest('script, style, svg, noscript')) return false; // Inside a code element, only markup that is still verbatim in the
172199
+ // block's source can be edited: the render is discarded on save, so an
172200
+ // edit to anything the code built itself would not survive it. The
172201
+ // block itself is never text — its content is edited as code, and
172202
+ // writing to it here would overwrite the source template.
172203
+
172204
+ if (el.matches('[data-cb-code]')) return false;
172205
+ if (!codeSourceEditable(el)) return false;
171503
172206
  if (!this.hasOwnText(el)) return false;
171504
172207
 
171505
172208
  for (let parent = el.parentElement; parent && parent !== overlay; parent = parent.parentElement) {
@@ -171546,6 +172249,9 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
171546
172249
  if (el.closest('.is-container')) return false;
171547
172250
  if (el.closest('[data-html]')) return false;
171548
172251
  if (el.closest('[data-cb-type]')) return false;
172252
+ if (el.matches('[data-cb-code]')) return false; // code element — see isOverlayTextElement
172253
+
172254
+ if (!codeSourceEditable(el)) return false;
171549
172255
  if (el.tagName.toLowerCase() === 'img') return !el.hasAttribute('data-fixed');
171550
172256
  return this.hasBackgroundImage(el);
171551
172257
  }
@@ -172528,19 +173234,21 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
172528
173234
  sectionTool.style.transformOrigin = 'top right';
172529
173235
  }
172530
173236
 
172531
- const btnSectionUp = sectionTool.querySelector('.btn-move-section-up');
172532
- const btnSectionDown = sectionTool.querySelector('.btn-move-section-down');
172533
- const btnSectionRemove = sectionTool.querySelector('.btn-section-remove');
172534
-
172535
- if (box.offsetHeight < 160) {
172536
- btnSectionUp.style.display = 'none';
172537
- btnSectionDown.style.display = 'none';
172538
- btnSectionRemove.style.display = 'none';
172539
- sectionTool.style.top = '17px'; //'calc(50% - 17px)';
172540
- } else {
172541
- btnSectionUp.style.display = '';
172542
- btnSectionDown.style.display = '';
172543
- btnSectionRemove.style.display = '';
173237
+ if (sectionTool) {
173238
+ const btnSectionUp = sectionTool.querySelector('.btn-move-section-up');
173239
+ const btnSectionDown = sectionTool.querySelector('.btn-move-section-down');
173240
+ const btnSectionRemove = sectionTool.querySelector('.btn-section-remove');
173241
+
173242
+ if (box.offsetHeight < 160) {
173243
+ btnSectionUp.style.display = 'none';
173244
+ btnSectionDown.style.display = 'none';
173245
+ btnSectionRemove.style.display = 'none';
173246
+ sectionTool.style.top = '17px'; //'calc(50% - 17px)';
173247
+ } else {
173248
+ btnSectionUp.style.display = '';
173249
+ btnSectionDown.style.display = '';
173250
+ btnSectionRemove.style.display = '';
173251
+ }
172544
173252
  } //new box tool
172545
173253
 
172546
173254
 
@@ -173274,7 +173982,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
173274
173982
  // run ([data-cb-code]). Legacy [data-html] blocks are untouched.
173275
173983
 
173276
173984
 
173277
- html = wrapCodeSource(html); // Render html content
173985
+ html = wrapCodeSource$1(html); // Render html content
173278
173986
 
173279
173987
  let range = document.createRange();
173280
173988
  wrapper.innerHTML = '';