brut-js 0.0.1

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.
@@ -0,0 +1,1284 @@
1
+ (() => {
2
+ // src/Logger.js
3
+ var Logger = class {
4
+ /** Create a logger for the given prefix.
5
+ *
6
+ * @param {string|false} stringOrFalse - if false,returns a {@link BufferedLogger}. Otherwise, returns a {@link PrefixedLogger} using the param's value as the prefix.
7
+ *
8
+ * @returns {Logger}
9
+ */
10
+ static forPrefix(stringOrFalse) {
11
+ if (!stringOrFalse) {
12
+ return new BufferedLogger();
13
+ } else {
14
+ return new PrefixedLogger(stringOrFalse);
15
+ }
16
+ }
17
+ /** Subclasses must implement this.
18
+ *
19
+ * @param {string} level - 'info' or 'warn' to indicate the logging level
20
+ * @param {...*} args - args to pass directly to console.log
21
+ */
22
+ log() {
23
+ throw `Subclass must implement`;
24
+ }
25
+ /** Log an informational bit of information */
26
+ info(...args) {
27
+ this.log("info", ...args);
28
+ }
29
+ /** Log a warning */
30
+ warn(...args) {
31
+ this.log("warn", ...args);
32
+ }
33
+ };
34
+ var BufferedLogger = class extends Logger {
35
+ constructor() {
36
+ super();
37
+ this.messages = [];
38
+ }
39
+ log(...args) {
40
+ this.messages.push(args);
41
+ }
42
+ };
43
+ var PrefixedLogger = class extends Logger {
44
+ /** Create a PrefixedLogger.
45
+ *
46
+ * @param {string|true} prefixOrTrue - if true, uses the prefix `"debug"`, otherwise uses the param as the prefix to all
47
+ * messages output.
48
+ */
49
+ constructor(prefixOrTrue) {
50
+ super();
51
+ this.prefix = prefixOrTrue === true ? "debug" : prefixOrTrue;
52
+ }
53
+ /** Dumps hte contents of a {@link BufferedLogger} to this logger's output.
54
+ *
55
+ * @param {BufferedLogger} bufferedLogger - a logger with pent-up messages, waiting to be logged.
56
+ */
57
+ dump(bufferedLogger) {
58
+ if (bufferedLogger instanceof BufferedLogger) {
59
+ bufferedLogger.messages.forEach((args) => {
60
+ this.log(...args);
61
+ });
62
+ }
63
+ }
64
+ log(level, ...args) {
65
+ if (typeof args[0] === "string") {
66
+ const message = `[prefix:${this.prefix}]:${args[0]}`;
67
+ console[level](message, ...args.slice(1));
68
+ } else {
69
+ console[level](this.prefix, ...args);
70
+ }
71
+ }
72
+ };
73
+ var Logger_default = Logger;
74
+
75
+ // src/RichString.js
76
+ var RichString = class _RichString {
77
+ /** Prefer this over the constructor, as this will
78
+ * wrap `possiblyDefinedStringOrRichString` only if necessary
79
+ * as well as handle `null`.
80
+ *
81
+ * @param {null|undefined|String|RichString} possiblyDefinedStringOrRichString - if `null`, `undefined`, or otherwise falsey, this method returns `null`. If a String, returns a new `RichString` wrapping it. If a `RichString`, returns the `RichString` unchanged.
82
+ */
83
+ static fromString(possiblyDefinedStringOrRichString) {
84
+ if (possiblyDefinedStringOrRichString instanceof _RichString) {
85
+ return possiblyDefinedStringOrRichString;
86
+ }
87
+ if (!possiblyDefinedStringOrRichString) {
88
+ return null;
89
+ }
90
+ return new _RichString(String(possiblyDefinedStringOrRichString));
91
+ }
92
+ /** Prefer `fromString` */
93
+ constructor(string) {
94
+ if (typeof string !== "string") {
95
+ throw `You may only construct a RichString with a String, not a ${typeof string}`;
96
+ }
97
+ this.string = string;
98
+ }
99
+ /** Returns a `RichString` with the string capitalized. */
100
+ capitalize() {
101
+ return new _RichString(this.string.charAt(0).toUpperCase() + this.string.slice(1));
102
+ }
103
+ /** Returns a `RichString` with the string un-capitalized. */
104
+ decapitalize() {
105
+ return new _RichString(this.string.charAt(0).toLowerCase() + this.string.slice(1));
106
+ }
107
+ /** Returns a `RichString` with the string converted from snake or kebab case into camel case. */
108
+ camelize() {
109
+ return _RichString.fromString(this.string.replace(/[_.-](\w|$)/g, function(_, x) {
110
+ return x.toUpperCase();
111
+ }));
112
+ }
113
+ /** Returns a 'humanized' `RichString`, which is basically a de-camelized version with the first letter
114
+ * capitalized.
115
+ */
116
+ humanize() {
117
+ return this.decamlize({ spacer: " " }).capitalize();
118
+ }
119
+ /** Returns a `RichString` with the string converted from camel case to snake or kebab case.
120
+ *
121
+ * @param {Object} parameters
122
+ * @param {string} parameters.spacer ["_"] - a string to use when joining words together.
123
+ *
124
+ */
125
+ decamlize({ spacer = "_" } = {}) {
126
+ if (this.string.length < 2) {
127
+ return new _RichString(this.string.toLowerCase());
128
+ }
129
+ const replacement = `$1${spacer}$2`;
130
+ const decamelized = this.string.replace(
131
+ /([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu,
132
+ replacement
133
+ );
134
+ return new _RichString(
135
+ decamelized.replace(
136
+ /(\p{Uppercase_Letter})(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
137
+ replacement
138
+ ).toLowerCase()
139
+ );
140
+ }
141
+ /** Return the underlying String value */
142
+ toString() {
143
+ return this.string;
144
+ }
145
+ /** Return the underlying String value or null if the string is blank */
146
+ toStringOrNull() {
147
+ if (this.isBlank()) {
148
+ return null;
149
+ } else {
150
+ return this.string;
151
+ }
152
+ }
153
+ /* Returns true if this string has only whitespace in it */
154
+ isBlank() {
155
+ return this.string.trim() == "";
156
+ }
157
+ };
158
+ var RichString_default = RichString;
159
+
160
+ // src/BaseCustomElement.js
161
+ var BaseCustomElement = class extends HTMLElement {
162
+ /** A {@link Logger} you can use to write warning messages. By default, these
163
+ * messages are not shown in the console. If you put `show-warnings` as an attribute on your
164
+ * element, warnings sent to this logger *are* shown.
165
+ */
166
+ logger = Logger_default.forPrefix(null);
167
+ #_connectedCallbackCalled = false;
168
+ #_disconnectedCallbackCalled = false;
169
+ constructor() {
170
+ super();
171
+ this.logger = Logger_default.forPrefix(null);
172
+ }
173
+ /** You must call this to define the custom element. This is bascially
174
+ * a wrapper around `customElements.define`. It is recommended that you call
175
+ * this inside a `DOMContentLoaded` event, or after the page's HTML has been processed.
176
+ *
177
+ * @see external:CustomElementRegistry
178
+ */
179
+ static define() {
180
+ if (!this.tagName) {
181
+ throw `To use BaseCustomElement, you must define the static member tagName to return your custom tag's name`;
182
+ }
183
+ customElements.define(this.tagName, this);
184
+ }
185
+ showWarningsChangedCallback({ oldValue, newValue }) {
186
+ let oldLogger;
187
+ if (!oldValue && newValue) {
188
+ oldLogger = this.logger;
189
+ }
190
+ let prefix = newValue == "" ? this.id : newValue;
191
+ if (!prefix) {
192
+ prefix = "UNKNOWN COMPONENT";
193
+ }
194
+ this.logger = Logger_default.forPrefix(prefix);
195
+ if (oldLogger) {
196
+ this.logger.dump(oldLogger);
197
+ }
198
+ }
199
+ /**
200
+ * Overrides the standard callback to allow subclasses to have a slightly easier API when responding
201
+ * to attribute changes. You can override this to use the custom element callback directly. Note that if
202
+ * you do, `show-warnings` will not have any affect and you probably don't need to bother using
203
+ * this class as your base class.
204
+ *
205
+ * This method will locate a per-attribute method and call that.
206
+ * Attribute names are assumed to be in kebab-case and are translated to camelCase to create a method name.
207
+ * That method is `«attributeInCamelCase»ChangedCallback`, so if your attribute is `hex-code`,
208
+ * a method named `hexCodeChangedCallback` in invoked. If no such method is defined, a
209
+ * warning is logged in the console, regardless of the `show-warnings` attribute.
210
+ *
211
+ * The method is invoked with `{oldValue,newValue,newValueAsBoolean}` - i.e. an object and not positional parameters. This
212
+ * means your implementation can omit any parameters it doesn't care about. `newValueAsBoolean` is not part of
213
+ * the custom element spec, but is provided as an unambiguous way to know if a boolean attribute was set or not. This is
214
+ * because if the value is set, it is likely to be the empty string, which is considered false by JavaScript. Cool.
215
+ *
216
+ * The return value of the method is ignored.
217
+ *
218
+ * After your method is called, if there is a method named `update`, it is called with no arguments.
219
+ *
220
+ * What this allows you to do is separate how you manage your element's attributes from how your logic
221
+ * is managed. For complex elements that take a lot of attributes, this can simplify your element's code without straying too far from the spec.
222
+ *
223
+ * @example
224
+ *
225
+ * // If your element accepts the attribute `warning-message` that will be trimmed of whitespace
226
+ * // then placed into all `H1` tags inside the element, you can manage that like so:
227
+ * class MyElement extends BaseCustomElement {
228
+ * static tagName = "my-element"
229
+ * static observedAttributes = [
230
+ * "warning-message",
231
+ * ]
232
+ *
233
+ * // called by attributeChangedCallback when warning-message's value changes
234
+ * warningMessageChangedCallback({newValue}) {
235
+ * this.warningMessage = (newValue || "").trim()
236
+ * }
237
+ *
238
+ * // called after attributeChangedCallback calls warningMessageChangedCallback
239
+ * update() {
240
+ * this.querySelectorAll("h1").forEach( (e) => e.textContent = this.warningMessage )
241
+ * }
242
+ * }
243
+ *
244
+ *
245
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements}
246
+ *
247
+ */
248
+ attributeChangedCallback(name, oldValue, newValue) {
249
+ const callbackName = `${new RichString_default(name).camelize()}ChangedCallback`;
250
+ if (this[callbackName]) {
251
+ const newValueAsBoolean = newValue !== null;
252
+ this[callbackName]({ oldValue, newValue, newValueAsBoolean });
253
+ } else if (this.constructor.observedAttributes.indexOf(name) != -1) {
254
+ console.warn("Observing %s but no method named %s was found to handle it", name, callbackName);
255
+ }
256
+ this.__update();
257
+ }
258
+ /** Overrides the custom element callback to set internal flags allowing you to know if your
259
+ * element has been disconnected. When an element is disconnected, `update` is not called.
260
+ *
261
+ * If you want to add your own logic during disconnection, override {@link onDisconnected}.
262
+ *
263
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements}
264
+ */
265
+ disconnectedCallback() {
266
+ this.#_disconnectedCallbackCalled = true;
267
+ this.onDisconnected();
268
+ }
269
+ /** Override this to add logic when `disconnectedCallback` is called by the browser. This will
270
+ * not be called if you overrode `disconnectedCallback`.
271
+ */
272
+ onDisconnected() {
273
+ }
274
+ /** Overrides the custom element callback to set internal flags allowing you to know if your
275
+ * element has been connected. `update` is still called for elements that have not yet connected, however
276
+ * in practice your element will be connected before any codepath that calls `update` is called.
277
+ *
278
+ * To add logic when your element is connected, override {@link onConnected}
279
+ *
280
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements}
281
+ * @see BaseCustomElement#connectedCallbackCalled
282
+ */
283
+ connectedCallback() {
284
+ this.#_connectedCallbackCalled = true;
285
+ this.onConnected();
286
+ this.__update();
287
+ }
288
+ /** Override this to add logic when `connectedCallback` is called by the browser. This will
289
+ * not be called if you overrode `connectedCallback`
290
+ */
291
+ onConnected() {
292
+ }
293
+ /** Returns true if this element is connected and the connected callback has been called.
294
+ * This is different from `Node#isConnected`, which can return true before `connectedCallback` is called.
295
+ */
296
+ get connectedCallbackCalled() {
297
+ return !!this.#_connectedCallbackCalled;
298
+ }
299
+ /** Override this to perform whatever logic your element must perform.
300
+ * Because changes to your element's attributes can happen at any time and in any order,
301
+ * you will want to consolidate all logic into one method—this one. You will also
302
+ * want to make sure that this method is idempotent and fault-tolerant.
303
+ *
304
+ * It is called by {@link BaseCustomElement#attributeChangedCallback|attributeChangedCallback} and {@link BaseCustomElement#connectedCallback|connectedCallback}, however
305
+ * it will *not* be called after the elment has been disconnected.
306
+ *
307
+ * That means that any event listeners, rendering, content manipulation, or other behavior should happen hear
308
+ * and it *must* be idempotent. In particular, any event listeners you attach must be done with care. Using
309
+ * anonymous functions could result in duplicate listeners.
310
+ */
311
+ update() {
312
+ }
313
+ __update() {
314
+ if (this.#_disconnectedCallbackCalled) {
315
+ return;
316
+ }
317
+ this.update();
318
+ }
319
+ };
320
+ var BaseCustomElement_default = BaseCustomElement;
321
+
322
+ // src/I18nTranslation.js
323
+ var I18nTranslation = class extends BaseCustomElement_default {
324
+ static tagName = "brut-i18n-translation";
325
+ static observedAttributes = [
326
+ "show-warnings",
327
+ "key",
328
+ "value"
329
+ ];
330
+ #key = null;
331
+ #value = "";
332
+ keyChangedCallback({ newValue }) {
333
+ this.#key = newValue;
334
+ }
335
+ valueChangedCallback({ newValue }) {
336
+ this.#value = newValue ? String(newValue) : "";
337
+ }
338
+ /**
339
+ * Called by other JavaScript to get the translated string.
340
+ * @param {Object} interpolatedValues - Object where the keys are placeholders in the string for interpolation and the values are
341
+ * the values to replace. Placeholders not in the translated value are ignored. Missing placeholders won't cause an error, but the
342
+ * placeholder will be present verbatim in the translated string.
343
+ *
344
+ * @example
345
+ * const element = document.querySeletor("brut-i18n-translation[key='greeting']")
346
+ * if (element) {
347
+ * const translation = element.translation({ username: "Pat" })
348
+ * alert(translation) // Shows 'Hello Pat'
349
+ * }
350
+ */
351
+ translation(interpolatedValues) {
352
+ return this.#value.replaceAll(/%\{([^}%]+)\}/g, (match, key) => {
353
+ if (interpolatedValues[key]) {
354
+ return interpolatedValues[key];
355
+ }
356
+ return match;
357
+ });
358
+ }
359
+ };
360
+ var I18nTranslation_default = I18nTranslation;
361
+
362
+ // src/ConstraintViolationMessage.js
363
+ var ConstraintViolationMessage = class _ConstraintViolationMessage extends BaseCustomElement_default {
364
+ static tagName = "brut-constraint-violation-message";
365
+ static observedAttributes = [
366
+ "show-warnings",
367
+ "key",
368
+ "input-name"
369
+ ];
370
+ static createElement(document2, attributes) {
371
+ const element = document2.createElement(_ConstraintViolationMessage.tagName);
372
+ element.setAttribute("key", this.i18nKey("fe", attributes.key));
373
+ element.setAttribute("input-name", attributes["input-name"]);
374
+ if (Object.hasOwn(attributes, "show-warnings")) {
375
+ element.setAttribute("show-warnings", attributes["show-warnings"]);
376
+ }
377
+ return element;
378
+ }
379
+ /** Returns the I18N key used for front-end constraint violations. This is useful
380
+ * if you need to construct a key and want to follow Brut's conventions on how they
381
+ * are managed.
382
+ *
383
+ * @param {...String} keyPath - parts of the path of the key after the namespace that Brut manages.
384
+ */
385
+ static i18nKey(...keyPath) {
386
+ const path = ["general", "cv"];
387
+ return path.concat(keyPath).join(".");
388
+ }
389
+ #key = null;
390
+ #inputNameKey = null;
391
+ #thisFieldKey = this.#i18nKey("this_field");
392
+ keyChangedCallback({ newValue }) {
393
+ this.#key = newValue;
394
+ }
395
+ inputNameChangedCallback({ newValue }) {
396
+ this.#inputNameKey = this.#i18nKey("fe", "fieldNames", newValue);
397
+ }
398
+ update() {
399
+ if (!this.#key) {
400
+ this.logger.info("No key attribute, so can't do anything");
401
+ return;
402
+ }
403
+ const selector = `${I18nTranslation_default.tagName}[key='${this.#key}']`;
404
+ const translation = document.querySelector(selector);
405
+ if (!translation) {
406
+ this.logger.info("Could not find translation based on selector '%s'", selector);
407
+ return;
408
+ }
409
+ const fieldNameSelector = `${I18nTranslation_default.tagName}[key='${this.#inputNameKey}']`;
410
+ const thisFieldSelector = `${I18nTranslation_default.tagName}[key='${this.#thisFieldKey}']`;
411
+ let fieldNameTranslation = document.querySelector(fieldNameSelector);
412
+ if (!fieldNameTranslation) {
413
+ this.logger.info("Could not find translation for input/field name based on selector '%s'. Will try 'this field' fallback", fieldNameSelector);
414
+ fieldNameTranslation = document.querySelector(thisFieldSelector);
415
+ if (!fieldNameTranslation) {
416
+ this.logger.info("Could not find translation for 'this field' fallback key, based on selector '%s'", thisFieldSelector);
417
+ }
418
+ }
419
+ const fieldName = fieldNameTranslation ? fieldNameTranslation.translation() : "this field";
420
+ this.textContent = RichString_default.fromString(translation.translation({ field: fieldName })).capitalize().toString();
421
+ }
422
+ /** Helper that calls the static version */
423
+ #i18nKey(...keyPath) {
424
+ return this.constructor.i18nKey(...keyPath);
425
+ }
426
+ };
427
+ var ConstraintViolationMessage_default = ConstraintViolationMessage;
428
+
429
+ // src/ConstraintViolationMessages.js
430
+ var ConstraintViolationMessages = class extends BaseCustomElement_default {
431
+ static tagName = "brut-constraint-violation-messages";
432
+ static observedAttributes = [
433
+ "show-warnings",
434
+ "server-side",
435
+ "input-name"
436
+ ];
437
+ serverSideChangedCallback({ newValueAsBoolean }) {
438
+ }
439
+ inputNameChangedCallback({ newValue }) {
440
+ }
441
+ /**
442
+ * Creates error messages based on the passed `ValidityState` and input name.
443
+ *
444
+ * This should be called as part of a Form validation event to provide a customized UX for
445
+ * the error messages, beyond what the browser would do by default. The keys used are the same
446
+ * as the attributes of a `ValidityState`, so for example, a range underflow would mean that `validity.rangeUnderflow` would return
447
+ * true. Thus, a `<brut-constraint-violation-message>` would be created with `key="general.cv.fe.rangeUnderflow"`.
448
+ *
449
+ * The `cv.fe` is hard-coded to be consistent with Brut's server-side translation management.
450
+ *
451
+ * @param {ValidityState} validityState - the return from an element's `validity` when it's found to have constraint violations.
452
+ * @param {String} inputName - the element's `name`.
453
+ */
454
+ createMessages({ validityState, inputName }) {
455
+ const errors = this.#VALIDITY_STATE_ATTRIBUTES.filter((attribute) => validityState[attribute]);
456
+ this.clearMessages();
457
+ errors.forEach((key) => {
458
+ const options = {
459
+ key,
460
+ "input-name": inputName
461
+ };
462
+ const showWarnings = this.getAttribute("show-warnings");
463
+ if (showWarnings != null) {
464
+ options["show-warnings"] = showWarnings;
465
+ }
466
+ const element = ConstraintViolationMessage_default.createElement(document, options);
467
+ this.appendChild(element);
468
+ });
469
+ }
470
+ /**
471
+ * Clear all messages. Useful for when an input has become valid during a session.
472
+ */
473
+ clearMessages() {
474
+ this.textContent = "";
475
+ }
476
+ #VALIDITY_STATE_ATTRIBUTES = [
477
+ "badInput",
478
+ "customError",
479
+ "patternMismatch",
480
+ "rangeOverflow",
481
+ "rangeUnderflow",
482
+ "stepMismatch",
483
+ "tooLong",
484
+ "tooShort",
485
+ "typeMismatch",
486
+ "valueMissing"
487
+ ];
488
+ };
489
+ var ConstraintViolationMessages_default = ConstraintViolationMessages;
490
+
491
+ // src/AjaxSubmit.js
492
+ var AjaxSubmit = class extends BaseCustomElement_default {
493
+ static tagName = "brut-ajax-submit";
494
+ static observedAttributes = [
495
+ "show-warnings",
496
+ "requesting",
497
+ "submitted",
498
+ "submitted-lifetime",
499
+ "request-timeout",
500
+ "max-retry-attempts",
501
+ "log-request-errors"
502
+ ];
503
+ #requestErrorLogger = () => {
504
+ };
505
+ #formSubmitDelay = 0;
506
+ #submittedLifetime = 2e3;
507
+ #requestTimeout = 5e3;
508
+ #maxRetryAttempts = 25;
509
+ submittedLifetimeChangedCallback({ newValue }) {
510
+ const newValueAsInt = parseInt(newValue);
511
+ if (isNaN(newValueAsInt)) {
512
+ throw `submitted-lifetime must be a number, not '${newValue}'`;
513
+ }
514
+ this.#submittedLifetime = newValueAsInt;
515
+ }
516
+ maxRetryAttemptsChangedCallback({ newValue }) {
517
+ const num = parseInt(newValue);
518
+ if (isNaN(num)) {
519
+ this.logger.warn(`max-retry-attempts '${newValue}' is not a number. Using 1 as a fallback`);
520
+ this.#maxRetryAttempts = 1;
521
+ } else {
522
+ this.#maxRetryAttempts = num;
523
+ }
524
+ }
525
+ requestTimeoutChangedCallback({ newValue }) {
526
+ const newValueAsInt = parseInt(newValue);
527
+ if (isNaN(newValueAsInt)) {
528
+ throw `request-timeout must be a number, not '${newValue}'`;
529
+ }
530
+ this.#requestTimeout = newValueAsInt;
531
+ }
532
+ submittedChangedCallback({ newValueAsBoolean }) {
533
+ }
534
+ requestingChangedCallback({ newValueAsBoolean }) {
535
+ if (this.#button()) {
536
+ if (newValueAsBoolean) {
537
+ this.#button().setAttribute("disabled", true);
538
+ } else {
539
+ this.#button().removeAttribute("disabled", true);
540
+ }
541
+ }
542
+ }
543
+ logRequestErrorsChangedCallback({ newValueAsBoolean }) {
544
+ if (newValueAsBoolean) {
545
+ this.#requestErrorLogger = console.warn;
546
+ this.#formSubmitDelay = 2e3;
547
+ } else {
548
+ this.#requestErrorLogger = () => {
549
+ };
550
+ this.#formSubmitDelay = 0;
551
+ }
552
+ }
553
+ update() {
554
+ const button = this.#button();
555
+ if (!button) {
556
+ this.logger.info("Could not find a <button> to attach behavior to");
557
+ return;
558
+ }
559
+ const form2 = button.form;
560
+ if (!form2) {
561
+ this.logger.info("%o did not have a form associated with it - cannot attach behavior", button);
562
+ return;
563
+ }
564
+ button.form.addEventListener("submit", this.#formSubmitted);
565
+ }
566
+ #formSubmitted = (event) => {
567
+ const submitter = event.submitter;
568
+ if (submitter == this.#button()) {
569
+ event.preventDefault();
570
+ const now = Date.now();
571
+ this.#submitForm(event.target, now, 0);
572
+ }
573
+ };
574
+ #submitForm(form2, firstSubmittedAt, numAttempts) {
575
+ const headers = new Headers();
576
+ headers.append("X-Requested-With", "XMLHttpRequest");
577
+ headers.append("Content-Type", "application/x-www-form-urlencoded");
578
+ const formData = new FormData(form2);
579
+ const urlSearchParams = new URLSearchParams(formData);
580
+ const timeoutSignal = AbortSignal.timeout(this.#requestTimeout);
581
+ const request = new Request(
582
+ form2.action,
583
+ {
584
+ headers,
585
+ method: form2.method,
586
+ body: urlSearchParams,
587
+ signal: timeoutSignal
588
+ }
589
+ );
590
+ if (numAttempts > this.#maxRetryAttempts) {
591
+ this.#requestErrorLogger("%d attempts. Giving up", numAttempts);
592
+ this.#submitFormThroughBrowser(form2);
593
+ return;
594
+ }
595
+ this.setAttribute("requesting", true);
596
+ fetch(request).then((response) => {
597
+ if (response.ok) {
598
+ this.removeAttribute("requesting");
599
+ this.setAttribute("submitted", true);
600
+ setTimeout(() => this.removeAttribute("submitted"), this.#submittedLifetime);
601
+ this.dispatchEvent(new CustomEvent("brut:submitok"));
602
+ } else {
603
+ let retry = false;
604
+ let resubmit2 = false;
605
+ if (Date.now() - firstSubmittedAt > this.#requestTimeout) {
606
+ this.#requestErrorLogger("Since initial button press %d, it's taken more than %d ms to get a response.", firstSubmittedAt, this.#requestTimeout);
607
+ retry = false;
608
+ resubmit2 = true;
609
+ } else {
610
+ const status = parseInt(response.status);
611
+ if (isNaN(status)) {
612
+ this.#requestErrorLogger("Got unparseable status: %d", response.status);
613
+ retry = false;
614
+ } else if (status >= 500) {
615
+ this.#requestErrorLogger("Got a %d, maybe retry will fix", status);
616
+ retry = true;
617
+ } else {
618
+ retry = false;
619
+ if (status == 422) {
620
+ this.#handleConstraintViolations(response);
621
+ }
622
+ }
623
+ }
624
+ if (retry) {
625
+ this.#requestErrorLogger("Trying again (attempt %d)", numAttempts + 1);
626
+ setTimeout(() => this.#submitForm(form2, firstSubmittedAt, numAttempts + 1), numAttempts * 10);
627
+ } else if (resubmit2) {
628
+ this.#requestErrorLogger("'retry' was marked false, but resubmit is 'true', so submitting through browser");
629
+ this.#submitFormThroughBrowser(form2);
630
+ this.removeAttribute("requesting");
631
+ }
632
+ }
633
+ }).catch((error) => {
634
+ this.#requestErrorLogger("Got %o, which cannot be retried", error);
635
+ this.#submitFormThroughBrowser(form2);
636
+ });
637
+ }
638
+ #button = () => {
639
+ return this.querySelector("button");
640
+ };
641
+ #submitFormThroughBrowser(form2) {
642
+ form2.removeEventListener("submit", this.#formSubmitted);
643
+ if (this.#formSubmitDelay > 0) {
644
+ console.log("Form submission has been delayed by %d ms in order to allow examining the log", this.#formSubmitDelay);
645
+ setTimeout(() => form2.requestSubmit(this.#button()), this.#formSubmitDelay);
646
+ } else {
647
+ form2.requestSubmit(this.#button());
648
+ }
649
+ }
650
+ #handleConstraintViolations(response) {
651
+ response.text().then((text) => {
652
+ try {
653
+ const inputsToMessages = ErrorMessagesForInput.mapInputsToErrorMessages(
654
+ this.#errorMessagesFromServer(text),
655
+ this.#requestErrorLogger
656
+ );
657
+ let inputToScrollToAfterReportingValidity;
658
+ for (const [inputName, { input, messagesElement, errorMessages }] of Object.entries(inputsToMessages)) {
659
+ if (!inputToScrollToAfterReportingValidity) {
660
+ inputToScrollToAfterReportingValidity = input;
661
+ }
662
+ messagesElement.clearMessages();
663
+ errorMessages.forEach((element) => messagesElement.appendChild(element));
664
+ this.#setCustomValidityThatClearsOnChange(input, errorMessages);
665
+ }
666
+ if (inputToScrollToAfterReportingValidity) {
667
+ inputToScrollToAfterReportingValidity.scrollIntoView();
668
+ }
669
+ resubmit = false;
670
+ this.removeAttribute("requesting");
671
+ this.dispatchEvent(new CustomEvent("brut:submitinvalid"));
672
+ } catch (e) {
673
+ this.#requestErrorLogger("While parsing %s, got %s", text, e);
674
+ resubmit = true;
675
+ }
676
+ if (resubmit) {
677
+ this.#submitFormThroughBrowser(form);
678
+ }
679
+ });
680
+ }
681
+ #errorMessagesFromServer(text) {
682
+ const parser = new DOMParser();
683
+ const fragment = parser.parseFromString(text, "text/html");
684
+ return fragment.querySelectorAll(ConstraintViolationMessage_default.tagName);
685
+ }
686
+ #setCustomValidityThatClearsOnChange(input, errorMessages) {
687
+ input.setCustomValidity(errorMessages[0].textContent);
688
+ input.reportValidity();
689
+ input.addEventListener("change", () => input.setCustomValidity(""));
690
+ }
691
+ };
692
+ var ErrorMessagesForInput = class _ErrorMessagesForInput {
693
+ static mapInputsToErrorMessages(errorMessages, requestErrorLogger) {
694
+ const inputsToMessages = {};
695
+ Array.from(errorMessages).map((element) => {
696
+ return new _ErrorMessagesForInput({
697
+ element,
698
+ inputName: element.getAttribute("input-name"),
699
+ document
700
+ });
701
+ }).forEach((errorMessagesForInput) => {
702
+ if (errorMessagesForInput.allElementsFound()) {
703
+ if (!inputsToMessages[errorMessagesForInput.inputName]) {
704
+ inputsToMessages[errorMessagesForInput.inputName] = {
705
+ input: errorMessagesForInput.input,
706
+ messagesElement: errorMessagesForInput.messagesElement,
707
+ errorMessages: []
708
+ };
709
+ }
710
+ inputsToMessages[errorMessagesForInput.inputName].errorMessages.push(errorMessagesForInput.element);
711
+ } else {
712
+ requestErrorLogger("Server message %o could not be shown to the user: %s", errorMessagesForInput.element, errorMessagesForInput.reasonNotAllElementsFound());
713
+ }
714
+ });
715
+ return inputsToMessages;
716
+ }
717
+ constructor({ element, inputName, document: document2 }) {
718
+ this.element = element;
719
+ this.inputName = inputName;
720
+ if (this.inputName) {
721
+ const selector = `${ConstraintViolationMessages_default.tagName}[server-side][input-name='${this.inputName}']`;
722
+ this.messagesElement = document2.querySelector(selector);
723
+ if (this.messagesElement) {
724
+ this.closestForm = this.messagesElement.closest("form");
725
+ }
726
+ if (this.inputName && this.closestForm) {
727
+ this.input = this.closestForm.elements.namedItem(this.inputName);
728
+ }
729
+ }
730
+ }
731
+ allElementsFound() {
732
+ return !!this.input;
733
+ }
734
+ reasonNotAllElementsFound() {
735
+ let reason;
736
+ if (this.inputName) {
737
+ if (this.messagesElement) {
738
+ if (this.closestForm) {
739
+ reason = `Form did not contain an input named ${this.inputName}`;
740
+ } else {
741
+ reason = `Could not find a form that contained the ${ConstraintViolationMessages_default.tagName} element`;
742
+ }
743
+ } else {
744
+ reason = `Could not find a ${ConstraintViolationMessages_default.tagName} element for ${this.inputName}`;
745
+ }
746
+ } else {
747
+ reason = "server message was missing an input-name";
748
+ }
749
+ return reason;
750
+ }
751
+ };
752
+ var AjaxSubmit_default = AjaxSubmit;
753
+
754
+ // src/ConfirmationDialog.js
755
+ var ConfirmationDialog = class extends BaseCustomElement_default {
756
+ static tagName = "brut-confirmation-dialog";
757
+ static observedAttributes = [
758
+ "message",
759
+ "confirm-label",
760
+ "show-warnings"
761
+ ];
762
+ #onClose = () => {
763
+ };
764
+ #message = new RichString_default("");
765
+ #confirmLabel = new RichString_default("OK");
766
+ constructor() {
767
+ super();
768
+ this.okListener = (event) => {
769
+ this.#closeDialog();
770
+ this.#onClose(true);
771
+ };
772
+ this.cancelListener = (event) => {
773
+ this.#closeDialog();
774
+ this.#onClose(false);
775
+ };
776
+ }
777
+ messageChangedCallback({ newValue }) {
778
+ this.#message = RichString_default.fromString(newValue);
779
+ }
780
+ confirmLabelChangedCallback({ newValue }) {
781
+ this.#confirmLabel = RichString_default.fromString(newValue);
782
+ }
783
+ /**
784
+ * Call this to show the dialog. When the dialog is closed, `onClose` is called with the result.
785
+ *
786
+ * @param {function} onClose - a function called with either `true` or `false`, if the dialog was confirmed or
787
+ * denied, respectively.
788
+ *
789
+ * @example
790
+ * dialog.showModal( (confirmed) => {
791
+ * if (confirmed) {
792
+ * form.submit()
793
+ * }
794
+ * else {
795
+ * // do nothing
796
+ * }
797
+ * })
798
+ */
799
+ showModal(onClose) {
800
+ const dialog = this.#dialog;
801
+ if (dialog) {
802
+ this.#onClose = onClose || (() => {
803
+ });
804
+ dialog.showModal();
805
+ } else {
806
+ this.logger.warn("No <dialog> found to show");
807
+ }
808
+ }
809
+ get #dialog() {
810
+ return this.querySelector("dialog");
811
+ }
812
+ #closeDialog() {
813
+ const dialog = this.#dialog;
814
+ if (dialog) {
815
+ dialog.close();
816
+ }
817
+ }
818
+ update() {
819
+ const dialog = this.#dialog;
820
+ if (!dialog) {
821
+ this.logger.warn("Could not find a <dialog> - this custom element won't do anything");
822
+ return;
823
+ }
824
+ this.#setMessage(dialog);
825
+ this.#setupButtons();
826
+ }
827
+ #setMessage(dialog) {
828
+ const h1 = dialog.querySelector("h1");
829
+ if (h1) {
830
+ if (this.#message.isBlank()) {
831
+ h1.textContent = null;
832
+ } else {
833
+ h1.textContent = this.#message.toString();
834
+ }
835
+ } else {
836
+ this.logger.warn("Dialog had no <h1>, so nowhere to put the message");
837
+ }
838
+ }
839
+ #setupButtons() {
840
+ const okButton = this.querySelector("button[value='ok']");
841
+ const cancelButton = this.querySelector("button[value='cancel']");
842
+ if (!okButton || !cancelButton) {
843
+ if (!okButton) {
844
+ this.logger.warn("no <button value='ok'> which is required for this dialog to work");
845
+ }
846
+ if (!cancelButton) {
847
+ this.logger.warn("no <button value='cancel'> which is required for this dialog to work");
848
+ }
849
+ return;
850
+ }
851
+ okButton.textContent = this.#confirmLabel;
852
+ okButton.addEventListener("click", this.okListener);
853
+ cancelButton.addEventListener("click", this.cancelListener);
854
+ }
855
+ };
856
+ var ConfirmationDialog_default = ConfirmationDialog;
857
+
858
+ // src/ConfirmSubmit.js
859
+ var ConfirmSubmit = class extends BaseCustomElement_default {
860
+ static tagName = "brut-confirm-submit";
861
+ static observedAttributes = [
862
+ "message",
863
+ "dialog",
864
+ "show-warnings"
865
+ ];
866
+ #message = new RichString_default("");
867
+ #confirming = false;
868
+ #dialogId = null;
869
+ messageChangedCallback({ newValue }) {
870
+ this.#message = new RichString_default(newValue || "");
871
+ }
872
+ dialogChangedCallback({ newValue }) {
873
+ this.#dialogId = RichString_default.fromString(newValue);
874
+ }
875
+ constructor() {
876
+ super();
877
+ this.onClick = (event) => {
878
+ if (this.#confirming) {
879
+ this.#confirming = false;
880
+ return;
881
+ }
882
+ if (this.#message.isBlank()) {
883
+ this.logger.warn("No message provided, so cannot confirm");
884
+ return;
885
+ }
886
+ const dialog = this.#findDialog();
887
+ if (dialog) {
888
+ event.preventDefault();
889
+ dialog.setAttribute("message", this.#message.toString());
890
+ const buttonLabel = event.target.getAttribute("aria-label") || event.target.textContent;
891
+ dialog.setAttribute("confirm-label", buttonLabel);
892
+ this.#confirming = true;
893
+ dialog.showModal((confirm) => {
894
+ if (confirm) {
895
+ event.target.click();
896
+ } else {
897
+ this.#confirming = false;
898
+ }
899
+ });
900
+ } else {
901
+ const result = window.confirm(this.#message);
902
+ if (!result) {
903
+ event.preventDefault();
904
+ }
905
+ }
906
+ };
907
+ }
908
+ #findDialog() {
909
+ if (this.#dialogId) {
910
+ const dialog = document.getElementById(this.#dialogId);
911
+ if (dialog) {
912
+ if (dialog.tagName.toLowerCase() != ConfirmationDialog_default.tagName) {
913
+ throw `${this.#dialogId} is the id of a '${dialog.tagName}', not '${ConfirmationDialog_default.tagName}'`;
914
+ }
915
+ return dialog;
916
+ }
917
+ this.logger.warn(`No dialog with id ${this.#dialogId} - using window.confirm as a fallback`);
918
+ return null;
919
+ }
920
+ const dialogs = document.querySelectorAll(ConfirmationDialog_default.tagName);
921
+ if (dialogs.length == 1) {
922
+ return dialogs[0];
923
+ }
924
+ if (dialogs.length == 0) {
925
+ this.logger.warn(`No '${ConfirmationDialog_default.tagName}' found in document - using window.confirm as a fallback`);
926
+ return null;
927
+ }
928
+ throw `Found ${dialogs.length} '${ConfirmationDialog_default.tagName}' elements. Not sure which to use. Remove all but one or specify the 'dialog' attribute on this element to specify which one to use`;
929
+ }
930
+ update() {
931
+ this.querySelectorAll("button").forEach((button) => button.addEventListener("click", this.onClick));
932
+ this.querySelectorAll("input[type=submit]").forEach((button) => button.addEventListener("click", this.onClick));
933
+ }
934
+ };
935
+ var ConfirmSubmit_default = ConfirmSubmit;
936
+
937
+ // src/Form.js
938
+ var Form = class extends BaseCustomElement_default {
939
+ static tagName = "brut-form";
940
+ static observedAttributes = [
941
+ "show-warnings"
942
+ ];
943
+ #markFormSubmitted = (event) => {
944
+ const form2 = event.target.form;
945
+ if (!form2) {
946
+ this.logger.warn("%o had no form", event.target);
947
+ return;
948
+ }
949
+ form2.dataset["submitted"] = true;
950
+ };
951
+ #updateValidity = (event) => {
952
+ this.#updateErrorMessages(event);
953
+ };
954
+ #sendValid = () => {
955
+ this.dispatchEvent(new CustomEvent("brut:valid"));
956
+ };
957
+ #sendInvalid = () => {
958
+ this.dispatchEvent(new CustomEvent("brut:invalid"));
959
+ };
960
+ update() {
961
+ const forms = this.querySelectorAll("form");
962
+ if (forms.length == 0) {
963
+ this.logger.warn("Didn't find any forms. Ignoring");
964
+ return;
965
+ }
966
+ forms.forEach((form2) => {
967
+ Array.from(form2.elements).forEach((formElement) => {
968
+ formElement.addEventListener("invalid", this.#updateValidity);
969
+ formElement.addEventListener("invalid", this.#markFormSubmitted);
970
+ formElement.addEventListener("input", this.#updateValidity);
971
+ });
972
+ form2.querySelectorAll(AjaxSubmit_default.tagName).forEach((ajaxSubmits) => {
973
+ ajaxSubmits.addEventListener("brut:submitok", this.#sendValid);
974
+ ajaxSubmits.addEventListener("brut:submitinvalid", this.#sendInvalid);
975
+ });
976
+ });
977
+ }
978
+ #updateErrorMessages(event) {
979
+ const element = event.target;
980
+ const selector = `${ConstraintViolationMessages_default.tagName}:not([server-side])`;
981
+ const errorLabels = element.parentNode.querySelectorAll(selector);
982
+ if (errorLabels.length == 0) {
983
+ this.logger.warn(`Did not find any elements matching ${selector}, so no error messages will be shown`);
984
+ return;
985
+ }
986
+ let anyErrors = false;
987
+ errorLabels.forEach((errorLabel) => {
988
+ if (element.validity.valid) {
989
+ errorLabel.clearMessages();
990
+ } else {
991
+ anyErrors = true;
992
+ errorLabel.createMessages({
993
+ validityState: element.validity,
994
+ inputName: element.name
995
+ });
996
+ }
997
+ });
998
+ if (anyErrors) {
999
+ this.#sendInvalid();
1000
+ } else {
1001
+ this.#sendValid();
1002
+ }
1003
+ }
1004
+ };
1005
+ var Form_default = Form;
1006
+
1007
+ // src/Message.js
1008
+ var Message = class _Message extends BaseCustomElement_default {
1009
+ static tagName = "brut-message";
1010
+ static observedAttributes = [
1011
+ "show-warnings",
1012
+ "key"
1013
+ ];
1014
+ static createElement(document2, attributes) {
1015
+ const element = document2.createElement(_Message.tagName);
1016
+ element.setAttribute("key", attributes.key);
1017
+ element.setAttribute("show-warnings", attributes["show-warnings"]);
1018
+ return element;
1019
+ }
1020
+ #key = null;
1021
+ keyChangedCallback({ newValue }) {
1022
+ this.#key = newValue;
1023
+ }
1024
+ update() {
1025
+ if (!this.#key) {
1026
+ this.logger.info("No key attribute, so can't do anything");
1027
+ return;
1028
+ }
1029
+ const selector = `${I18nTranslation_default.tagName}[key='${this.#key}']`;
1030
+ const translation = document.querySelector(selector);
1031
+ if (!translation) {
1032
+ this.logger.info("Could not find translation based on selector '%s'", selector);
1033
+ return;
1034
+ }
1035
+ this.textContent = RichString_default.fromString(translation.translation()).capitalize().toString();
1036
+ }
1037
+ };
1038
+ var Message_default = Message;
1039
+
1040
+ // src/Tabs.js
1041
+ var Tabs = class extends BaseCustomElement_default {
1042
+ static tagName = "brut-tabs";
1043
+ static observedAttributes = [
1044
+ "tab-selection-pushes-and-restores-state",
1045
+ "show-warnings"
1046
+ ];
1047
+ tabSelectionPushesAndRestoresStateChangedCallback({ newValue, oldValue }) {
1048
+ this.#pushAndRestoreTabState = newValue != null;
1049
+ }
1050
+ update() {
1051
+ this.#tabs().forEach((tab) => {
1052
+ tab.addEventListener("click", this.#tabClicked);
1053
+ });
1054
+ }
1055
+ #pushAndRestoreTabState = false;
1056
+ #tabClicked = (event) => {
1057
+ event.preventDefault();
1058
+ this.#setTabAsSelected(event.target);
1059
+ event.preventDefault();
1060
+ };
1061
+ #reloadTab = (event) => {
1062
+ const tab = document.getElementById(event.state.tabId);
1063
+ if (tab) {
1064
+ this.#setTabAsSelected(tab, { skipPushState: true });
1065
+ }
1066
+ };
1067
+ #setTabAsSelected(selectedTab, { skipPushState = false } = {}) {
1068
+ this.#tabs().forEach((tab) => {
1069
+ const tabPanels = [];
1070
+ const ariaControls = tab.getAttribute("aria-controls");
1071
+ if (ariaControls) {
1072
+ ariaControls.split(/\s+/).forEach((id) => {
1073
+ const panel = document.getElementById(id);
1074
+ if (panel) {
1075
+ tabPanels.push(panel);
1076
+ } else {
1077
+ this.logger.warn("Tab %o references panel with id %s, but no such element exists with that id", tab, id);
1078
+ }
1079
+ });
1080
+ }
1081
+ if (tab == selectedTab) {
1082
+ tab.setAttribute("aria-selected", true);
1083
+ tab.setAttribute("tabindex", "0");
1084
+ tabPanels.forEach((panel) => panel.removeAttribute("hidden"));
1085
+ if (this.#pushAndRestoreTabState && !skipPushState) {
1086
+ let href = tab.getAttribute("href") || "";
1087
+ if (href.startsWith("?")) {
1088
+ let hrefQueryString = href.slice(1);
1089
+ const anchorIndex = hrefQueryString.indexOf("#");
1090
+ if (anchorIndex != -1) {
1091
+ hrefQueryString = hrefQueryString.slice(-1 * (hrefQueryString.length - anchorIndex - 1));
1092
+ }
1093
+ const currentQuery = new URLSearchParams(window.location.search);
1094
+ const hrefQuery = new URLSearchParams(hrefQueryString);
1095
+ hrefQuery.forEach((value, key) => {
1096
+ currentQuery.set(key, value);
1097
+ });
1098
+ href = "?" + currentQuery.toString() + (anchorIndex == -1 ? "" : hrefQueryString.slice(anchorIndex));
1099
+ }
1100
+ window.history.pushState({ tabId: tab.id }, "", href);
1101
+ window.addEventListener("popstate", this.#reloadTab);
1102
+ }
1103
+ this.dispatchEvent(new CustomEvent("brut:tabselected", { tabId: tab.id }));
1104
+ } else {
1105
+ tab.setAttribute("aria-selected", false);
1106
+ tab.setAttribute("tabindex", "-1");
1107
+ tabPanels.forEach((panel) => panel.setAttribute("hidden", true));
1108
+ }
1109
+ });
1110
+ }
1111
+ #tabs() {
1112
+ const tabs = [];
1113
+ this.querySelectorAll("[role=tab]").forEach((tab) => {
1114
+ if (tab.tagName.toLowerCase() == "a" || tab.tagName.toLowerCase() == "button") {
1115
+ tabs.push(tab);
1116
+ } else {
1117
+ this.logger.warn("An element with tag %s was assigned role=tab, and %s doesn't work that way. Use an <a> or a <button>", tab.tagName, this.constructor.name);
1118
+ }
1119
+ });
1120
+ return tabs;
1121
+ }
1122
+ };
1123
+ var Tabs_default = Tabs;
1124
+
1125
+ // src/LocaleDetection.js
1126
+ var LocaleDetection = class extends BaseCustomElement_default {
1127
+ static tagName = "brut-locale-detection";
1128
+ static observedAttributes = [
1129
+ "locale-from-server",
1130
+ "timezone-from-server",
1131
+ "url",
1132
+ "timeout-before-ping-ms",
1133
+ "show-warnings"
1134
+ ];
1135
+ #localeFromServer = null;
1136
+ #timezoneFromServer = null;
1137
+ #reportingURL = null;
1138
+ #timeoutBeforePing = 1e3;
1139
+ #serverContacted = false;
1140
+ localeFromServerChangedCallback({ newValue }) {
1141
+ this.#localeFromServer = newValue;
1142
+ }
1143
+ timezoneFromServerChangedCallback({ newValue }) {
1144
+ this.#timezoneFromServer = newValue;
1145
+ }
1146
+ urlChangedCallback({ newValue }) {
1147
+ if (this.#serverContacted) {
1148
+ this.#serverContacted = false;
1149
+ }
1150
+ this.#reportingURL = newValue;
1151
+ }
1152
+ timeoutBeforePingMsChangedCallback({ newValue }) {
1153
+ this.#timeoutBeforePing = newValue;
1154
+ }
1155
+ update() {
1156
+ if (this.#timeoutBeforePing == 0) {
1157
+ this.#pingServerWithLocaleInfo();
1158
+ } else {
1159
+ setTimeout(this.#pingServerWithLocaleInfo.bind(this), this.#timeoutBeforePing);
1160
+ }
1161
+ }
1162
+ #pingServerWithLocaleInfo() {
1163
+ if (!this.#reportingURL) {
1164
+ this.logger.info("no url= set, so nowhere to report to");
1165
+ return;
1166
+ }
1167
+ if (this.#localeFromServer && this.#timezoneFromServer) {
1168
+ this.logger.info("locale and timezone both set, not contacting server");
1169
+ return;
1170
+ }
1171
+ if (this.#serverContacted) {
1172
+ this.logger.info("server has already been contacted at the given url, not doing it again");
1173
+ return;
1174
+ }
1175
+ this.#serverContacted = true;
1176
+ const formatOptions = Intl.DateTimeFormat().resolvedOptions();
1177
+ const request = new Request(
1178
+ this.#reportingURL,
1179
+ {
1180
+ headers: {
1181
+ "Content-Type": "application/json"
1182
+ },
1183
+ method: "POST",
1184
+ body: JSON.stringify({
1185
+ locale: formatOptions.locale,
1186
+ timeZone: formatOptions.timeZone
1187
+ })
1188
+ }
1189
+ );
1190
+ window.fetch(request).then((response) => {
1191
+ if (response.ok) {
1192
+ this.logger.info("Server gave us the OK");
1193
+ } else {
1194
+ console.warn(response);
1195
+ }
1196
+ }).catch((e) => {
1197
+ console.warn(e);
1198
+ });
1199
+ }
1200
+ };
1201
+ var LocaleDetection_default = LocaleDetection;
1202
+
1203
+ // src/Autosubmit.js
1204
+ var Autosubmit = class extends BaseCustomElement_default {
1205
+ static tagName = "brut-autosubmit";
1206
+ static observedAttributes = [
1207
+ "show-warnings"
1208
+ ];
1209
+ #submitForm = (event) => {
1210
+ const form2 = this.closest("form");
1211
+ if (!form2) {
1212
+ this.logger.info("No longer a form containing this element");
1213
+ return;
1214
+ }
1215
+ if (event.target.form != form2) {
1216
+ this.logger.info("Event target %o's form is not the form that contains this element", event.target);
1217
+ return;
1218
+ }
1219
+ form2.requestSubmit();
1220
+ };
1221
+ update() {
1222
+ const form2 = this.closest("form");
1223
+ if (!form2) {
1224
+ this.logger.info("No form containing this element - nothing to autosubmit");
1225
+ return;
1226
+ }
1227
+ const inputs = Array.from(this.querySelectorAll("input, textarea, select")).filter((element) => {
1228
+ return element.form == form2;
1229
+ });
1230
+ if (inputs.length == 0) {
1231
+ this.logger.info("No input, textarea, or select inside this element belongs to the form containing this element");
1232
+ return;
1233
+ }
1234
+ inputs.forEach((input) => {
1235
+ input.addEventListener("change", this.#submitForm);
1236
+ });
1237
+ }
1238
+ };
1239
+ var Autosubmit_default = Autosubmit;
1240
+
1241
+ // src/index.js
1242
+ var BrutCustomElements = class {
1243
+ static elementClasses = [];
1244
+ static define() {
1245
+ this.elementClasses.forEach((e) => {
1246
+ e.define();
1247
+ });
1248
+ }
1249
+ static addElementClasses(...classes) {
1250
+ this.elementClasses.push(...classes);
1251
+ }
1252
+ };
1253
+ BrutCustomElements.addElementClasses(
1254
+ // Ordering is important here - TBD how to make sure these are created in order
1255
+ I18nTranslation_default,
1256
+ Message_default,
1257
+ ConfirmSubmit_default,
1258
+ ConfirmationDialog_default,
1259
+ ConstraintViolationMessages_default,
1260
+ Form_default,
1261
+ AjaxSubmit_default,
1262
+ ConstraintViolationMessage_default,
1263
+ Tabs_default,
1264
+ LocaleDetection_default,
1265
+ Autosubmit_default
1266
+ );
1267
+
1268
+ // src/appForTestingOnly.js
1269
+ document.addEventListener("DOMContentLoaded", () => {
1270
+ BrutCustomElements.define();
1271
+ if (!HTMLDialogElement.prototype.showModal) {
1272
+ HTMLDialogElement.prototype.showModal = function() {
1273
+ this.open = true;
1274
+ };
1275
+ }
1276
+ if (!HTMLDialogElement.prototype.close) {
1277
+ HTMLDialogElement.prototype.close = function(returnValue) {
1278
+ this.open = false;
1279
+ this.returnValue = returnValue;
1280
+ };
1281
+ }
1282
+ });
1283
+ })();
1284
+ //# sourceMappingURL=bundle.js.map