fancoolo-fx 1.7.1 → 1.8.5

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/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Fancoolo FX — Changelog
2
+
3
+ ## 1.8.3
4
+ _(current release)_
5
+
6
+ ## 1.8.2
7
+ - **Fix:** `textReveal` now detects containers with block-level children (divs, sections, forms) and only splits text-bearing elements (h1–h6, p, blockquote, etc.) — prevents breakage of interactive widgets like accordions, tabs, and sliders
8
+
9
+ ## 1.8.1
10
+ - **Fix:** Removed `split.revert()` calls from `textReveal`, `typeWriter`, and `splitWords` — revert destroys JS state (event listeners, injected DOM) added after SplitText ran
11
+ - **Fix:** `textReveal` resize re-splitting fully handled by `autoSplit`, no manual revert needed
12
+
13
+ ## 1.8.0
14
+ - **Refactor:** `textReveal` uses native SplitText `autoSplit`, `mask: "lines"`, and `onSplit` — removes manual overflow wrappers and resize handler
15
+ - **Refactor:** Responsive and reduced-motion handling via `gsap.matchMedia()` — animations auto-revert when conditions change
16
+ - **Refactor:** Idempotent `init()` using persistent WeakSet — safe to call multiple times without double-animating
17
+ - **Refactor:** Scrub effects (`tiltIn`, `parallax`, `drawSVG` scrub) routed through `buildScrollTrigger()` — now support debug markers and `fx-start-[...]` overrides
18
+ - **Removed:** Manual resize listener, `_splitRegistry`, `document.fonts.ready` blocking — all handled natively by GSAP
19
+ - **Enhancement:** `FX.refresh()` simplified to `ScrollTrigger.refresh()`
20
+
21
+ ## 1.7.1
22
+ - **Fix:** FOUC prevention — all effects now use `autoAlpha` instead of `opacity`; elements start with `visibility:hidden` and are revealed by GSAP
23
+ - **New:** WordPress plugin injects `visibility:hidden` CSS automatically in the head
24
+ - **New:** Text-based effects set parent visibility before animating children to prevent flash
25
+ - **Enhancement:** `clipUp`/`clipDown` now include `autoAlpha` for consistent FOUC handling
26
+
27
+ ## 1.7.0
28
+ - **Fix:** Text-based effects (`textReveal`, `typeWriter`, `splitWords`) now re-split on browser resize — line breaks stay correct at every viewport width
29
+ - **New:** SplitText is reverted after one-shot animations complete — text reflows naturally without extra DOM wrappers
30
+ - **New:** `FX.refresh()` public method — manually re-split text after layout changes (sidebar toggle, font load)
31
+ - **New:** Automatic debounced resize handler (200ms, width-only) for pending scroll-triggered text animations
32
+
33
+ ## 1.6.1
34
+ - **Refactor:** Split plugin into namespaced classes under `inc/` with `FancooloFX` namespace
35
+ - `Settings.php`, `Frontend.php`, `Editor.php`, `Admin.php`, `AdminPage.php`, `SaveHandler.php`
36
+ - **New:** Integrated dPlugins self-hoster update checker
37
+ - Updated release workflow to use `rsync` + `.distignore` pattern
38
+
39
+ ## 1.6.0
40
+ - **New:** Gutenberg inspector panel — "FX Animation" added to every block's sidebar
41
+ - Effect dropdown with all 15 effects
42
+ - Trigger toggle: Item (scroll) / Trigger (section) / Page (load)
43
+ - Duration, delay, ease, start position modifiers
44
+ - Parallax Y-shift and `drawSVG` scrub options
45
+ - Copy/Paste FX between blocks (sidebar + context menu)
46
+ - **New:** Gutenberg integration toggle in plugin settings
47
+ - Build workflow updated to compile editor assets on release
48
+
49
+ ## 1.5.0
50
+ - **New:** 5 additional animation effects (fade-in, blur-in, clip-up, clip-down, slide-left/right, draw-svg, parallax, split-words, type-writer, tilt-in)
51
+ - **New:** Plugin settings page with import/export
52
+
53
+ ## 1.3.0
54
+ - **New:** Settings sidebar in the editor tab
55
+
56
+ ## 1.2.0
57
+ - **New:** `tiltIn` 3D perspective effect (scrub-based)
58
+
59
+ ## 1.1.0
60
+ - **Fix:** Syntax error — `stagger-all` block was outside `init()`
61
+
62
+ ## 1.0.2
63
+ - **Fix:** `-st` scroll trigger
64
+
65
+ ## 1.0.1
66
+ - Version bump
67
+
68
+ ## 1.0.0
69
+ - Initial release
70
+ - 5 animation effects (text-reveal, reveal, spin-reveal, bg-reveal, scale-in)
71
+ - Page load, scroll trigger, and section trigger modes
72
+ - Modifier classes for timing overrides
73
+ - Custom JavaScript editor with CodeMirror
74
+ - Built-in quick reference table
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fancoolo-fx",
3
- "version": "1.7.1",
3
+ "version": "1.8.5",
4
4
  "description": "A class-driven GSAP animation wrapper for WordPress and static sites.",
5
5
  "main": "src/fx.js",
6
6
  "homepage": "https://krstivoja.github.io/fancoolo-fx/",
package/readme.txt CHANGED
@@ -68,6 +68,21 @@ Yes. Use the `fx-start-[top center]` modifier class, or set `scrollStart` in the
68
68
 
69
69
  == Changelog ==
70
70
 
71
+ = 1.8.2 =
72
+ * Fix: textReveal now detects containers with block-level children (divs, sections, forms) and only splits text-bearing elements (h1-h6, p, blockquote, etc.) — prevents breakage of interactive widgets like accordions, tabs, and sliders
73
+
74
+ = 1.8.1 =
75
+ * Fix: Removed split.revert() calls from textReveal, typeWriter, and splitWords — revert destroys JS state (event listeners, injected DOM) added after SplitText ran
76
+ * Fix: textReveal resize re-splitting fully handled by autoSplit, no manual revert needed
77
+
78
+ = 1.8.0 =
79
+ * Refactor: textReveal uses native SplitText `autoSplit`, `mask: "lines"`, and `onSplit` — removes manual overflow wrappers and resize handler
80
+ * Refactor: Responsive and reduced-motion handling via `gsap.matchMedia()` — animations auto-revert when conditions change
81
+ * Refactor: Idempotent `init()` using persistent WeakSet — safe to call multiple times without double-animating
82
+ * Refactor: Scrub effects (tiltIn, parallax, drawSVG scrub) routed through `buildScrollTrigger()` — now support debug markers and `fx-start-[...]` overrides
83
+ * Removed: Manual resize listener, `_splitRegistry`, `document.fonts.ready` blocking — all handled natively by GSAP
84
+ * Enhancement: `FX.refresh()` simplified to `ScrollTrigger.refresh()`
85
+
71
86
  = 1.7.1 =
72
87
  * Fix: FOUC prevention — all effects now use autoAlpha instead of opacity, elements start with visibility:hidden and are revealed by GSAP
73
88
  * New: WordPress plugin injects visibility:hidden CSS automatically in the head
package/src/fx.js CHANGED
@@ -109,6 +109,11 @@
109
109
  slideIn: { duration: 1, ease: 'power3.out' },
110
110
  };
111
111
 
112
+ // ── State ───────────────────────────────────
113
+
114
+ var _animated = new WeakSet();
115
+ var _mm = gsap.matchMedia();
116
+
112
117
  // ── Helpers ──────────────────────────────────
113
118
 
114
119
  function getClassModifier(el, name, fallback) {
@@ -157,90 +162,85 @@
157
162
  return st;
158
163
  }
159
164
 
160
- // ── SplitText resize handling ───────────────
161
-
162
- var _splitRegistry = [];
163
- var _lastWidth = window.innerWidth;
164
- var _resizeTimer;
165
-
166
- function registerSplit(entry) {
167
- _splitRegistry.push(entry);
168
- }
165
+ // ── Effects ──────────────────────────────────
169
166
 
170
- function unregisterSplit(entry) {
171
- var idx = _splitRegistry.indexOf(entry);
172
- if (idx > -1) _splitRegistry.splice(idx, 1);
167
+ // Exclude an element from TranslatePress's dynamic (DOM-change) re-translation.
168
+ // SplitText restructures text into line/word/char spans; TranslatePress's
169
+ // MutationObserver (trp-translate-dom-changes.js) sees those mutations and
170
+ // re-translates the fragments, racing with SplitText and flickering the text
171
+ // between languages. The server already renders the translated text, so the
172
+ // dynamic pass is redundant here. `data-no-dynamic-translation` is in
173
+ // TranslatePress's own skip-selector list; it's an inert data attribute when
174
+ // TranslatePress isn't present.
175
+ function markNoDynamicTranslation(el) {
176
+ if (el && el.setAttribute) {
177
+ el.setAttribute('data-no-dynamic-translation', '');
178
+ }
173
179
  }
174
180
 
175
- function refreshSplits() {
176
- if (_splitRegistry.length === 0) return;
181
+ function splitTextReveal(target, o, isScroll, triggerEl, opts) {
182
+ markNoDynamicTranslation(target);
183
+ SplitText.create(target, {
184
+ type: 'lines',
185
+ mask: 'lines',
186
+ autoSplit: true,
187
+ onSplit: function (self) {
188
+ var tweenVars = {
189
+ y: '100%',
190
+ autoAlpha: 0,
191
+ duration: o.duration,
192
+ ease: o.ease,
193
+ stagger: o.stagger,
194
+ delay: o.delay,
195
+ };
177
196
 
178
- var pending = [];
179
-
180
- for (var i = _splitRegistry.length - 1; i >= 0; i--) {
181
- var entry = _splitRegistry[i];
182
- if (entry.tween) entry.tween.kill();
183
- if (entry.split) entry.split.revert();
184
- pending.push(entry);
185
- }
186
-
187
- _splitRegistry.length = 0;
197
+ if (isScroll) {
198
+ tweenVars.scrollTrigger = buildScrollTrigger(triggerEl, opts.scrollTrigger || {});
199
+ }
188
200
 
189
- pending.forEach(function (entry) {
190
- entry.effectFn(entry.el, entry.opts);
201
+ return gsap.from(self.lines, tweenVars);
202
+ },
191
203
  });
192
-
193
- ScrollTrigger.refresh();
194
204
  }
195
205
 
196
- window.addEventListener('resize', function () {
197
- if (window.innerWidth === _lastWidth) return;
198
- _lastWidth = window.innerWidth;
199
- clearTimeout(_resizeTimer);
200
- _resizeTimer = setTimeout(refreshSplits, 200);
201
- });
202
-
203
- // ── Effects ──────────────────────────────────
206
+ // Block-level tags whose presence signals a container with interactive children.
207
+ // When these are found as direct children, SplitText is applied only to
208
+ // text-bearing siblings to avoid restructuring interactive widgets.
209
+ var CONTAINER_CHILD_TAGS = /^(DIV|SECTION|ARTICLE|ASIDE|NAV|HEADER|FOOTER|MAIN|FORM|TABLE|DETAILS|FIELDSET)$/;
210
+ var TEXT_CHILD_TAGS = /^(H[1-6]|P|BLOCKQUOTE|FIGCAPTION|PRE|LABEL)$/;
204
211
 
205
212
  function textReveal(el, opts) {
206
213
  opts = opts || {};
207
214
  var o = resolveOptions(el, 'textReveal', opts);
215
+ var isScroll = opts.trigger === 'scroll' || opts.scrollTrigger;
208
216
 
209
217
  gsap.set(el, { visibility: 'inherit' });
210
- var split = new SplitText(el, { type: 'lines', linesClass: 'line-wrapper' });
211
-
212
- split.lines.forEach(function (line) {
213
- var wrapper = document.createElement('div');
214
- wrapper.style.overflow = 'hidden';
215
- line.parentNode.insertBefore(wrapper, line);
216
- wrapper.appendChild(line);
217
- });
218
-
219
- var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
220
- var entry = { el: el, split: split, tween: null, effectFn: textReveal, opts: opts };
221
-
222
- var tweenVars = {
223
- y: '100%',
224
- autoAlpha: 0,
225
- duration: o.duration,
226
- ease: o.ease,
227
- stagger: o.stagger,
228
- delay: o.delay,
229
- };
230
218
 
231
- if (isOneShot) {
232
- tweenVars.onComplete = function () {
233
- split.revert();
234
- unregisterSplit(entry);
235
- };
219
+ // If the element contains block-level children (divs, sections, etc.),
220
+ // only split text-bearing children to avoid breaking interactive widgets
221
+ // like accordions, tabs, or sliders inside the same container.
222
+ var hasBlockChild = false;
223
+ for (var c = 0; c < el.children.length; c++) {
224
+ if (CONTAINER_CHILD_TAGS.test(el.children[c].tagName)) {
225
+ hasBlockChild = true;
226
+ break;
227
+ }
236
228
  }
237
229
 
238
- if (opts.trigger === 'scroll' || opts.scrollTrigger) {
239
- tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
230
+ if (hasBlockChild) {
231
+ var targets = [];
232
+ for (var t = 0; t < el.children.length; t++) {
233
+ if (TEXT_CHILD_TAGS.test(el.children[t].tagName)) {
234
+ targets.push(el.children[t]);
235
+ }
236
+ }
237
+ targets.forEach(function (textEl) {
238
+ splitTextReveal(textEl, o, isScroll, el, opts);
239
+ });
240
+ return;
240
241
  }
241
242
 
242
- entry.tween = gsap.from(split.lines, tweenVars);
243
- registerSplit(entry);
243
+ splitTextReveal(el, o, isScroll, el, opts);
244
244
  }
245
245
 
246
246
  function reveal(el, opts) {
@@ -400,6 +400,11 @@
400
400
  opts = opts || {};
401
401
  var o = resolveOptions(el, 'tiltIn', opts);
402
402
 
403
+ var st = buildScrollTrigger(el, opts.scrollTrigger || {});
404
+ st.end = opts.end || 'top 20%';
405
+ st.scrub = opts.scrub != null ? opts.scrub : 0.6;
406
+ delete st.once;
407
+
403
408
  gsap.fromTo(el, {
404
409
  rotationX: opts.rotationX != null ? opts.rotationX : 45,
405
410
  scale: opts.scale != null ? opts.scale : 0.8,
@@ -412,12 +417,7 @@
412
417
  autoAlpha: 1,
413
418
  transformPerspective: 1000,
414
419
  ease: o.ease,
415
- scrollTrigger: {
416
- trigger: (opts.scrollTrigger && opts.scrollTrigger.trigger) || el,
417
- start: config.scrollStart || 'top 85%',
418
- end: opts.end || 'top 20%',
419
- scrub: opts.scrub != null ? opts.scrub : 0.6,
420
- },
420
+ scrollTrigger: st,
421
421
  });
422
422
  }
423
423
 
@@ -426,12 +426,10 @@
426
426
  var o = resolveOptions(el, 'typeWriter', opts);
427
427
 
428
428
  gsap.set(el, { visibility: 'inherit' });
429
+ markNoDynamicTranslation(el);
429
430
  var split = new SplitText(el, { type: 'chars' });
430
431
  gsap.set(split.chars, { autoAlpha: 0 });
431
432
 
432
- var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
433
- var entry = { el: el, split: split, tween: null, effectFn: typeWriter, opts: opts };
434
-
435
433
  var tweenVars = {
436
434
  autoAlpha: 1,
437
435
  duration: o.duration,
@@ -440,19 +438,11 @@
440
438
  delay: o.delay,
441
439
  };
442
440
 
443
- if (isOneShot) {
444
- tweenVars.onComplete = function () {
445
- split.revert();
446
- unregisterSplit(entry);
447
- };
448
- }
449
-
450
441
  if (opts.trigger === 'scroll' || opts.scrollTrigger) {
451
442
  tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
452
443
  }
453
444
 
454
- entry.tween = gsap.to(split.chars, tweenVars);
455
- registerSplit(entry);
445
+ gsap.to(split.chars, tweenVars);
456
446
  }
457
447
 
458
448
  function drawSVG(el, opts) {
@@ -476,15 +466,15 @@
476
466
  // Scrub mode: SVG draws as user scrolls (class fx-scrub-[0.6] or opts.scrub)
477
467
  var scrubVal = getClassModifier(el, 'scrub', opts.scrub != null ? opts.scrub : null);
478
468
  if (scrubVal !== null) {
469
+ var st = buildScrollTrigger(el, opts.scrollTrigger || {});
470
+ st.end = opts.end || 'top 20%';
471
+ st.scrub = scrubVal === true || scrubVal === 'true' ? true : scrubVal;
472
+ delete st.once;
473
+
479
474
  gsap.to(paths, {
480
475
  strokeDashoffset: 0,
481
476
  ease: o.ease,
482
- scrollTrigger: {
483
- trigger: (opts.scrollTrigger && opts.scrollTrigger.trigger) || el,
484
- start: config.scrollStart || 'top 85%',
485
- end: opts.end || 'top 20%',
486
- scrub: scrubVal === true || scrubVal === 'true' ? true : scrubVal,
487
- },
477
+ scrollTrigger: st,
488
478
  });
489
479
  return;
490
480
  }
@@ -508,17 +498,17 @@
508
498
  // Read y from modifier class fx-y-[80] or opts or default 50
509
499
  var yShift = getClassModifier(el, 'y', opts.y != null ? opts.y : 50);
510
500
 
501
+ var st = buildScrollTrigger(el, opts.scrollTrigger || {});
502
+ st.end = opts.end || 'bottom top';
503
+ st.scrub = opts.scrub != null ? opts.scrub : true;
504
+ delete st.once;
505
+
511
506
  gsap.fromTo(el, {
512
507
  y: -yShift,
513
508
  }, {
514
509
  y: yShift,
515
510
  ease: 'none',
516
- scrollTrigger: {
517
- trigger: (opts.scrollTrigger && opts.scrollTrigger.trigger) || el,
518
- start: config.scrollStart || 'top 85%',
519
- end: opts.end || 'bottom top',
520
- scrub: opts.scrub != null ? opts.scrub : true,
521
- },
511
+ scrollTrigger: st,
522
512
  });
523
513
  }
524
514
 
@@ -527,11 +517,9 @@
527
517
  var o = resolveOptions(el, 'splitWords', opts);
528
518
 
529
519
  gsap.set(el, { visibility: 'inherit' });
520
+ markNoDynamicTranslation(el);
530
521
  var split = new SplitText(el, { type: 'words' });
531
522
 
532
- var isOneShot = !(opts.trigger === 'scroll' || opts.scrollTrigger) || config.scrollOnce;
533
- var entry = { el: el, split: split, tween: null, effectFn: splitWords, opts: opts };
534
-
535
523
  var tweenVars = {
536
524
  y: opts.y != null ? opts.y : 30,
537
525
  autoAlpha: 0,
@@ -541,19 +529,11 @@
541
529
  delay: o.delay,
542
530
  };
543
531
 
544
- if (isOneShot) {
545
- tweenVars.onComplete = function () {
546
- split.revert();
547
- unregisterSplit(entry);
548
- };
549
- }
550
-
551
532
  if (opts.trigger === 'scroll' || opts.scrollTrigger) {
552
533
  tweenVars.scrollTrigger = buildScrollTrigger(el, opts.scrollTrigger || {});
553
534
  }
554
535
 
555
- entry.tween = gsap.from(split.words, tweenVars);
556
- registerSplit(entry);
536
+ gsap.from(split.words, tweenVars);
557
537
  }
558
538
 
559
539
  function slideIn(el, opts) {
@@ -650,7 +630,6 @@
650
630
  // ── Init ────────────────────────────────────
651
631
 
652
632
  function init() {
653
- var processed = new Set();
654
633
 
655
634
  Object.keys(effects).forEach(function (name) {
656
635
  var fn = effects[name];
@@ -660,8 +639,9 @@
660
639
  plGroups.forEach(function (group) {
661
640
  group = group.filter(function (el) { return !isExcluded(el); });
662
641
  group.forEach(function (el, i) {
642
+ if (_animated.has(el)) return;
663
643
  fn(el, { delay: i * 0.15 });
664
- processed.add(el);
644
+ _animated.add(el);
665
645
  });
666
646
  });
667
647
 
@@ -673,12 +653,13 @@
673
653
  stGroups.forEach(function (group) {
674
654
  group = group.filter(function (el) { return !isExcluded(el); });
675
655
  group.forEach(function (el, i) {
656
+ if (_animated.has(el)) return;
676
657
  fn(el, {
677
658
  trigger: 'scroll',
678
659
  delay: i * 0.15,
679
660
  scrollTrigger: { trigger: el },
680
661
  });
681
- processed.add(el);
662
+ _animated.add(el);
682
663
  });
683
664
  });
684
665
 
@@ -688,18 +669,19 @@
688
669
  if (config.sectionSelector) {
689
670
  document.querySelectorAll(config.sectionSelector).forEach(function (section) {
690
671
  var bareEls = Array.from(section.querySelectorAll('.' + name))
691
- .filter(function (el) { return !processed.has(el) && !isExcluded(el); });
672
+ .filter(function (el) { return !_animated.has(el) && !isExcluded(el); });
692
673
  if (bareEls.length === 0) return;
693
674
 
694
675
  var groups = groupByParent(bareEls);
695
676
  groups.forEach(function (group) {
696
677
  group.forEach(function (el, i) {
678
+ if (_animated.has(el)) return;
697
679
  fn(el, {
698
680
  trigger: 'scroll',
699
681
  delay: i * 0.15,
700
682
  scrollTrigger: { trigger: el },
701
683
  });
702
- processed.add(el);
684
+ _animated.add(el);
703
685
  });
704
686
  });
705
687
  });
@@ -708,21 +690,21 @@
708
690
 
709
691
  // 4. Scrub-based effects — always scroll-linked, processed before tagMap.
710
692
  document.querySelectorAll('.fx-tilt-in-st, .fx-tilt-in-pl, .fx-tilt-in').forEach(function (el) {
711
- if (!processed.has(el) && !isExcluded(el)) {
693
+ if (!_animated.has(el) && !isExcluded(el)) {
712
694
  tiltIn(el);
713
- processed.add(el);
695
+ _animated.add(el);
714
696
  }
715
697
  });
716
698
  document.querySelectorAll('.fx-parallax-st, .fx-parallax-pl, .fx-parallax').forEach(function (el) {
717
- if (!processed.has(el) && !isExcluded(el)) {
699
+ if (!_animated.has(el) && !isExcluded(el)) {
718
700
  parallax(el);
719
- processed.add(el);
701
+ _animated.add(el);
720
702
  }
721
703
  });
722
704
  document.querySelectorAll('.fx-draw-svg-scrub').forEach(function (el) {
723
- if (!processed.has(el) && !isExcluded(el)) {
705
+ if (!_animated.has(el) && !isExcluded(el)) {
724
706
  drawSVG(el, { scrub: getClassModifier(el, 'scrub', 0.6) });
725
- processed.add(el);
707
+ _animated.add(el);
726
708
  }
727
709
  });
728
710
 
@@ -735,18 +717,19 @@
735
717
  if (!fn) return;
736
718
 
737
719
  var els = Array.from(section.querySelectorAll(selector))
738
- .filter(function (el) { return !processed.has(el) && !isExcluded(el); });
720
+ .filter(function (el) { return !_animated.has(el) && !isExcluded(el); });
739
721
  if (els.length === 0) return;
740
722
 
741
723
  var groups = groupByParent(els);
742
724
  groups.forEach(function (group) {
743
725
  applyScrollGroup(fn, group, section);
744
- group.forEach(function (el) { processed.add(el); });
726
+ group.forEach(function (el) { _animated.add(el); });
745
727
  });
746
728
  });
747
729
  });
748
730
  }
749
- // 5. fx-stagger-all-[selector] — target children, effect from sibling class
731
+
732
+ // 6. fx-stagger-all-[selector] — target children, effect from sibling class
750
733
  // Requires an effect class on the same element (e.g. fx-reveal-st).
751
734
  document.querySelectorAll('[class*="fx-stagger-all-"]').forEach(function (container) {
752
735
  // Parse selector from fx-stagger-all-[img,p]
@@ -776,7 +759,7 @@
776
759
  var isScroll = container.classList.contains(effectName + '-st') ||
777
760
  container.classList.contains(effectName);
778
761
  var children = Array.from(container.querySelectorAll(childSelector))
779
- .filter(function (el) { return !processed.has(el); });
762
+ .filter(function (el) { return !_animated.has(el); });
780
763
  if (children.length === 0) return;
781
764
 
782
765
  children.forEach(function (child, i) {
@@ -786,7 +769,7 @@
786
769
  opts.scrollTrigger = { trigger: child };
787
770
  }
788
771
  effectFn(child, opts);
789
- processed.add(child);
772
+ _animated.add(child);
790
773
  });
791
774
  });
792
775
 
@@ -808,20 +791,38 @@
808
791
  if (pre.excludeSelectors !== undefined) config.excludeSelectors = pre.excludeSelectors;
809
792
  }
810
793
 
794
+ // Detect the TranslatePress translation editor. It loads the page inside an
795
+ // iframe with a `trp-edit-translation` URL param. FX must stand down there:
796
+ // SplitText fragments text into spans (the real string survives only in
797
+ // aria-label), and its autoSplit ResizeObserver fights TranslatePress's DOM
798
+ // rewriting — producing an endless re-split flicker and untranslatable blocks.
799
+ function isTranslationEditor() {
800
+ try {
801
+ return /[?&]trp-edit-translation=/.test(window.location.search);
802
+ } catch (e) {
803
+ return false;
804
+ }
805
+ }
806
+
811
807
  function boot() {
808
+ if (isTranslationEditor()) return;
809
+
812
810
  applyPreConfig();
813
811
 
814
- // Skip animations if OS reduced motion is enabled
815
- if (config.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
816
- return;
812
+ // Build media query from config — animations auto-revert when conditions stop matching
813
+ var parts = [];
814
+ if (config.disableMobile) {
815
+ parts.push('(min-width: ' + (config.mobileBreakpoint + 1) + 'px)');
817
816
  }
818
-
819
- // Skip animations on mobile
820
- if (config.disableMobile && window.innerWidth <= config.mobileBreakpoint) {
821
- return;
817
+ if (config.respectReducedMotion) {
818
+ parts.push('(prefers-reduced-motion: no-preference)');
822
819
  }
820
+ var conditions = parts.length > 0 ? parts.join(' and ') : 'all';
823
821
 
824
- init();
822
+ _mm.add(conditions, function () {
823
+ _animated = new WeakSet();
824
+ init();
825
+ });
825
826
  }
826
827
 
827
828
  if (document.readyState === 'loading') {
@@ -850,6 +851,6 @@
850
851
  splitWords: splitWords,
851
852
  slideIn: slideIn,
852
853
  init: init,
853
- refresh: refreshSplits,
854
+ refresh: function () { ScrollTrigger.refresh(); },
854
855
  };
855
856
  })();