@unabridged/midwest 0.6.2 → 0.7.0

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.
@@ -12,6 +12,8 @@ import Video from '../../../components/midwest/video_component/video_component_c
12
12
  import DropdownItem from '../../../components/midwest/dropdown_item_component/dropdown_item_component_controller'
13
13
  import Tabs from '../../../components/midwest/tabs_component/tabs_component_controller'
14
14
  import Tooltip from '../../../components/midwest/tooltip_component/tooltip_component_controller'
15
+ import Carousel from '../../../components/midwest/carousel_component/carousel_component_controller'
16
+ import FormLiveSummary from '../../../components/midwest/form/live_summary_component/live_summary_component_controller'
15
17
  /* IMPORTS */
16
18
 
17
19
  export { Card, Banner }
@@ -31,5 +33,7 @@ export function registerMidwestControllers (application: any) {
31
33
  application.register('midwest-dropdown-item', DropdownItem)
32
34
  application.register('midwest-tabs', Tabs)
33
35
  application.register('midwest-tooltip', Tooltip)
36
+ application.register('midwest-carousel', Carousel)
37
+ application.register('midwest-form-live-summary', FormLiveSummary)
34
38
  /* EXPORTS */
35
39
  }
@@ -2973,6 +2973,257 @@ class Tooltip extends Controller {
2973
2973
  }
2974
2974
  }
2975
2975
 
2976
+ const NON_SUBMITTING_INPUT_TYPES = /* @__PURE__ */ new Set([
2977
+ "button",
2978
+ "checkbox",
2979
+ "color",
2980
+ "file",
2981
+ "hidden",
2982
+ "image",
2983
+ "radio",
2984
+ "range",
2985
+ "reset",
2986
+ "submit"
2987
+ ]);
2988
+ class Carousel extends Controller {
2989
+ static targets = ["track", "prev", "next", "dot"];
2990
+ static values = {
2991
+ wizard: { type: Boolean, default: false }
2992
+ };
2993
+ observer = null;
2994
+ _currentIndex = 0;
2995
+ _maxUnlocked = 0;
2996
+ connect() {
2997
+ this.setupObserver();
2998
+ this.updateNavigation();
2999
+ if (this.wizardValue) {
3000
+ this.element.addEventListener("keydown", this.handleKeydown);
3001
+ }
3002
+ }
3003
+ disconnect() {
3004
+ this.observer?.disconnect();
3005
+ this.element.removeEventListener("keydown", this.handleKeydown);
3006
+ }
3007
+ next() {
3008
+ this.trackTarget.scrollBy({ left: this.trackTarget.clientWidth, behavior: "smooth" });
3009
+ }
3010
+ previous() {
3011
+ this.trackTarget.scrollBy({ left: -this.trackTarget.clientWidth, behavior: "smooth" });
3012
+ }
3013
+ goto(event) {
3014
+ const button = event.currentTarget;
3015
+ const index = parseInt(button.dataset.index ?? "0", 10);
3016
+ const slide = this.slides[index];
3017
+ slide?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
3018
+ }
3019
+ // Unlocks the next slide and advances to it. Intended for wizard mode: wire
3020
+ // a "Continue" button inside a slide to this action so users progress
3021
+ // linearly through steps before the carousel nav becomes available.
3022
+ //
3023
+ // <button type="button" data-action="click->midwest-carousel#unlockNext">
3024
+ // Continue
3025
+ // </button>
3026
+ unlockNext() {
3027
+ const nextIndex = this._currentIndex + 1;
3028
+ if (nextIndex > this._maxUnlocked) {
3029
+ this._maxUnlocked = nextIndex;
3030
+ }
3031
+ this.next();
3032
+ }
3033
+ scrolled() {
3034
+ this.updateCurrentIndex();
3035
+ this.updateNavigation();
3036
+ }
3037
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
3038
+ handleKeydown = (event) => {
3039
+ if (event.key !== "Enter")
3040
+ return;
3041
+ const target = event.target;
3042
+ if (target.tagName === "TEXTAREA" || target.tagName !== "INPUT")
3043
+ return;
3044
+ if (NON_SUBMITTING_INPUT_TYPES.has(target.type))
3045
+ return;
3046
+ if (this._currentIndex >= this.slides.length - 1)
3047
+ return;
3048
+ event.preventDefault();
3049
+ this.unlockNext();
3050
+ };
3051
+ get slides() {
3052
+ return Array.from(this.trackTarget.children);
3053
+ }
3054
+ updateCurrentIndex() {
3055
+ const track = this.trackTarget;
3056
+ if (track.clientWidth > 0) {
3057
+ this._currentIndex = Math.round(track.scrollLeft / track.clientWidth);
3058
+ }
3059
+ }
3060
+ setupObserver() {
3061
+ if (!this.hasDotTarget)
3062
+ return;
3063
+ if (!("IntersectionObserver" in window))
3064
+ return;
3065
+ this.observer = new IntersectionObserver(
3066
+ (entries) => {
3067
+ entries.forEach((entry) => {
3068
+ const index = this.slides.indexOf(entry.target);
3069
+ if (index >= 0) {
3070
+ this.dotTargets[index]?.classList.toggle("active", entry.isIntersecting);
3071
+ }
3072
+ });
3073
+ },
3074
+ { root: this.trackTarget, threshold: 0.5 }
3075
+ );
3076
+ this.slides.forEach((slide) => this.observer?.observe(slide));
3077
+ }
3078
+ updateNavigation() {
3079
+ const track = this.trackTarget;
3080
+ const atStart = track.scrollLeft <= 0;
3081
+ const atEnd = track.scrollLeft >= track.scrollWidth - track.clientWidth - 1;
3082
+ if (this.hasPrevTarget)
3083
+ this.prevTarget.classList.toggle("hide", atStart);
3084
+ if (this.hasNextTarget) {
3085
+ const frontierReached = this.wizardValue && this._currentIndex >= this._maxUnlocked;
3086
+ this.nextTarget.classList.toggle("hide", atEnd || frontierReached);
3087
+ }
3088
+ if (this.wizardValue && this.hasDotTarget) {
3089
+ this.dotTargets.forEach((dot, i) => {
3090
+ const locked = i > this._maxUnlocked;
3091
+ dot.disabled = locked;
3092
+ dot.classList.toggle("locked", locked);
3093
+ });
3094
+ }
3095
+ }
3096
+ }
3097
+
3098
+ const SKIPPED_INPUT_TYPES = /* @__PURE__ */ new Set([
3099
+ "hidden",
3100
+ "submit",
3101
+ "reset",
3102
+ "button",
3103
+ "image"
3104
+ ]);
3105
+ class FormLiveSummary extends Controller {
3106
+ static targets = ["list"];
3107
+ connect() {
3108
+ this.render();
3109
+ this.form?.addEventListener("input", this.render);
3110
+ this.form?.addEventListener("change", this.render);
3111
+ }
3112
+ disconnect() {
3113
+ this.form?.removeEventListener("input", this.render);
3114
+ this.form?.removeEventListener("change", this.render);
3115
+ }
3116
+ get form() {
3117
+ return this.element.closest("form");
3118
+ }
3119
+ render = () => {
3120
+ const fields = this.collectFields();
3121
+ if (fields.length === 0) {
3122
+ this.listTarget.replaceChildren(
3123
+ Object.assign(document.createElement("p"), {
3124
+ className: "midwest-form-live-summary-empty",
3125
+ textContent: "No values to display."
3126
+ })
3127
+ );
3128
+ return;
3129
+ }
3130
+ const dl = document.createElement("dl");
3131
+ dl.className = "midwest-form-live-summary-list";
3132
+ fields.forEach(({ label, value }) => {
3133
+ const dt = Object.assign(document.createElement("dt"), {
3134
+ className: "midwest-form-live-summary-term",
3135
+ textContent: label
3136
+ });
3137
+ const dd = Object.assign(document.createElement("dd"), {
3138
+ className: "midwest-form-live-summary-definition",
3139
+ textContent: value || "\u2014"
3140
+ });
3141
+ dl.append(dt, dd);
3142
+ });
3143
+ this.listTarget.replaceChildren(dl);
3144
+ };
3145
+ collectFields() {
3146
+ const form = this.form;
3147
+ if (!form)
3148
+ return [];
3149
+ const fields = [];
3150
+ const seenRadioNames = /* @__PURE__ */ new Set();
3151
+ Array.from(form.elements).forEach((el) => {
3152
+ const element = el;
3153
+ if (!element.name)
3154
+ return;
3155
+ if (this.element.contains(element))
3156
+ return;
3157
+ if (element instanceof HTMLButtonElement)
3158
+ return;
3159
+ if (element instanceof HTMLInputElement) {
3160
+ if (SKIPPED_INPUT_TYPES.has(element.type))
3161
+ return;
3162
+ if (element.type === "radio") {
3163
+ if (seenRadioNames.has(element.name))
3164
+ return;
3165
+ seenRadioNames.add(element.name);
3166
+ const checked = form.querySelector(
3167
+ `input[type="radio"][name="${CSS.escape(element.name)}"]:checked`
3168
+ );
3169
+ if (!checked)
3170
+ return;
3171
+ fields.push({
3172
+ label: this.groupLabelFor(element),
3173
+ value: this.labelFor(checked) || checked.value
3174
+ });
3175
+ return;
3176
+ }
3177
+ if (element.type === "checkbox") {
3178
+ fields.push({
3179
+ label: this.labelFor(element),
3180
+ value: element.checked ? "Yes" : "No"
3181
+ });
3182
+ return;
3183
+ }
3184
+ }
3185
+ if (element instanceof HTMLSelectElement) {
3186
+ fields.push({
3187
+ label: this.labelFor(element),
3188
+ value: Array.from(element.selectedOptions).map((o) => o.text).join(", ") || "\u2014"
3189
+ });
3190
+ return;
3191
+ }
3192
+ fields.push({
3193
+ label: this.labelFor(element),
3194
+ value: element.value
3195
+ });
3196
+ });
3197
+ return fields;
3198
+ }
3199
+ // Finds the label associated with a specific element via `for` attribute,
3200
+ // wrapping label, or falls back to a prettified version of the element name.
3201
+ labelFor(el) {
3202
+ const id = el.id;
3203
+ if (id) {
3204
+ const label = this.form?.querySelector(`label[for="${CSS.escape(id)}"]`);
3205
+ if (label)
3206
+ return label.textContent?.trim() ?? "";
3207
+ }
3208
+ const wrapper = el.closest("label");
3209
+ if (wrapper)
3210
+ return wrapper.textContent?.trim() ?? "";
3211
+ return this.prettifyName(el.name ?? "");
3212
+ }
3213
+ // For radio groups, prefer the containing fieldset legend over the
3214
+ // individual radio's label, then fall back to prettifying the name.
3215
+ groupLabelFor(el) {
3216
+ const legend = el.closest("fieldset")?.querySelector("legend");
3217
+ if (legend)
3218
+ return legend.textContent?.trim() ?? "";
3219
+ return this.prettifyName(el.name ?? "");
3220
+ }
3221
+ // Converts "user[first_name]" or "first_name" into "First name".
3222
+ prettifyName(name) {
3223
+ return name.replace(/\[.*?\]/g, "").replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase()).trim();
3224
+ }
3225
+ }
3226
+
2976
3227
  function registerMidwestControllers(application) {
2977
3228
  application.register("midwest-card", Card);
2978
3229
  application.register("midwest-banner", Banner);
@@ -2988,6 +3239,8 @@ function registerMidwestControllers(application) {
2988
3239
  application.register("midwest-dropdown-item", DropdownItem);
2989
3240
  application.register("midwest-tabs", Tabs);
2990
3241
  application.register("midwest-tooltip", Tooltip);
3242
+ application.register("midwest-carousel", Carousel);
3243
+ application.register("midwest-form-live-summary", FormLiveSummary);
2991
3244
  }
2992
3245
 
2993
3246
  export { Banner, Card, registerMidwestControllers };