brut-js 0.0.9 → 0.0.11

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brut-js",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "keywords": [ "WebComponents", "Custom Elements" ],
6
6
  "bugs": {
@@ -19,7 +19,75 @@ describe("<brut-form>", () => {
19
19
  <button>Save Ajaxily</button>
20
20
  </brut-ajax-submit>
21
21
  </form>
22
- </brut-form>`).test("Works with ajax submissions to keep errors consistent", ({window,document,assert}) => {
22
+ </brut-form>
23
+ `).test("sets submitted-invalid on submit with invalid form + updates messages", ({window,document,assert}) => {
24
+
25
+ const brutForm = document.querySelector("brut-form")
26
+ const form = brutForm.querySelector("form")
27
+ const button = form.querySelector("input[type=submit]")
28
+ const textFieldLabel = form.querySelector("label:has(input[type=text])")
29
+ const numberFieldLabel = form.querySelector("label:has(input[type=number])")
30
+
31
+ let submitted = false
32
+ let gotInvalid = false
33
+ let gotValid = false
34
+
35
+ form.addEventListener("submit", (event) => {
36
+ event.preventDefault()
37
+ submitted = true
38
+ })
39
+ brutForm.addEventListener("brut:valid", () => {
40
+ gotValid = true
41
+ })
42
+ brutForm.addEventListener("brut:invalid", () => {
43
+ gotInvalid = true
44
+ })
45
+ assert.equal(brutForm.getAttribute("submitted-invalid"),null)
46
+
47
+ button.click()
48
+
49
+ assert(!submitted)
50
+ assert(!gotValid)
51
+ assert(gotInvalid)
52
+ assert.equal(brutForm.getAttribute("submitted-invalid"),"")
53
+
54
+ let error = textFieldLabel.querySelector("brut-cv[key='general.cv.fe.valueMissing']")
55
+ assert(error)
56
+ error = numberFieldLabel.querySelector("brut-cv[key='general.cv.fe.valueMissing']")
57
+ assert(error)
58
+
59
+ const textField = textFieldLabel.querySelector("input")
60
+ textField.value = "Some Value"
61
+ textField.dispatchEvent(new window.Event("input"))
62
+
63
+ submitted = false
64
+ gotInvalid = false
65
+ gotValid = false
66
+
67
+ button.click()
68
+
69
+ assert(!submitted)
70
+ assert(!gotValid)
71
+ assert(gotInvalid)
72
+ assert.equal(brutForm.getAttribute("submitted-invalid"),"")
73
+
74
+ error = textFieldLabel.querySelector("brut-cv[key='general.cv.fe.valueMissing']")
75
+ assert(!error)
76
+ error = numberFieldLabel.querySelector("brut-cv[key='general.cv.fe.valueMissing']")
77
+ assert(error)
78
+
79
+ const numberField = numberFieldLabel.querySelector("input")
80
+ numberField.value = "99"
81
+ numberField.dispatchEvent(new window.Event("input"))
82
+
83
+ submitted = false
84
+ gotInvalid = false
85
+ gotValid = false
86
+
87
+ button.click()
88
+
89
+ assert(submitted)
90
+ }).test("Works with ajax submissions to keep errors consistent", ({window,document,assert}) => {
23
91
 
24
92
  const brutForm = document.querySelector("brut-form")
25
93
  const form = brutForm.querySelector("form")
@@ -104,6 +172,7 @@ describe("<brut-form>", () => {
104
172
  assert(!submitted)
105
173
  assert(!gotValid)
106
174
  assert(gotInvalid)
175
+ assert.equal(brutForm.getAttribute("submitted-invalid"),"")
107
176
 
108
177
  let error = brutForm.querySelector("brut-cv[input-name='text'][key='general.cv.fe.valueMissing']")
109
178
  assert(error)
@@ -299,7 +299,7 @@
299
299
  /** Override this to perform whatever logic your element must perform.
300
300
  * Because changes to your element's attributes can happen at any time and in any order,
301
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.
302
+ * want to make sure that this method is idempotent and fault-tolerant. It will be called multiple times.
303
303
  *
304
304
  * It is called by {@link BaseCustomElement#attributeChangedCallback|attributeChangedCallback} and {@link BaseCustomElement#connectedCallback|connectedCallback}, however
305
305
  * it will *not* be called after the elment has been disconnected.
@@ -785,6 +785,44 @@
785
785
  };
786
786
  var AjaxSubmit_default = AjaxSubmit;
787
787
 
788
+ // src/Autosubmit.js
789
+ var Autosubmit = class extends BaseCustomElement_default {
790
+ static tagName = "brut-autosubmit";
791
+ static observedAttributes = [
792
+ "show-warnings"
793
+ ];
794
+ #submitForm = (event) => {
795
+ const form2 = this.closest("form");
796
+ if (!form2) {
797
+ this.logger.info("No longer a form containing this element");
798
+ return;
799
+ }
800
+ if (event.target.form != form2) {
801
+ this.logger.info("Event target %o's form is not the form that contains this element", event.target);
802
+ return;
803
+ }
804
+ form2.requestSubmit();
805
+ };
806
+ update() {
807
+ const form2 = this.closest("form");
808
+ if (!form2) {
809
+ this.logger.info("No form containing this element - nothing to autosubmit");
810
+ return;
811
+ }
812
+ const inputs = Array.from(this.querySelectorAll("input, textarea, select")).filter((element) => {
813
+ return element.form == form2;
814
+ });
815
+ if (inputs.length == 0) {
816
+ this.logger.info("No input, textarea, or select inside this element belongs to the form containing this element");
817
+ return;
818
+ }
819
+ inputs.forEach((input) => {
820
+ input.addEventListener("change", this.#submitForm);
821
+ });
822
+ }
823
+ };
824
+ var Autosubmit_default = Autosubmit;
825
+
788
826
  // src/ConfirmationDialog.js
789
827
  var ConfirmationDialog = class extends BaseCustomElement_default {
790
828
  static tagName = "brut-confirmation-dialog";
@@ -1022,8 +1060,12 @@
1022
1060
  var Form = class extends BaseCustomElement_default {
1023
1061
  static tagName = "brut-form";
1024
1062
  static observedAttributes = [
1063
+ "submitted-invalid",
1025
1064
  "show-warnings"
1026
1065
  ];
1066
+ #markFormSubmittedInvalid = (event) => {
1067
+ this.setAttribute("submitted-invalid", "");
1068
+ };
1027
1069
  #updateValidity = (event) => {
1028
1070
  this.#updateErrorMessages(event);
1029
1071
  };
@@ -1033,6 +1075,8 @@
1033
1075
  #sendInvalid = () => {
1034
1076
  this.dispatchEvent(new CustomEvent("brut:invalid"));
1035
1077
  };
1078
+ submittedInvalidChangedCallback() {
1079
+ }
1036
1080
  update() {
1037
1081
  const forms = this.querySelectorAll("form");
1038
1082
  if (forms.length == 0) {
@@ -1042,6 +1086,7 @@
1042
1086
  forms.forEach((form2) => {
1043
1087
  Array.from(form2.elements).forEach((formElement) => {
1044
1088
  formElement.addEventListener("invalid", this.#updateValidity);
1089
+ formElement.addEventListener("invalid", this.#markFormSubmittedInvalid);
1045
1090
  formElement.addEventListener("input", this.#updateValidity);
1046
1091
  });
1047
1092
  form2.querySelectorAll(AjaxSubmit_default.tagName).forEach((ajaxSubmits) => {
@@ -1094,6 +1139,84 @@
1094
1139
  };
1095
1140
  var Form_default = Form;
1096
1141
 
1142
+ // src/LocaleDetection.js
1143
+ var LocaleDetection = class extends BaseCustomElement_default {
1144
+ static tagName = "brut-locale-detection";
1145
+ static observedAttributes = [
1146
+ "locale-from-server",
1147
+ "timezone-from-server",
1148
+ "url",
1149
+ "timeout-before-ping-ms",
1150
+ "show-warnings"
1151
+ ];
1152
+ #localeFromServer = null;
1153
+ #timezoneFromServer = null;
1154
+ #reportingURL = null;
1155
+ #timeoutBeforePing = 1e3;
1156
+ #serverContacted = false;
1157
+ localeFromServerChangedCallback({ newValue }) {
1158
+ this.#localeFromServer = newValue;
1159
+ }
1160
+ timezoneFromServerChangedCallback({ newValue }) {
1161
+ this.#timezoneFromServer = newValue;
1162
+ }
1163
+ urlChangedCallback({ newValue }) {
1164
+ if (this.#serverContacted) {
1165
+ this.#serverContacted = false;
1166
+ }
1167
+ this.#reportingURL = newValue;
1168
+ }
1169
+ timeoutBeforePingMsChangedCallback({ newValue }) {
1170
+ this.#timeoutBeforePing = newValue;
1171
+ }
1172
+ update() {
1173
+ if (this.#timeoutBeforePing == 0) {
1174
+ this.#pingServerWithLocaleInfo();
1175
+ } else {
1176
+ setTimeout(this.#pingServerWithLocaleInfo.bind(this), this.#timeoutBeforePing);
1177
+ }
1178
+ }
1179
+ #pingServerWithLocaleInfo() {
1180
+ if (!this.#reportingURL) {
1181
+ this.logger.info("no url= set, so nowhere to report to");
1182
+ return;
1183
+ }
1184
+ if (this.#localeFromServer && this.#timezoneFromServer) {
1185
+ this.logger.info("locale and timezone both set, not contacting server");
1186
+ return;
1187
+ }
1188
+ if (this.#serverContacted) {
1189
+ this.logger.info("server has already been contacted at the given url, not doing it again");
1190
+ return;
1191
+ }
1192
+ this.#serverContacted = true;
1193
+ const formatOptions = Intl.DateTimeFormat().resolvedOptions();
1194
+ const request = new Request(
1195
+ this.#reportingURL,
1196
+ {
1197
+ headers: {
1198
+ "Content-Type": "application/json"
1199
+ },
1200
+ method: "POST",
1201
+ body: JSON.stringify({
1202
+ locale: formatOptions.locale,
1203
+ timeZone: formatOptions.timeZone
1204
+ })
1205
+ }
1206
+ );
1207
+ window.fetch(request).then((response) => {
1208
+ if (response.ok) {
1209
+ this.logger.info("Server gave us the OK");
1210
+ } else {
1211
+ console.warn(response);
1212
+ }
1213
+ }).catch((e) => {
1214
+ console.warn(e);
1215
+ });
1216
+ }
1217
+ };
1218
+ var LocaleDetection_default = LocaleDetection;
1219
+
1097
1220
  // src/Message.js
1098
1221
  var Message = class _Message extends BaseCustomElement_default {
1099
1222
  static tagName = "brut-message";
@@ -1212,121 +1335,174 @@
1212
1335
  };
1213
1336
  var Tabs_default = Tabs;
1214
1337
 
1215
- // src/LocaleDetection.js
1216
- var LocaleDetection = class extends BaseCustomElement_default {
1217
- static tagName = "brut-locale-detection";
1338
+ // src/Tracing.js
1339
+ var Tracing = class extends BaseCustomElement_default {
1340
+ static tagName = "brut-tracing";
1218
1341
  static observedAttributes = [
1219
- "locale-from-server",
1220
- "timezone-from-server",
1221
1342
  "url",
1222
- "timeout-before-ping-ms",
1223
1343
  "show-warnings"
1224
1344
  ];
1225
- #localeFromServer = null;
1226
- #timezoneFromServer = null;
1227
- #reportingURL = null;
1228
- #timeoutBeforePing = 1e3;
1229
- #serverContacted = false;
1230
- localeFromServerChangedCallback({ newValue }) {
1231
- this.#localeFromServer = newValue;
1232
- }
1233
- timezoneFromServerChangedCallback({ newValue }) {
1234
- this.#timezoneFromServer = newValue;
1235
- }
1236
- urlChangedCallback({ newValue }) {
1237
- if (this.#serverContacted) {
1238
- this.#serverContacted = false;
1345
+ #url = null;
1346
+ #sent = {};
1347
+ #payload = {};
1348
+ #timeOrigin = null;
1349
+ #supportedTypes = [];
1350
+ #performanceObserver = new PerformanceObserver((entries) => {
1351
+ const navigation = entries.getEntriesByType("navigation")[0];
1352
+ if (navigation && navigation.loadEventEnd != 0 && !this.#payload.navigation) {
1353
+ this.#payload.navigation = this.#parseNavigation(navigation);
1239
1354
  }
1240
- this.#reportingURL = newValue;
1355
+ const largestContentfulPaint = entries.getEntriesByType("largest-contentful-paint");
1356
+ if (largestContentfulPaint.length > 0 && !this.#payload["largest-contentful-paint"]) {
1357
+ this.#payload["largest-contentful-paint"] = this.#parseLargestContentfulPaint(largestContentfulPaint);
1358
+ }
1359
+ const paint = entries.getEntriesByName("first-contentful-paint", "paint")[0];
1360
+ if (paint && !this.#payload.paint) {
1361
+ this.#payload.paint = this.#parseFirstContentfulPaint(paint);
1362
+ }
1363
+ if (this.#supportedTypes.every((type) => this.#payload[type])) {
1364
+ this.#sendSpans();
1365
+ this.#payload = {};
1366
+ }
1367
+ });
1368
+ constructor() {
1369
+ super();
1370
+ this.#timeOrigin = Date.now();
1371
+ this.#supportedTypes = [
1372
+ "navigation",
1373
+ "largest-contentful-paint",
1374
+ "paint"
1375
+ ].filter((type) => {
1376
+ return PerformanceObserver.supportedEntryTypes.includes(type);
1377
+ });
1241
1378
  }
1242
- timeoutBeforePingMsChangedCallback({ newValue }) {
1243
- this.#timeoutBeforePing = newValue;
1379
+ urlChangedCallback({ newValue }) {
1380
+ this.#url = newValue;
1244
1381
  }
1245
1382
  update() {
1246
- if (this.#timeoutBeforePing == 0) {
1247
- this.#pingServerWithLocaleInfo();
1248
- } else {
1249
- setTimeout(this.#pingServerWithLocaleInfo.bind(this), this.#timeoutBeforePing);
1250
- }
1383
+ this.#supportedTypes.forEach((type) => {
1384
+ this.#performanceObserver.observe({ type, buffered: true });
1385
+ });
1251
1386
  }
1252
- #pingServerWithLocaleInfo() {
1253
- if (!this.#reportingURL) {
1254
- this.logger.info("no url= set, so nowhere to report to");
1387
+ #sendSpans() {
1388
+ const headers = this.#initializerHeadersIfCanContinue();
1389
+ if (!headers) {
1255
1390
  return;
1256
1391
  }
1257
- if (this.#localeFromServer && this.#timezoneFromServer) {
1258
- this.logger.info("locale and timezone both set, not contacting server");
1259
- return;
1392
+ const span = this.#payload.navigation;
1393
+ if (this.#payload.paint) {
1394
+ span.events.push({
1395
+ name: this.#payload.paint.name,
1396
+ timestamp: this.#timeOrigin + this.#payload.paint.startTime
1397
+ });
1260
1398
  }
1261
- if (this.#serverContacted) {
1262
- this.logger.info("server has already been contacted at the given url, not doing it again");
1263
- return;
1399
+ if (this.#payload["largest-contentful-paint"]) {
1400
+ this.#payload["largest-contentful-paint"].forEach((event) => {
1401
+ span.events.push({
1402
+ name: event.name,
1403
+ timestamp: this.#timeOrigin + event.startTime,
1404
+ attributes: {
1405
+ "element.tag": event.element?.tagName,
1406
+ "element.class": event.element?.className
1407
+ }
1408
+ });
1409
+ });
1264
1410
  }
1265
- this.#serverContacted = true;
1266
- const formatOptions = Intl.DateTimeFormat().resolvedOptions();
1411
+ this.#sent[this.#url] = true;
1412
+ headers.append("tracestate", `brut=${window.btoa(JSON.stringify(span))}`);
1267
1413
  const request = new Request(
1268
- this.#reportingURL,
1414
+ this.#url,
1269
1415
  {
1270
- headers: {
1271
- "Content-Type": "application/json"
1272
- },
1273
- method: "POST",
1274
- body: JSON.stringify({
1275
- locale: formatOptions.locale,
1276
- timeZone: formatOptions.timeZone
1277
- })
1416
+ headers,
1417
+ method: "GET"
1278
1418
  }
1279
1419
  );
1280
- window.fetch(request).then((response) => {
1281
- if (response.ok) {
1282
- this.logger.info("Server gave us the OK");
1283
- } else {
1284
- console.warn(response);
1420
+ fetch(request).then((response) => {
1421
+ if (!response.ok) {
1422
+ console.warn("Problem sending instrumentation: %s/%s", response.status, response.statusText);
1285
1423
  }
1286
- }).catch((e) => {
1287
- console.warn(e);
1424
+ }).catch((error) => {
1425
+ console.warn("Problem sending instrumentation: %o", error);
1288
1426
  });
1289
1427
  }
1290
- };
1291
- var LocaleDetection_default = LocaleDetection;
1292
-
1293
- // src/Autosubmit.js
1294
- var Autosubmit = class extends BaseCustomElement_default {
1295
- static tagName = "brut-autosubmit";
1296
- static observedAttributes = [
1297
- "show-warnings"
1298
- ];
1299
- #submitForm = (event) => {
1300
- const form2 = this.closest("form");
1301
- if (!form2) {
1302
- this.logger.info("No longer a form containing this element");
1428
+ #parseNavigation(navigation) {
1429
+ const documentFetch = {
1430
+ name: "browser.documentFetch",
1431
+ start_timestamp: navigation.fetchStart + this.#timeOrigin,
1432
+ end_timestamp: navigation.responseEnd + this.#timeOrigin,
1433
+ attributes: {
1434
+ "http.url": navigation.name
1435
+ }
1436
+ };
1437
+ const events = [
1438
+ "fetchStart",
1439
+ "unloadEventStart",
1440
+ "unloadEventEnd",
1441
+ "domInteractive",
1442
+ "domInteractive",
1443
+ "domContentLoadedEventStart",
1444
+ "domContentLoadedEventEnd",
1445
+ "domComplete",
1446
+ "loadEventStart",
1447
+ "loadEventEnd"
1448
+ ];
1449
+ return {
1450
+ name: "browser.documentLoad",
1451
+ start_timestamp: navigation.fetchStart + this.#timeOrigin,
1452
+ end_timestamp: navigation.loadEventEnd + this.#timeOrigin,
1453
+ attributes: {
1454
+ "http.url": navigation.name,
1455
+ "http.user_agent": window.navigator.userAgent
1456
+ },
1457
+ events: events.map((eventName) => {
1458
+ return {
1459
+ name: eventName,
1460
+ timestamp: this.#timeOrigin + navigation[eventName]
1461
+ };
1462
+ }),
1463
+ spans: [
1464
+ documentFetch
1465
+ ]
1466
+ };
1467
+ }
1468
+ #parseFirstContentfulPaint(paint) {
1469
+ return {
1470
+ name: "browser.first-contentful-paint",
1471
+ startTime: paint.startTime
1472
+ };
1473
+ }
1474
+ #parseLargestContentfulPaint(largestContentfulPaint) {
1475
+ return largestContentfulPaint.map((entry) => {
1476
+ return {
1477
+ name: "browser.largest-contentful-paint",
1478
+ startTime: entry.startTime,
1479
+ element: entry.element
1480
+ };
1481
+ });
1482
+ }
1483
+ #initializerHeadersIfCanContinue() {
1484
+ if (!this.#url) {
1485
+ this.logger.info("No url set, no traces will be reported");
1303
1486
  return;
1304
1487
  }
1305
- if (event.target.form != form2) {
1306
- this.logger.info("Event target %o's form is not the form that contains this element", event.target);
1488
+ const $traceparent = document.querySelector("meta[name='traceparent']");
1489
+ if (!$traceparent) {
1490
+ this.logger.info("No <meta name='traceparent' ...> in the document, no traces can be reported");
1307
1491
  return;
1308
1492
  }
1309
- form2.requestSubmit();
1310
- };
1311
- update() {
1312
- const form2 = this.closest("form");
1313
- if (!form2) {
1314
- this.logger.info("No form containing this element - nothing to autosubmit");
1493
+ if (this.#sent[this.#url]) {
1494
+ this.logger.info("Already sent to %s", this.#url);
1315
1495
  return;
1316
1496
  }
1317
- const inputs = Array.from(this.querySelectorAll("input, textarea, select")).filter((element) => {
1318
- return element.form == form2;
1319
- });
1320
- if (inputs.length == 0) {
1321
- this.logger.info("No input, textarea, or select inside this element belongs to the form containing this element");
1497
+ const traceparent = $traceparent.getAttribute("content");
1498
+ if (!traceparent) {
1499
+ this.logger.info("%o had no value for the content attribute, no traces can be reported", $traceparent);
1322
1500
  return;
1323
1501
  }
1324
- inputs.forEach((input) => {
1325
- input.addEventListener("change", this.#submitForm);
1326
- });
1502
+ return new Headers({ traceparent });
1327
1503
  }
1328
1504
  };
1329
- var Autosubmit_default = Autosubmit;
1505
+ var Tracing_default = Tracing;
1330
1506
 
1331
1507
  // src/index.js
1332
1508
  var BrutCustomElements = class {
@@ -1353,7 +1529,8 @@
1353
1529
  ConstraintViolationMessage_default,
1354
1530
  Tabs_default,
1355
1531
  LocaleDetection_default,
1356
- Autosubmit_default
1532
+ Autosubmit_default,
1533
+ Tracing_default
1357
1534
  );
1358
1535
 
1359
1536
  // src/appForTestingOnly.js