uchi 0.1.6 → 0.1.7

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -0
  3. data/app/assets/javascripts/controllers/fields/belongs_to_controller.js +130 -0
  4. data/app/assets/javascripts/controllers/fields/has_many_controller.js +146 -0
  5. data/app/assets/javascripts/uchi/application.js +804 -3
  6. data/app/assets/javascripts/uchi.js +9 -0
  7. data/app/assets/stylesheets/uchi/application.css +81 -1549
  8. data/app/assets/tailwind/uchi.css +2 -2
  9. data/app/components/uchi/field/belongs_to/edit.html.erb +73 -1
  10. data/app/components/uchi/field/belongs_to.rb +25 -25
  11. data/app/components/uchi/field/has_and_belongs_to_many/show.html.erb +1 -1
  12. data/app/components/uchi/field/has_many/edit.html.erb +86 -1
  13. data/app/components/uchi/field/has_many/show.html.erb +1 -1
  14. data/app/components/uchi/field/has_many.rb +59 -11
  15. data/app/components/uchi/ui/navigation/navigation.html.erb +1 -1
  16. data/app/components/uchi/ui/page_header/page_header.html.erb +7 -7
  17. data/app/controllers/uchi/belongs_to/associated_records_controller.rb +89 -0
  18. data/app/controllers/uchi/has_many/associated_records_controller.rb +89 -0
  19. data/app/views/layouts/uchi/_javascript.html.erb +1 -0
  20. data/app/views/layouts/uchi/_stylesheets.html.erb +1 -0
  21. data/app/views/layouts/uchi/application.html.erb +4 -4
  22. data/app/views/uchi/belongs_to/associated_records/index.html.erb +13 -0
  23. data/app/views/uchi/has_many/associated_records/index.html.erb +26 -0
  24. data/app/views/uchi/navigation/_main.html.erb +83 -0
  25. data/lib/generators/uchi/controller/controller_generator.rb +0 -4
  26. data/lib/generators/uchi/install/install_generator.rb +1 -1
  27. data/lib/uchi/field/configuration.rb +1 -1
  28. data/lib/uchi/repository.rb +13 -2
  29. data/lib/uchi/routes.rb +45 -0
  30. data/lib/uchi/version.rb +1 -1
  31. data/lib/uchi.rb +4 -1
  32. metadata +10 -1
@@ -6106,12 +6106,12 @@
6106
6106
  this.unorderedBindings.delete(binding);
6107
6107
  }
6108
6108
  handleEvent(event) {
6109
- const extendedEvent = extendEvent(event);
6109
+ const extendedEvent2 = extendEvent(event);
6110
6110
  for (const binding of this.bindings) {
6111
- if (extendedEvent.immediatePropagationStopped) {
6111
+ if (extendedEvent2.immediatePropagationStopped) {
6112
6112
  break;
6113
6113
  } else {
6114
- binding.handleEvent(extendedEvent);
6114
+ binding.handleEvent(extendedEvent2);
6115
6115
  }
6116
6116
  }
6117
6117
  }
@@ -8534,9 +8534,121 @@
8534
8534
  Controller.values = {};
8535
8535
 
8536
8536
  // node_modules/stimulus-use/dist/index.js
8537
+ var composeEventName = (name, controller, eventPrefix) => {
8538
+ let composedName = name;
8539
+ if (eventPrefix === true) {
8540
+ composedName = `${controller.identifier}:${name}`;
8541
+ } else if (typeof eventPrefix === "string") {
8542
+ composedName = `${eventPrefix}:${name}`;
8543
+ }
8544
+ return composedName;
8545
+ };
8546
+ var extendedEvent = (type, event, detail) => {
8547
+ const { bubbles, cancelable, composed } = event || {
8548
+ bubbles: true,
8549
+ cancelable: true,
8550
+ composed: true
8551
+ };
8552
+ if (event) {
8553
+ Object.assign(detail, {
8554
+ originalEvent: event
8555
+ });
8556
+ }
8557
+ const customEvent = new CustomEvent(type, {
8558
+ bubbles,
8559
+ cancelable,
8560
+ composed,
8561
+ detail
8562
+ });
8563
+ return customEvent;
8564
+ };
8565
+ function isElementInViewport(el) {
8566
+ const rect = el.getBoundingClientRect();
8567
+ const windowHeight = window.innerHeight || document.documentElement.clientHeight;
8568
+ const windowWidth = window.innerWidth || document.documentElement.clientWidth;
8569
+ const vertInView = rect.top <= windowHeight && rect.top + rect.height > 0;
8570
+ const horInView = rect.left <= windowWidth && rect.left + rect.width > 0;
8571
+ return vertInView && horInView;
8572
+ }
8573
+ var defaultOptions$5 = {
8574
+ events: ["click", "touchend"],
8575
+ onlyVisible: true,
8576
+ dispatchEvent: true,
8577
+ eventPrefix: true
8578
+ };
8579
+ var useClickOutside = (composableController, options = {}) => {
8580
+ const controller = composableController;
8581
+ const { onlyVisible, dispatchEvent: dispatchEvent2, events, eventPrefix } = Object.assign({}, defaultOptions$5, options);
8582
+ const onEvent = (event) => {
8583
+ const targetElement = (options === null || options === void 0 ? void 0 : options.element) || controller.element;
8584
+ if (targetElement.contains(event.target) || !isElementInViewport(targetElement) && onlyVisible) {
8585
+ return;
8586
+ }
8587
+ if (controller.clickOutside) {
8588
+ controller.clickOutside(event);
8589
+ }
8590
+ if (dispatchEvent2) {
8591
+ const eventName = composeEventName("click:outside", controller, eventPrefix);
8592
+ const clickOutsideEvent = extendedEvent(eventName, event, {
8593
+ controller
8594
+ });
8595
+ targetElement.dispatchEvent(clickOutsideEvent);
8596
+ }
8597
+ };
8598
+ const observe = () => {
8599
+ events === null || events === void 0 ? void 0 : events.forEach(((event) => {
8600
+ window.addEventListener(event, onEvent, true);
8601
+ }));
8602
+ };
8603
+ const unobserve = () => {
8604
+ events === null || events === void 0 ? void 0 : events.forEach(((event) => {
8605
+ window.removeEventListener(event, onEvent, true);
8606
+ }));
8607
+ };
8608
+ const controllerDisconnect = controller.disconnect.bind(controller);
8609
+ Object.assign(controller, {
8610
+ disconnect() {
8611
+ unobserve();
8612
+ controllerDisconnect();
8613
+ }
8614
+ });
8615
+ observe();
8616
+ return [observe, unobserve];
8617
+ };
8537
8618
  var DebounceController = class extends Controller {
8538
8619
  };
8539
8620
  DebounceController.debounces = [];
8621
+ var defaultWait$1 = 200;
8622
+ var debounce2 = (fn, wait = defaultWait$1) => {
8623
+ let timeoutId = null;
8624
+ return function() {
8625
+ const args = Array.from(arguments);
8626
+ const context = this;
8627
+ const params = args.map(((arg) => arg.params));
8628
+ const callback = () => {
8629
+ args.forEach(((arg, index) => arg.params = params[index]));
8630
+ return fn.apply(context, args);
8631
+ };
8632
+ if (timeoutId) {
8633
+ clearTimeout(timeoutId);
8634
+ }
8635
+ timeoutId = setTimeout(callback, wait);
8636
+ };
8637
+ };
8638
+ var useDebounce = (composableController, options) => {
8639
+ const controller = composableController;
8640
+ const constructor = controller.constructor;
8641
+ constructor.debounces.forEach(((func) => {
8642
+ if (typeof func === "string") {
8643
+ controller[func] = debounce2(controller[func], options === null || options === void 0 ? void 0 : options.wait);
8644
+ }
8645
+ if (typeof func === "object") {
8646
+ const { name, wait } = func;
8647
+ if (!name) return;
8648
+ controller[name] = debounce2(controller[name], wait || (options === null || options === void 0 ? void 0 : options.wait));
8649
+ }
8650
+ }));
8651
+ };
8540
8652
  var ThrottleController = class extends Controller {
8541
8653
  };
8542
8654
  ThrottleController.throttles = [];
@@ -8709,9 +8821,698 @@
8709
8821
  _Dropdown.targets = ["menu"];
8710
8822
  var Dropdown = _Dropdown;
8711
8823
 
8824
+ // node_modules/@rails/request.js/src/fetch_response.js
8825
+ var FetchResponse2 = class {
8826
+ constructor(response) {
8827
+ this.response = response;
8828
+ }
8829
+ get statusCode() {
8830
+ return this.response.status;
8831
+ }
8832
+ get redirected() {
8833
+ return this.response.redirected;
8834
+ }
8835
+ get ok() {
8836
+ return this.response.ok;
8837
+ }
8838
+ get unauthenticated() {
8839
+ return this.statusCode === 401;
8840
+ }
8841
+ get unprocessableEntity() {
8842
+ return this.statusCode === 422;
8843
+ }
8844
+ get authenticationURL() {
8845
+ return this.response.headers.get("WWW-Authenticate");
8846
+ }
8847
+ get contentType() {
8848
+ const contentType = this.response.headers.get("Content-Type") || "";
8849
+ return contentType.replace(/;.*$/, "");
8850
+ }
8851
+ get headers() {
8852
+ return this.response.headers;
8853
+ }
8854
+ get html() {
8855
+ if (this.contentType.match(/^(application|text)\/(html|xhtml\+xml)$/)) {
8856
+ return this.text;
8857
+ }
8858
+ return Promise.reject(new Error(`Expected an HTML response but got "${this.contentType}" instead`));
8859
+ }
8860
+ get json() {
8861
+ if (this.contentType.match(/^application\/.*json$/)) {
8862
+ return this.responseJson || (this.responseJson = this.response.json());
8863
+ }
8864
+ return Promise.reject(new Error(`Expected a JSON response but got "${this.contentType}" instead`));
8865
+ }
8866
+ get text() {
8867
+ return this.responseText || (this.responseText = this.response.text());
8868
+ }
8869
+ get isTurboStream() {
8870
+ return this.contentType.match(/^text\/vnd\.turbo-stream\.html/);
8871
+ }
8872
+ get isScript() {
8873
+ return this.contentType.match(/\b(?:java|ecma)script\b/);
8874
+ }
8875
+ async renderTurboStream() {
8876
+ if (this.isTurboStream) {
8877
+ if (window.Turbo) {
8878
+ await window.Turbo.renderStreamMessage(await this.text);
8879
+ } else {
8880
+ console.warn("You must set `window.Turbo = Turbo` to automatically process Turbo Stream events with request.js");
8881
+ }
8882
+ } else {
8883
+ return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`));
8884
+ }
8885
+ }
8886
+ async activeScript() {
8887
+ if (this.isScript) {
8888
+ const script = document.createElement("script");
8889
+ const metaTag = document.querySelector("meta[name=csp-nonce]");
8890
+ if (metaTag) {
8891
+ const nonce = metaTag.nonce === "" ? metaTag.content : metaTag.nonce;
8892
+ if (nonce) {
8893
+ script.setAttribute("nonce", nonce);
8894
+ }
8895
+ }
8896
+ script.innerHTML = await this.text;
8897
+ document.body.appendChild(script);
8898
+ } else {
8899
+ return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`));
8900
+ }
8901
+ }
8902
+ };
8903
+
8904
+ // node_modules/@rails/request.js/src/request_interceptor.js
8905
+ var RequestInterceptor = class {
8906
+ static register(interceptor) {
8907
+ this.interceptor = interceptor;
8908
+ }
8909
+ static get() {
8910
+ return this.interceptor;
8911
+ }
8912
+ static reset() {
8913
+ this.interceptor = void 0;
8914
+ }
8915
+ };
8916
+
8917
+ // node_modules/@rails/request.js/src/lib/utils.js
8918
+ function getCookie(name) {
8919
+ const cookies = document.cookie ? document.cookie.split("; ") : [];
8920
+ const prefix = `${encodeURIComponent(name)}=`;
8921
+ const cookie = cookies.find((cookie2) => cookie2.startsWith(prefix));
8922
+ if (cookie) {
8923
+ const value = cookie.split("=").slice(1).join("=");
8924
+ if (value) {
8925
+ return decodeURIComponent(value);
8926
+ }
8927
+ }
8928
+ }
8929
+ function compact(object) {
8930
+ const result = {};
8931
+ for (const key in object) {
8932
+ const value = object[key];
8933
+ if (value !== void 0) {
8934
+ result[key] = value;
8935
+ }
8936
+ }
8937
+ return result;
8938
+ }
8939
+ function metaContent(name) {
8940
+ const element = document.head.querySelector(`meta[name="${name}"]`);
8941
+ return element && element.content;
8942
+ }
8943
+ function stringEntriesFromFormData(formData) {
8944
+ return [...formData].reduce((entries, [name, value]) => {
8945
+ return entries.concat(typeof value === "string" ? [[name, value]] : []);
8946
+ }, []);
8947
+ }
8948
+ function mergeEntries(searchParams, entries) {
8949
+ for (const [name, value] of entries) {
8950
+ if (value instanceof window.File) continue;
8951
+ if (searchParams.has(name) && !name.includes("[]")) {
8952
+ searchParams.delete(name);
8953
+ searchParams.set(name, value);
8954
+ } else {
8955
+ searchParams.append(name, value);
8956
+ }
8957
+ }
8958
+ }
8959
+
8960
+ // node_modules/@rails/request.js/src/fetch_request.js
8961
+ var FetchRequest2 = class {
8962
+ constructor(method, url, options = {}) {
8963
+ this.method = method;
8964
+ this.options = options;
8965
+ this.originalUrl = url.toString();
8966
+ }
8967
+ async perform() {
8968
+ try {
8969
+ const requestInterceptor = RequestInterceptor.get();
8970
+ if (requestInterceptor) {
8971
+ await requestInterceptor(this);
8972
+ }
8973
+ } catch (error2) {
8974
+ console.error(error2);
8975
+ }
8976
+ const fetch2 = window.Turbo ? window.Turbo.fetch : window.fetch;
8977
+ const response = new FetchResponse2(await fetch2(this.url, this.fetchOptions));
8978
+ if (response.unauthenticated && response.authenticationURL) {
8979
+ return Promise.reject(window.location.href = response.authenticationURL);
8980
+ }
8981
+ if (response.isScript) {
8982
+ await response.activeScript();
8983
+ }
8984
+ const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity;
8985
+ if (responseStatusIsTurboStreamable && response.isTurboStream) {
8986
+ await response.renderTurboStream();
8987
+ }
8988
+ return response;
8989
+ }
8990
+ addHeader(key, value) {
8991
+ const headers = this.additionalHeaders;
8992
+ headers[key] = value;
8993
+ this.options.headers = headers;
8994
+ }
8995
+ sameHostname() {
8996
+ if (!this.originalUrl.startsWith("http:") && !this.originalUrl.startsWith("https:")) {
8997
+ return true;
8998
+ }
8999
+ try {
9000
+ return new URL(this.originalUrl).hostname === window.location.hostname;
9001
+ } catch (_) {
9002
+ return true;
9003
+ }
9004
+ }
9005
+ get fetchOptions() {
9006
+ return {
9007
+ method: this.method.toUpperCase(),
9008
+ headers: this.headers,
9009
+ body: this.formattedBody,
9010
+ signal: this.signal,
9011
+ credentials: this.credentials,
9012
+ redirect: this.redirect,
9013
+ keepalive: this.keepalive
9014
+ };
9015
+ }
9016
+ get headers() {
9017
+ const baseHeaders = {
9018
+ "X-Requested-With": "XMLHttpRequest",
9019
+ "Content-Type": this.contentType,
9020
+ Accept: this.accept
9021
+ };
9022
+ if (this.sameHostname()) {
9023
+ baseHeaders["X-CSRF-Token"] = this.csrfToken;
9024
+ }
9025
+ return compact(
9026
+ Object.assign(baseHeaders, this.additionalHeaders)
9027
+ );
9028
+ }
9029
+ get csrfToken() {
9030
+ return getCookie(metaContent("csrf-param")) || metaContent("csrf-token");
9031
+ }
9032
+ get contentType() {
9033
+ if (this.options.contentType) {
9034
+ return this.options.contentType;
9035
+ } else if (this.body == null || this.body instanceof window.FormData) {
9036
+ return void 0;
9037
+ } else if (this.body instanceof window.File) {
9038
+ return this.body.type;
9039
+ }
9040
+ return "application/json";
9041
+ }
9042
+ get accept() {
9043
+ switch (this.responseKind) {
9044
+ case "html":
9045
+ return "text/html, application/xhtml+xml";
9046
+ case "turbo-stream":
9047
+ return "text/vnd.turbo-stream.html, text/html, application/xhtml+xml";
9048
+ case "json":
9049
+ return "application/json, application/vnd.api+json";
9050
+ case "script":
9051
+ return "text/javascript, application/javascript";
9052
+ default:
9053
+ return "*/*";
9054
+ }
9055
+ }
9056
+ get body() {
9057
+ return this.options.body;
9058
+ }
9059
+ get query() {
9060
+ const originalQuery = (this.originalUrl.split("?")[1] || "").split("#")[0];
9061
+ const params = new URLSearchParams(originalQuery);
9062
+ let requestQuery = this.options.query;
9063
+ if (requestQuery instanceof window.FormData) {
9064
+ requestQuery = stringEntriesFromFormData(requestQuery);
9065
+ } else if (requestQuery instanceof window.URLSearchParams) {
9066
+ requestQuery = requestQuery.entries();
9067
+ } else {
9068
+ requestQuery = Object.entries(requestQuery || {});
9069
+ }
9070
+ mergeEntries(params, requestQuery);
9071
+ const query = params.toString();
9072
+ return query.length > 0 ? `?${query}` : "";
9073
+ }
9074
+ get url() {
9075
+ return this.originalUrl.split("?")[0].split("#")[0] + this.query;
9076
+ }
9077
+ get responseKind() {
9078
+ return this.options.responseKind || "html";
9079
+ }
9080
+ get signal() {
9081
+ return this.options.signal;
9082
+ }
9083
+ get redirect() {
9084
+ return this.options.redirect || "follow";
9085
+ }
9086
+ get credentials() {
9087
+ return this.options.credentials || "same-origin";
9088
+ }
9089
+ get keepalive() {
9090
+ return this.options.keepalive || false;
9091
+ }
9092
+ get additionalHeaders() {
9093
+ return this.options.headers || {};
9094
+ }
9095
+ get formattedBody() {
9096
+ const bodyIsAString = Object.prototype.toString.call(this.body) === "[object String]";
9097
+ const contentTypeIsJson = this.headers["Content-Type"] === "application/json";
9098
+ if (contentTypeIsJson && !bodyIsAString) {
9099
+ return JSON.stringify(this.body);
9100
+ }
9101
+ return this.body;
9102
+ }
9103
+ };
9104
+
9105
+ // node_modules/@rails/request.js/src/verbs.js
9106
+ async function get(url, options) {
9107
+ const request = new FetchRequest2("get", url, options);
9108
+ return request.perform();
9109
+ }
9110
+
9111
+ // node_modules/@github/combobox-nav/dist/index.js
9112
+ var Combobox = class {
9113
+ constructor(input, list, { tabInsertsSuggestions, firstOptionSelectionMode, scrollIntoViewOptions } = {}) {
9114
+ this.input = input;
9115
+ this.list = list;
9116
+ this.tabInsertsSuggestions = tabInsertsSuggestions !== null && tabInsertsSuggestions !== void 0 ? tabInsertsSuggestions : true;
9117
+ this.firstOptionSelectionMode = firstOptionSelectionMode !== null && firstOptionSelectionMode !== void 0 ? firstOptionSelectionMode : "none";
9118
+ this.scrollIntoViewOptions = scrollIntoViewOptions !== null && scrollIntoViewOptions !== void 0 ? scrollIntoViewOptions : { block: "nearest", inline: "nearest" };
9119
+ this.isComposing = false;
9120
+ if (!list.id) {
9121
+ list.id = `combobox-${Math.random().toString().slice(2, 6)}`;
9122
+ }
9123
+ this.ctrlBindings = !!navigator.userAgent.match(/Macintosh/);
9124
+ this.keyboardEventHandler = (event) => keyboardBindings(event, this);
9125
+ this.compositionEventHandler = (event) => trackComposition(event, this);
9126
+ this.inputHandler = this.clearSelection.bind(this);
9127
+ input.setAttribute("role", "combobox");
9128
+ input.setAttribute("aria-controls", list.id);
9129
+ input.setAttribute("aria-expanded", "false");
9130
+ input.setAttribute("aria-autocomplete", "list");
9131
+ input.setAttribute("aria-haspopup", "listbox");
9132
+ }
9133
+ destroy() {
9134
+ this.clearSelection();
9135
+ this.stop();
9136
+ this.input.removeAttribute("role");
9137
+ this.input.removeAttribute("aria-controls");
9138
+ this.input.removeAttribute("aria-expanded");
9139
+ this.input.removeAttribute("aria-autocomplete");
9140
+ this.input.removeAttribute("aria-haspopup");
9141
+ }
9142
+ start() {
9143
+ this.input.setAttribute("aria-expanded", "true");
9144
+ this.input.addEventListener("compositionstart", this.compositionEventHandler);
9145
+ this.input.addEventListener("compositionend", this.compositionEventHandler);
9146
+ this.input.addEventListener("input", this.inputHandler);
9147
+ this.input.addEventListener("keydown", this.keyboardEventHandler);
9148
+ this.list.addEventListener("click", commitWithElement);
9149
+ this.resetSelection();
9150
+ }
9151
+ stop() {
9152
+ this.clearSelection();
9153
+ this.input.setAttribute("aria-expanded", "false");
9154
+ this.input.removeEventListener("compositionstart", this.compositionEventHandler);
9155
+ this.input.removeEventListener("compositionend", this.compositionEventHandler);
9156
+ this.input.removeEventListener("input", this.inputHandler);
9157
+ this.input.removeEventListener("keydown", this.keyboardEventHandler);
9158
+ this.list.removeEventListener("click", commitWithElement);
9159
+ }
9160
+ indicateDefaultOption() {
9161
+ var _a;
9162
+ if (this.firstOptionSelectionMode === "active") {
9163
+ (_a = Array.from(this.list.querySelectorAll('[role="option"]:not([aria-disabled="true"])')).filter(visible)[0]) === null || _a === void 0 ? void 0 : _a.setAttribute("data-combobox-option-default", "true");
9164
+ } else if (this.firstOptionSelectionMode === "selected") {
9165
+ this.navigate(1);
9166
+ }
9167
+ }
9168
+ navigate(indexDiff = 1) {
9169
+ const focusEl = Array.from(this.list.querySelectorAll('[aria-selected="true"]')).filter(visible)[0];
9170
+ const els = Array.from(this.list.querySelectorAll('[role="option"]')).filter(visible);
9171
+ const focusIndex = els.indexOf(focusEl);
9172
+ if (focusIndex === els.length - 1 && indexDiff === 1 || focusIndex === 0 && indexDiff === -1) {
9173
+ this.clearSelection();
9174
+ this.input.focus();
9175
+ return;
9176
+ }
9177
+ let indexOfItem = indexDiff === 1 ? 0 : els.length - 1;
9178
+ if (focusEl && focusIndex >= 0) {
9179
+ const newIndex = focusIndex + indexDiff;
9180
+ if (newIndex >= 0 && newIndex < els.length)
9181
+ indexOfItem = newIndex;
9182
+ }
9183
+ const target = els[indexOfItem];
9184
+ if (!target)
9185
+ return;
9186
+ for (const el of els) {
9187
+ el.removeAttribute("data-combobox-option-default");
9188
+ if (target === el) {
9189
+ this.input.setAttribute("aria-activedescendant", target.id);
9190
+ target.setAttribute("aria-selected", "true");
9191
+ fireSelectEvent(target);
9192
+ target.scrollIntoView(this.scrollIntoViewOptions);
9193
+ } else {
9194
+ el.removeAttribute("aria-selected");
9195
+ }
9196
+ }
9197
+ }
9198
+ clearSelection() {
9199
+ this.input.removeAttribute("aria-activedescendant");
9200
+ for (const el of this.list.querySelectorAll('[aria-selected="true"], [data-combobox-option-default="true"]')) {
9201
+ el.removeAttribute("aria-selected");
9202
+ el.removeAttribute("data-combobox-option-default");
9203
+ }
9204
+ }
9205
+ resetSelection() {
9206
+ this.clearSelection();
9207
+ this.indicateDefaultOption();
9208
+ }
9209
+ };
9210
+ function keyboardBindings(event, combobox) {
9211
+ if (event.shiftKey || event.metaKey || event.altKey)
9212
+ return;
9213
+ if (!combobox.ctrlBindings && event.ctrlKey)
9214
+ return;
9215
+ if (combobox.isComposing)
9216
+ return;
9217
+ switch (event.key) {
9218
+ case "Enter":
9219
+ if (commit(combobox.input, combobox.list)) {
9220
+ event.preventDefault();
9221
+ }
9222
+ break;
9223
+ case "Tab":
9224
+ if (combobox.tabInsertsSuggestions && commit(combobox.input, combobox.list)) {
9225
+ event.preventDefault();
9226
+ }
9227
+ break;
9228
+ case "Escape":
9229
+ combobox.clearSelection();
9230
+ break;
9231
+ case "ArrowDown":
9232
+ combobox.navigate(1);
9233
+ event.preventDefault();
9234
+ break;
9235
+ case "ArrowUp":
9236
+ combobox.navigate(-1);
9237
+ event.preventDefault();
9238
+ break;
9239
+ case "n":
9240
+ if (combobox.ctrlBindings && event.ctrlKey) {
9241
+ combobox.navigate(1);
9242
+ event.preventDefault();
9243
+ }
9244
+ break;
9245
+ case "p":
9246
+ if (combobox.ctrlBindings && event.ctrlKey) {
9247
+ combobox.navigate(-1);
9248
+ event.preventDefault();
9249
+ }
9250
+ break;
9251
+ default:
9252
+ if (event.ctrlKey)
9253
+ break;
9254
+ combobox.resetSelection();
9255
+ }
9256
+ }
9257
+ function commitWithElement(event) {
9258
+ if (!(event.target instanceof Element))
9259
+ return;
9260
+ const target = event.target.closest('[role="option"]');
9261
+ if (!target)
9262
+ return;
9263
+ if (target.getAttribute("aria-disabled") === "true")
9264
+ return;
9265
+ fireCommitEvent(target, { event });
9266
+ }
9267
+ function commit(input, list) {
9268
+ const target = list.querySelector('[aria-selected="true"], [data-combobox-option-default="true"]');
9269
+ if (!target)
9270
+ return false;
9271
+ if (target.getAttribute("aria-disabled") === "true")
9272
+ return true;
9273
+ target.click();
9274
+ return true;
9275
+ }
9276
+ function fireCommitEvent(target, detail) {
9277
+ target.dispatchEvent(new CustomEvent("combobox-commit", { bubbles: true, detail }));
9278
+ }
9279
+ function fireSelectEvent(target) {
9280
+ target.dispatchEvent(new Event("combobox-select", { bubbles: true }));
9281
+ }
9282
+ function visible(el) {
9283
+ return !el.hidden && !(el instanceof HTMLInputElement && el.type === "hidden") && (el.offsetWidth > 0 || el.offsetHeight > 0);
9284
+ }
9285
+ function trackComposition(event, combobox) {
9286
+ combobox.isComposing = event.type === "compositionstart";
9287
+ const list = document.getElementById(combobox.input.getAttribute("aria-controls") || "");
9288
+ if (!list)
9289
+ return;
9290
+ combobox.clearSelection();
9291
+ }
9292
+
9293
+ // app/assets/javascripts/controllers/fields/belongs_to_controller.js
9294
+ var belongs_to_controller_default = class extends Controller {
9295
+ static debounces = ["handleChange"];
9296
+ static targets = ["id", "dropdown", "input", "label", "list"];
9297
+ static values = {
9298
+ backendUrl: String
9299
+ };
9300
+ buildCombobox() {
9301
+ return new Combobox(this.inputTarget, this.listTarget);
9302
+ }
9303
+ clickOutside(event) {
9304
+ if (!this.dropdownTarget.hidden) {
9305
+ this.closeDropdown();
9306
+ }
9307
+ }
9308
+ closeDropdown() {
9309
+ this.combobox.stop();
9310
+ this.dropdownTarget.hidden = true;
9311
+ }
9312
+ connect() {
9313
+ useClickOutside(this, { element: this.dropdownTarget });
9314
+ useDebounce(this);
9315
+ this.combobox = this.buildCombobox();
9316
+ this.listTarget.addEventListener("combobox-commit", this.handleComboboxCommit.bind(this));
9317
+ this.dropdownTarget.hidden = true;
9318
+ }
9319
+ disconnect() {
9320
+ this.listTarget.removeEventListener("combobox-commit", this.handleComboboxCommit);
9321
+ this.combobox.destroy();
9322
+ }
9323
+ fetchOptions(options) {
9324
+ get(this.backendUrlValue, {
9325
+ query: { query: this.inputTarget.value }
9326
+ }).then(({ response }) => {
9327
+ return response.text();
9328
+ }).then((html) => {
9329
+ this.listTarget.innerHTML = html;
9330
+ this.openDropdown();
9331
+ this.markSelectedOption();
9332
+ if (options?.scrollToSelected) {
9333
+ this.scrollToSelectedOption();
9334
+ }
9335
+ }).catch((error2) => {
9336
+ console.error("Failed to fetch options:", error2);
9337
+ this.dropdownTarget.hidden = true;
9338
+ });
9339
+ }
9340
+ handleChange() {
9341
+ this.combobox.stop();
9342
+ this.fetchOptions();
9343
+ }
9344
+ handleComboboxCommit(event) {
9345
+ this.setValuesFromElement(event.target);
9346
+ this.closeDropdown();
9347
+ }
9348
+ handleFocus() {
9349
+ this.fetchOptions({ scrollToSelected: true });
9350
+ }
9351
+ markSelectedOption() {
9352
+ const options = this.listTarget.querySelectorAll('[role="option"]');
9353
+ options.forEach((option) => {
9354
+ option.removeAttribute("aria-selected");
9355
+ const recordId = option.getAttribute("data-id");
9356
+ if (recordId === this.idTarget.value) {
9357
+ option.setAttribute("aria-selected", "true");
9358
+ }
9359
+ });
9360
+ }
9361
+ openDropdown() {
9362
+ this.combobox.start();
9363
+ this.dropdownTarget.hidden = false;
9364
+ this.inputTarget.focus();
9365
+ }
9366
+ scrollToSelectedOption() {
9367
+ const selectedOption = this.listTarget.querySelector('[aria-selected="true"]');
9368
+ if (selectedOption) {
9369
+ selectedOption.scrollIntoView({
9370
+ // Aligns the element at the center of the scrollable container,
9371
+ // positioning it in the middle of the visible area.
9372
+ block: "center",
9373
+ inline: "center",
9374
+ // Only the nearest scrollable container is impacted by the scroll.
9375
+ container: "nearest"
9376
+ });
9377
+ }
9378
+ }
9379
+ selectOption(event) {
9380
+ this.combobox.clearSelection();
9381
+ event.target.setAttribute("aria-selected", "true");
9382
+ this.setValuesFromElement(event.target);
9383
+ }
9384
+ setValuesFromElement(element) {
9385
+ const recordId = element.getAttribute("data-id");
9386
+ this.idTarget.value = recordId;
9387
+ this.labelTarget.textContent = element.textContent.trim();
9388
+ }
9389
+ toggle() {
9390
+ if (this.dropdownTarget.hidden) {
9391
+ this.openDropdown();
9392
+ } else {
9393
+ this.closeDropdown();
9394
+ }
9395
+ }
9396
+ };
9397
+
9398
+ // app/assets/javascripts/controllers/fields/has_many_controller.js
9399
+ var has_many_controller_default = class extends Controller {
9400
+ static debounces = ["handleChange"];
9401
+ static targets = ["checkbox", "dropdown", "idField", "idsContainer", "input", "label", "list"];
9402
+ static values = {
9403
+ backendUrl: String,
9404
+ fieldName: String
9405
+ };
9406
+ clickOutside(event) {
9407
+ if (!this.dropdownTarget.hidden) {
9408
+ this.closeDropdown();
9409
+ }
9410
+ }
9411
+ closeDropdown() {
9412
+ this.dropdownTarget.hidden = true;
9413
+ }
9414
+ connect() {
9415
+ useClickOutside(this, { element: this.dropdownTarget });
9416
+ useDebounce(this);
9417
+ this.dropdownTarget.hidden = true;
9418
+ }
9419
+ fetchOptions() {
9420
+ get(this.backendUrlValue, {
9421
+ query: { query: this.inputTarget.value }
9422
+ }).then(({ response }) => {
9423
+ return response.text();
9424
+ }).then((html) => {
9425
+ this.listTarget.innerHTML = html;
9426
+ this.openDropdown();
9427
+ this.updateCheckboxStates();
9428
+ }).catch((error2) => {
9429
+ console.error("Failed to fetch options:", error2);
9430
+ this.dropdownTarget.hidden = true;
9431
+ });
9432
+ }
9433
+ getSelectedIds() {
9434
+ return this.idFieldTargets.map((field) => String(field.value));
9435
+ }
9436
+ handleChange() {
9437
+ this.fetchOptions();
9438
+ }
9439
+ handleFocus() {
9440
+ this.fetchOptions();
9441
+ }
9442
+ openDropdown() {
9443
+ this.dropdownTarget.hidden = false;
9444
+ this.inputTarget.focus();
9445
+ }
9446
+ handleCheckboxChange(event) {
9447
+ const checkbox = event.target;
9448
+ const listItem = checkbox.closest("li[data-id]");
9449
+ const recordId = listItem?.getAttribute("data-id");
9450
+ if (!recordId) return;
9451
+ if (checkbox.checked) {
9452
+ const label = listItem?.querySelector("label");
9453
+ const title = label ? label.textContent.trim() : "";
9454
+ this.addId(recordId, title);
9455
+ } else {
9456
+ this.removeId(recordId);
9457
+ }
9458
+ this.updateLabel();
9459
+ }
9460
+ addId(id, title) {
9461
+ const selectedIds = this.getSelectedIds();
9462
+ if (selectedIds.includes(id)) return;
9463
+ const hiddenField = document.createElement("input");
9464
+ hiddenField.type = "hidden";
9465
+ hiddenField.name = this.fieldNameValue;
9466
+ hiddenField.value = id;
9467
+ hiddenField.setAttribute("data-has-many-target", "idField");
9468
+ hiddenField.setAttribute("data-title", title);
9469
+ this.idsContainerTarget.appendChild(hiddenField);
9470
+ }
9471
+ removeId(id) {
9472
+ const field = this.idFieldTargets.find((f) => f.value === id);
9473
+ if (field) {
9474
+ field.remove();
9475
+ }
9476
+ }
9477
+ toggle() {
9478
+ if (this.dropdownTarget.hidden) {
9479
+ this.openDropdown();
9480
+ } else {
9481
+ this.closeDropdown();
9482
+ }
9483
+ }
9484
+ updateCheckboxStates() {
9485
+ const selectedIds = this.getSelectedIds();
9486
+ this.checkboxTargets.forEach((checkbox) => {
9487
+ const listItem = checkbox.closest("li[data-id]");
9488
+ if (!listItem) return;
9489
+ const recordId = listItem.getAttribute("data-id");
9490
+ const isSelected = selectedIds.includes(recordId);
9491
+ checkbox.checked = isSelected;
9492
+ if (isSelected) {
9493
+ listItem.setAttribute("aria-selected", "true");
9494
+ } else {
9495
+ listItem.removeAttribute("aria-selected");
9496
+ }
9497
+ });
9498
+ }
9499
+ updateLabel() {
9500
+ const titles = this.idFieldTargets.map((field) => field.getAttribute("data-title")).filter((title) => title);
9501
+ if (titles.length === 0) {
9502
+ this.labelTarget.innerHTML = '<span class="text-body-subtle">Select items...</span>';
9503
+ return;
9504
+ }
9505
+ this.labelTarget.textContent = titles.join(", ");
9506
+ }
9507
+ };
9508
+
8712
9509
  // app/assets/javascripts/uchi.js
8713
9510
  var application = Application.start();
9511
+ application.register("belongs-to", belongs_to_controller_default);
8714
9512
  application.register("dropdown", Dropdown);
9513
+ application.register("has-many", has_many_controller_default);
9514
+ window.uchi ||= {};
9515
+ window.uchi.application ||= application;
8715
9516
  if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
8716
9517
  document.documentElement.classList.add("dark");
8717
9518
  }