@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
@@ -1 +1 @@
1
- {"version":3,"file":"load_more_controller.js","sources":["../../../../../../../app/components/midwest/table_component/load_more_controller.ts"],"sourcesContent":["import { Controller } from '@hotwired/stimulus'\n\n// Enables infinite scroll on a `midwest-table` by observing a sentinel <tr>\n// element at the bottom of the tbody. When the sentinel scrolls near the\n// viewport, this controller fetches the next-page URL, extracts the new <tr>\n// elements from the returned HTML, and appends them (plus the new sentinel,\n// if present) to the current tbody — replacing itself in the process.\n//\n// Values\n// url — URL for the next page (built by TablePresenter#next_page_url)\n// tbodyId — DOM id of the <tbody> to append rows into\nexport default class LoadMore extends Controller<HTMLTableRowElement> {\n static values = {\n url: String,\n tbodyId: String,\n }\n\n declare urlValue: string\n declare tbodyIdValue: string\n\n private observer: IntersectionObserver | null = null\n private loading = false\n\n connect () {\n // No URL means this is the last page — nothing to load.\n if (!this.urlValue) return\n\n this.observer = new IntersectionObserver(\n (entries) => { if (entries[0].isIntersecting) this.load() },\n // Start fetching 300px before the sentinel enters the viewport so rows\n // appear before the user actually reaches the bottom.\n { rootMargin: '0px 0px 300px 0px' }\n )\n this.observer.observe(this.element)\n }\n\n disconnect () {\n this.observer?.disconnect()\n this.observer = null\n }\n\n private async load () {\n if (this.loading) return\n this.loading = true\n this.observer?.disconnect()\n\n try {\n const response = await fetch(this.urlValue, {\n headers: {\n Accept: 'text/html',\n 'X-Requested-With': 'XMLHttpRequest',\n },\n credentials: 'same-origin',\n })\n\n if (!response.ok) return\n\n const html = await response.text()\n const doc = new DOMParser().parseFromString(html, 'text/html')\n const tbody = document.getElementById(this.tbodyIdValue)\n const newRows = Array.from(\n doc.querySelectorAll<HTMLTableRowElement>('.midwest-table-body tr')\n )\n\n if (!tbody || newRows.length === 0) return\n\n // Remove the current sentinel before appending new rows so its position\n // doesn't shift the table layout during the DOM update.\n this.element.remove()\n\n // Import each row (including any new sentinel for the next page) into the\n // current document and append it to the live tbody.\n newRows.forEach(row => tbody.appendChild(document.importNode(row, true)))\n } catch {\n // Silently fail — the table remains fully functional, just without\n // auto-loading more rows.\n this.loading = false\n }\n }\n}\n"],"names":[],"mappings":";;AAWA,MAAqB,iBAAiB,UAAgC,CAAA;AAAA,EACpE,OAAO,MAAS,GAAA;AAAA,IACd,GAAS,EAAA,MAAA;AAAA,IACT,OAAS,EAAA,MAAA;AAAA,GACX,CAAA;AAAA,EAKQ,QAAwC,GAAA,IAAA,CAAA;AAAA,EACxC,OAAU,GAAA,KAAA,CAAA;AAAA,EAElB,OAAW,GAAA;AAET,IAAA,IAAI,CAAC,IAAK,CAAA,QAAA;AAAU,MAAA,OAAA;AAEpB,IAAA,IAAA,CAAK,WAAW,IAAI,oBAAA;AAAA,MAClB,CAAC,OAAY,KAAA;AAAE,QAAI,IAAA,OAAA,CAAQ,CAAC,CAAE,CAAA,cAAA;AAAgB,UAAA,IAAA,CAAK,IAAK,EAAA,CAAA;AAAA,OAAE;AAAA;AAAA;AAAA,MAG1D,EAAE,YAAY,mBAAoB,EAAA;AAAA,KACpC,CAAA;AACA,IAAK,IAAA,CAAA,QAAA,CAAS,OAAQ,CAAA,IAAA,CAAK,OAAO,CAAA,CAAA;AAAA,GACpC;AAAA,EAEA,UAAc,GAAA;AACZ,IAAA,IAAA,CAAK,UAAU,UAAW,EAAA,CAAA;AAC1B,IAAA,IAAA,CAAK,QAAW,GAAA,IAAA,CAAA;AAAA,GAClB;AAAA,EAEA,MAAc,IAAQ,GAAA;AACpB,IAAA,IAAI,IAAK,CAAA,OAAA;AAAS,MAAA,OAAA;AAClB,IAAA,IAAA,CAAK,OAAU,GAAA,IAAA,CAAA;AACf,IAAA,IAAA,CAAK,UAAU,UAAW,EAAA,CAAA;AAE1B,IAAI,IAAA;AACF,MAAA,MAAM,QAAW,GAAA,MAAM,KAAM,CAAA,IAAA,CAAK,QAAU,EAAA;AAAA,QAC1C,OAAS,EAAA;AAAA,UACP,MAAmB,EAAA,WAAA;AAAA,UACnB,kBAAoB,EAAA,gBAAA;AAAA,SACtB;AAAA,QACA,WAAa,EAAA,aAAA;AAAA,OACd,CAAA,CAAA;AAED,MAAA,IAAI,CAAC,QAAS,CAAA,EAAA;AAAI,QAAA,OAAA;AAElB,MAAM,MAAA,IAAA,GAAU,MAAM,QAAA,CAAS,IAAK,EAAA,CAAA;AACpC,MAAA,MAAM,MAAU,IAAI,SAAA,EAAY,CAAA,eAAA,CAAgB,MAAM,WAAW,CAAA,CAAA;AACjE,MAAA,MAAM,KAAU,GAAA,QAAA,CAAS,cAAe,CAAA,IAAA,CAAK,YAAY,CAAA,CAAA;AACzD,MAAA,MAAM,UAAU,KAAM,CAAA,IAAA;AAAA,QACpB,GAAA,CAAI,iBAAsC,wBAAwB,CAAA;AAAA,OACpE,CAAA;AAEA,MAAI,IAAA,CAAC,KAAS,IAAA,OAAA,CAAQ,MAAW,KAAA,CAAA;AAAG,QAAA,OAAA;AAIpC,MAAA,IAAA,CAAK,QAAQ,MAAO,EAAA,CAAA;AAIpB,MAAQ,OAAA,CAAA,OAAA,CAAQ,SAAO,KAAM,CAAA,WAAA,CAAY,SAAS,UAAW,CAAA,GAAA,EAAK,IAAI,CAAC,CAAC,CAAA,CAAA;AAAA,KACxE,CAAA,MAAA;AAGA,MAAA,IAAA,CAAK,OAAU,GAAA,KAAA,CAAA;AAAA,KACjB;AAAA,GACF;AACF;;;;"}
1
+ {"version":3,"file":"load_more_controller.js","sources":["../../../../../../../app/components/midwest/table_component/load_more_controller.ts"],"sourcesContent":["import { Controller } from '@hotwired/stimulus'\nimport { observeViewport } from '../../../assets/javascript/midwest/viewport_observer'\n\n// Enables infinite scroll on a `midwest-table` by observing a sentinel <tr>\n// element at the bottom of the tbody. When the sentinel scrolls near the\n// viewport, this controller fetches the next-page URL, extracts the new <tr>\n// elements from the returned HTML, and appends them (plus the new sentinel,\n// if present) to the current tbody — replacing itself in the process.\n//\n// Values\n// url — URL for the next page (built by TablePresenter#next_page_url)\n// tbodyId — DOM id of the <tbody> to append rows into\nexport default class LoadMore extends Controller<HTMLTableRowElement> {\n static values = {\n url: String,\n tbodyId: String,\n }\n\n declare urlValue: string\n declare tbodyIdValue: string\n\n private teardown: (() => void) | null = null\n private loading = false\n\n connect () {\n // No URL means this is the last page — nothing to load.\n if (!this.urlValue) return\n\n // The sentinel loads exactly once, then removes itself and is replaced by\n // the next page's sentinel — so `once` matches the lifecycle. `rootMargin`\n // starts the fetch 300px early so rows appear before the user hits bottom.\n this.teardown = observeViewport(this.element, {\n rootMargin: '0px 0px 300px 0px',\n once: true,\n onEnter: () => this.load(),\n })\n }\n\n disconnect () {\n this.teardown?.()\n this.teardown = null\n }\n\n private async load () {\n if (this.loading) return\n this.loading = true\n this.teardown?.()\n\n try {\n const response = await fetch(this.urlValue, {\n headers: {\n Accept: 'text/html',\n 'X-Requested-With': 'XMLHttpRequest',\n },\n credentials: 'same-origin',\n })\n\n if (!response.ok) return\n\n const html = await response.text()\n const doc = new DOMParser().parseFromString(html, 'text/html')\n const tbody = document.getElementById(this.tbodyIdValue)\n const newRows = Array.from(\n doc.querySelectorAll<HTMLTableRowElement>('.midwest-table-body tr')\n )\n\n if (!tbody || newRows.length === 0) return\n\n // Remove the current sentinel before appending new rows so its position\n // doesn't shift the table layout during the DOM update.\n this.element.remove()\n\n // Import each row (including any new sentinel for the next page) into the\n // current document and append it to the live tbody.\n newRows.forEach(row => tbody.appendChild(document.importNode(row, true)))\n } catch {\n // Silently fail — the table remains fully functional, just without\n // auto-loading more rows.\n this.loading = false\n }\n }\n}\n"],"names":[],"mappings":";;;AAYA,MAAqB,iBAAiB,UAAgC,CAAA;AAAA,EACpE,OAAO,MAAS,GAAA;AAAA,IACd,GAAS,EAAA,MAAA;AAAA,IACT,OAAS,EAAA,MAAA;AAAA,GACX,CAAA;AAAA,EAKQ,QAAgC,GAAA,IAAA,CAAA;AAAA,EAChC,OAAU,GAAA,KAAA,CAAA;AAAA,EAElB,OAAW,GAAA;AAET,IAAA,IAAI,CAAC,IAAK,CAAA,QAAA;AAAU,MAAA,OAAA;AAKpB,IAAK,IAAA,CAAA,QAAA,GAAW,eAAgB,CAAA,IAAA,CAAK,OAAS,EAAA;AAAA,MAC5C,UAAY,EAAA,mBAAA;AAAA,MACZ,IAAM,EAAA,IAAA;AAAA,MACN,OAAA,EAAS,MAAM,IAAA,CAAK,IAAK,EAAA;AAAA,KAC1B,CAAA,CAAA;AAAA,GACH;AAAA,EAEA,UAAc,GAAA;AACZ,IAAA,IAAA,CAAK,QAAW,IAAA,CAAA;AAChB,IAAA,IAAA,CAAK,QAAW,GAAA,IAAA,CAAA;AAAA,GAClB;AAAA,EAEA,MAAc,IAAQ,GAAA;AACpB,IAAA,IAAI,IAAK,CAAA,OAAA;AAAS,MAAA,OAAA;AAClB,IAAA,IAAA,CAAK,OAAU,GAAA,IAAA,CAAA;AACf,IAAA,IAAA,CAAK,QAAW,IAAA,CAAA;AAEhB,IAAI,IAAA;AACF,MAAA,MAAM,QAAW,GAAA,MAAM,KAAM,CAAA,IAAA,CAAK,QAAU,EAAA;AAAA,QAC1C,OAAS,EAAA;AAAA,UACP,MAAmB,EAAA,WAAA;AAAA,UACnB,kBAAoB,EAAA,gBAAA;AAAA,SACtB;AAAA,QACA,WAAa,EAAA,aAAA;AAAA,OACd,CAAA,CAAA;AAED,MAAA,IAAI,CAAC,QAAS,CAAA,EAAA;AAAI,QAAA,OAAA;AAElB,MAAM,MAAA,IAAA,GAAU,MAAM,QAAA,CAAS,IAAK,EAAA,CAAA;AACpC,MAAA,MAAM,MAAU,IAAI,SAAA,EAAY,CAAA,eAAA,CAAgB,MAAM,WAAW,CAAA,CAAA;AACjE,MAAA,MAAM,KAAU,GAAA,QAAA,CAAS,cAAe,CAAA,IAAA,CAAK,YAAY,CAAA,CAAA;AACzD,MAAA,MAAM,UAAU,KAAM,CAAA,IAAA;AAAA,QACpB,GAAA,CAAI,iBAAsC,wBAAwB,CAAA;AAAA,OACpE,CAAA;AAEA,MAAI,IAAA,CAAC,KAAS,IAAA,OAAA,CAAQ,MAAW,KAAA,CAAA;AAAG,QAAA,OAAA;AAIpC,MAAA,IAAA,CAAK,QAAQ,MAAO,EAAA,CAAA;AAIpB,MAAQ,OAAA,CAAA,OAAA,CAAQ,SAAO,KAAM,CAAA,WAAA,CAAY,SAAS,UAAW,CAAA,GAAA,EAAK,IAAI,CAAC,CAAC,CAAA,CAAA;AAAA,KACxE,CAAA,MAAA;AAGA,MAAA,IAAA,CAAK,OAAU,GAAA,KAAA,CAAA;AAAA,KACjB;AAAA,GACF;AACF;;;;"}
@@ -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 };