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