@unabridged/midwest 0.24.0 → 0.24.3

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.
Files changed (26) hide show
  1. package/app/assets/javascript/midwest/index.ts +2 -0
  2. package/app/assets/javascript/midwest/viewport_observer.ts +49 -0
  3. package/app/assets/javascript/midwest.js +84 -64
  4. package/app/assets/javascript/midwest.js.map +1 -1
  5. package/app/assets/stylesheets/midwest.css +1 -1
  6. package/app/assets/stylesheets/midwest.tailwind.css +2 -1
  7. package/dist/css/midwest.css +1 -1
  8. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +2 -0
  9. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  10. package/dist/javascript/collection/app/assets/javascript/midwest/viewport_observer.js +22 -0
  11. package/dist/javascript/collection/app/assets/javascript/midwest/viewport_observer.js.map +1 -0
  12. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js +4 -7
  13. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js.map +1 -1
  14. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +0 -6
  15. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
  16. package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js +20 -25
  17. package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -1
  18. package/dist/javascript/collection/app/components/midwest/motion_component/motion_component_controller.js +32 -0
  19. package/dist/javascript/collection/app/components/midwest/motion_component/motion_component_controller.js.map +1 -0
  20. package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js +4 -12
  21. package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js.map +1 -1
  22. package/dist/javascript/collection/app/components/midwest/table_component/load_more_controller.js +10 -14
  23. package/dist/javascript/collection/app/components/midwest/table_component/load_more_controller.js.map +1 -1
  24. package/dist/javascript/midwest.js +84 -64
  25. package/dist/javascript/midwest.js.map +1 -1
  26. package/package.json +1 -1
@@ -38,6 +38,7 @@ import Onboarding from '../../../components/midwest/onboarding_component/onboard
38
38
  import ThemeSwitch from '../../../components/midwest/form/switch_component/theme_switch_controller'
39
39
  import Panorama from '../../../components/midwest/panorama_component/panorama_component_controller'
40
40
  import Playlist from '../../../components/midwest/playlist_component/playlist_component_controller'
41
+ import Motion from '../../../components/midwest/motion_component/motion_component_controller'
41
42
  /* IMPORTS */
42
43
 
43
44
  export { Card, Banner, CountdownTimer, Chart }
@@ -83,5 +84,6 @@ export function registerMidwestControllers (application: any) {
83
84
  application.register('midwest-theme-switch', ThemeSwitch)
84
85
  application.register('midwest-panorama', Panorama)
85
86
  application.register('midwest-playlist', Playlist)
87
+ application.register('midwest-motion', Motion)
86
88
  /* EXPORTS */
87
89
  }
@@ -0,0 +1,49 @@
1
+ // Shared viewport-detection helper for components that react to scrolling into
2
+ // and out of view (IntersectionObserverComponent, MotionComponent, …).
3
+ //
4
+ // Wraps the IntersectionObserver API so controllers only describe *what* to do
5
+ // on enter/leave, not the observer plumbing. Components own the reaction (toggle
6
+ // a class, dispatch an event, reveal an animation); this module owns creating,
7
+ // observing, and tearing down the observer so the behavior stays consistent.
8
+
9
+ export interface ViewportObserverOptions {
10
+ // Fraction(s) of the element that must be visible to count as intersecting.
11
+ threshold?: number | number[]
12
+ // Margin around the viewport root (CSS shorthand string).
13
+ rootMargin?: string
14
+ // Stop observing after the first time the element enters the viewport.
15
+ once?: boolean
16
+ // Called each time the element enters the viewport.
17
+ onEnter?: (entry: IntersectionObserverEntry) => void
18
+ // Called each time the element leaves the viewport. Never called after the
19
+ // first enter when `once` is true (the observer has disconnected by then).
20
+ onLeave?: (entry: IntersectionObserverEntry) => void
21
+ }
22
+
23
+ // Observe `element` crossing the viewport threshold, invoking the callbacks as
24
+ // it enters and leaves. Returns a teardown function that disconnects the
25
+ // observer; callers should invoke it from their controller's `disconnect()`.
26
+ export function observeViewport (
27
+ element: Element,
28
+ options: ViewportObserverOptions = {}
29
+ ): () => void {
30
+ const { threshold = 0, rootMargin = '0px', once = false, onEnter, onLeave } = options
31
+
32
+ const observer = new IntersectionObserver(
33
+ (entries) => {
34
+ for (const entry of entries) {
35
+ if (entry.isIntersecting) {
36
+ onEnter?.(entry)
37
+ if (once) observer.disconnect()
38
+ } else {
39
+ onLeave?.(entry)
40
+ }
41
+ }
42
+ },
43
+ { threshold, rootMargin }
44
+ )
45
+
46
+ observer.observe(element)
47
+
48
+ return () => observer.disconnect()
49
+ }
@@ -2557,7 +2557,6 @@ class Dropdown extends Controller {
2557
2557
  supported: { type: Boolean, default: true },
2558
2558
  align: { type: String, default: "right" }
2559
2559
  };
2560
- observer = new IntersectionObserver(this.computedCB.bind(this), { threshold: 1 });
2561
2560
  cleanup;
2562
2561
  connect() {
2563
2562
  this.supportedValue = CSS.supports("anchor-name: --myanchor");
@@ -2623,11 +2622,6 @@ class Dropdown extends Controller {
2623
2622
  (item) => item.closest('[data-controller~="midwest-dropdown"]') === this.element && getComputedStyle(item).display !== "none"
2624
2623
  );
2625
2624
  }
2626
- computedCB(entries) {
2627
- entries.forEach(async () => {
2628
- this.computation();
2629
- });
2630
- }
2631
2625
  closeIfOpen = () => {
2632
2626
  this.element.hidePopover();
2633
2627
  };
@@ -3012,7 +3006,7 @@ class Carousel extends Controller {
3012
3006
  }
3013
3007
  const atEnd = this._currentIndex >= this.slides.length - 1;
3014
3008
  if (this.loopValue && atEnd) {
3015
- this.slides[0]?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
3009
+ this.trackTarget.scrollTo({ left: 0, behavior: "smooth" });
3016
3010
  } else {
3017
3011
  this.trackTarget.scrollBy({ left: this.trackTarget.clientWidth, behavior: "smooth" });
3018
3012
  }
@@ -3026,8 +3020,7 @@ class Carousel extends Controller {
3026
3020
  }
3027
3021
  const atStart = this._currentIndex <= 0;
3028
3022
  if (this.loopValue && atStart) {
3029
- const last = this.slides[this.slides.length - 1];
3030
- last?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
3023
+ this.trackTarget.scrollTo({ left: (this.slides.length - 1) * this.trackTarget.clientWidth, behavior: "smooth" });
3031
3024
  } else {
3032
3025
  this.trackTarget.scrollBy({ left: -this.trackTarget.clientWidth, behavior: "smooth" });
3033
3026
  }
@@ -3039,8 +3032,7 @@ class Carousel extends Controller {
3039
3032
  this.activateSlide(index);
3040
3033
  return;
3041
3034
  }
3042
- const slide = this.slides[index];
3043
- slide?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
3035
+ this.trackTarget.scrollTo({ left: index * this.trackTarget.clientWidth, behavior: "smooth" });
3044
3036
  }
3045
3037
  // Unlocks the next slide and advances to it. Intended for wizard mode: wire
3046
3038
  // a "Continue" button inside a slide to this action so users progress
@@ -3144,8 +3136,7 @@ class Carousel extends Controller {
3144
3136
  }
3145
3137
  const atEnd = this._currentIndex >= this.slides.length - 1;
3146
3138
  if (atEnd) {
3147
- const firstSlide = this.slides[0];
3148
- firstSlide?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
3139
+ this.trackTarget.scrollTo({ left: 0, behavior: "smooth" });
3149
3140
  } else {
3150
3141
  this.next();
3151
3142
  }
@@ -4871,10 +4862,7 @@ class Notification extends Controller {
4871
4862
  connect() {
4872
4863
  this.container = this.element.closest("[popover]");
4873
4864
  this.container?.showPopover?.();
4874
- requestAnimationFrame(() => {
4875
- this.element.classList.add("visible");
4876
- });
4877
- const frame = this.element.parentElement;
4865
+ const frame = this.element.closest("turbo-frame");
4878
4866
  if (frame) {
4879
4867
  this.observer = new MutationObserver(() => this.syncCountdown());
4880
4868
  this.observer.observe(frame, { childList: true });
@@ -4893,14 +4881,8 @@ class Notification extends Controller {
4893
4881
  }
4894
4882
  }
4895
4883
  close() {
4896
- if (!this.element.classList.contains("animated")) {
4897
- this.element.remove();
4898
- return;
4899
- }
4900
4884
  this.element.classList.add("closing");
4901
- setTimeout(() => {
4902
- this.element.remove();
4903
- }, 350);
4885
+ setTimeout(() => (this.element.parentElement ?? this.element).remove(), 350);
4904
4886
  }
4905
4887
  syncCountdown() {
4906
4888
  const timerEl = this.element.querySelector('[data-controller~="midwest-countdown-timer"]');
@@ -4912,7 +4894,8 @@ class Notification extends Controller {
4912
4894
  );
4913
4895
  if (!timer)
4914
4896
  return;
4915
- const isTop = this.element === this.element.parentElement?.firstElementChild;
4897
+ const wrapper = this.element.parentElement ?? this.element;
4898
+ const isTop = wrapper === wrapper.parentElement?.firstElementChild;
4916
4899
  if (isTop) {
4917
4900
  timer.resume();
4918
4901
  } else {
@@ -4986,36 +4969,51 @@ class Table extends Controller {
4986
4969
  }
4987
4970
  }
4988
4971
 
4972
+ function observeViewport(element, options = {}) {
4973
+ const { threshold = 0, rootMargin = "0px", once = false, onEnter, onLeave } = options;
4974
+ const observer = new IntersectionObserver(
4975
+ (entries) => {
4976
+ for (const entry of entries) {
4977
+ if (entry.isIntersecting) {
4978
+ onEnter?.(entry);
4979
+ if (once)
4980
+ observer.disconnect();
4981
+ } else {
4982
+ onLeave?.(entry);
4983
+ }
4984
+ }
4985
+ },
4986
+ { threshold, rootMargin }
4987
+ );
4988
+ observer.observe(element);
4989
+ return () => observer.disconnect();
4990
+ }
4991
+
4989
4992
  class LoadMore extends Controller {
4990
4993
  static values = {
4991
4994
  url: String,
4992
4995
  tbodyId: String
4993
4996
  };
4994
- observer = null;
4997
+ teardown = null;
4995
4998
  loading = false;
4996
4999
  connect() {
4997
5000
  if (!this.urlValue)
4998
5001
  return;
4999
- this.observer = new IntersectionObserver(
5000
- (entries) => {
5001
- if (entries[0].isIntersecting)
5002
- this.load();
5003
- },
5004
- // Start fetching 300px before the sentinel enters the viewport so rows
5005
- // appear before the user actually reaches the bottom.
5006
- { rootMargin: "0px 0px 300px 0px" }
5007
- );
5008
- this.observer.observe(this.element);
5002
+ this.teardown = observeViewport(this.element, {
5003
+ rootMargin: "0px 0px 300px 0px",
5004
+ once: true,
5005
+ onEnter: () => this.load()
5006
+ });
5009
5007
  }
5010
5008
  disconnect() {
5011
- this.observer?.disconnect();
5012
- this.observer = null;
5009
+ this.teardown?.();
5010
+ this.teardown = null;
5013
5011
  }
5014
5012
  async load() {
5015
5013
  if (this.loading)
5016
5014
  return;
5017
5015
  this.loading = true;
5018
- this.observer?.disconnect();
5016
+ this.teardown?.();
5019
5017
  try {
5020
5018
  const response = await fetch(this.urlValue, {
5021
5019
  headers: {
@@ -6947,35 +6945,29 @@ class ViewportObserver extends Controller {
6947
6945
  once: { type: Boolean, default: false },
6948
6946
  visibleClass: { type: String, default: "" }
6949
6947
  };
6950
- observer = null;
6948
+ teardown = null;
6951
6949
  connect() {
6952
- this.observer = new IntersectionObserver(
6953
- (entries) => {
6954
- for (const entry of entries)
6955
- this.handle(entry);
6950
+ this.teardown = observeViewport(this.element, {
6951
+ threshold: this.thresholdValue,
6952
+ rootMargin: this.rootMarginValue,
6953
+ once: this.onceValue,
6954
+ onEnter: (entry) => {
6955
+ this.element.setAttribute("data-intersecting", "true");
6956
+ if (this.visibleClassValue)
6957
+ this.element.classList.add(this.visibleClassValue);
6958
+ this.dispatch("enter", { detail: { entry }, bubbles: true });
6956
6959
  },
6957
- { threshold: this.thresholdValue, rootMargin: this.rootMarginValue }
6958
- );
6959
- this.observer.observe(this.element);
6960
+ onLeave: (entry) => {
6961
+ this.element.setAttribute("data-intersecting", "false");
6962
+ if (this.visibleClassValue)
6963
+ this.element.classList.remove(this.visibleClassValue);
6964
+ this.dispatch("leave", { detail: { entry }, bubbles: true });
6965
+ }
6966
+ });
6960
6967
  }
6961
6968
  disconnect() {
6962
- this.observer?.disconnect();
6963
- this.observer = null;
6964
- }
6965
- handle(entry) {
6966
- if (entry.isIntersecting) {
6967
- this.element.setAttribute("data-intersecting", "true");
6968
- if (this.visibleClassValue)
6969
- this.element.classList.add(this.visibleClassValue);
6970
- this.dispatch("enter", { detail: { entry }, bubbles: true });
6971
- if (this.onceValue)
6972
- this.disconnect();
6973
- } else {
6974
- this.element.setAttribute("data-intersecting", "false");
6975
- if (this.visibleClassValue)
6976
- this.element.classList.remove(this.visibleClassValue);
6977
- this.dispatch("leave", { detail: { entry }, bubbles: true });
6978
- }
6969
+ this.teardown?.();
6970
+ this.teardown = null;
6979
6971
  }
6980
6972
  }
6981
6973
 
@@ -7873,6 +7865,33 @@ class Playlist extends Controller {
7873
7865
  }
7874
7866
  }
7875
7867
 
7868
+ class Motion extends Controller {
7869
+ static values = {
7870
+ threshold: { type: Number, default: 0.15 },
7871
+ once: { type: Boolean, default: true }
7872
+ };
7873
+ teardown = null;
7874
+ connect() {
7875
+ this.teardown = observeViewport(this.element, {
7876
+ threshold: this.thresholdValue,
7877
+ once: this.onceValue,
7878
+ onEnter: () => this.reveal(),
7879
+ onLeave: this.onceValue ? void 0 : () => this.reset()
7880
+ });
7881
+ }
7882
+ disconnect() {
7883
+ this.teardown?.();
7884
+ this.teardown = null;
7885
+ }
7886
+ reveal() {
7887
+ this.element.setAttribute("data-motion-visible", "");
7888
+ this.dispatch("reveal", { bubbles: true });
7889
+ }
7890
+ reset() {
7891
+ this.element.removeAttribute("data-motion-visible");
7892
+ }
7893
+ }
7894
+
7876
7895
  function registerMidwestControllers(application) {
7877
7896
  application.register("midwest-card", Card);
7878
7897
  application.register("midwest-banner", Banner);
@@ -7914,6 +7933,7 @@ function registerMidwestControllers(application) {
7914
7933
  application.register("midwest-theme-switch", ThemeSwitch);
7915
7934
  application.register("midwest-panorama", Panorama);
7916
7935
  application.register("midwest-playlist", Playlist);
7936
+ application.register("midwest-motion", Motion);
7917
7937
  }
7918
7938
 
7919
7939
  export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };