@innovastudio/contentbox 1.6.200 → 1.6.202
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +12 -0
- package/package.json +4 -3
- package/public/contentbox/contentbox.css +90 -34
- package/public/contentbox/contentbox.esm.js +1708 -942
- package/public/contentbox/contentbox.min.js +6 -6
- package/public/contentbox/lang/en.js +44 -0
- package/readme.txt +1 -1
|
@@ -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
|
-
|
|
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:
|
|
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>'
|
|
@@ -15505,10 +15507,12 @@ class PanelButton {
|
|
|
15505
15507
|
const inpHref = panel.querySelector('.inp-href');
|
|
15506
15508
|
inpHref.addEventListener('input', () => {
|
|
15507
15509
|
this.edit(btn => setHref$1(btn, inpHref.value), false);
|
|
15510
|
+
this.syncLinkDialog();
|
|
15508
15511
|
});
|
|
15509
15512
|
const chkNewTab = panel.querySelector('.chk-newtab');
|
|
15510
15513
|
chkNewTab.addEventListener('change', () => {
|
|
15511
15514
|
this.edit(btn => setNewTab$1(btn, chkNewTab.checked), false);
|
|
15515
|
+
this.syncLinkDialog();
|
|
15512
15516
|
}); // --- Colours ---
|
|
15513
15517
|
|
|
15514
15518
|
panel.querySelectorAll('[data-colorstate]').forEach(btn => {
|
|
@@ -15578,6 +15582,20 @@ class PanelButton {
|
|
|
15578
15582
|
this.getState();
|
|
15579
15583
|
this.repositionTools();
|
|
15580
15584
|
}
|
|
15585
|
+
} // ContentBuilder's link dialog edits the same href/target and can be open
|
|
15586
|
+
// alongside this panel. It reads its fields when it opens, so without this
|
|
15587
|
+
// its Ok would write back the link as it was before the panel touched it.
|
|
15588
|
+
// (The other direction is the editor's own: see elementhyperlink.js.)
|
|
15589
|
+
|
|
15590
|
+
|
|
15591
|
+
syncLinkDialog() {
|
|
15592
|
+
const btn = this.target();
|
|
15593
|
+
const modal = this.builder.editor.builderStuff.querySelector('.is-modal.createlink');
|
|
15594
|
+
if (!btn || !modal || !modal.classList.contains('active')) return;
|
|
15595
|
+
const url = modal.querySelector('.input-url');
|
|
15596
|
+
if (url && document.activeElement !== url) url.value = btn.getAttribute('href') || '';
|
|
15597
|
+
const newwindow = modal.querySelector('.input-newwindow');
|
|
15598
|
+
if (newwindow) newwindow.checked = btn.getAttribute('target') === '_blank';
|
|
15581
15599
|
} // Size and padding change the button's box, which leaves the floating
|
|
15582
15600
|
// tools pointing at where it used to be.
|
|
15583
15601
|
|
|
@@ -17802,6 +17820,926 @@ class PanelPlugin {
|
|
|
17802
17820
|
|
|
17803
17821
|
}
|
|
17804
17822
|
|
|
17823
|
+
/*
|
|
17824
|
+
codesource.js
|
|
17825
|
+
------------------------------------------------------------------
|
|
17826
|
+
The two pure helpers ContentBox needs for [data-cb-code] blocks. Derived
|
|
17827
|
+
from ContentBuilder's codeelement.js, which also defines a CodeElement
|
|
17828
|
+
editor class — ContentBox never instantiates that, so it is not carried
|
|
17829
|
+
here. Named for what this file is, not for where it came from.
|
|
17830
|
+
|
|
17831
|
+
wrapCodeSource() is a string pre-pass. Call it on any HTML string about to
|
|
17832
|
+
be inserted with createContextualFragment (which executes scripts). It
|
|
17833
|
+
parses with DOMParser -- where scripts can never run -- and moves each
|
|
17834
|
+
[data-cb-code] block's source into an inert <template data-source>, so
|
|
17835
|
+
nothing has run by the time the content is in the DOM.
|
|
17836
|
+
*/
|
|
17837
|
+
const ATTR$1 = 'data-cb-code';
|
|
17838
|
+
const SEL$1 = '[' + ATTR$1 + ']';
|
|
17839
|
+
const TPL_SEL$1 = ':scope > template[data-source]';
|
|
17840
|
+
function escapeCodeSource$1(src) {
|
|
17841
|
+
return src.replace(/<\/template>/gi, '<\\/template>');
|
|
17842
|
+
}
|
|
17843
|
+
/*
|
|
17844
|
+
STRING PRE-PASS (inert).
|
|
17845
|
+
Call this on any HTML *string* that is about to be inserted with
|
|
17846
|
+
createContextualFragment (which executes scripts). It parses the string with
|
|
17847
|
+
DOMParser -- where scripts can never run -- and moves each [data-cb-code]
|
|
17848
|
+
block's source into an inert <template data-source>. After insertion nothing
|
|
17849
|
+
has run; init()/render() brings them to life.
|
|
17850
|
+
|
|
17851
|
+
Exported standalone as well, so it can be used where no builder instance
|
|
17852
|
+
exists yet (ContentBox injects content before creating the editor).
|
|
17853
|
+
*/
|
|
17854
|
+
|
|
17855
|
+
function wrapCodeSource$1(html) {
|
|
17856
|
+
if (!html) return html;
|
|
17857
|
+
if (html.indexOf(ATTR$1) === -1) return html; // fast path
|
|
17858
|
+
|
|
17859
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
17860
|
+
parsed.querySelectorAll(SEL$1).forEach(block => {
|
|
17861
|
+
// Already wrapped (round-trip within the editor) => skip
|
|
17862
|
+
if (block.querySelector(TPL_SEL$1)) return; // Snippet form (data-src) is already inert => handled by init()
|
|
17863
|
+
|
|
17864
|
+
if (block.hasAttribute('data-src')) return; // The block's innerHTML IS the pristine source (what flatten() wrote)
|
|
17865
|
+
|
|
17866
|
+
const src = block.innerHTML;
|
|
17867
|
+
block.innerHTML = '';
|
|
17868
|
+
const tpl = parsed.createElement('template');
|
|
17869
|
+
tpl.setAttribute('data-source', '');
|
|
17870
|
+
tpl.innerHTML = escapeCodeSource$1(src);
|
|
17871
|
+
block.appendChild(tpl);
|
|
17872
|
+
});
|
|
17873
|
+
return parsed.body.innerHTML;
|
|
17874
|
+
}
|
|
17875
|
+
/*
|
|
17876
|
+
SOURCE MAPPING (live render -> template source).
|
|
17877
|
+
------------------------------------------------------------------
|
|
17878
|
+
A code element keeps two copies of itself: the <template data-source>,
|
|
17879
|
+
which is what gets saved, and the live render beside it, which is thrown
|
|
17880
|
+
away on every save. An edit written to the render alone therefore
|
|
17881
|
+
disappears the moment the page is saved.
|
|
17882
|
+
|
|
17883
|
+
These helpers find the node in the SOURCE that a live node was built from,
|
|
17884
|
+
so an edit can be written to both.
|
|
17885
|
+
|
|
17886
|
+
The mapping is positional: the render IS the source, parsed. A node's path
|
|
17887
|
+
of child indexes is the same in both -- except for <script> elements, which
|
|
17888
|
+
render() deletes once they have run, and the block's own <template>, which
|
|
17889
|
+
is never part of the render. Both are skipped on each side, so the two
|
|
17890
|
+
walks stay in step.
|
|
17891
|
+
|
|
17892
|
+
A node that the running code created or rewrote has no counterpart, or one
|
|
17893
|
+
holding something else. Those are NOT editable: the next render would
|
|
17894
|
+
overwrite the edit anyway. codeSourceEditable() is that test, and the
|
|
17895
|
+
panels use it to decide what they offer.
|
|
17896
|
+
*/
|
|
17897
|
+
// The code element `el` belongs to, or null.
|
|
17898
|
+
|
|
17899
|
+
function codeElementOf(el) {
|
|
17900
|
+
if (!el || !el.closest) return null;
|
|
17901
|
+
return el.closest(SEL$1);
|
|
17902
|
+
} // Element children as the RENDER sees them.
|
|
17903
|
+
|
|
17904
|
+
function renderedChildren(parent) {
|
|
17905
|
+
return Array.prototype.filter.call(parent.children, child => {
|
|
17906
|
+
const tag = child.tagName.toLowerCase();
|
|
17907
|
+
if (tag === 'script') return false; // deleted after running
|
|
17908
|
+
|
|
17909
|
+
if (tag === 'template' && child.hasAttribute('data-source')) return false; // the source itself
|
|
17910
|
+
|
|
17911
|
+
return true;
|
|
17912
|
+
});
|
|
17913
|
+
} // The node in the block's source that this live node came from, or null.
|
|
17914
|
+
|
|
17915
|
+
|
|
17916
|
+
function codeSourceNodeFor(el) {
|
|
17917
|
+
const block = codeElementOf(el);
|
|
17918
|
+
if (!block || el === block) return null;
|
|
17919
|
+
const tpl = block.querySelector(TPL_SEL$1);
|
|
17920
|
+
if (!tpl || !tpl.content) return null; // Path from the block down to `el`.
|
|
17921
|
+
|
|
17922
|
+
const path = [];
|
|
17923
|
+
|
|
17924
|
+
for (let node = el; node && node !== block; node = node.parentElement) {
|
|
17925
|
+
const parent = node.parentElement;
|
|
17926
|
+
if (!parent) return null; // detached
|
|
17927
|
+
|
|
17928
|
+
const index = renderedChildren(parent).indexOf(node);
|
|
17929
|
+
if (index === -1) return null;
|
|
17930
|
+
path.unshift({
|
|
17931
|
+
index: index,
|
|
17932
|
+
tag: node.tagName
|
|
17933
|
+
});
|
|
17934
|
+
}
|
|
17935
|
+
|
|
17936
|
+
if (!path.length) return null; // The same walk through the source.
|
|
17937
|
+
|
|
17938
|
+
let node = tpl.content;
|
|
17939
|
+
|
|
17940
|
+
for (let i = 0; i < path.length; i++) {
|
|
17941
|
+
node = renderedChildren(node)[path[i].index];
|
|
17942
|
+
if (!node) return null;
|
|
17943
|
+
if (node.tagName !== path[i].tag) return null; // the code rebuilt this part
|
|
17944
|
+
}
|
|
17945
|
+
|
|
17946
|
+
return node;
|
|
17947
|
+
} // May an edit to this live node be written back to the source?
|
|
17948
|
+
|
|
17949
|
+
function codeSourceEditable(el) {
|
|
17950
|
+
const block = codeElementOf(el);
|
|
17951
|
+
if (!block) return true; // not a code element's business
|
|
17952
|
+
|
|
17953
|
+
if (el === block) return true; // the block itself is ordinary markup, saved as it is
|
|
17954
|
+
|
|
17955
|
+
const src = codeSourceNodeFor(el);
|
|
17956
|
+
if (!src) return false; // It must still hold what the source says it holds. Where the code rewrote
|
|
17957
|
+
// it (a clock's own text, a lazily swapped image) an edit here would be
|
|
17958
|
+
// undone by the next render, so it is not offered at all.
|
|
17959
|
+
|
|
17960
|
+
if (src.innerHTML !== el.innerHTML) return false;
|
|
17961
|
+
if (el.tagName === 'IMG' && src.getAttribute('src') !== el.getAttribute('src')) return false;
|
|
17962
|
+
return true;
|
|
17963
|
+
}
|
|
17964
|
+
/*
|
|
17965
|
+
Write an edit made to the live node into its source counterpart, so it
|
|
17966
|
+
survives the save. A no-op (returns false) for anything outside a code
|
|
17967
|
+
element, or for a node the code built itself.
|
|
17968
|
+
|
|
17969
|
+
`spec` names what to carry over, and only that:
|
|
17970
|
+
{ html: true } the node's content
|
|
17971
|
+
{ attrs: ['src','alt'] } those attributes
|
|
17972
|
+
{ styles: ['background-image'] } those inline style properties
|
|
17973
|
+
{ fx: true } every data-fx-* attribute
|
|
17974
|
+
|
|
17975
|
+
Deliberately never the whole element: a running script and the motion
|
|
17976
|
+
runtime both write to the live node (ids, aria state, inline transforms),
|
|
17977
|
+
and none of that belongs in the saved source.
|
|
17978
|
+
*/
|
|
17979
|
+
|
|
17980
|
+
function syncToCodeSource(el, spec) {
|
|
17981
|
+
if (!el || !spec) return false;
|
|
17982
|
+
const src = codeSourceNodeFor(el);
|
|
17983
|
+
if (!src) return false;
|
|
17984
|
+
if (spec.html) src.innerHTML = el.innerHTML;
|
|
17985
|
+
if (spec.attrs) spec.attrs.forEach(name => {
|
|
17986
|
+
const value = el.getAttribute(name);
|
|
17987
|
+
if (value === null) src.removeAttribute(name);else src.setAttribute(name, value);
|
|
17988
|
+
});
|
|
17989
|
+
if (spec.styles) spec.styles.forEach(prop => {
|
|
17990
|
+
src.style.setProperty(prop, el.style.getPropertyValue(prop), el.style.getPropertyPriority(prop));
|
|
17991
|
+
});
|
|
17992
|
+
|
|
17993
|
+
if (spec.fx) {
|
|
17994
|
+
Array.prototype.slice.call(src.attributes).forEach(attr => {
|
|
17995
|
+
if (attr.name.indexOf('data-fx-') === 0) src.removeAttribute(attr.name);
|
|
17996
|
+
});
|
|
17997
|
+
Array.prototype.slice.call(el.attributes).forEach(attr => {
|
|
17998
|
+
if (attr.name.indexOf('data-fx-') === 0) src.setAttribute(attr.name, attr.value);
|
|
17999
|
+
});
|
|
18000
|
+
}
|
|
18001
|
+
|
|
18002
|
+
return true;
|
|
18003
|
+
}
|
|
18004
|
+
|
|
18005
|
+
/*
|
|
18006
|
+
* motion-presets.js — the declarative vocabulary of the Motion panel.
|
|
18007
|
+
*
|
|
18008
|
+
* Everything the Motion UI can apply lives here as DATA, so adding a new
|
|
18009
|
+
* effect is a table edit, never a code edit. Nothing in this file touches
|
|
18010
|
+
* the DOM or the builder.
|
|
18011
|
+
*
|
|
18012
|
+
* Three kinds of thing, because Kinetra genuinely has three:
|
|
18013
|
+
*
|
|
18014
|
+
* 1. RECIPES — canned percent keyframes (data-fx-0 … data-fx-100).
|
|
18015
|
+
* "Fade up", "Zoom in", "Wipe in". These are just a
|
|
18016
|
+
* starting set of keyframes; the Motion dock (phase 2)
|
|
18017
|
+
* edits them freely afterwards.
|
|
18018
|
+
* 2. PRESETS — single-attribute effects that are NOT keyframes:
|
|
18019
|
+
* parallax, horizontal, split, typewriter, draw, marquee.
|
|
18020
|
+
* Each is identified by one "key" attribute.
|
|
18021
|
+
* 3. POINTER — micro-interactions (tilt, magnetic). Not scene-bound
|
|
18022
|
+
* at all; they are per-element toggles.
|
|
18023
|
+
*
|
|
18024
|
+
* Option types:
|
|
18025
|
+
* 'text' — free text input, written verbatim to the attribute
|
|
18026
|
+
* 'select' — dropdown; value written to the attribute
|
|
18027
|
+
* 'toggle' — VALUELESS boolean attribute (present / absent)
|
|
18028
|
+
*/
|
|
18029
|
+
|
|
18030
|
+
/* Ease names are NOT listed here. motion-ease.js derives them from kinetra's
|
|
18031
|
+
* exported `easings` table, so there is exactly one source of truth and a copy
|
|
18032
|
+
* can't drift when someone adds or renames a curve. */
|
|
18033
|
+
|
|
18034
|
+
/* ------------------------------------------------------------------
|
|
18035
|
+
* 1. KEYFRAME RECIPES
|
|
18036
|
+
*
|
|
18037
|
+
* `frames` maps percent -> declaration string. Applying a recipe REPLACES
|
|
18038
|
+
* every existing data-fx-N on the element (a recipe is a fresh start, not
|
|
18039
|
+
* an overlay), and sets data-fx-ease.
|
|
18040
|
+
*
|
|
18041
|
+
* Compound-string rule: both ends of a clip-path/box-shadow/etc. must share
|
|
18042
|
+
* the same token count, or Kinetra snaps instead of tweening. Every recipe
|
|
18043
|
+
* below already obeys that — keep it that way when adding new ones.
|
|
18044
|
+
* ------------------------------------------------------------------ */
|
|
18045
|
+
const RECIPES = [{
|
|
18046
|
+
id: 'fade',
|
|
18047
|
+
duration: 0.6,
|
|
18048
|
+
label: 'Fade in',
|
|
18049
|
+
ease: 'easeOut',
|
|
18050
|
+
frames: {
|
|
18051
|
+
0: 'opacity:0',
|
|
18052
|
+
100: 'opacity:1'
|
|
18053
|
+
}
|
|
18054
|
+
}, {
|
|
18055
|
+
id: 'fade-up',
|
|
18056
|
+
duration: 0.6,
|
|
18057
|
+
label: 'Fade up',
|
|
18058
|
+
ease: 'smooth',
|
|
18059
|
+
// kinetra's closest match to CSS ease-in-out
|
|
18060
|
+
frames: {
|
|
18061
|
+
0: 'opacity:0; transform:translateY(70px)',
|
|
18062
|
+
// .is-fadeInUp was 70px
|
|
18063
|
+
100: 'opacity:1; transform:translateY(0px)'
|
|
18064
|
+
}
|
|
18065
|
+
}, {
|
|
18066
|
+
id: 'fade-down',
|
|
18067
|
+
duration: 0.6,
|
|
18068
|
+
label: 'Fade down',
|
|
18069
|
+
ease: 'smooth',
|
|
18070
|
+
frames: {
|
|
18071
|
+
0: 'opacity:0; transform:translateY(-70px)',
|
|
18072
|
+
100: 'opacity:1; transform:translateY(0px)'
|
|
18073
|
+
}
|
|
18074
|
+
}, {
|
|
18075
|
+
id: 'fade-left',
|
|
18076
|
+
duration: 0.6,
|
|
18077
|
+
label: 'From left',
|
|
18078
|
+
ease: 'smooth',
|
|
18079
|
+
frames: {
|
|
18080
|
+
0: 'opacity:0; transform:translateX(-70px)',
|
|
18081
|
+
100: 'opacity:1; transform:translateX(0px)'
|
|
18082
|
+
}
|
|
18083
|
+
}, {
|
|
18084
|
+
id: 'fade-right',
|
|
18085
|
+
duration: 0.6,
|
|
18086
|
+
label: 'From right',
|
|
18087
|
+
ease: 'smooth',
|
|
18088
|
+
frames: {
|
|
18089
|
+
0: 'opacity:0; transform:translateX(70px)',
|
|
18090
|
+
100: 'opacity:1; transform:translateX(0px)'
|
|
18091
|
+
}
|
|
18092
|
+
}, {
|
|
18093
|
+
id: 'zoom-in',
|
|
18094
|
+
duration: 0.8,
|
|
18095
|
+
label: 'Zoom in',
|
|
18096
|
+
ease: 'smooth',
|
|
18097
|
+
frames: {
|
|
18098
|
+
0: 'opacity:0; transform:scale(0.8)',
|
|
18099
|
+
// .is-zoomIn was scale(.8)
|
|
18100
|
+
100: 'opacity:1; transform:scale(1)'
|
|
18101
|
+
}
|
|
18102
|
+
}, {
|
|
18103
|
+
id: 'zoom-out',
|
|
18104
|
+
duration: 0.8,
|
|
18105
|
+
label: 'Zoom out',
|
|
18106
|
+
ease: 'smooth',
|
|
18107
|
+
frames: {
|
|
18108
|
+
0: 'opacity:0; transform:scale(1.2)',
|
|
18109
|
+
// .is-zoomOut was scale(1.2)
|
|
18110
|
+
100: 'opacity:1; transform:scale(1)'
|
|
18111
|
+
}
|
|
18112
|
+
}, {
|
|
18113
|
+
id: 'slide-up',
|
|
18114
|
+
duration: 0.6,
|
|
18115
|
+
label: 'Slide up',
|
|
18116
|
+
ease: 'smooth',
|
|
18117
|
+
frames: {
|
|
18118
|
+
0: 'transform:translateY(70px)',
|
|
18119
|
+
100: 'transform:translateY(0px)'
|
|
18120
|
+
}
|
|
18121
|
+
}, {
|
|
18122
|
+
id: 'slide-down',
|
|
18123
|
+
duration: 0.6,
|
|
18124
|
+
label: 'Slide down',
|
|
18125
|
+
ease: 'smooth',
|
|
18126
|
+
frames: {
|
|
18127
|
+
0: 'transform:translateY(-70px)',
|
|
18128
|
+
100: 'transform:translateY(0px)'
|
|
18129
|
+
}
|
|
18130
|
+
}, {
|
|
18131
|
+
id: 'slide-left',
|
|
18132
|
+
duration: 0.6,
|
|
18133
|
+
label: 'Slide from left',
|
|
18134
|
+
ease: 'smooth',
|
|
18135
|
+
frames: {
|
|
18136
|
+
0: 'transform:translateX(-70px)',
|
|
18137
|
+
100: 'transform:translateX(0px)'
|
|
18138
|
+
}
|
|
18139
|
+
}, {
|
|
18140
|
+
id: 'slide-right',
|
|
18141
|
+
duration: 0.6,
|
|
18142
|
+
label: 'Slide from right',
|
|
18143
|
+
ease: 'smooth',
|
|
18144
|
+
frames: {
|
|
18145
|
+
0: 'transform:translateX(70px)',
|
|
18146
|
+
100: 'transform:translateX(0px)'
|
|
18147
|
+
}
|
|
18148
|
+
}, {
|
|
18149
|
+
id: 'blur-in',
|
|
18150
|
+
duration: 0.6,
|
|
18151
|
+
label: 'Blur in',
|
|
18152
|
+
ease: 'easeOut',
|
|
18153
|
+
frames: {
|
|
18154
|
+
0: 'opacity:0; filter:blur(12px)',
|
|
18155
|
+
100: 'opacity:1; filter:blur(0px)'
|
|
18156
|
+
}
|
|
18157
|
+
}, {
|
|
18158
|
+
id: 'flip-x',
|
|
18159
|
+
label: 'Flip X',
|
|
18160
|
+
duration: 0.8,
|
|
18161
|
+
ease: 'smooth',
|
|
18162
|
+
frames: {
|
|
18163
|
+
0: 'opacity:0; transform:perspective(2500px) rotateX(100deg)',
|
|
18164
|
+
100: 'opacity:1; transform:perspective(2500px) rotateX(0deg)'
|
|
18165
|
+
}
|
|
18166
|
+
}, {
|
|
18167
|
+
id: 'flip-y',
|
|
18168
|
+
label: 'Flip Y',
|
|
18169
|
+
duration: 0.8,
|
|
18170
|
+
ease: 'smooth',
|
|
18171
|
+
frames: {
|
|
18172
|
+
0: 'opacity:0; transform:perspective(2500px) rotateY(100deg)',
|
|
18173
|
+
100: 'opacity:1; transform:perspective(2500px) rotateY(0deg)'
|
|
18174
|
+
}
|
|
18175
|
+
}, {
|
|
18176
|
+
id: 'pulse',
|
|
18177
|
+
label: 'Pulse',
|
|
18178
|
+
duration: 0.6,
|
|
18179
|
+
ease: 'linear',
|
|
18180
|
+
frames: {
|
|
18181
|
+
0: 'opacity:0; transform:scale(0.9)',
|
|
18182
|
+
50: 'opacity:1; transform:scale(1.05)',
|
|
18183
|
+
100: 'opacity:1; transform:scale(1)'
|
|
18184
|
+
}
|
|
18185
|
+
}, {
|
|
18186
|
+
id: 'bounce-in',
|
|
18187
|
+
label: 'Bounce in',
|
|
18188
|
+
duration: 0.6,
|
|
18189
|
+
ease: 'linear',
|
|
18190
|
+
frames: {
|
|
18191
|
+
0: 'opacity:0; transform:translateX(0px)',
|
|
18192
|
+
40: 'opacity:1; transform:translateX(-20px)',
|
|
18193
|
+
60: 'opacity:1; transform:translateX(0px)',
|
|
18194
|
+
80: 'opacity:1; transform:translateX(-15px)',
|
|
18195
|
+
100: 'opacity:1; transform:translateX(0px)'
|
|
18196
|
+
}
|
|
18197
|
+
}, {
|
|
18198
|
+
id: 'wipe-right',
|
|
18199
|
+
duration: 0.6,
|
|
18200
|
+
label: 'Wipe in',
|
|
18201
|
+
ease: 'expoOut',
|
|
18202
|
+
frames: {
|
|
18203
|
+
0: 'opacity:1; clip-path:inset(0% 100% 0% 0%)',
|
|
18204
|
+
100: 'opacity:1; clip-path:inset(0% 0% 0% 0%)'
|
|
18205
|
+
}
|
|
18206
|
+
}, {
|
|
18207
|
+
id: 'rise-mask',
|
|
18208
|
+
duration: 0.7,
|
|
18209
|
+
label: 'Rise + mask',
|
|
18210
|
+
ease: 'expoOut',
|
|
18211
|
+
frames: {
|
|
18212
|
+
0: 'opacity:0; transform:translateY(70px); clip-path:inset(100% 0% 0% 0%)',
|
|
18213
|
+
100: 'opacity:1; transform:translateY(0px); clip-path:inset(0% 0% 0% 0%)'
|
|
18214
|
+
}
|
|
18215
|
+
}, {
|
|
18216
|
+
id: 'rotate-in',
|
|
18217
|
+
duration: 0.6,
|
|
18218
|
+
label: 'Rotate in',
|
|
18219
|
+
ease: 'backOut',
|
|
18220
|
+
frames: {
|
|
18221
|
+
0: 'opacity:0; transform:rotate(-8deg) scale(0.94)',
|
|
18222
|
+
100: 'opacity:1; transform:rotate(0deg) scale(1)'
|
|
18223
|
+
}
|
|
18224
|
+
}, {
|
|
18225
|
+
id: 'drift',
|
|
18226
|
+
duration: 0.6,
|
|
18227
|
+
label: 'Drift through',
|
|
18228
|
+
ease: 'linear',
|
|
18229
|
+
noCascade: true,
|
|
18230
|
+
// a through-motion, not an entrance — pointless in a cascade
|
|
18231
|
+
frames: {
|
|
18232
|
+
0: 'transform:translateY(60px)',
|
|
18233
|
+
100: 'transform:translateY(-60px)'
|
|
18234
|
+
}
|
|
18235
|
+
}, {
|
|
18236
|
+
id: 'hold-scale',
|
|
18237
|
+
duration: 0.6,
|
|
18238
|
+
label: 'Scale back + hold',
|
|
18239
|
+
ease: 'linear',
|
|
18240
|
+
note: 'Card-stack layer: shrinks as the next layer covers it.',
|
|
18241
|
+
frames: {
|
|
18242
|
+
0: 'transform:scale(1)',
|
|
18243
|
+
50: 'transform:scale(0.9)',
|
|
18244
|
+
100: 'transform:scale(0.9)'
|
|
18245
|
+
}
|
|
18246
|
+
}];
|
|
18247
|
+
/* ------------------------------------------------------------------
|
|
18248
|
+
* 2. SCROLL / TEXT PRESETS (single-attribute effects)
|
|
18249
|
+
* ------------------------------------------------------------------ */
|
|
18250
|
+
|
|
18251
|
+
const PRESETS = [{
|
|
18252
|
+
id: 'parallax',
|
|
18253
|
+
label: 'Parallax',
|
|
18254
|
+
group: 'scroll',
|
|
18255
|
+
key: 'data-fx-parallax',
|
|
18256
|
+
defaults: {
|
|
18257
|
+
'data-fx-parallax': '120'
|
|
18258
|
+
},
|
|
18259
|
+
hint: 'Drifts as it scrolls. Works in any section.',
|
|
18260
|
+
options: [{
|
|
18261
|
+
attr: 'data-fx-parallax',
|
|
18262
|
+
label: 'Travel',
|
|
18263
|
+
type: 'text',
|
|
18264
|
+
placeholder: '120 (px) or 20%'
|
|
18265
|
+
}]
|
|
18266
|
+
}, {
|
|
18267
|
+
id: 'horizontal',
|
|
18268
|
+
label: 'Horizontal track',
|
|
18269
|
+
group: 'scroll',
|
|
18270
|
+
key: 'data-fx-horizontal',
|
|
18271
|
+
pinnedOnly: true,
|
|
18272
|
+
defaults: {
|
|
18273
|
+
'data-fx-horizontal': ''
|
|
18274
|
+
},
|
|
18275
|
+
hint: 'Vertical scroll drives a sideways track. Needs a pinned scene, and children with flex:0 0 auto.',
|
|
18276
|
+
options: []
|
|
18277
|
+
}, {
|
|
18278
|
+
id: 'split',
|
|
18279
|
+
label: 'Split text',
|
|
18280
|
+
group: 'text',
|
|
18281
|
+
key: 'data-fx-split',
|
|
18282
|
+
defaults: {
|
|
18283
|
+
'data-fx-split': 'words'
|
|
18284
|
+
},
|
|
18285
|
+
hint: 'Staggered word/char entrance. Formatting inside the element is preserved.',
|
|
18286
|
+
options: [{
|
|
18287
|
+
attr: 'data-fx-split',
|
|
18288
|
+
label: 'Unit',
|
|
18289
|
+
type: 'select',
|
|
18290
|
+
choices: [['words', 'Words'], ['chars', 'Characters']]
|
|
18291
|
+
}, {
|
|
18292
|
+
attr: 'data-fx-split-mask',
|
|
18293
|
+
label: 'Mask reveal',
|
|
18294
|
+
type: 'toggle'
|
|
18295
|
+
}, {
|
|
18296
|
+
attr: 'data-fx-split-stagger',
|
|
18297
|
+
label: 'Stagger',
|
|
18298
|
+
type: 'text',
|
|
18299
|
+
placeholder: '0.08 (sec)'
|
|
18300
|
+
}, {
|
|
18301
|
+
attr: 'data-fx-split-y',
|
|
18302
|
+
label: 'Travel',
|
|
18303
|
+
type: 'text',
|
|
18304
|
+
placeholder: '115% or 18px'
|
|
18305
|
+
}, {
|
|
18306
|
+
attr: 'data-fx-split-scrub',
|
|
18307
|
+
label: 'Scrub through scene',
|
|
18308
|
+
type: 'toggle',
|
|
18309
|
+
hint: 'Best in a pinned scene — units brighten in reading order as you scroll.'
|
|
18310
|
+
}, {
|
|
18311
|
+
attr: 'data-fx-split-from',
|
|
18312
|
+
label: 'Dim opacity',
|
|
18313
|
+
type: 'text',
|
|
18314
|
+
placeholder: '0.12',
|
|
18315
|
+
requires: 'data-fx-split-scrub'
|
|
18316
|
+
}]
|
|
18317
|
+
}, {
|
|
18318
|
+
id: 'typewriter',
|
|
18319
|
+
label: 'Typewriter',
|
|
18320
|
+
group: 'text',
|
|
18321
|
+
key: 'data-fx-typewriter',
|
|
18322
|
+
defaults: {
|
|
18323
|
+
'data-fx-typewriter': ''
|
|
18324
|
+
},
|
|
18325
|
+
hint: 'The scrollbar types the text. Plain text only.',
|
|
18326
|
+
options: [{
|
|
18327
|
+
attr: 'data-fx-typewriter-until',
|
|
18328
|
+
label: 'Done at',
|
|
18329
|
+
type: 'text',
|
|
18330
|
+
placeholder: '0.85 (progress)'
|
|
18331
|
+
}]
|
|
18332
|
+
}, {
|
|
18333
|
+
id: 'marquee',
|
|
18334
|
+
label: 'Marquee',
|
|
18335
|
+
group: 'text',
|
|
18336
|
+
key: 'data-fx-marquee',
|
|
18337
|
+
defaults: {
|
|
18338
|
+
'data-fx-marquee': ''
|
|
18339
|
+
},
|
|
18340
|
+
hint: 'Drifting band. Content should be a repeating strip.',
|
|
18341
|
+
options: [{
|
|
18342
|
+
attr: 'data-fx-marquee-speed',
|
|
18343
|
+
label: 'Speed',
|
|
18344
|
+
type: 'text',
|
|
18345
|
+
placeholder: '90 (px/sec)'
|
|
18346
|
+
}, {
|
|
18347
|
+
attr: 'data-fx-marquee-lean',
|
|
18348
|
+
label: 'Lean with velocity',
|
|
18349
|
+
type: 'toggle'
|
|
18350
|
+
}]
|
|
18351
|
+
}, {
|
|
18352
|
+
id: 'path',
|
|
18353
|
+
label: 'Motion path',
|
|
18354
|
+
group: 'scroll',
|
|
18355
|
+
key: 'data-fx-path',
|
|
18356
|
+
defaults: {
|
|
18357
|
+
'data-fx-path': '-200,60; 0,-80; 200,60'
|
|
18358
|
+
},
|
|
18359
|
+
hint: 'Travels a smooth curve through waypoints. Edit the stops in the Motion dock; px offsets from the element\'s resting position.',
|
|
18360
|
+
options: [{
|
|
18361
|
+
attr: 'data-fx-path',
|
|
18362
|
+
label: 'Waypoints',
|
|
18363
|
+
type: 'text',
|
|
18364
|
+
placeholder: 'x,y; x,y @tx,ty; …'
|
|
18365
|
+
}, {
|
|
18366
|
+
attr: 'data-fx-path-dwell',
|
|
18367
|
+
label: 'Dwell',
|
|
18368
|
+
type: 'text',
|
|
18369
|
+
placeholder: '0.78 (0 = constant, 1 = stops)'
|
|
18370
|
+
}, {
|
|
18371
|
+
attr: 'data-fx-path-curve',
|
|
18372
|
+
label: 'Curve',
|
|
18373
|
+
type: 'text',
|
|
18374
|
+
placeholder: '1 (0 = straight lines)'
|
|
18375
|
+
}, {
|
|
18376
|
+
attr: 'data-fx-path-rotate',
|
|
18377
|
+
label: 'Auto-rotate along path',
|
|
18378
|
+
type: 'toggle'
|
|
18379
|
+
}, {
|
|
18380
|
+
attr: 'data-fx-path-rotate',
|
|
18381
|
+
label: 'Rotate offset',
|
|
18382
|
+
type: 'text',
|
|
18383
|
+
placeholder: '0 (deg; art faces right 0, up 90)',
|
|
18384
|
+
requires: 'data-fx-path-rotate'
|
|
18385
|
+
}]
|
|
18386
|
+
}, {
|
|
18387
|
+
id: 'draw',
|
|
18388
|
+
label: 'Draw SVG',
|
|
18389
|
+
group: 'scroll',
|
|
18390
|
+
key: 'data-fx-draw',
|
|
18391
|
+
defaults: {
|
|
18392
|
+
'data-fx-draw': ''
|
|
18393
|
+
},
|
|
18394
|
+
tags: ['svg', 'path'],
|
|
18395
|
+
hint: 'Paths draw themselves as the scene scrubs. Put on an <svg> or a <path>.',
|
|
18396
|
+
options: []
|
|
18397
|
+
}];
|
|
18398
|
+
/* ------------------------------------------------------------------
|
|
18399
|
+
* 3. POINTER MICRO-INTERACTIONS
|
|
18400
|
+
* ------------------------------------------------------------------ */
|
|
18401
|
+
|
|
18402
|
+
const POINTER = [{
|
|
18403
|
+
id: 'tilt',
|
|
18404
|
+
label: 'Tilt',
|
|
18405
|
+
key: 'data-fx-tilt',
|
|
18406
|
+
defaults: {
|
|
18407
|
+
'data-fx-tilt': ''
|
|
18408
|
+
},
|
|
18409
|
+
options: [{
|
|
18410
|
+
attr: 'data-fx-tilt-max',
|
|
18411
|
+
label: 'Max angle',
|
|
18412
|
+
type: 'text',
|
|
18413
|
+
placeholder: '14 (deg)'
|
|
18414
|
+
}]
|
|
18415
|
+
}, {
|
|
18416
|
+
id: 'magnetic',
|
|
18417
|
+
label: 'Magnetic',
|
|
18418
|
+
key: 'data-fx-magnetic',
|
|
18419
|
+
defaults: {
|
|
18420
|
+
'data-fx-magnetic': ''
|
|
18421
|
+
},
|
|
18422
|
+
options: [{
|
|
18423
|
+
attr: 'data-fx-magnetic-strength',
|
|
18424
|
+
label: 'Strength',
|
|
18425
|
+
type: 'text',
|
|
18426
|
+
placeholder: '0.35'
|
|
18427
|
+
}]
|
|
18428
|
+
}];
|
|
18429
|
+
/* ------------------------------------------------------------------
|
|
18430
|
+
* Helpers — every consumer of this table should go through these, so the
|
|
18431
|
+
* "one system per element" and "attributes only" rules stay in one place.
|
|
18432
|
+
* ------------------------------------------------------------------ */
|
|
18433
|
+
|
|
18434
|
+
/* High-traffic legacy attributes. Mirrors LEGACY_PROBE in contentbox-effects.js:
|
|
18435
|
+
* an element carrying any of these is owned by the old timeline and must not
|
|
18436
|
+
* receive data-fx-*. */
|
|
18437
|
+
|
|
18438
|
+
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'];
|
|
18439
|
+
/* THE SECOND LEGACY SYSTEM.
|
|
18440
|
+
*
|
|
18441
|
+
* animatescroll.js's "On View" wizard never wrote attributes at all — it wrote
|
|
18442
|
+
* CSS CLASSES, toggled at runtime by inViewSetup()'s IntersectionObserver. So
|
|
18443
|
+
* an element can be legacy-animated while carrying no legacy attribute
|
|
18444
|
+
* whatsoever, which means contentbox-effects' hasLegacyAttrs() guard sails
|
|
18445
|
+
* straight past it. An element with both class="is-animated is-fadeInUp" and
|
|
18446
|
+
* data-fx-0 would get animated TWICE, by two systems both writing transform
|
|
18447
|
+
* and opacity.
|
|
18448
|
+
*
|
|
18449
|
+
* This list is lifted verbatim from animatescroll.js's cleanupBasic(). */
|
|
18450
|
+
|
|
18451
|
+
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'];
|
|
18452
|
+
function hasLegacyOnView(el) {
|
|
18453
|
+
if (!el || !el.classList) return false;
|
|
18454
|
+
return el.classList.contains('is-animated') || el.classList.contains('is-scale-animated');
|
|
18455
|
+
}
|
|
18456
|
+
/** Strip every trace of the class-based system from one element. */
|
|
18457
|
+
|
|
18458
|
+
function stripLegacyOnView(el) {
|
|
18459
|
+
if (!el || !el.classList) return false;
|
|
18460
|
+
let touched = false;
|
|
18461
|
+
LEGACY_ONVIEW_CLASSES.forEach(c => {
|
|
18462
|
+
if (el.classList.contains(c)) {
|
|
18463
|
+
el.classList.remove(c);
|
|
18464
|
+
touched = true;
|
|
18465
|
+
}
|
|
18466
|
+
}); // delay-0ms … delay-2000ms
|
|
18467
|
+
|
|
18468
|
+
Array.from(el.classList).forEach(c => {
|
|
18469
|
+
if (/^delay-\d+ms$/.test(c)) {
|
|
18470
|
+
el.classList.remove(c);
|
|
18471
|
+
touched = true;
|
|
18472
|
+
}
|
|
18473
|
+
});
|
|
18474
|
+
if (el.getAttribute('class') === '') el.removeAttribute('class');
|
|
18475
|
+
return touched;
|
|
18476
|
+
}
|
|
18477
|
+
/** Any element the OLD systems own — attributes (skrollr) OR classes (on-view). */
|
|
18478
|
+
|
|
18479
|
+
function hasLegacyMotion(el) {
|
|
18480
|
+
if (!el || !el.hasAttribute) return false;
|
|
18481
|
+
if (hasLegacyOnView(el)) return true;
|
|
18482
|
+
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
|
|
18483
|
+
|
|
18484
|
+
for (const attr of el.attributes) {
|
|
18485
|
+
if (/^data-(xs-|sm-)?t(-\d+)?$/.test(attr.name)) return true;
|
|
18486
|
+
}
|
|
18487
|
+
|
|
18488
|
+
return false;
|
|
18489
|
+
}
|
|
18490
|
+
/** Every data-fx-* attribute name currently on the element. */
|
|
18491
|
+
|
|
18492
|
+
function fxAttrs(el) {
|
|
18493
|
+
if (!el || !el.attributes) return [];
|
|
18494
|
+
return Array.from(el.attributes).map(a => a.name).filter(n => n.startsWith('data-fx-'));
|
|
18495
|
+
}
|
|
18496
|
+
function hasMotion(el) {
|
|
18497
|
+
return fxAttrs(el).some(n => n !== MUTE_ATTR);
|
|
18498
|
+
}
|
|
18499
|
+
/* ---- mute ----
|
|
18500
|
+
*
|
|
18501
|
+
* "Off" is an ATTRIBUTE, not editor memory. The tempting alternative — stash
|
|
18502
|
+
* the data-fx-* attributes in JS, strip them from the DOM, restore on unmute —
|
|
18503
|
+
* loses the user's animation the first time the editor autosaves while muted,
|
|
18504
|
+
* because builder.html() serializes the DOM and the attributes aren't in it.
|
|
18505
|
+
*
|
|
18506
|
+
* As an attribute it survives save, undo, reload and hand-off; it's visible in
|
|
18507
|
+
* the HTML so nobody is ever confused about why an animation isn't running;
|
|
18508
|
+
* and it stops being a debug toggle and becomes a real feature ("turn this off
|
|
18509
|
+
* but keep the setup"). It costs a few dead bytes in the published page.
|
|
18510
|
+
*
|
|
18511
|
+
* contentbox-effects._eligible() is the single gate every feature passes
|
|
18512
|
+
* through, so one check there disables keyframes, split, typewriter, parallax,
|
|
18513
|
+
* horizontal, draw, marquee, tilt and magnetic at once. On a .is-section it
|
|
18514
|
+
* mutes the whole scene. */
|
|
18515
|
+
|
|
18516
|
+
const MUTE_ATTR = 'data-fx-off';
|
|
18517
|
+
/* ---- scene attributes ----
|
|
18518
|
+
*
|
|
18519
|
+
* These live on the .is-section and describe the SCENE, not any element's own
|
|
18520
|
+
* motion — so clearing an element must never touch them (a section is both).
|
|
18521
|
+
*
|
|
18522
|
+
* This list used to be copy-pasted into motion.js, panel-motion.js and
|
|
18523
|
+
* motion-cascade.js. Then data-fx-duration and data-fx-once were added and only
|
|
18524
|
+
* one copy learned about them, so "remove track" on a section quietly deleted
|
|
18525
|
+
* the cascade's timing. One list, one place.
|
|
18526
|
+
*
|
|
18527
|
+
* ADDING A NEW SCENE ATTRIBUTE? It goes here, and in animation-framwork-ai.md.
|
|
18528
|
+
*/
|
|
18529
|
+
|
|
18530
|
+
const SCENE_ATTRS = ['data-fx-length', // pinned: how much scroll the pin eats (vh)
|
|
18531
|
+
'data-fx-scrub', // seconds of catch-up lag, or "off" for time-driven
|
|
18532
|
+
'data-fx-start', // where progress 0 is
|
|
18533
|
+
'data-fx-end', // where progress 100 is
|
|
18534
|
+
'data-fx-duration', // seconds — the scene's own clock, when not scrubbing
|
|
18535
|
+
'data-fx-once', // don't replay on re-entry
|
|
18536
|
+
'data-fx-emit'];
|
|
18537
|
+
/* ---- what is NOT the user's content ----
|
|
18538
|
+
*
|
|
18539
|
+
* ContentBox injects its toolbars INTO the row, as siblings of the column:
|
|
18540
|
+
*
|
|
18541
|
+
* div.row
|
|
18542
|
+
* ├ div.column ← real content
|
|
18543
|
+
* ├ div.is-tool.is-row-tool ← editor UI
|
|
18544
|
+
* ├ div.is-tool.is-col-tool ← editor UI
|
|
18545
|
+
* └ div.is-rowadd-tool ← editor UI
|
|
18546
|
+
*
|
|
18547
|
+
* So anything walking row.children sees toolbars as columns. They aren't empty
|
|
18548
|
+
* — they hold buttons — so they pass every "does this cell have content?" test,
|
|
18549
|
+
* and the cascade cheerfully animates the editor's own move/delete buttons.
|
|
18550
|
+
*
|
|
18551
|
+
* Kinetra also generates nodes (split-text spans, pinned-section clones). Those
|
|
18552
|
+
* aren't content either.
|
|
18553
|
+
*
|
|
18554
|
+
* NOTE the deliberate absence of `.column` / `.row` here: those class names are
|
|
18555
|
+
* user-configurable, so structure is read positionally and only the chrome is
|
|
18556
|
+
* named. */
|
|
18557
|
+
|
|
18558
|
+
const EDITOR_CHROME_SELECTOR = ['.is-tool', // row + column toolbars (contentbuilder's controlSelector)
|
|
18559
|
+
'.is-rowadd-tool', '.is-section-tool', '.is-box-tool', '.is-section-info'].join(',');
|
|
18560
|
+
/* NOT `.is-overlay-bg`. It is authored markup saved with the page, and it is
|
|
18561
|
+
* where the parallax of most section templates lives — and where converting a
|
|
18562
|
+
* legacy section writes its keyframes. Excluding it hid a section's most
|
|
18563
|
+
* common animation from the timeline dock, which lists a scene's animated
|
|
18564
|
+
* elements. It stays out of the ancestor picker and out of the cascade, but
|
|
18565
|
+
* that is each of their own rule, not this one. */
|
|
18566
|
+
|
|
18567
|
+
const NOT_CONTENT_SELECTOR = [EDITOR_CHROME_SELECTOR, '.kinetra-word', '.kinetra-char', '.kinetra-mask', '[data-section-clone]', '[data-fx-mirrors]'].join(',');
|
|
18568
|
+
/** Editor chrome or Kinetra-generated — never animate it, never list it. */
|
|
18569
|
+
|
|
18570
|
+
function isNotContent(el) {
|
|
18571
|
+
if (!el || el.nodeType !== 1 || !el.closest) return true;
|
|
18572
|
+
return !!el.closest(NOT_CONTENT_SELECTOR); // closest → catches buttons *inside* a toolbar
|
|
18573
|
+
}
|
|
18574
|
+
function isMuted(el) {
|
|
18575
|
+
return !!(el && el.hasAttribute && el.hasAttribute(MUTE_ATTR));
|
|
18576
|
+
}
|
|
18577
|
+
/** Percent keyframes on the element, sorted: [{ pct, value }] */
|
|
18578
|
+
|
|
18579
|
+
function keyframesOf(el) {
|
|
18580
|
+
if (!el || !el.attributes) return [];
|
|
18581
|
+
const out = [];
|
|
18582
|
+
|
|
18583
|
+
for (const a of el.attributes) {
|
|
18584
|
+
const m = /^data-fx-(\d{1,3})$/.exec(a.name);
|
|
18585
|
+
if (!m) continue;
|
|
18586
|
+
const pct = parseInt(m[1], 10);
|
|
18587
|
+
if (pct < 0 || pct > 100) continue;
|
|
18588
|
+
out.push({
|
|
18589
|
+
pct,
|
|
18590
|
+
value: a.value
|
|
18591
|
+
});
|
|
18592
|
+
}
|
|
18593
|
+
|
|
18594
|
+
return out.sort((x, y) => x.pct - y.pct);
|
|
18595
|
+
}
|
|
18596
|
+
/** Which preset (if any) is applied. Returns the preset object or null. */
|
|
18597
|
+
|
|
18598
|
+
function activePreset(el, table) {
|
|
18599
|
+
if (!el || !el.hasAttribute) return null;
|
|
18600
|
+
return (table || PRESETS).find(p => el.hasAttribute(p.key)) || null;
|
|
18601
|
+
}
|
|
18602
|
+
/** Remove every data-fx-N keyframe (leaves presets and ease alone). */
|
|
18603
|
+
|
|
18604
|
+
function clearKeyframes(el) {
|
|
18605
|
+
for (const name of fxAttrs(el)) {
|
|
18606
|
+
if (/^data-fx-\d{1,3}$/.test(name)) el.removeAttribute(name);
|
|
18607
|
+
}
|
|
18608
|
+
}
|
|
18609
|
+
/** Remove one preset and all of its option attributes. */
|
|
18610
|
+
|
|
18611
|
+
function clearPreset(el, preset) {
|
|
18612
|
+
el.removeAttribute(preset.key);
|
|
18613
|
+
(preset.options || []).forEach(o => {
|
|
18614
|
+
if (o.attr !== preset.key) el.removeAttribute(o.attr);
|
|
18615
|
+
});
|
|
18616
|
+
}
|
|
18617
|
+
/** Apply a keyframe recipe: replaces all existing keyframes. */
|
|
18618
|
+
|
|
18619
|
+
function applyRecipe(el, recipe) {
|
|
18620
|
+
clearKeyframes(el);
|
|
18621
|
+
Object.keys(recipe.frames).forEach(pct => {
|
|
18622
|
+
el.setAttribute('data-fx-' + pct, recipe.frames[pct]);
|
|
18623
|
+
});
|
|
18624
|
+
if (recipe.ease) el.setAttribute('data-fx-ease', recipe.ease);else el.removeAttribute('data-fx-ease');
|
|
18625
|
+
}
|
|
18626
|
+
/** Best-effort: which recipe do the current keyframes look like? */
|
|
18627
|
+
|
|
18628
|
+
function matchRecipe(el) {
|
|
18629
|
+
const frames = keyframesOf(el);
|
|
18630
|
+
if (!frames.length) return null;
|
|
18631
|
+
|
|
18632
|
+
const norm = s => s.replace(/\s+/g, '').replace(/;$/, '').toLowerCase();
|
|
18633
|
+
|
|
18634
|
+
return RECIPES.find(r => {
|
|
18635
|
+
const keys = Object.keys(r.frames);
|
|
18636
|
+
if (keys.length !== frames.length) return false;
|
|
18637
|
+
return keys.every(pct => {
|
|
18638
|
+
const f = frames.find(x => x.pct === parseInt(pct, 10));
|
|
18639
|
+
return f && norm(f.value) === norm(r.frames[pct]);
|
|
18640
|
+
});
|
|
18641
|
+
}) || null;
|
|
18642
|
+
}
|
|
18643
|
+
/* ---- Scene (section) helpers ---- */
|
|
18644
|
+
|
|
18645
|
+
function isPinned(section) {
|
|
18646
|
+
return !!(section && section.classList.contains('section-pin'));
|
|
18647
|
+
}
|
|
18648
|
+
const SCENE_DEFAULTS = {
|
|
18649
|
+
length: '320',
|
|
18650
|
+
// vh, pinned only
|
|
18651
|
+
scrub: '0.25',
|
|
18652
|
+
// seconds of catch-up lag
|
|
18653
|
+
duration: '1',
|
|
18654
|
+
// seconds — only read when NOT scrubbing
|
|
18655
|
+
start: 'top bottom',
|
|
18656
|
+
// end: 'bottom top',
|
|
18657
|
+
end: 'bottom bottom'
|
|
18658
|
+
};
|
|
18659
|
+
/* The scene window, as a choice rather than a string.
|
|
18660
|
+
*
|
|
18661
|
+
* There are only two answers that matter:
|
|
18662
|
+
* - fully in view — the entrance is the scene. Right for ~everything.
|
|
18663
|
+
* - leaves the screen — keep moving the whole time the section is on screen.
|
|
18664
|
+
* Right for drifting, parallax-like motion.
|
|
18665
|
+
* Anything else is a fine-tune, and stays available as raw edge strings.
|
|
18666
|
+
*
|
|
18667
|
+
* NOTE: the window is IGNORED when the scene is time-driven (scrub="off") —
|
|
18668
|
+
* an IntersectionObserver does the triggering there. */
|
|
18669
|
+
|
|
18670
|
+
const SCENE_WINDOWS = [{
|
|
18671
|
+
id: 'inview',
|
|
18672
|
+
label: 'the section is fully in view',
|
|
18673
|
+
start: 'top bottom',
|
|
18674
|
+
end: 'bottom bottom'
|
|
18675
|
+
}, {
|
|
18676
|
+
id: 'offscreen',
|
|
18677
|
+
label: 'the section leaves the screen',
|
|
18678
|
+
start: 'top bottom',
|
|
18679
|
+
end: 'bottom top'
|
|
18680
|
+
}];
|
|
18681
|
+
function windowOf(section) {
|
|
18682
|
+
if (!section) return 'inview';
|
|
18683
|
+
const s = (section.getAttribute('data-fx-start') || '').trim();
|
|
18684
|
+
const e = (section.getAttribute('data-fx-end') || '').trim();
|
|
18685
|
+
if (!s && !e) return 'inview'; // no attributes = defaults
|
|
18686
|
+
|
|
18687
|
+
const m = SCENE_WINDOWS.find(w => (s === '' || s === w.start) && e === w.end);
|
|
18688
|
+
return m ? m.id : 'custom';
|
|
18689
|
+
}
|
|
18690
|
+
function setWindow(section, id) {
|
|
18691
|
+
if (!section) return;
|
|
18692
|
+
|
|
18693
|
+
if (id === 'inview') {
|
|
18694
|
+
// the default — write nothing
|
|
18695
|
+
section.removeAttribute('data-fx-start');
|
|
18696
|
+
section.removeAttribute('data-fx-end');
|
|
18697
|
+
return;
|
|
18698
|
+
}
|
|
18699
|
+
|
|
18700
|
+
const w = SCENE_WINDOWS.find(x => x.id === id);
|
|
18701
|
+
if (!w) return; // 'custom' — leave the strings alone
|
|
18702
|
+
|
|
18703
|
+
section.removeAttribute('data-fx-start'); // start default is already right
|
|
18704
|
+
|
|
18705
|
+
section.setAttribute('data-fx-end', w.end);
|
|
18706
|
+
}
|
|
18707
|
+
/* Which clock drives the scene. `data-fx-scrub="off"` is the switch:
|
|
18708
|
+
* the scrollbar stops being the clock and the scene plays itself. */
|
|
18709
|
+
|
|
18710
|
+
function triggerOf(section) {
|
|
18711
|
+
if (!section) return 'scroll';
|
|
18712
|
+
const v = (section.getAttribute('data-fx-scrub') || '').trim().toLowerCase();
|
|
18713
|
+
return v === 'off' || v === 'none' || v === 'false' ? 'enter' : 'scroll';
|
|
18714
|
+
}
|
|
18715
|
+
/* The trigger a scene gets when nobody has chosen one.
|
|
18716
|
+
*
|
|
18717
|
+
* Note what this is NOT: it does not reinterpret a missing data-fx-scrub.
|
|
18718
|
+
* The runtime's fallback for that attribute is scrub 0.25 (contentbox-effects
|
|
18719
|
+
* sceneOptsFor), so a saved page with no trigger on it is scroll-driven and
|
|
18720
|
+
* will stay that way. This is the value the editor WRITES the first time it
|
|
18721
|
+
* gives a section motion — content made from now on plays on enter, content
|
|
18722
|
+
* already out there keeps whatever it has. */
|
|
18723
|
+
|
|
18724
|
+
const DEFAULT_TRIGGER = 'enter';
|
|
18725
|
+
/* Is anything in this section actually animated?
|
|
18726
|
+
*
|
|
18727
|
+
* Scene attributes don't count — data-fx-duration on a bare section describes
|
|
18728
|
+
* a clock nothing is running on. What counts is an element's own motion:
|
|
18729
|
+
* keyframes or a preset, on the section itself or anywhere inside it. */
|
|
18730
|
+
|
|
18731
|
+
function sceneHasMotion(section) {
|
|
18732
|
+
if (!section) return false;
|
|
18733
|
+
const ownMotion = fxAttrs(section).some(n => n !== MUTE_ATTR && SCENE_ATTRS.indexOf(n) === -1);
|
|
18734
|
+
if (ownMotion) return true;
|
|
18735
|
+
return Array.from(section.querySelectorAll('*')).some(el => hasMotion(el));
|
|
18736
|
+
}
|
|
18737
|
+
|
|
18738
|
+
// block's source for an anchor.
|
|
18739
|
+
|
|
18740
|
+
const LINK_ATTRS = {
|
|
18741
|
+
attrs: ['href', 'title', 'target']
|
|
18742
|
+
}; // Panel for elements that live in a BOX OVERLAY but outside div.is-container:
|
|
17805
18743
|
//
|
|
17806
18744
|
// .is-section.is-box > .is-overlay > .is-overlay-content > ... > <img>
|
|
17807
18745
|
//
|
|
@@ -17848,7 +18786,28 @@ class PanelOverlay {
|
|
|
17848
18786
|
|
|
17849
18787
|
<div class="inp-text" role="textbox"></div>
|
|
17850
18788
|
|
|
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>
|
|
18789
|
+
<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>
|
|
18790
|
+
|
|
18791
|
+
<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>
|
|
18792
|
+
|
|
18793
|
+
</div>
|
|
18794
|
+
|
|
18795
|
+
<div class="div-overlay-link" style="display:none">
|
|
18796
|
+
|
|
18797
|
+
<label class="label">
|
|
18798
|
+
<div>${out('Link')}:</div>
|
|
18799
|
+
<input type="text" class="inp-href" id="_inp_ovl_href_${this.random()}">
|
|
18800
|
+
</label>
|
|
18801
|
+
|
|
18802
|
+
<label class="label mt-2">
|
|
18803
|
+
<div>${out('Title')}:</div>
|
|
18804
|
+
<input type="text" class="inp-linktitle" id="_inp_ovl_linktitle_${this.random()}">
|
|
18805
|
+
</label>
|
|
18806
|
+
|
|
18807
|
+
<label class="label checkbox mt-3">
|
|
18808
|
+
<input type="checkbox" class="chk-newwindow" id="_chk_ovl_newwindow_${this.random()}">
|
|
18809
|
+
<span>${out('Open in new tab')}</span>
|
|
18810
|
+
</label>
|
|
17852
18811
|
|
|
17853
18812
|
</div>
|
|
17854
18813
|
|
|
@@ -17873,7 +18832,7 @@ class PanelOverlay {
|
|
|
17873
18832
|
</div>
|
|
17874
18833
|
|
|
17875
18834
|
<label class="label mt-2 label-title">
|
|
17876
|
-
<div>${out('
|
|
18835
|
+
<div>${out('Alt')}:</div>
|
|
17877
18836
|
<input type="text" class="inp-title" id="_inp_ovl_title_${this.random()}">
|
|
17878
18837
|
</label>
|
|
17879
18838
|
|
|
@@ -17881,6 +18840,15 @@ class PanelOverlay {
|
|
|
17881
18840
|
|
|
17882
18841
|
</div>
|
|
17883
18842
|
|
|
18843
|
+
<div class="div-overlay-code mt-4" style="display:none">
|
|
18844
|
+
<div class="group single flex" style="width:100%">
|
|
18845
|
+
<button title="${out('Edit Code')}" class="btn-editcode">
|
|
18846
|
+
<svg><use xlink:href="#icon-viewcode"></use></svg>
|
|
18847
|
+
<span>${out('Edit Code')}</span>
|
|
18848
|
+
</button>
|
|
18849
|
+
</div>
|
|
18850
|
+
</div>
|
|
18851
|
+
|
|
17884
18852
|
<div class="div-overlay-list" style="display:none">
|
|
17885
18853
|
<div class="label-note list-caption">${out('Everything else in this layer')}:</div>
|
|
17886
18854
|
<div class="overlay-list"></div>
|
|
@@ -17926,13 +18894,70 @@ class PanelOverlay {
|
|
|
17926
18894
|
}, btnOpenAsset);
|
|
17927
18895
|
}); // Show/hide button
|
|
17928
18896
|
|
|
17929
|
-
if (!(this.builder.onImageSelectClick || this.builder.imageSelect)) btnOpenAsset.style.display = 'none';
|
|
18897
|
+
if (!(this.builder.onImageSelectClick || this.builder.imageSelect)) btnOpenAsset.style.display = 'none'; // --- Link ---
|
|
18898
|
+
//
|
|
18899
|
+
// These fields only ever EDIT an anchor that is already in the layer;
|
|
18900
|
+
// nothing here creates one. Wrapping a layer's text or image in a link
|
|
18901
|
+
// restructures the design, which is the same reason the floating image
|
|
18902
|
+
// tool is reduced for overlay images (see controlpanel.js getActive).
|
|
18903
|
+
|
|
18904
|
+
const inpHref = panel.querySelector('.inp-href');
|
|
18905
|
+
inpHref.addEventListener('input', () => {
|
|
18906
|
+
const link = this.link();
|
|
18907
|
+
if (!link) return;
|
|
18908
|
+
this.builder.editor.saveForUndo();
|
|
18909
|
+
const url = inpHref.value.trim();
|
|
18910
|
+
if (url) link.setAttribute('href', url);else link.removeAttribute('href'); // emptied: no link, but the element stays as it is
|
|
18911
|
+
|
|
18912
|
+
syncToCodeSource(link, LINK_ATTRS);
|
|
18913
|
+
this.builder.editor.onChange();
|
|
18914
|
+
});
|
|
18915
|
+
const inpLinkTitle = panel.querySelector('.inp-linktitle');
|
|
18916
|
+
inpLinkTitle.addEventListener('input', () => {
|
|
18917
|
+
const link = this.link();
|
|
18918
|
+
if (!link) return;
|
|
18919
|
+
this.builder.editor.saveForUndo();
|
|
18920
|
+
const title = inpLinkTitle.value;
|
|
18921
|
+
if (title) link.setAttribute('title', title);else link.removeAttribute('title');
|
|
18922
|
+
syncToCodeSource(link, LINK_ATTRS);
|
|
18923
|
+
this.builder.editor.onChange();
|
|
18924
|
+
});
|
|
18925
|
+
const chkNewWindow = panel.querySelector('.chk-newwindow');
|
|
18926
|
+
chkNewWindow.addEventListener('change', () => {
|
|
18927
|
+
const link = this.link();
|
|
18928
|
+
if (!link) return;
|
|
18929
|
+
this.builder.editor.saveForUndo(); // target alone, as the editor's own link dialog writes it
|
|
18930
|
+
// (elementhyperlink.js).
|
|
18931
|
+
|
|
18932
|
+
if (chkNewWindow.checked) link.setAttribute('target', '_blank');else link.removeAttribute('target');
|
|
18933
|
+
syncToCodeSource(link, LINK_ATTRS);
|
|
18934
|
+
this.builder.editor.onChange();
|
|
18935
|
+
}); // The layer is a code block: its markup is edited here, its code there.
|
|
18936
|
+
// Without this the code is out of reach while one of its elements is
|
|
18937
|
+
// selected, since the panel showing is this one, not the Code panel.
|
|
18938
|
+
|
|
18939
|
+
const btnEditCode = panel.querySelector('.btn-editcode');
|
|
18940
|
+
btnEditCode.addEventListener('click', () => {
|
|
18941
|
+
const el = this.target();
|
|
18942
|
+
const block = el ? el.closest('[data-cb-code]') : null;
|
|
18943
|
+
const codeElement = this.builder.editor.codeElement;
|
|
18944
|
+
if (!block || !codeElement) return; // Applying the dialog rebuilds the block from its source, replacing
|
|
18945
|
+
// the element this panel is on. Select the block first, so what the
|
|
18946
|
+
// panel shows is still on the page when the dialog closes.
|
|
18947
|
+
|
|
18948
|
+
this.builder.controlpanel.selectOverlayElement(block);
|
|
18949
|
+
codeElement.activeBlock = block;
|
|
18950
|
+
codeElement.edit();
|
|
18951
|
+
});
|
|
17930
18952
|
const inpTitle = panel.querySelector('.inp-title');
|
|
17931
18953
|
inpTitle.addEventListener('input', () => {
|
|
17932
18954
|
this.builder.editor.saveForUndo();
|
|
17933
18955
|
let img = this.target();
|
|
17934
18956
|
if (!img) return;
|
|
17935
18957
|
img.setAttribute('alt', inpTitle.value);
|
|
18958
|
+
syncToCodeSource(img, {
|
|
18959
|
+
attrs: ['alt']
|
|
18960
|
+
});
|
|
17936
18961
|
this.builder.editor.onChange();
|
|
17937
18962
|
}); // --- Text ---
|
|
17938
18963
|
|
|
@@ -18012,7 +19037,14 @@ class PanelOverlay {
|
|
|
18012
19037
|
const html = this.unwrapBlock(this.ed.getHTML());
|
|
18013
19038
|
if (html === el.innerHTML) return;
|
|
18014
19039
|
this.builder.editor.saveForUndo();
|
|
18015
|
-
el.innerHTML = html;
|
|
19040
|
+
el.innerHTML = html; // An element inside a code block lives in the block's RENDER, which is
|
|
19041
|
+
// thrown away on save; the wording has to reach the block's source too.
|
|
19042
|
+
// No-op everywhere else. (Elements the code built itself never get
|
|
19043
|
+
// here: isOverlayTextElement refuses them.)
|
|
19044
|
+
|
|
19045
|
+
syncToCodeSource(el, {
|
|
19046
|
+
html: true
|
|
19047
|
+
});
|
|
18016
19048
|
this.refreshRow(el);
|
|
18017
19049
|
this.builder.editor.onChange();
|
|
18018
19050
|
}
|
|
@@ -18026,6 +19058,41 @@ class PanelOverlay {
|
|
|
18026
19058
|
}
|
|
18027
19059
|
|
|
18028
19060
|
return html;
|
|
19061
|
+
} // The anchor the current selection belongs to, or the single one inside it.
|
|
19062
|
+
//
|
|
19063
|
+
// Null where there is none, and null where the region holds SEVERAL: which
|
|
19064
|
+
// of them the fields would then be editing is a guess, and a wrong guess
|
|
19065
|
+
// rewrites a link the user was not looking at.
|
|
19066
|
+
|
|
19067
|
+
|
|
19068
|
+
linkFor(el) {
|
|
19069
|
+
if (!el || !el.closest) return null;
|
|
19070
|
+
const scope = el.closest(this.builder.overlayEditableSelector);
|
|
19071
|
+
if (!scope) return null;
|
|
19072
|
+
let link = el.closest('a');
|
|
19073
|
+
if (link && !scope.contains(link)) link = null;
|
|
19074
|
+
|
|
19075
|
+
if (!link) {
|
|
19076
|
+
const inside = el.querySelectorAll('a');
|
|
19077
|
+
if (inside.length === 1) link = inside[0];
|
|
19078
|
+
}
|
|
19079
|
+
|
|
19080
|
+
if (!link) return null; // Inside a code block, only an anchor still verbatim in the block's
|
|
19081
|
+
// source can be edited — the same rule as the wording.
|
|
19082
|
+
|
|
19083
|
+
if (!codeSourceEditable(link)) return null;
|
|
19084
|
+
return link;
|
|
19085
|
+
}
|
|
19086
|
+
|
|
19087
|
+
link() {
|
|
19088
|
+
return this.linkFor(this.target());
|
|
19089
|
+
}
|
|
19090
|
+
|
|
19091
|
+
getStateLink(link) {
|
|
19092
|
+
const panel = this.panel;
|
|
19093
|
+
panel.querySelector('.inp-href').value = link.getAttribute('href') || '';
|
|
19094
|
+
panel.querySelector('.inp-linktitle').value = link.getAttribute('title') || '';
|
|
19095
|
+
panel.querySelector('.chk-newwindow').checked = link.getAttribute('target') === '_blank';
|
|
18029
19096
|
} // The element this panel is currently editing. ControlPanel keeps it in
|
|
18030
19097
|
// activeElement, the same as every other panel.
|
|
18031
19098
|
|
|
@@ -18057,8 +19124,13 @@ class PanelOverlay {
|
|
|
18057
19124
|
// selection lost (eg. the image got wrapped in a link)
|
|
18058
19125
|
this.builder.controlpanel.select('');
|
|
18059
19126
|
return;
|
|
18060
|
-
}
|
|
19127
|
+
} // The floating tool writes to the live element and knows nothing
|
|
19128
|
+
// about code blocks; carry the change into the source as well.
|
|
19129
|
+
|
|
18061
19130
|
|
|
19131
|
+
syncToCodeSource(img, {
|
|
19132
|
+
attrs: ['src', 'alt']
|
|
19133
|
+
});
|
|
18062
19134
|
this.getState();
|
|
18063
19135
|
};
|
|
18064
19136
|
|
|
@@ -18070,6 +19142,10 @@ class PanelOverlay {
|
|
|
18070
19142
|
return;
|
|
18071
19143
|
}
|
|
18072
19144
|
|
|
19145
|
+
const el = this.target();
|
|
19146
|
+
if (el) syncToCodeSource(el, {
|
|
19147
|
+
attrs: ['src', 'alt']
|
|
19148
|
+
});
|
|
18073
19149
|
this.getState();
|
|
18074
19150
|
};
|
|
18075
19151
|
} // `target` is the {type, element} descriptor from ControlPanel.overlayTarget().
|
|
@@ -18091,19 +19167,30 @@ class PanelOverlay {
|
|
|
18091
19167
|
const hasText = target ? !!target.hasText : type === 'text';
|
|
18092
19168
|
const hasSource = target ? !!target.hasSource : type !== 'text';
|
|
18093
19169
|
const isImg = el.tagName.toLowerCase() === 'img';
|
|
18094
|
-
this.sourceKind = isImg ? 'image' : 'background';
|
|
19170
|
+
this.sourceKind = isImg ? 'image' : 'background'; // A link is an ASPECT too, and an independent one: a heading can be a
|
|
19171
|
+
// link, so can an image, and so can an icon with neither text nor
|
|
19172
|
+
// picture in it (which is why the decoration note steps aside for it).
|
|
19173
|
+
|
|
19174
|
+
const link = this.linkFor(el);
|
|
18095
19175
|
const divText = this.panel.querySelector('.div-overlay-text');
|
|
19176
|
+
const divLink = this.panel.querySelector('.div-overlay-link');
|
|
18096
19177
|
const divImage = this.panel.querySelector('.div-overlay-image');
|
|
18097
19178
|
const divDeco = this.panel.querySelector('.div-overlay-decoration');
|
|
18098
19179
|
divText.style.display = hasText ? '' : 'none';
|
|
19180
|
+
divLink.style.display = link ? '' : 'none';
|
|
18099
19181
|
divImage.style.display = hasSource ? '' : 'none';
|
|
18100
|
-
if (divDeco) divDeco.style.display = !hasText && !hasSource ? '' : 'none';
|
|
19182
|
+
if (divDeco) divDeco.style.display = !hasText && !hasSource && !link ? '' : 'none';
|
|
18101
19183
|
/* [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
19184
|
|
|
18105
|
-
|
|
19185
|
+
if (link) this.getStateLink(link); // More than one section on screen at once (an element painting its image
|
|
19186
|
+
// inside its own letterforms; a heading that is also a link): each one
|
|
19187
|
+
// after the first needs separating from what precedes it.
|
|
19188
|
+
|
|
19189
|
+
divLink.classList.toggle('stacked', !!link && hasText);
|
|
19190
|
+
divImage.classList.toggle('stacked', hasSource && (hasText || !!link));
|
|
18106
19191
|
this.panel.querySelector('.label-title').style.display = isImg ? '' : 'none';
|
|
19192
|
+
const divCode = this.panel.querySelector('.div-overlay-code');
|
|
19193
|
+
divCode.style.display = this.builder.editor.codeElement && el.closest('[data-cb-code]') ? '' : 'none';
|
|
18107
19194
|
if (hasText) this.getStateText(el);
|
|
18108
19195
|
if (hasSource) this.getStateSource(el);
|
|
18109
19196
|
this.renderList(el);
|
|
@@ -18219,11 +19306,28 @@ class PanelOverlay {
|
|
|
18219
19306
|
if (!value || value === 'none') return '';
|
|
18220
19307
|
const found = /url\((['"]?)(.*?)\1\)/.exec(value);
|
|
18221
19308
|
return found ? found[2] : '';
|
|
19309
|
+
} // Does this layer move on the Motion tab? Only then does the note that
|
|
19310
|
+
// points at it say anything true: a layer can be a static caption, or a
|
|
19311
|
+
// code block, or animate from CSS keyframes the tab has no part in.
|
|
19312
|
+
// Resolved up the layer, since the keyframes are often on a wrapper.
|
|
19313
|
+
|
|
19314
|
+
|
|
19315
|
+
layerHasMotion(el) {
|
|
19316
|
+
const stop = el.closest(this.builder.overlayEditableSelector);
|
|
19317
|
+
|
|
19318
|
+
for (let node = el; node && node !== stop; node = node.parentElement) {
|
|
19319
|
+
if (hasMotion(node)) return true;
|
|
19320
|
+
}
|
|
19321
|
+
|
|
19322
|
+
return false;
|
|
18222
19323
|
}
|
|
18223
19324
|
|
|
18224
19325
|
getStateText(el) {
|
|
18225
19326
|
const panel = this.panel;
|
|
18226
|
-
if (!el) return;
|
|
19327
|
+
if (!el) return;
|
|
19328
|
+
const animated = this.layerHasMotion(el);
|
|
19329
|
+
panel.querySelector('.note-motion').style.display = animated ? '' : 'none';
|
|
19330
|
+
panel.querySelector('.note-plain').style.display = animated ? 'none' : ''; // Load the element's content into the field. Guarded so the write-back
|
|
18227
19331
|
// does not fire on our own load, and skipped while the user is typing in
|
|
18228
19332
|
// it (which would move their caret to the end on every keystroke).
|
|
18229
19333
|
|
|
@@ -18314,6 +19418,9 @@ class PanelOverlay {
|
|
|
18314
19418
|
el.style.backgroundImage = `url("${src}")`;
|
|
18315
19419
|
}
|
|
18316
19420
|
|
|
19421
|
+
syncToCodeSource(el, {
|
|
19422
|
+
styles: ['background-image']
|
|
19423
|
+
});
|
|
18317
19424
|
this.refreshRow(el);
|
|
18318
19425
|
return;
|
|
18319
19426
|
}
|
|
@@ -18322,6 +19429,9 @@ class PanelOverlay {
|
|
|
18322
19429
|
this.builder.editor.element.image.repositionImageTool();
|
|
18323
19430
|
});
|
|
18324
19431
|
el.setAttribute('src', src);
|
|
19432
|
+
syncToCodeSource(el, {
|
|
19433
|
+
attrs: ['src']
|
|
19434
|
+
});
|
|
18325
19435
|
this.refreshRow(el);
|
|
18326
19436
|
}
|
|
18327
19437
|
|
|
@@ -25309,739 +26419,6 @@ js$1.exports;
|
|
|
25309
26419
|
var jsExports = js$1.exports;
|
|
25310
26420
|
var JsBeautify$1 = /*@__PURE__*/getDefaultExportFromCjs(jsExports);
|
|
25311
26421
|
|
|
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
26422
|
/*
|
|
26046
26423
|
* section-anim.js — the Section panel's ready-made "Animation" presets,
|
|
26047
26424
|
* expressed as Motion (data-fx-*) instead of the legacy skrollr attributes.
|
|
@@ -33184,6 +33561,11 @@ class PanelSection {
|
|
|
33184
33561
|
<button title="${out('Duplicate')}" class="btn-duplicate">
|
|
33185
33562
|
<svg><use xlink:href="#icon-copy"></use></svg>
|
|
33186
33563
|
</button>
|
|
33564
|
+
${builder.sectionHtmlButton ? `
|
|
33565
|
+
<button title="${out('Section HTML')}" class="btn-html">
|
|
33566
|
+
<svg><use xlink:href="#icon-code"></use></svg>
|
|
33567
|
+
</button>
|
|
33568
|
+
` : ''}
|
|
33187
33569
|
<button title="${out('Delete')}" class="btn-delete">
|
|
33188
33570
|
<svg><use xlink:href="#icon-trash"></use></svg>
|
|
33189
33571
|
</button>
|
|
@@ -33385,6 +33767,10 @@ class PanelSection {
|
|
|
33385
33767
|
btnDuplicate.addEventListener('click', () => {
|
|
33386
33768
|
this.section.sectionDuplicate();
|
|
33387
33769
|
});
|
|
33770
|
+
const btnHtml = panel.querySelector('.btn-html');
|
|
33771
|
+
if (btnHtml) btnHtml.addEventListener('click', () => {
|
|
33772
|
+
this.builder.openSectionHtml();
|
|
33773
|
+
});
|
|
33388
33774
|
const btnDelete = panel.querySelector('.btn-delete');
|
|
33389
33775
|
btnDelete.addEventListener('click', () => {
|
|
33390
33776
|
this.section.sectionDelete();
|
|
@@ -35920,6 +36306,14 @@ class PanelMotion {
|
|
|
35920
36306
|
const builder = this.builder;
|
|
35921
36307
|
builder.editor.saveForUndo();
|
|
35922
36308
|
mutate();
|
|
36309
|
+
/* An element inside a code block lives in the block's RENDER, which is
|
|
36310
|
+
* discarded on save. Carry its motion into the block's source so the
|
|
36311
|
+
* animation survives. No-op for every other element. */
|
|
36312
|
+
|
|
36313
|
+
const inCode = this.target();
|
|
36314
|
+
if (inCode) syncToCodeSource(inCode, {
|
|
36315
|
+
fx: true
|
|
36316
|
+
});
|
|
35923
36317
|
this.refreshRuntime(rerender);
|
|
35924
36318
|
builder.onChange();
|
|
35925
36319
|
const dock = builder.motion;
|
|
@@ -42340,47 +42734,100 @@ function effectiveFullviewWidth(preferred, availableWidth) {
|
|
|
42340
42734
|
}
|
|
42341
42735
|
|
|
42342
42736
|
class Settings {
|
|
42343
|
-
constructor(
|
|
42737
|
+
constructor(builder) {
|
|
42344
42738
|
this.builder = builder;
|
|
42345
|
-
|
|
42346
|
-
|
|
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">
|
|
42739
|
+
} // Built on first open, not with the control panel: the dialog lives in the
|
|
42740
|
+
// editor's UI root, and the editor is created after the panel.
|
|
42350
42741
|
|
|
42351
|
-
<div class="label" style="font-size:18px">${out('Settings')}</div>
|
|
42352
|
-
|
|
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
42742
|
|
|
42361
|
-
|
|
42362
|
-
|
|
42363
|
-
|
|
42364
|
-
|
|
42365
|
-
|
|
42743
|
+
render() {
|
|
42744
|
+
const builder = this.builder; // A settings dialog in the editor's own dialog world: the shell, the
|
|
42745
|
+
// title bar and every control below are the components the Preferences
|
|
42746
|
+
// and Column/Row dialogs use, so this reads as one of them rather than
|
|
42747
|
+
// a side panel that happens to float. See the editor's
|
|
42748
|
+
// docs/settings-ui-guide.md.
|
|
42366
42749
|
|
|
42367
|
-
|
|
42368
|
-
|
|
42369
|
-
|
|
42370
|
-
|
|
42750
|
+
const html = `
|
|
42751
|
+
<div class="is-modal cbxsettings" tabindex="-1" role="dialog" aria-modal="true" aria-hidden="true">
|
|
42752
|
+
<div class="is-modal-content">
|
|
42753
|
+
<div class="is-modal-bar is-draggable">
|
|
42754
|
+
${out('Settings')}
|
|
42755
|
+
<button type="button" class="is-modal-close" title="${out('Close')}" tabindex="-1">✕</button>
|
|
42371
42756
|
</div>
|
|
42372
42757
|
|
|
42373
|
-
|
|
42374
|
-
|
|
42375
|
-
|
|
42376
|
-
|
|
42377
|
-
|
|
42758
|
+
<div style="padding:0;margin-top:35px;">
|
|
42759
|
+
<div class="cbx-set-body">
|
|
42760
|
+
|
|
42761
|
+
<div class="cbx-set-row2">
|
|
42762
|
+
<div class="cb-field">
|
|
42763
|
+
<span class="cb-grouplabel">${out('Paste')}</span>
|
|
42764
|
+
<div class="cb-sel">
|
|
42765
|
+
<select class="inp-paste">
|
|
42766
|
+
<option value="auto">${out('Auto')}</option>
|
|
42767
|
+
<option value="html-without-styles">${out('HTML (without styles)')}</option>
|
|
42768
|
+
<option value="html">${out('HTML (with styles)')}</option>
|
|
42769
|
+
<option value="text">${out('Text only')}</option>
|
|
42770
|
+
</select>
|
|
42771
|
+
</div>
|
|
42772
|
+
</div>
|
|
42773
|
+
|
|
42774
|
+
<div class="cb-field">
|
|
42775
|
+
<span class="cb-grouplabel">${out('HTML View')}</span>
|
|
42776
|
+
<div class="cb-sel">
|
|
42777
|
+
<select class="inp-htmlview">
|
|
42778
|
+
<option value="shorten">${out('Shorten HTML')}</option>
|
|
42779
|
+
<option value="actual">${out('Actual')}</option>
|
|
42780
|
+
</select>
|
|
42781
|
+
</div>
|
|
42782
|
+
</div>
|
|
42783
|
+
</div>
|
|
42784
|
+
|
|
42785
|
+
<div class="cb-field editing-width-field">
|
|
42786
|
+
<span class="cb-grouplabel editing-width-label">${out('Editing Width')}</span>
|
|
42787
|
+
<div class="cb-seg desktopwidth" role="group" aria-label="${out('Editing Width')}">
|
|
42788
|
+
<button type="button" data-width="auto">${out('Auto')}</button>
|
|
42789
|
+
${FULLVIEW_WIDTHS.map(w => `<button type="button" data-width="${w}">${w}</button>`).join('')}
|
|
42790
|
+
</div>
|
|
42791
|
+
</div>
|
|
42792
|
+
|
|
42793
|
+
<div class="cbx-set-sep"></div>
|
|
42794
|
+
|
|
42795
|
+
<div class="cb-switchrow">
|
|
42796
|
+
<span class="lbl">${out('Quick Section Controls')}</span>
|
|
42797
|
+
<label class="switch"><input type="checkbox" class="inp-sectionquicktool"><span class="slider"></span></label>
|
|
42798
|
+
</div>
|
|
42799
|
+
<div class="cb-switchrow">
|
|
42800
|
+
<span class="lbl">${out('Section Tools')}</span>
|
|
42801
|
+
<label class="switch"><input type="checkbox" class="inp-sectiontool"><span class="slider"></span></label>
|
|
42802
|
+
</div>
|
|
42803
|
+
<div class="cb-switchrow">
|
|
42804
|
+
<span class="lbl">${out('Box Tools')}</span>
|
|
42805
|
+
<label class="switch"><input type="checkbox" class="inp-boxtool"><span class="slider"></span></label>
|
|
42806
|
+
</div>
|
|
42807
|
+
|
|
42808
|
+
${this.builder.themes && this.builder.themes.length > 0 ? `
|
|
42809
|
+
<div class="cbx-set-sep"></div>
|
|
42810
|
+
|
|
42811
|
+
<div class="cb-field">
|
|
42812
|
+
<span class="cb-grouplabel">${out('Theme')}</span>
|
|
42813
|
+
<div class="cbx-themes">
|
|
42814
|
+
<button type="button" title="${out('Light')}" data-theme="light" style="background:#fff;border:rgb(132 132 132 / 16%) 1px solid;"></button>
|
|
42815
|
+
<button type="button" title="${out('Dark')}" data-theme="dark" style="background:#000;border:rgba(0,0,0,0.15) 1px solid;"></button>
|
|
42816
|
+
</div>
|
|
42817
|
+
</div>
|
|
42818
|
+
` : ''}
|
|
42819
|
+
|
|
42820
|
+
</div>
|
|
42378
42821
|
</div>
|
|
42379
|
-
` : ''}
|
|
42380
|
-
|
|
42381
42822
|
</div>
|
|
42382
|
-
|
|
42383
|
-
|
|
42823
|
+
</div>
|
|
42824
|
+
`; // In the editor's UI root, not ContentBox's: that is where the dialog
|
|
42825
|
+
// components live ([data-cb-ui]).
|
|
42826
|
+
|
|
42827
|
+
const stuff = builder.editor.builderStuff;
|
|
42828
|
+
stuff.insertAdjacentHTML('beforeend', html);
|
|
42829
|
+
const dialog = stuff.querySelector('.is-modal.cbxsettings');
|
|
42830
|
+
this.modal = dialog;
|
|
42384
42831
|
this.dialog = dialog;
|
|
42385
42832
|
const inpPaste = dialog.querySelector('.inp-paste');
|
|
42386
42833
|
inpPaste.addEventListener('change', () => {
|
|
@@ -42399,6 +42846,27 @@ class Settings {
|
|
|
42399
42846
|
}
|
|
42400
42847
|
|
|
42401
42848
|
localStorage.setItem('_htmlview', htmlView);
|
|
42849
|
+
}); // The tools on the page. Everything they do is on the control panel too,
|
|
42850
|
+
// so this is a preference, not a capability: it is remembered per user
|
|
42851
|
+
// and the page is re-rendered at once so the choice is visible.
|
|
42852
|
+
|
|
42853
|
+
const inpSectionQuickTool = dialog.querySelector('.inp-sectionquicktool');
|
|
42854
|
+
inpSectionQuickTool.addEventListener('change', () => {
|
|
42855
|
+
this.builder.sectionQuickTool = inpSectionQuickTool.checked;
|
|
42856
|
+
localStorage.setItem('_sectionquicktool', inpSectionQuickTool.checked ? '1' : '0');
|
|
42857
|
+
this.builder.controlpanel.refreshSectionQuickTool();
|
|
42858
|
+
});
|
|
42859
|
+
const inpSectionTool = dialog.querySelector('.inp-sectiontool');
|
|
42860
|
+
inpSectionTool.addEventListener('change', () => {
|
|
42861
|
+
this.builder.sectionTool = inpSectionTool.checked;
|
|
42862
|
+
localStorage.setItem('_sectiontool', inpSectionTool.checked ? '1' : '0');
|
|
42863
|
+
this.builder.refreshPageTools();
|
|
42864
|
+
});
|
|
42865
|
+
const inpBoxTool = dialog.querySelector('.inp-boxtool');
|
|
42866
|
+
inpBoxTool.addEventListener('change', () => {
|
|
42867
|
+
this.builder.boxTool = inpBoxTool.checked;
|
|
42868
|
+
localStorage.setItem('_boxtool', inpBoxTool.checked ? '1' : '0');
|
|
42869
|
+
this.builder.refreshPageTools();
|
|
42402
42870
|
});
|
|
42403
42871
|
const btnTheme = dialog.querySelectorAll('[data-theme]');
|
|
42404
42872
|
btnTheme.forEach(btn => {
|
|
@@ -42441,11 +42909,7 @@ class Settings {
|
|
|
42441
42909
|
// it is actually on screen.
|
|
42442
42910
|
|
|
42443
42911
|
window.addEventListener('resize', () => {
|
|
42444
|
-
if (this.
|
|
42445
|
-
});
|
|
42446
|
-
const btnBack = dialog.querySelector('.btn-back');
|
|
42447
|
-
btnBack.addEventListener('click', () => {
|
|
42448
|
-
this.close();
|
|
42912
|
+
if (this.modal.classList.contains('active')) this.renderDesktopWidth();
|
|
42449
42913
|
});
|
|
42450
42914
|
}
|
|
42451
42915
|
|
|
@@ -42477,6 +42941,9 @@ class Settings {
|
|
|
42477
42941
|
inpHtmlView.value = 'actual';
|
|
42478
42942
|
}
|
|
42479
42943
|
|
|
42944
|
+
dialog.querySelector('.inp-sectionquicktool').checked = this.builder.sectionQuickTool !== false;
|
|
42945
|
+
dialog.querySelector('.inp-sectiontool').checked = this.builder.sectionTool !== false;
|
|
42946
|
+
dialog.querySelector('.inp-boxtool').checked = this.builder.boxTool !== false;
|
|
42480
42947
|
this.renderDesktopWidth();
|
|
42481
42948
|
}
|
|
42482
42949
|
/**
|
|
@@ -42499,8 +42966,8 @@ class Settings {
|
|
|
42499
42966
|
|
|
42500
42967
|
renderDesktopWidth() {
|
|
42501
42968
|
const dialog = this.dialog;
|
|
42502
|
-
const label = dialog.querySelector('.editing-width-
|
|
42503
|
-
const group = dialog.querySelector('.
|
|
42969
|
+
const label = dialog.querySelector('.editing-width-field');
|
|
42970
|
+
const group = dialog.querySelector('.desktopwidth');
|
|
42504
42971
|
if (!group) return; // The workspace, not the window. In fullview #editPanel is inset by the left rail
|
|
42505
42972
|
// and by the control panel this dialog sits in, so a 1290px window leaves about
|
|
42506
42973
|
// 939px here, and the difference decides whether any of this is worth showing.
|
|
@@ -42514,8 +42981,8 @@ class Settings {
|
|
|
42514
42981
|
|
|
42515
42982
|
const selectable = FULLVIEW_WIDTHS.some(width => width > available && fullviewWidthFits(width, available));
|
|
42516
42983
|
const show = this.builder.scaleFullview && selectable;
|
|
42517
|
-
label.style.display = show ? '' : 'none';
|
|
42518
|
-
|
|
42984
|
+
label.style.display = show ? '' : 'none'; // the whole field: label + segments
|
|
42985
|
+
|
|
42519
42986
|
if (!show) return; // Auto is the state with nothing stored, so storage is what decides it, not the
|
|
42520
42987
|
// resolved width: under Auto on a wide workspace the width resolves to the
|
|
42521
42988
|
// fullviewLogicalWidth default, and marking that too would light up two buttons
|
|
@@ -42558,12 +43025,20 @@ class Settings {
|
|
|
42558
43025
|
}
|
|
42559
43026
|
|
|
42560
43027
|
open() {
|
|
42561
|
-
this.
|
|
42562
|
-
this.getState();
|
|
43028
|
+
if (!this.modal) this.render();
|
|
43029
|
+
this.getState(); // Dragging is wired on first open: the shell is rendered with the panel,
|
|
43030
|
+
// long before the editor is ready to take a selector.
|
|
43031
|
+
|
|
43032
|
+
if (!this.draggable) {
|
|
43033
|
+
this.draggable = true;
|
|
43034
|
+
this.builder.editor.draggable('.is-modal.cbxsettings .is-draggable');
|
|
43035
|
+
}
|
|
43036
|
+
|
|
43037
|
+
this.builder.editor.util.showModal(this.modal);
|
|
42563
43038
|
}
|
|
42564
43039
|
|
|
42565
43040
|
close() {
|
|
42566
|
-
this.
|
|
43041
|
+
if (this.modal) this.builder.editor.util.hideModal(this.modal);
|
|
42567
43042
|
}
|
|
42568
43043
|
|
|
42569
43044
|
}
|
|
@@ -42679,6 +43154,28 @@ class ControlPanel {
|
|
|
42679
43154
|
|
|
42680
43155
|
<div class="panelnav">
|
|
42681
43156
|
<div class="part-breadcrumb"></div>
|
|
43157
|
+
|
|
43158
|
+
<!-- Quick access to the section's own actions, right under the
|
|
43159
|
+
Section crumb. A click in the page selects the Box, so these
|
|
43160
|
+
would otherwise cost a trip to the Section panel first.
|
|
43161
|
+
Deliberately smaller than the panel's own controls: it reads
|
|
43162
|
+
as a shortcut belonging to the crumb above it, not as part of
|
|
43163
|
+
the panel below. Shown only when Section is the only crumb (a
|
|
43164
|
+
Box is selected); deeper selections have their own arrows and
|
|
43165
|
+
these would read as "move this element". -->
|
|
43166
|
+
<div class="part-sectionmove group" role="group" aria-label="${out('Section')}" style="display:none">
|
|
43167
|
+
<button title="${out('Top')}" data-sectionmove="top"><svg><use xlink:href="#icon-arrow-top"></use></svg></button>
|
|
43168
|
+
<button title="${out('Up')}" data-sectionmove="up"><svg><use xlink:href="#icon-arrow-up"></use></svg></button>
|
|
43169
|
+
<button title="${out('Down')}" data-sectionmove="down"><svg><use xlink:href="#icon-arrow-down"></use></svg></button>
|
|
43170
|
+
<button title="${out('Bottom')}" data-sectionmove="bottom"><svg><use xlink:href="#icon-arrow-bottom"></use></svg></button>
|
|
43171
|
+
<button class="btn-sectionmore" title="${out('Height')}" aria-haspopup="true" aria-expanded="false"><svg><use xlink:href="#icon-dots"></use></svg></button>
|
|
43172
|
+
<button class="btn-sectiondup" title="${out('Duplicate')}"><svg><use xlink:href="#icon-copy"></use></svg></button>
|
|
43173
|
+
${this.builder.sectionHtmlButton ? `
|
|
43174
|
+
<button class="btn-sectionhtml" title="${out('Section HTML')}"><svg><use xlink:href="#icon-code"></use></svg></button>
|
|
43175
|
+
` : ''}
|
|
43176
|
+
<button class="btn-sectiondel" title="${out('Delete')}"><svg><use xlink:href="#icon-trash"></use></svg></button>
|
|
43177
|
+
</div>
|
|
43178
|
+
|
|
42682
43179
|
<h3 class="part-title"></h3>
|
|
42683
43180
|
<div class="part-empty">${out('No Selection.')}</div>
|
|
42684
43181
|
<div class="part-tabs" style="display:none">
|
|
@@ -42710,12 +43207,23 @@ class ControlPanel {
|
|
|
42710
43207
|
<div class="panel-dialog icons"></div>
|
|
42711
43208
|
<div class="panel-dialog blocks"></div>
|
|
42712
43209
|
<div class="panel-dialog imageadjust"></div>
|
|
42713
|
-
<div class="panel-dialog settings"></div>
|
|
42714
43210
|
<div class="panel-dialog group"></div>
|
|
42715
43211
|
<div class="panel-dialog blockoptions"></div>
|
|
42716
43212
|
<div class="panel-dialog shadow"></div>
|
|
42717
43213
|
<div class="panel-dialog symbols"></div>
|
|
42718
43214
|
|
|
43215
|
+
<!-- Section height, the one remaining section control used often
|
|
43216
|
+
enough to be worth reaching from the Box selection. A pop
|
|
43217
|
+
rather than four more icons: the presets are a grid, and the
|
|
43218
|
+
strip has to stay small to keep reading as a shortcut. -->
|
|
43219
|
+
<div class="panel-pop heightpop" tabIndex="-1" aria-hidden="true">
|
|
43220
|
+
<div class="label">${out('Height')}:</div>
|
|
43221
|
+
<div class="group sectionheight">
|
|
43222
|
+
${[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('')}
|
|
43223
|
+
<button title="${out('Clear')}" class="btn-clear" data-sectionheight="0"><svg><use xlink:href="#icon-eraser"></use></svg></button>
|
|
43224
|
+
</div>
|
|
43225
|
+
</div>
|
|
43226
|
+
|
|
42719
43227
|
<div class="panel-pop more" tabIndex="-1" aria-hidden="true">
|
|
42720
43228
|
<!--<button class="btn-preferences" title="${out('Preferences')}">${out('Preferences')}</button>-->
|
|
42721
43229
|
<button class="btn-settings" title="${out('Settings')}">${out('Settings')}</button>
|
|
@@ -42725,14 +43233,6 @@ class ControlPanel {
|
|
|
42725
43233
|
<div class="plugins"></div>
|
|
42726
43234
|
</div>
|
|
42727
43235
|
|
|
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
43236
|
</div>
|
|
42737
43237
|
|
|
42738
43238
|
`;
|
|
@@ -42770,14 +43270,64 @@ class ControlPanel {
|
|
|
42770
43270
|
this.showPanel(this.what);
|
|
42771
43271
|
});
|
|
42772
43272
|
});
|
|
42773
|
-
this.breadcrumb = controlPanel.querySelector('.part-breadcrumb');
|
|
43273
|
+
this.breadcrumb = controlPanel.querySelector('.part-breadcrumb'); // builder.activeSection is kept current whenever a box is activated, and
|
|
43274
|
+
// every action below reads it, so these need no selection of their own
|
|
43275
|
+
// and leave the open panel untouched.
|
|
43276
|
+
|
|
43277
|
+
this.sectionMoveBar = controlPanel.querySelector('.part-sectionmove');
|
|
43278
|
+
this.sectionMoveBar.querySelectorAll('[data-sectionmove]').forEach(btn => {
|
|
43279
|
+
btn.addEventListener('click', () => {
|
|
43280
|
+
this.builder.section.sectionMove(btn.getAttribute('data-sectionmove'));
|
|
43281
|
+
});
|
|
43282
|
+
});
|
|
43283
|
+
this.sectionMoveBar.querySelector('.btn-sectiondup').addEventListener('click', () => {
|
|
43284
|
+
this.builder.section.sectionDuplicate();
|
|
43285
|
+
});
|
|
43286
|
+
const btnSectionHtml = this.sectionMoveBar.querySelector('.btn-sectionhtml');
|
|
43287
|
+
if (btnSectionHtml) btnSectionHtml.addEventListener('click', () => {
|
|
43288
|
+
this.builder.openSectionHtml();
|
|
43289
|
+
});
|
|
43290
|
+
this.heightPop = controlPanel.querySelector('.panel-pop.heightpop');
|
|
43291
|
+
this.btnSectionMore = this.sectionMoveBar.querySelector('.btn-sectionmore');
|
|
43292
|
+
this.btnSectionMore.addEventListener('click', () => {
|
|
43293
|
+
this.markSectionHeight();
|
|
43294
|
+
this.showPop(this.heightPop, () => {
|
|
43295
|
+
this.btnSectionMore.setAttribute('aria-expanded', false);
|
|
43296
|
+
this.btnSectionMore.focus();
|
|
43297
|
+
}, this.btnSectionMore); // showPop right-aligns the pop to its button, which is how the
|
|
43298
|
+
// toolbar's own pops sit. This one belongs to the strip, so it hangs
|
|
43299
|
+
// under the whole strip and lines up with its left edge, pulled back
|
|
43300
|
+
// in only if the window is narrow enough to push it off screen.
|
|
43301
|
+
|
|
43302
|
+
const r = this.sectionMoveBar.getBoundingClientRect();
|
|
43303
|
+
this.heightPop.style.top = `${r.bottom + 5}px`;
|
|
43304
|
+
this.heightPop.style.left = `${Math.max(8, Math.min(r.left, window.innerWidth - this.heightPop.offsetWidth - 8))}px`;
|
|
43305
|
+
this.btnSectionMore.setAttribute('aria-expanded', true);
|
|
43306
|
+
});
|
|
43307
|
+
this.heightPop.querySelectorAll('[data-sectionheight]').forEach(btn => {
|
|
43308
|
+
btn.addEventListener('click', () => {
|
|
43309
|
+
this.builder.section.setSectionHeight(btn.getAttribute('data-sectionheight'));
|
|
43310
|
+
this.markSectionHeight(btn.getAttribute('data-sectionheight')); // Left open: picking a height is a try-and-look action, and the
|
|
43311
|
+
// next try should not cost another trip to the button.
|
|
43312
|
+
});
|
|
43313
|
+
}); // Same confirm as the section tool's trash: this is the same action at
|
|
43314
|
+
// the same scale, and the whole section goes with one small click.
|
|
43315
|
+
|
|
43316
|
+
this.sectionMoveBar.querySelector('.btn-sectiondel').addEventListener('click', () => {
|
|
43317
|
+
if (this.builder.settings.deleteConfirm) {
|
|
43318
|
+
this.builder.editor.util.showModal(this.builder.builderStuff.querySelector('.is-modal.delsectionconfirm'));
|
|
43319
|
+
return;
|
|
43320
|
+
}
|
|
43321
|
+
|
|
43322
|
+
this.builder.section.sectionDelete();
|
|
43323
|
+
if (this.builder.onSelectChange) this.builder.onSelectChange();
|
|
43324
|
+
});
|
|
42774
43325
|
this.title = controlPanel.querySelector('.part-title');
|
|
42775
43326
|
this.empty = controlPanel.querySelector('.part-empty');
|
|
42776
43327
|
this.dialogIcons = controlPanel.querySelector('.panel-dialog.icons'); // this.dialogBlocks = controlPanel.querySelector('.panel-dialog.blocks');
|
|
42777
43328
|
|
|
42778
43329
|
this.dialogShadow = controlPanel.querySelector('.panel-dialog.shadow');
|
|
42779
43330
|
this.dialogImageAdjust = controlPanel.querySelector('.panel-dialog.imageadjust');
|
|
42780
|
-
this.dialogSettings = controlPanel.querySelector('.panel-dialog.settings');
|
|
42781
43331
|
this.dialogGroup = controlPanel.querySelector('.panel-dialog.group');
|
|
42782
43332
|
this.dialogBlockOptions = controlPanel.querySelector('.panel-dialog.blockoptions');
|
|
42783
43333
|
this.dialogSymbols = controlPanel.querySelector('.panel-dialog.symbols');
|
|
@@ -42785,8 +43335,11 @@ class ControlPanel {
|
|
|
42785
43335
|
this.objDialogIcons = new Icons(this.dialogIcons, this.builder); // this.objDialogBlocks = new Blocks(this.dialogBlocks, this.builder);
|
|
42786
43336
|
|
|
42787
43337
|
this.objDialogShadow = new Shadow(this.dialogShadow, this.builder);
|
|
42788
|
-
this.objDialogImageAdjust = new ImageAdjust(this.dialogImageAdjust, this.builder);
|
|
42789
|
-
|
|
43338
|
+
this.objDialogImageAdjust = new ImageAdjust(this.dialogImageAdjust, this.builder); // Settings is a facility, not a selection level: it has nothing to do with
|
|
43339
|
+
// what is selected, so it builds its own dialog in the editor's UI root
|
|
43340
|
+
// and opens over everything.
|
|
43341
|
+
|
|
43342
|
+
this.objDialogSettings = new Settings(this.builder);
|
|
42790
43343
|
this.objDialogGroup = new Group(this.dialogGroup, this.builder);
|
|
42791
43344
|
this.objDialogBlockOptions = new BlockOptions(this.dialogBlockOptions, this.builder);
|
|
42792
43345
|
this.objPanelText = new PanelText(this.panelText, this.builder); // comment/disable if using ContentBuilder's controlpanel
|
|
@@ -42945,9 +43498,7 @@ class ControlPanel {
|
|
|
42945
43498
|
|
|
42946
43499
|
const btnSettings = controlPanel.querySelector('.btn-settings');
|
|
42947
43500
|
btnSettings.addEventListener('click', () => {
|
|
42948
|
-
this.objDialogSettings.open();
|
|
42949
|
-
// this.modal.showModal(modal);
|
|
42950
|
-
|
|
43501
|
+
this.objDialogSettings.open();
|
|
42951
43502
|
this.hidePop(this.more);
|
|
42952
43503
|
}); // Accordion // Move to editorReady if using ContentBuilder's controlpanel
|
|
42953
43504
|
|
|
@@ -43178,7 +43729,9 @@ class ControlPanel {
|
|
|
43178
43729
|
return;
|
|
43179
43730
|
}
|
|
43180
43731
|
|
|
43181
|
-
|
|
43732
|
+
const settingsModal = this.objDialogSettings.modal;
|
|
43733
|
+
|
|
43734
|
+
if (settingsModal && settingsModal.classList.contains('active')) {
|
|
43182
43735
|
this.objDialogSettings.close();
|
|
43183
43736
|
return;
|
|
43184
43737
|
} // The element dock last: the pickers above are transient, opened for one
|
|
@@ -43394,7 +43947,11 @@ class ControlPanel {
|
|
|
43394
43947
|
|
|
43395
43948
|
const target = textElement || el;
|
|
43396
43949
|
const hasText = !!textElement;
|
|
43397
|
-
const hasSource = this.builder.isOverlayImageElement(target);
|
|
43950
|
+
const hasSource = this.builder.isOverlayImageElement(target); // A link is an aspect of its own: a heading can be one, so can an
|
|
43951
|
+
// image, and so can an icon that is neither. panel-overlay.js owns the
|
|
43952
|
+
// rule (which anchor, and whether it can be saved); this only names it.
|
|
43953
|
+
|
|
43954
|
+
const linkOf = node => !!(this.objPanelOverlay && this.objPanelOverlay.linkFor(node));
|
|
43398
43955
|
|
|
43399
43956
|
if (!hasText && !hasSource) {
|
|
43400
43957
|
// [FX-PATH] Decoration aspect. Ornament layers — a cloud, a
|
|
@@ -43409,7 +43966,8 @@ class ControlPanel {
|
|
|
43409
43966
|
type: 'decoration',
|
|
43410
43967
|
element: deco,
|
|
43411
43968
|
hasText: false,
|
|
43412
|
-
hasSource: false
|
|
43969
|
+
hasSource: false,
|
|
43970
|
+
hasLink: linkOf(deco)
|
|
43413
43971
|
};
|
|
43414
43972
|
}
|
|
43415
43973
|
|
|
@@ -43419,7 +43977,8 @@ class ControlPanel {
|
|
|
43419
43977
|
type: type,
|
|
43420
43978
|
element: target,
|
|
43421
43979
|
hasText: hasText,
|
|
43422
|
-
hasSource: hasSource
|
|
43980
|
+
hasSource: hasSource,
|
|
43981
|
+
hasLink: linkOf(target)
|
|
43423
43982
|
};
|
|
43424
43983
|
} // [FX-PATH] Which element a decoration click selects. Path-first, matching
|
|
43425
43984
|
// the Motion picker: a compound object (3D die tumbling inside a flying
|
|
@@ -43765,6 +44324,7 @@ class ControlPanel {
|
|
|
43765
44324
|
}
|
|
43766
44325
|
|
|
43767
44326
|
this.breadcrumb.innerHTML = breadcrumbHtml;
|
|
44327
|
+
this.showSectionQuickTool(selection);
|
|
43768
44328
|
const links = this.breadcrumb.querySelectorAll('a');
|
|
43769
44329
|
links.forEach(link => {
|
|
43770
44330
|
link.addEventListener('click', e => {
|
|
@@ -43775,6 +44335,51 @@ class ControlPanel {
|
|
|
43775
44335
|
});
|
|
43776
44336
|
});
|
|
43777
44337
|
}
|
|
44338
|
+
/**
|
|
44339
|
+
* The quick section controls, shown only when Section is the only crumb (a
|
|
44340
|
+
* Box is selected). Deeper selections have their own arrows and these would
|
|
44341
|
+
* read as "move this element".
|
|
44342
|
+
*/
|
|
44343
|
+
|
|
44344
|
+
|
|
44345
|
+
showSectionQuickTool(selection) {
|
|
44346
|
+
this.quickToolSelection = selection;
|
|
44347
|
+
const show = selection === 'box' && this.builder.sectionQuickTool !== false;
|
|
44348
|
+
this.sectionMoveBar.style.display = show ? '' : 'none';
|
|
44349
|
+
this.btnSectionMore.style.display = show && this.sectionHeightAllowed() ? '' : 'none';
|
|
44350
|
+
this.hidePop(this.heightPop);
|
|
44351
|
+
}
|
|
44352
|
+
/** Re-apply the switch in Settings to the selection already on screen. */
|
|
44353
|
+
|
|
44354
|
+
|
|
44355
|
+
refreshSectionQuickTool() {
|
|
44356
|
+
this.showSectionQuickTool(this.quickToolSelection);
|
|
44357
|
+
} // Light up the height the section is on. `value` is passed straight after a
|
|
44358
|
+
// click so the state is right even though the class lands on a timeout.
|
|
44359
|
+
|
|
44360
|
+
|
|
44361
|
+
markSectionHeight(value) {
|
|
44362
|
+
const section = this.builder.activeSection;
|
|
44363
|
+
if (!section) return;
|
|
44364
|
+
this.heightPop.querySelectorAll('[data-sectionheight]').forEach(btn => {
|
|
44365
|
+
const h = btn.getAttribute('data-sectionheight');
|
|
44366
|
+
const on = value !== undefined ? h === value && h !== '0' : section.classList.contains('is-section-' + h);
|
|
44367
|
+
btn.classList.toggle('on', on);
|
|
44368
|
+
});
|
|
44369
|
+
} // A plugin that owns its whole section can refuse the height presets
|
|
44370
|
+
// (contentbox.sectionHeight === false), exactly as the Section panel reads
|
|
44371
|
+
// it. Then the pop has nothing to show and the button goes with it.
|
|
44372
|
+
|
|
44373
|
+
|
|
44374
|
+
sectionHeightAllowed() {
|
|
44375
|
+
const section = this.builder.activeSection;
|
|
44376
|
+
if (!section || !section.classList.contains('is-box')) return true;
|
|
44377
|
+
const el = section.querySelector('.is-overlay-content [data-cb-type]');
|
|
44378
|
+
if (!el) return true;
|
|
44379
|
+
const runtime = this.builder.win.builderRuntime;
|
|
44380
|
+
const plugin = runtime && runtime.getPlugin(el.getAttribute('data-cb-type'));
|
|
44381
|
+
return !(plugin && plugin.contentbox && plugin.contentbox.sectionHeight === false);
|
|
44382
|
+
}
|
|
43778
44383
|
|
|
43779
44384
|
animSelection(elm) {
|
|
43780
44385
|
elm.classList.add('selection-active');
|
|
@@ -43796,6 +44401,7 @@ class ControlPanel {
|
|
|
43796
44401
|
if (this.builder.contentSize) this.builder.contentSize.hide();
|
|
43797
44402
|
this.markOverlaySelection(null);
|
|
43798
44403
|
this.breadcrumb.innerHTML = '';
|
|
44404
|
+
this.showSectionQuickTool('');
|
|
43799
44405
|
this.title.innerHTML = '';
|
|
43800
44406
|
this.title.style.display = 'none';
|
|
43801
44407
|
this.empty.style.display = '';
|
|
@@ -44055,9 +44661,15 @@ class ControlPanel {
|
|
|
44055
44661
|
const overlayTarget = this.overlayTarget(element); // Resolved once, here, not inside the branch below: it reads
|
|
44056
44662
|
// computed style, and the dispatch would otherwise call it twice.
|
|
44057
44663
|
|
|
44058
|
-
const buttonNow = this.buttonTarget(element);
|
|
44664
|
+
const buttonNow = this.buttonTarget(element); // A code element sitting in an overlay is not a decorative layer:
|
|
44665
|
+
// it has an editor of its own. Without this it would land on the
|
|
44666
|
+
// Decoration note, which is the one place its code cannot be
|
|
44667
|
+
// reached from. Only the block ITSELF — a layer that merely
|
|
44668
|
+
// contains one still belongs to the overlay panel.
|
|
44669
|
+
|
|
44670
|
+
const overlayCodeBlock = overlayTarget && overlayTarget.element.matches('[data-cb-code]') ? overlayTarget.element : null;
|
|
44059
44671
|
|
|
44060
|
-
if (overlayTarget) {
|
|
44672
|
+
if (overlayTarget && !overlayCodeBlock) {
|
|
44061
44673
|
// Overlay layer content (outside .is-container) — reduced panel
|
|
44062
44674
|
// Name every aspect: titling an element 'Text' while the panel
|
|
44063
44675
|
// leads with an image source reads as the wrong panel.
|
|
@@ -44069,6 +44681,7 @@ class ControlPanel {
|
|
|
44069
44681
|
// backdrop setting that is not what they clicked.
|
|
44070
44682
|
const parts = [];
|
|
44071
44683
|
if (overlayTarget.hasText) parts.push(out('Text'));
|
|
44684
|
+
if (overlayTarget.hasLink) parts.push(out('Link'));
|
|
44072
44685
|
if (overlayTarget.hasSource) parts.push(out('Image'));
|
|
44073
44686
|
if (!parts.length) parts.push(out('Decoration')); // [FX-PATH]
|
|
44074
44687
|
|
|
@@ -61382,9 +61995,9 @@ class UndoRedo {
|
|
|
61382
61995
|
[data-html] and [data-html] logic never touches [data-cb-code].
|
|
61383
61996
|
*/
|
|
61384
61997
|
|
|
61385
|
-
const ATTR
|
|
61386
|
-
const SEL
|
|
61387
|
-
const TPL_SEL
|
|
61998
|
+
const ATTR = 'data-cb-code';
|
|
61999
|
+
const SEL = '[' + ATTR + ']';
|
|
62000
|
+
const TPL_SEL = ':scope > template[data-source]';
|
|
61388
62001
|
|
|
61389
62002
|
/*
|
|
61390
62003
|
Prevent a literal </template> inside user code from prematurely closing the
|
|
@@ -61392,7 +62005,7 @@ const TPL_SEL$1 = ':scope > template[data-source]';
|
|
|
61392
62005
|
(save -> load). Inside a JS string, <\/template> is identical to </template>
|
|
61393
62006
|
at runtime. (Same convention as the classic <\/script>.)
|
|
61394
62007
|
*/
|
|
61395
|
-
function escapeCodeSource
|
|
62008
|
+
function escapeCodeSource(src) {
|
|
61396
62009
|
return src.replace(/<\/template>/gi, '<\\/template>');
|
|
61397
62010
|
}
|
|
61398
62011
|
|
|
@@ -61407,14 +62020,14 @@ function escapeCodeSource$1(src) {
|
|
|
61407
62020
|
Exported standalone as well, so it can be used where no builder instance
|
|
61408
62021
|
exists yet (ContentBox injects content before creating the editor).
|
|
61409
62022
|
*/
|
|
61410
|
-
function wrapCodeSource
|
|
62023
|
+
function wrapCodeSource(html) {
|
|
61411
62024
|
if (!html) return html;
|
|
61412
|
-
if (html.indexOf(ATTR
|
|
62025
|
+
if (html.indexOf(ATTR) === -1) return html; // fast path
|
|
61413
62026
|
|
|
61414
62027
|
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
61415
|
-
parsed.querySelectorAll(SEL
|
|
62028
|
+
parsed.querySelectorAll(SEL).forEach(block => {
|
|
61416
62029
|
// Already wrapped (round-trip within the editor) => skip
|
|
61417
|
-
if (block.querySelector(TPL_SEL
|
|
62030
|
+
if (block.querySelector(TPL_SEL)) return;
|
|
61418
62031
|
|
|
61419
62032
|
// Snippet form (data-src) is already inert => handled by init()
|
|
61420
62033
|
if (block.hasAttribute('data-src')) return;
|
|
@@ -61424,7 +62037,7 @@ function wrapCodeSource$1(html) {
|
|
|
61424
62037
|
block.innerHTML = '';
|
|
61425
62038
|
const tpl = parsed.createElement('template');
|
|
61426
62039
|
tpl.setAttribute('data-source', '');
|
|
61427
|
-
tpl.innerHTML = escapeCodeSource
|
|
62040
|
+
tpl.innerHTML = escapeCodeSource(src);
|
|
61428
62041
|
block.appendChild(tpl);
|
|
61429
62042
|
});
|
|
61430
62043
|
return parsed.body.innerHTML;
|
|
@@ -61443,7 +62056,7 @@ class CodeElement {
|
|
|
61443
62056
|
// hideControls), and so it also works in non-editable columns.
|
|
61444
62057
|
this.doDocumentClick = e => {
|
|
61445
62058
|
if (!e.target || !e.target.closest) return;
|
|
61446
|
-
const block = e.target.closest(SEL
|
|
62059
|
+
const block = e.target.closest(SEL);
|
|
61447
62060
|
if (block && this.inContentArea(block)) {
|
|
61448
62061
|
this.activeBlock = block;
|
|
61449
62062
|
// ContentBox drives editing from its side panel (same as
|
|
@@ -61485,7 +62098,7 @@ class CodeElement {
|
|
|
61485
62098
|
return this.builder.util.makeId();
|
|
61486
62099
|
}
|
|
61487
62100
|
getTemplate(block) {
|
|
61488
|
-
return block.querySelector(TPL_SEL
|
|
62101
|
+
return block.querySelector(TPL_SEL);
|
|
61489
62102
|
}
|
|
61490
62103
|
|
|
61491
62104
|
/*
|
|
@@ -61512,7 +62125,7 @@ class CodeElement {
|
|
|
61512
62125
|
return this.contentRoots().some(root => root.contains(block));
|
|
61513
62126
|
}
|
|
61514
62127
|
escapeSource(src) {
|
|
61515
|
-
return escapeCodeSource
|
|
62128
|
+
return escapeCodeSource(src);
|
|
61516
62129
|
}
|
|
61517
62130
|
|
|
61518
62131
|
/*
|
|
@@ -61528,7 +62141,7 @@ class CodeElement {
|
|
|
61528
62141
|
return ids;
|
|
61529
62142
|
}
|
|
61530
62143
|
wrapSource(html) {
|
|
61531
|
-
return wrapCodeSource
|
|
62144
|
+
return wrapCodeSource(html);
|
|
61532
62145
|
}
|
|
61533
62146
|
|
|
61534
62147
|
/*
|
|
@@ -61584,7 +62197,7 @@ class CodeElement {
|
|
|
61584
62197
|
rerenderAll(root) {
|
|
61585
62198
|
const scope = root || this.builder.doc;
|
|
61586
62199
|
if (!scope || !scope.querySelectorAll) return;
|
|
61587
|
-
scope.querySelectorAll(SEL
|
|
62200
|
+
scope.querySelectorAll(SEL).forEach(block => this.render(block));
|
|
61588
62201
|
}
|
|
61589
62202
|
|
|
61590
62203
|
/*
|
|
@@ -61617,7 +62230,7 @@ class CodeElement {
|
|
|
61617
62230
|
}
|
|
61618
62231
|
|
|
61619
62232
|
// 2) Earlier code element templates (not part of live DOM queries)
|
|
61620
|
-
const otherTpls = roots[r].querySelectorAll(SEL
|
|
62233
|
+
const otherTpls = roots[r].querySelectorAll(SEL + ' > template[data-source]');
|
|
61621
62234
|
for (let i = 0; i < otherTpls.length; i++) {
|
|
61622
62235
|
const otherTpl = otherTpls[i];
|
|
61623
62236
|
if (otherTpl === tpl) continue;
|
|
@@ -61659,7 +62272,7 @@ class CodeElement {
|
|
|
61659
62272
|
*/
|
|
61660
62273
|
init(container) {
|
|
61661
62274
|
if (!container || !container.querySelectorAll) return;
|
|
61662
|
-
const blocks = container.querySelectorAll(SEL
|
|
62275
|
+
const blocks = container.querySelectorAll(SEL);
|
|
61663
62276
|
if (blocks.length === 0) return;
|
|
61664
62277
|
this.ensureCss();
|
|
61665
62278
|
blocks.forEach(block => {
|
|
@@ -61840,7 +62453,7 @@ class CodeElement {
|
|
|
61840
62453
|
markup filled the section on the page.
|
|
61841
62454
|
*/
|
|
61842
62455
|
style.textContent = `
|
|
61843
|
-
[${ATTR
|
|
62456
|
+
[${ATTR}] { min-height: 30px; -webkit-user-select: none; user-select: none; }
|
|
61844
62457
|
`;
|
|
61845
62458
|
doc.head.appendChild(style);
|
|
61846
62459
|
}
|
|
@@ -73964,10 +74577,10 @@ class Image$1 {
|
|
|
73964
74577
|
|
|
73965
74578
|
<div class="cb-field">
|
|
73966
74579
|
<label class="label-checkbox">
|
|
73967
|
-
<input class="input-newwindow" id="__input_newwindow2" type="checkbox" /> ${util.out('Open new
|
|
74580
|
+
<input class="input-newwindow" id="__input_newwindow2" type="checkbox" /> ${util.out('Open in new tab')}
|
|
73968
74581
|
</label>
|
|
73969
|
-
<label class="label-checkbox" id="lblImageLinkOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
|
|
73970
|
-
<input class="input-openlightbox" id="__input_openlightbox2" type="checkbox" /> ${util.out('Open in a lightbox
|
|
74582
|
+
<label class="label-checkbox mt-1" id="lblImageLinkOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
|
|
74583
|
+
<input class="input-openlightbox" id="__input_openlightbox2" type="checkbox" /> ${util.out('Open in a lightbox')}
|
|
73971
74584
|
</label>
|
|
73972
74585
|
</div>
|
|
73973
74586
|
|
|
@@ -75047,9 +75660,22 @@ class ButtonEditor {
|
|
|
75047
75660
|
if (cmd === 'href') setHref(el, value);else if (cmd === 'newtab') setNewTab(el, value);else if (cmd === 'fontsize') setFontSize(el, value === '' ? '' : parseInt(value, 10));else if (cmd === 'tracking') {
|
|
75048
75661
|
if (value !== '') setTracking(el, parseInt(value, 10));
|
|
75049
75662
|
} else if (cmd === 'margin') setMargin(el, side, value === '' ? 0 : parseInt(value, 10));
|
|
75663
|
+
if (cmd === 'href' || cmd === 'newtab') this.syncLinkDialog(el);
|
|
75050
75664
|
this.builder.opts.onChange();
|
|
75051
75665
|
this.realtime();
|
|
75052
75666
|
}
|
|
75667
|
+
|
|
75668
|
+
// The link dialog edits the same href/target and can be open alongside this
|
|
75669
|
+
// panel. Its fields are read when it opens, so without this its Ok would
|
|
75670
|
+
// write back the link as it was before the panel touched it.
|
|
75671
|
+
syncLinkDialog(el) {
|
|
75672
|
+
const modal = this.builderStuff.querySelector('.is-modal.createlink');
|
|
75673
|
+
if (!modal || !this.dom.hasClass(modal, 'active')) return;
|
|
75674
|
+
const url = modal.querySelector('.input-url');
|
|
75675
|
+
if (url && document.activeElement !== url) url.value = el.getAttribute('href') || '';
|
|
75676
|
+
const newwindow = modal.querySelector('.input-newwindow');
|
|
75677
|
+
if (newwindow) newwindow.checked = el.getAttribute('target') === '_blank';
|
|
75678
|
+
}
|
|
75053
75679
|
openColor(cmd, btn) {
|
|
75054
75680
|
const el = this.getButton();
|
|
75055
75681
|
if (!el) return;
|
|
@@ -75368,6 +75994,23 @@ class Hyperlink {
|
|
|
75368
75994
|
this.buttonEditor = new ButtonEditor(this.builder);
|
|
75369
75995
|
}
|
|
75370
75996
|
}
|
|
75997
|
+
|
|
75998
|
+
// The link dialog and a button UI can be open at the same time and both
|
|
75999
|
+
// show the link's href and target. After the dialog writes to the link,
|
|
76000
|
+
// the button UI has to re-read it, or it keeps showing (and writing back)
|
|
76001
|
+
// the old values. The host can own that UI instead (onButtonEdit).
|
|
76002
|
+
syncButtonUI(link) {
|
|
76003
|
+
if (!link || !link.isConnected) return;
|
|
76004
|
+
if (this.builder.opts.onButtonEdit) {
|
|
76005
|
+
if (link === this.builder.activeLinkButton) this.builder.opts.onButtonEdit(link);
|
|
76006
|
+
return;
|
|
76007
|
+
}
|
|
76008
|
+
const dom = this.builder.dom;
|
|
76009
|
+
const buttonModal = this.builder.builderStuff.querySelector('.is-modal.buttoneditor');
|
|
76010
|
+
if (!buttonModal || !dom.hasClass(buttonModal, 'active')) return;
|
|
76011
|
+
if (!this.buttonEditor) this.buttonEditor = new ButtonEditor(this.builder);
|
|
76012
|
+
this.buttonEditor.realtime();
|
|
76013
|
+
}
|
|
75371
76014
|
renderPanel() {
|
|
75372
76015
|
const builderStuff = this.builder.builderStuff;
|
|
75373
76016
|
const util = this.builder.util;
|
|
@@ -75399,10 +76042,10 @@ class Hyperlink {
|
|
|
75399
76042
|
</div>
|
|
75400
76043
|
<div class="cb-field">
|
|
75401
76044
|
<label class="label-checkbox">
|
|
75402
|
-
<input class="input-newwindow" id="__input_newwindow" type="checkbox" /> ${util.out('Open new
|
|
76045
|
+
<input class="input-newwindow" id="__input_newwindow" type="checkbox" /> ${util.out('Open in new tab')}
|
|
75403
76046
|
</label>
|
|
75404
|
-
<label class="label-checkbox" id="lblOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
|
|
75405
|
-
<input class="input-openlightbox" id="__input_openlightbox" type="checkbox" /> ${util.out('Open in a lightbox
|
|
76047
|
+
<label class="label-checkbox mt-1" id="lblOpenLightbox" style="${this.builder.useLightbox ? '' : 'display:none'}">
|
|
76048
|
+
<input class="input-openlightbox" id="__input_openlightbox" type="checkbox" /> ${util.out('Open in a lightbox')}
|
|
75406
76049
|
</label>
|
|
75407
76050
|
</div>
|
|
75408
76051
|
<div class="cb-field">
|
|
@@ -75445,6 +76088,7 @@ class Hyperlink {
|
|
|
75445
76088
|
let inputOk = modal.querySelector('.input-ok');
|
|
75446
76089
|
dom.addEventListener(inputOk, 'click', () => {
|
|
75447
76090
|
this.builder.uo.saveForUndo();
|
|
76091
|
+
let editedLink = null;
|
|
75448
76092
|
let link = this.builder.activeLink;
|
|
75449
76093
|
if (link) {
|
|
75450
76094
|
// Edit Existing Link
|
|
@@ -75454,35 +76098,19 @@ class Hyperlink {
|
|
|
75454
76098
|
let linktext = modal.querySelector('.input-text').value;
|
|
75455
76099
|
if (linktext === '') linktext = url;
|
|
75456
76100
|
|
|
75457
|
-
// Engine seam:
|
|
75458
|
-
//
|
|
75459
|
-
|
|
76101
|
+
// Engine seam: only UNLINK (a cleared URL) goes through
|
|
76102
|
+
// Inscribe. An edit is applied in place below: region.link()
|
|
76103
|
+
// re-wraps the selection in a NEW plain <a>, which dropped
|
|
76104
|
+
// the link's classes and inline styles, so changing a
|
|
76105
|
+
// button's URL stripped the button. Icon links stay legacy.
|
|
76106
|
+
if (url === '' && this.builder.opts.engine === 'inscribe' && !this.builder.activeIcon) {
|
|
75460
76107
|
const region = this.builder.editingEngine.active;
|
|
75461
76108
|
if (region && link.isConnected) {
|
|
75462
76109
|
dom.selectElementContents(link); // target the link
|
|
75463
|
-
|
|
75464
|
-
|
|
75465
|
-
|
|
75466
|
-
|
|
75467
|
-
class: modal.querySelector('.input-openlightbox').checked ? 'is-lightbox' : ''
|
|
75468
|
-
};
|
|
75469
|
-
const created = region.link(url, attrs); // re-wraps, returns <a>
|
|
75470
|
-
if (created && linktext && created.textContent !== linktext) {
|
|
75471
|
-
created.textContent = linktext;
|
|
75472
|
-
}
|
|
75473
|
-
this.builder.opts.onChange();
|
|
75474
|
-
util.hideModal(modal);
|
|
75475
|
-
if (created && created.isConnected) {
|
|
75476
|
-
region.focus();
|
|
75477
|
-
dom.selectElementContents(created);
|
|
75478
|
-
util.saveSelection();
|
|
75479
|
-
}
|
|
75480
|
-
} else {
|
|
75481
|
-
region.unlink();
|
|
75482
|
-
this.hideTool();
|
|
75483
|
-
this.builder.opts.onChange();
|
|
75484
|
-
util.hideModal(modal);
|
|
75485
|
-
}
|
|
76110
|
+
region.unlink();
|
|
76111
|
+
this.hideTool();
|
|
76112
|
+
this.builder.opts.onChange();
|
|
76113
|
+
util.hideModal(modal);
|
|
75486
76114
|
return;
|
|
75487
76115
|
}
|
|
75488
76116
|
}
|
|
@@ -75490,6 +76118,7 @@ class Hyperlink {
|
|
|
75490
76118
|
link.setAttribute('href', url);
|
|
75491
76119
|
if (modal.querySelector('.input-newwindow').checked) {
|
|
75492
76120
|
link.setAttribute('target', '_blank');
|
|
76121
|
+
if (!link.getAttribute('rel')) link.setAttribute('rel', 'noopener noreferrer');
|
|
75493
76122
|
} else {
|
|
75494
76123
|
link.removeAttribute('target');
|
|
75495
76124
|
}
|
|
@@ -75501,7 +76130,14 @@ class Hyperlink {
|
|
|
75501
76130
|
if (this.builder.activeIcon) ; else {
|
|
75502
76131
|
link.innerHTML = linktext;
|
|
75503
76132
|
}
|
|
75504
|
-
link.setAttribute('title', title);
|
|
76133
|
+
if (title) link.setAttribute('title', title);else link.removeAttribute('title');
|
|
76134
|
+
|
|
76135
|
+
// A button UI (this editor's Button panel, or the
|
|
76136
|
+
// host's own) shows the same link and can be open at
|
|
76137
|
+
// the same time as this dialog, so it has to re-read
|
|
76138
|
+
// what was just written.
|
|
76139
|
+
this.syncButtonUI(link);
|
|
76140
|
+
editedLink = link;
|
|
75505
76141
|
} else {
|
|
75506
76142
|
// link.outerHTML = link.innerHTML;
|
|
75507
76143
|
this.hideTool();
|
|
@@ -75659,6 +76295,17 @@ class Hyperlink {
|
|
|
75659
76295
|
this.builder.opts.onRender();
|
|
75660
76296
|
}
|
|
75661
76297
|
util.hideModal(modal);
|
|
76298
|
+
|
|
76299
|
+
// hideModal() blurs, so the caret goes back on the edited link
|
|
76300
|
+
// afterwards: the dialog can be reopened straight away.
|
|
76301
|
+
if (editedLink && editedLink.isConnected && !this.builder.activeIcon) {
|
|
76302
|
+
const region = this.builder.editingEngine.active;
|
|
76303
|
+
if (region) {
|
|
76304
|
+
region.focus();
|
|
76305
|
+
dom.selectElementContents(editedLink);
|
|
76306
|
+
util.saveSelection();
|
|
76307
|
+
}
|
|
76308
|
+
}
|
|
75662
76309
|
});
|
|
75663
76310
|
let inputCancel = modal.querySelector('.input-cancel');
|
|
75664
76311
|
dom.addEventListener(inputCancel, 'click', () => {
|
|
@@ -75804,7 +76451,10 @@ class Hyperlink {
|
|
|
75804
76451
|
}
|
|
75805
76452
|
|
|
75806
76453
|
// Button Stuff
|
|
75807
|
-
|
|
76454
|
+
// A host with its own button UI (onButtonEdit) already opens it by
|
|
76455
|
+
// selecting the button, so the pencil is a second route to the same
|
|
76456
|
+
// panel and is left out.
|
|
76457
|
+
if (this.builder.useButtonPlugin || this.builder.opts.onButtonEdit) {
|
|
75808
76458
|
linkTool.querySelector('.link-button-edit').style.display = 'none';
|
|
75809
76459
|
} else {
|
|
75810
76460
|
if (link && this.builder.activeLinkButton) linkTool.querySelector('.link-button-edit').style.display = 'block';else linkTool.querySelector('.link-button-edit').style.display = 'none';
|
|
@@ -76108,6 +76758,11 @@ class Button {
|
|
|
76108
76758
|
this.buttonEditor.realtime();
|
|
76109
76759
|
}
|
|
76110
76760
|
const buttonTool = this.buttonTool;
|
|
76761
|
+
|
|
76762
|
+
// A host with its own button UI (onButtonEdit) already opens it by
|
|
76763
|
+
// selecting the button, so the pencil is a second route to the same
|
|
76764
|
+
// panel and is left out.
|
|
76765
|
+
buttonTool.querySelector('.button-edit').style.display = this.builder.opts.onButtonEdit ? 'none' : '';
|
|
76111
76766
|
let top = activeButton.getBoundingClientRect().top + this.builder.win.pageYOffset;
|
|
76112
76767
|
let left = activeButton.getBoundingClientRect().left;
|
|
76113
76768
|
buttonTool.style.display = 'flex';
|
|
@@ -90676,7 +91331,16 @@ class ColorPickerKelir {
|
|
|
90676
91331
|
}
|
|
90677
91332
|
|
|
90678
91333
|
// Close the picker on scroll — it's positioned once, so scrolling detaches it.
|
|
90679
|
-
|
|
91334
|
+
//
|
|
91335
|
+
// The page moving under the picker is the only scroll that counts. A text
|
|
91336
|
+
// input scrolls its own content whenever the caret passes the edge — on
|
|
91337
|
+
// paste, on Delete, on Cmd+Left/Right — and that is a scroll event too,
|
|
91338
|
+
// which a capture listener on window receives even though it does not
|
|
91339
|
+
// bubble. Without this guard the picker closed while its own hex field
|
|
91340
|
+
// was being edited.
|
|
91341
|
+
const hideOnScroll = e => {
|
|
91342
|
+
const target = e && e.target;
|
|
91343
|
+
if (target && target.nodeType === 1 && popPicker.contains(target)) return;
|
|
90680
91344
|
this.builder.util.hidePop(popPicker);
|
|
90681
91345
|
window.removeEventListener('scroll', hideOnScroll, true);
|
|
90682
91346
|
if (this.builder.win && this.builder.win !== window) this.builder.win.removeEventListener('scroll', hideOnScroll, true);
|
|
@@ -137250,6 +137914,90 @@ class ContentBuilder {
|
|
|
137250
137914
|
openFilePicker(type, callback) {
|
|
137251
137915
|
this.openAssetSelect(type, callback);
|
|
137252
137916
|
}
|
|
137917
|
+
|
|
137918
|
+
/**
|
|
137919
|
+
* Is an asset manager reachable for this type of file? Mirrors the branches
|
|
137920
|
+
* openAsset() takes: either a picker page (filePicker / imageSelect / ...)
|
|
137921
|
+
* or the host's own onXSelectClick handler.
|
|
137922
|
+
*/
|
|
137923
|
+
hasFilePicker(type) {
|
|
137924
|
+
if (type === 'media') return !!(this.onMediaSelectClick || this.onImageSelectClick || this.mediaSelect || this.imageSelect);
|
|
137925
|
+
if (type === 'video') return !!(this.onVideoSelectClick || this.videoSelect);
|
|
137926
|
+
if (type === 'audio') return !!(this.onAudioSelectClick || this.audioSelect);
|
|
137927
|
+
if (type === 'all') return !!(this.onFileSelectClick || this.fileSelect);
|
|
137928
|
+
return !!(this.onImageSelectClick || this.imageSelect);
|
|
137929
|
+
}
|
|
137930
|
+
|
|
137931
|
+
/**
|
|
137932
|
+
* Upload a file from the user's computer and hand its URL to `callback`.
|
|
137933
|
+
*
|
|
137934
|
+
* The per-type upload handler always exists — a builder configured without
|
|
137935
|
+
* one gets the data-URL fallback — so this is available whatever the host
|
|
137936
|
+
* set up. The URL comes back through returnUrl(), the same path the image
|
|
137937
|
+
* tool's upload button uses.
|
|
137938
|
+
*/
|
|
137939
|
+
openFileUpload(type, callback) {
|
|
137940
|
+
const accept = type === 'video' ? 'video/*' : type === 'audio' ? 'audio/*' : type === 'media' ? 'image/*,video/*' : type === 'all' ? '*' : 'image/*';
|
|
137941
|
+
const handler = type === 'video' ? this.onVideoUpload : type === 'audio' ? this.onAudioUpload : type === 'media' ? this.onMediaUpload : type === 'all' ? this.onFileUpload : this.onImageUpload;
|
|
137942
|
+
if (!handler) return;
|
|
137943
|
+
const inpFile = document.createElement('input');
|
|
137944
|
+
inpFile.type = 'file';
|
|
137945
|
+
inpFile.accept = accept;
|
|
137946
|
+
inpFile.style.display = 'none';
|
|
137947
|
+
document.body.appendChild(inpFile);
|
|
137948
|
+
inpFile.addEventListener('change', async e => {
|
|
137949
|
+
if (!e.target.files || !e.target.files.length) {
|
|
137950
|
+
inpFile.remove();
|
|
137951
|
+
return;
|
|
137952
|
+
}
|
|
137953
|
+
this.onAssetUpload = url => {
|
|
137954
|
+
if (callback) callback(url);
|
|
137955
|
+
};
|
|
137956
|
+
await handler(e);
|
|
137957
|
+
inpFile.remove();
|
|
137958
|
+
});
|
|
137959
|
+
inpFile.click();
|
|
137960
|
+
}
|
|
137961
|
+
|
|
137962
|
+
/**
|
|
137963
|
+
* The file controls for a URL field in a plugin's settings: select from the
|
|
137964
|
+
* asset manager, and upload from the computer.
|
|
137965
|
+
*
|
|
137966
|
+
* Returns the buttons to append next to the input, in the order and with the
|
|
137967
|
+
* icons the editor's own image dialog uses — select first, upload last, and
|
|
137968
|
+
* select only where an asset manager is configured. A plugin appends what it
|
|
137969
|
+
* gets rather than deciding, so a builder without an asset manager never
|
|
137970
|
+
* shows a button that cannot open one.
|
|
137971
|
+
*
|
|
137972
|
+
* const [ ...buttons ] = builder.createFileButtons('media', (url) => {...});
|
|
137973
|
+
* row.append(input, ...buttons);
|
|
137974
|
+
*
|
|
137975
|
+
* `type` may be a function for a field whose kind follows another control
|
|
137976
|
+
* (a Media Type select, say) — it is then read on each click.
|
|
137977
|
+
*/
|
|
137978
|
+
createFileButtons(type, callback) {
|
|
137979
|
+
const out = s => this.util.out(s);
|
|
137980
|
+
const fileType = () => typeof type === 'function' ? type() : type;
|
|
137981
|
+
const button = (label, icon, onClick) => {
|
|
137982
|
+
const btn = document.createElement('button');
|
|
137983
|
+
btn.type = 'button';
|
|
137984
|
+
btn.className = 'cbx-iconbtn';
|
|
137985
|
+
btn.title = label;
|
|
137986
|
+
btn.setAttribute('aria-label', label);
|
|
137987
|
+
btn.innerHTML = `<svg aria-hidden="true"><use xlink:href="#${icon}"></use></svg>`;
|
|
137988
|
+
btn.addEventListener('click', e => {
|
|
137989
|
+
e.preventDefault();
|
|
137990
|
+
onClick(btn);
|
|
137991
|
+
});
|
|
137992
|
+
return btn;
|
|
137993
|
+
};
|
|
137994
|
+
const buttons = [];
|
|
137995
|
+
if (this.hasFilePicker(fileType())) {
|
|
137996
|
+
buttons.push(button(out('Select'), 'icon-folder', btn => this.openFilePicker(fileType(), callback, btn)));
|
|
137997
|
+
}
|
|
137998
|
+
buttons.push(button(out('Upload'), 'icon-upload', () => this.openFileUpload(fileType(), callback)));
|
|
137999
|
+
return buttons;
|
|
138000
|
+
}
|
|
137253
138001
|
openAssetSelect(targetAssetType, callback, defaultValue) {
|
|
137254
138002
|
const inpUrl = document.createElement('input');
|
|
137255
138003
|
|
|
@@ -141372,59 +142120,6 @@ Please obtain a license at: https://innovastudio.com/contentbox`);
|
|
|
141372
142120
|
}
|
|
141373
142121
|
}
|
|
141374
142122
|
|
|
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
142123
|
class Draggable {
|
|
141429
142124
|
constructor(opts = {}) {
|
|
141430
142125
|
this.opts = opts;
|
|
@@ -164393,6 +165088,12 @@ class ContentBox {
|
|
|
164393
165088
|
// HTML button on left sidebar
|
|
164394
165089
|
sectionHtmlButton: true,
|
|
164395
165090
|
// HTML button on section tool (edits the selected section only)
|
|
165091
|
+
sectionQuickTool: false,
|
|
165092
|
+
// 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.
|
|
165093
|
+
sectionTool: true,
|
|
165094
|
+
// Section tools on the page (move, HTML, settings, remove). Control panel only; users can switch these off in Settings.
|
|
165095
|
+
boxTool: true,
|
|
165096
|
+
// Box tools on the page (background upload & select, box & module settings). Control panel only; users can switch these off in Settings.
|
|
164396
165097
|
undoRedoButtons: true,
|
|
164397
165098
|
// Undo & redo buttons on control panel
|
|
164398
165099
|
contentSizeSlider: true,
|
|
@@ -167804,7 +168505,12 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
167804
168505
|
localStorage.removeItem('_zoom'); // Reset zoom
|
|
167805
168506
|
|
|
167806
168507
|
localStorage.removeItem('_livepreview');
|
|
167807
|
-
localStorage.removeItem('_disableanimation');
|
|
168508
|
+
localStorage.removeItem('_disableanimation'); // The user's own pick in Settings stands in front of the shipped
|
|
168509
|
+
// default, like _fullviewwidth does.
|
|
168510
|
+
|
|
168511
|
+
if (localStorage.getItem('_sectionquicktool') !== null) this.sectionQuickTool = localStorage.getItem('_sectionquicktool') === '1';
|
|
168512
|
+
if (localStorage.getItem('_sectiontool') !== null) this.sectionTool = localStorage.getItem('_sectiontool') === '1';
|
|
168513
|
+
if (localStorage.getItem('_boxtool') !== null) this.boxTool = localStorage.getItem('_boxtool') === '1';
|
|
167808
168514
|
}
|
|
167809
168515
|
|
|
167810
168516
|
if (this.uploadFile) {
|
|
@@ -168060,7 +168766,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
168060
168766
|
// scripts can run ([data-cb-code]); codeElement.init() then
|
|
168061
168767
|
// renders them (called from applyBehaviorOn).
|
|
168062
168768
|
|
|
168063
|
-
html = wrapCodeSource(html); // Render html content
|
|
168769
|
+
html = wrapCodeSource$1(html); // Render html content
|
|
168064
168770
|
|
|
168065
168771
|
let range = document.createRange();
|
|
168066
168772
|
this.wrapperEl.innerHTML = '';
|
|
@@ -170845,6 +171551,18 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
170845
171551
|
this.editor.openAssetSelect(type, callback);
|
|
170846
171552
|
}
|
|
170847
171553
|
|
|
171554
|
+
hasFilePicker(type) {
|
|
171555
|
+
return this.editor.hasFilePicker(type);
|
|
171556
|
+
}
|
|
171557
|
+
|
|
171558
|
+
openFileUpload(type, callback) {
|
|
171559
|
+
this.editor.openFileUpload(type, callback);
|
|
171560
|
+
}
|
|
171561
|
+
|
|
171562
|
+
createFileButtons(type, callback) {
|
|
171563
|
+
return this.editor.createFileButtons(type, callback);
|
|
171564
|
+
}
|
|
171565
|
+
|
|
170848
171566
|
openColorPicker(currentColor, callback, btn) {
|
|
170849
171567
|
this.editor.openColorPicker(currentColor, callback, btn);
|
|
170850
171568
|
}
|
|
@@ -171003,7 +171721,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
171003
171721
|
sectionSetup(section) {
|
|
171004
171722
|
let tool = section.querySelector('.is-section-tool');
|
|
171005
171723
|
if (tool) tool.parentNode.removeChild(tool);
|
|
171006
|
-
dom.appendHtml(section, `<div class="is-section-tool">
|
|
171724
|
+
if (this.sectionToolEnabled) dom.appendHtml(section, `<div class="is-section-tool">
|
|
171007
171725
|
<button type="button" class="btn-move-section-up" tabindex="-1" data-title="${out('Move Up')}" title="${out('Move Up')}">
|
|
171008
171726
|
<svg class="is-icon-flex" style="transform:rotate(180deg)"><use xlink:href="#icon-arrow-down2"></use></svg>
|
|
171009
171727
|
</button>
|
|
@@ -171029,7 +171747,8 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
171029
171747
|
dom.appendHtml(section, '<div class="is-section-info">' + (sectionId ? `<div>#${sectionId}</div>` : '') + '</div>'); // this.editor.setTooltip(section);
|
|
171030
171748
|
// Prepare for tooltip
|
|
171031
171749
|
|
|
171032
|
-
|
|
171750
|
+
const sectionToolEl = section.querySelector('.is-section-tool');
|
|
171751
|
+
let elms = sectionToolEl ? sectionToolEl.querySelectorAll('[title]') : [];
|
|
171033
171752
|
Array.prototype.forEach.call(elms, elm => {
|
|
171034
171753
|
elm.setAttribute('data-title', elm.getAttribute('title'));
|
|
171035
171754
|
|
|
@@ -171040,15 +171759,15 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
171040
171759
|
}
|
|
171041
171760
|
});
|
|
171042
171761
|
const btnSectionUp = section.querySelector('.btn-move-section-up');
|
|
171043
|
-
btnSectionUp.addEventListener('click', () => {
|
|
171762
|
+
if (btnSectionUp) btnSectionUp.addEventListener('click', () => {
|
|
171044
171763
|
this.section.sectionMove('up');
|
|
171045
171764
|
});
|
|
171046
171765
|
const btnSectionDown = section.querySelector('.btn-move-section-down');
|
|
171047
|
-
btnSectionDown.addEventListener('click', () => {
|
|
171766
|
+
if (btnSectionDown) btnSectionDown.addEventListener('click', () => {
|
|
171048
171767
|
this.section.sectionMove('down');
|
|
171049
171768
|
});
|
|
171050
171769
|
const btnSectionEdit = section.querySelector('.btn-section-edit');
|
|
171051
|
-
btnSectionEdit.addEventListener('click', () => {
|
|
171770
|
+
if (btnSectionEdit) btnSectionEdit.addEventListener('click', () => {
|
|
171052
171771
|
if (this.controlPanel) {
|
|
171053
171772
|
this.controlpanel.open();
|
|
171054
171773
|
this.controlpanel.select('section', true);
|
|
@@ -171062,7 +171781,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
171062
171781
|
this.openSectionHtml(section);
|
|
171063
171782
|
});
|
|
171064
171783
|
const btnSectionRemove = section.querySelector('.btn-section-remove');
|
|
171065
|
-
btnSectionRemove.addEventListener('click', () => {
|
|
171784
|
+
if (btnSectionRemove) btnSectionRemove.addEventListener('click', () => {
|
|
171066
171785
|
const modal = document.querySelector('.is-modal.delsectionconfirm');
|
|
171067
171786
|
|
|
171068
171787
|
if (this.settings.deleteConfirm) {
|
|
@@ -171221,6 +171940,41 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
171221
171940
|
set activePlugin(el) {
|
|
171222
171941
|
if (this.editor) this.editor.activePlugin = el;
|
|
171223
171942
|
}
|
|
171943
|
+
/**
|
|
171944
|
+
* The page's own section and box tools, which the user can switch off in
|
|
171945
|
+
* Settings (sectionTool / boxTool).
|
|
171946
|
+
*
|
|
171947
|
+
* Only with the control panel: everything those tools do is on the panel
|
|
171948
|
+
* (the strip under the Section crumb, and the Box panel), so switching them
|
|
171949
|
+
* off costs nothing there. In legacy modal mode they are the only way in, so
|
|
171950
|
+
* the option is ignored and the tools always render.
|
|
171951
|
+
*/
|
|
171952
|
+
|
|
171953
|
+
|
|
171954
|
+
get sectionToolEnabled() {
|
|
171955
|
+
return this.controlPanel ? this.sectionTool !== false : true;
|
|
171956
|
+
}
|
|
171957
|
+
|
|
171958
|
+
get boxToolEnabled() {
|
|
171959
|
+
return this.controlPanel ? this.boxTool !== false : true;
|
|
171960
|
+
}
|
|
171961
|
+
/** Re-run the section/box setup so the tools appear or disappear right away. */
|
|
171962
|
+
|
|
171963
|
+
|
|
171964
|
+
refreshPageTools() {
|
|
171965
|
+
if (!this.wrapperEl) return;
|
|
171966
|
+
this.wrapperEl.querySelectorAll('.is-section').forEach(section => {
|
|
171967
|
+
this.sectionSetup(section);
|
|
171968
|
+
|
|
171969
|
+
if (dom.hasClass(section, 'is-box')) {
|
|
171970
|
+
this.boxSetup(section);
|
|
171971
|
+
} else {
|
|
171972
|
+
section.querySelectorAll('.is-box').forEach(box => {
|
|
171973
|
+
this.boxSetup(box);
|
|
171974
|
+
});
|
|
171975
|
+
}
|
|
171976
|
+
});
|
|
171977
|
+
}
|
|
171224
171978
|
|
|
171225
171979
|
boxSetup(box) {
|
|
171226
171980
|
let tool = box.querySelector('.is-box-tool');
|
|
@@ -171240,7 +171994,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
171240
171994
|
'</div>');
|
|
171241
171995
|
*/
|
|
171242
171996
|
|
|
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
|
|
171997
|
+
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
171998
|
|
|
171245
171999
|
tool = box.querySelector('.is-box-tool');
|
|
171246
172000
|
|
|
@@ -171499,7 +172253,14 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
171499
172253
|
|
|
171500
172254
|
if (el.closest('[data-cb-type]')) return false; // plugin — its own editable regions
|
|
171501
172255
|
|
|
171502
|
-
if (el.closest('script, style, svg, noscript')) return false;
|
|
172256
|
+
if (el.closest('script, style, svg, noscript')) return false; // Inside a code element, only markup that is still verbatim in the
|
|
172257
|
+
// block's source can be edited: the render is discarded on save, so an
|
|
172258
|
+
// edit to anything the code built itself would not survive it. The
|
|
172259
|
+
// block itself is never text — its content is edited as code, and
|
|
172260
|
+
// writing to it here would overwrite the source template.
|
|
172261
|
+
|
|
172262
|
+
if (el.matches('[data-cb-code]')) return false;
|
|
172263
|
+
if (!codeSourceEditable(el)) return false;
|
|
171503
172264
|
if (!this.hasOwnText(el)) return false;
|
|
171504
172265
|
|
|
171505
172266
|
for (let parent = el.parentElement; parent && parent !== overlay; parent = parent.parentElement) {
|
|
@@ -171546,6 +172307,9 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
171546
172307
|
if (el.closest('.is-container')) return false;
|
|
171547
172308
|
if (el.closest('[data-html]')) return false;
|
|
171548
172309
|
if (el.closest('[data-cb-type]')) return false;
|
|
172310
|
+
if (el.matches('[data-cb-code]')) return false; // code element — see isOverlayTextElement
|
|
172311
|
+
|
|
172312
|
+
if (!codeSourceEditable(el)) return false;
|
|
171549
172313
|
if (el.tagName.toLowerCase() === 'img') return !el.hasAttribute('data-fixed');
|
|
171550
172314
|
return this.hasBackgroundImage(el);
|
|
171551
172315
|
}
|
|
@@ -172528,19 +173292,21 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
172528
173292
|
sectionTool.style.transformOrigin = 'top right';
|
|
172529
173293
|
}
|
|
172530
173294
|
|
|
172531
|
-
|
|
172532
|
-
|
|
172533
|
-
|
|
172534
|
-
|
|
172535
|
-
|
|
172536
|
-
|
|
172537
|
-
|
|
172538
|
-
|
|
172539
|
-
|
|
172540
|
-
|
|
172541
|
-
|
|
172542
|
-
|
|
172543
|
-
|
|
173295
|
+
if (sectionTool) {
|
|
173296
|
+
const btnSectionUp = sectionTool.querySelector('.btn-move-section-up');
|
|
173297
|
+
const btnSectionDown = sectionTool.querySelector('.btn-move-section-down');
|
|
173298
|
+
const btnSectionRemove = sectionTool.querySelector('.btn-section-remove');
|
|
173299
|
+
|
|
173300
|
+
if (box.offsetHeight < 160) {
|
|
173301
|
+
btnSectionUp.style.display = 'none';
|
|
173302
|
+
btnSectionDown.style.display = 'none';
|
|
173303
|
+
btnSectionRemove.style.display = 'none';
|
|
173304
|
+
sectionTool.style.top = '17px'; //'calc(50% - 17px)';
|
|
173305
|
+
} else {
|
|
173306
|
+
btnSectionUp.style.display = '';
|
|
173307
|
+
btnSectionDown.style.display = '';
|
|
173308
|
+
btnSectionRemove.style.display = '';
|
|
173309
|
+
}
|
|
172544
173310
|
} //new box tool
|
|
172545
173311
|
|
|
172546
173312
|
|
|
@@ -173274,7 +174040,7 @@ Add an image for each feature.`, 'Create a new block showcasing a photo gallery
|
|
|
173274
174040
|
// run ([data-cb-code]). Legacy [data-html] blocks are untouched.
|
|
173275
174041
|
|
|
173276
174042
|
|
|
173277
|
-
html = wrapCodeSource(html); // Render html content
|
|
174043
|
+
html = wrapCodeSource$1(html); // Render html content
|
|
173278
174044
|
|
|
173279
174045
|
let range = document.createRange();
|
|
173280
174046
|
wrapper.innerHTML = '';
|