brut-js 0.0.10 → 0.0.20

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.10",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "keywords": [ "WebComponents", "Custom Elements" ],
6
6
  "bugs": {
@@ -11,8 +11,8 @@ describe("<brut-cv>", () => {
11
11
  })
12
12
 
13
13
  withHTML(`
14
- <brut-i18n-translation key="problem" value="%{field} has a problem"></brut-i18n-translation>
15
- <brut-i18n-translation key="general.cv.this_field" value="THAT FIELD"></brut-i18n-translation>
14
+ <brut-i18n-translation key="problem" value="%{field} has a problem"></brut-i18n-translation>
15
+ <brut-i18n-translation key="cv.this_field" value="THAT FIELD"></brut-i18n-translation>
16
16
 
17
17
  <brut-cv input-name="some-field" key="problem"></brut-cv>
18
18
  `).test("Inserts using the this_field key as the placeholder", ({document,assert}) => {
@@ -21,9 +21,9 @@ describe("<brut-cv>", () => {
21
21
  })
22
22
 
23
23
  withHTML(`
24
- <brut-i18n-translation key="problem" value="%{field} has a problem"></brut-i18n-translation>
25
- <brut-i18n-translation key="general.cv.this_field" value="THAT FIELD"></brut-i18n-translation>
26
- <brut-i18n-translation key="general.cv.fe.fieldNames.some-field" value="Some Field"></brut-i18n-translation>
24
+ <brut-i18n-translation key="problem" value="%{field} has a problem"></brut-i18n-translation>
25
+ <brut-i18n-translation key="cv.this_field" value="THAT FIELD"></brut-i18n-translation>
26
+ <brut-i18n-translation key="cv.fe.fieldNames.some-field" value="Some Field"></brut-i18n-translation>
27
27
 
28
28
  <brut-cv input-name="some-field" key="problem"></brut-cv>
29
29
  `).test("Inserts using the this_field key as the placeholder", ({document,assert}) => {
@@ -2,8 +2,8 @@ import { withHTML } from "./SpecHelper.js"
2
2
 
3
3
  describe("<brut-cv-messages>", () => {
4
4
  withHTML(`
5
- <brut-i18n-translation key="general.cv.fe.patternMismatch" value="%{field} does not match the pattern"></brut-i18n-translation>
6
- <brut-i18n-translation key="general.cv.fe.rangeOverflow" value="%{field} is above the range"></brut-i18n-translation>
5
+ <brut-i18n-translation key="cv.fe.patternMismatch" value="%{field} does not match the pattern"></brut-i18n-translation>
6
+ <brut-i18n-translation key="cv.fe.rangeOverflow" value="%{field} is above the range"></brut-i18n-translation>
7
7
 
8
8
  <brut-cv-messages input-name="some-field"></brut-cv-messages>
9
9
  `).test("Inserts constraint violation messages based on validity state", ({document,assert}) => {
@@ -51,9 +51,9 @@ describe("<brut-form>", () => {
51
51
  assert(gotInvalid)
52
52
  assert.equal(brutForm.getAttribute("submitted-invalid"),"")
53
53
 
54
- let error = textFieldLabel.querySelector("brut-cv[key='general.cv.fe.valueMissing']")
54
+ let error = textFieldLabel.querySelector("brut-cv[key='cv.fe.valueMissing']")
55
55
  assert(error)
56
- error = numberFieldLabel.querySelector("brut-cv[key='general.cv.fe.valueMissing']")
56
+ error = numberFieldLabel.querySelector("brut-cv[key='cv.fe.valueMissing']")
57
57
  assert(error)
58
58
 
59
59
  const textField = textFieldLabel.querySelector("input")
@@ -71,9 +71,9 @@ describe("<brut-form>", () => {
71
71
  assert(gotInvalid)
72
72
  assert.equal(brutForm.getAttribute("submitted-invalid"),"")
73
73
 
74
- error = textFieldLabel.querySelector("brut-cv[key='general.cv.fe.valueMissing']")
74
+ error = textFieldLabel.querySelector("brut-cv[key='cv.fe.valueMissing']")
75
75
  assert(!error)
76
- error = numberFieldLabel.querySelector("brut-cv[key='general.cv.fe.valueMissing']")
76
+ error = numberFieldLabel.querySelector("brut-cv[key='cv.fe.valueMissing']")
77
77
  assert(error)
78
78
 
79
79
  const numberField = numberFieldLabel.querySelector("input")
@@ -174,7 +174,7 @@ describe("<brut-form>", () => {
174
174
  assert(gotInvalid)
175
175
  assert.equal(brutForm.getAttribute("submitted-invalid"),"")
176
176
 
177
- let error = brutForm.querySelector("brut-cv[input-name='text'][key='general.cv.fe.valueMissing']")
177
+ let error = brutForm.querySelector("brut-cv[input-name='text'][key='cv.fe.valueMissing']")
178
178
  assert(error)
179
179
 
180
180
  })
@@ -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.
@@ -395,7 +395,7 @@
395
395
  * @param {...String} keyPath - parts of the path of the key after the namespace that Brut manages.
396
396
  */
397
397
  static i18nKey(...keyPath) {
398
- const path = ["general", "cv"];
398
+ const path = ["cv"];
399
399
  return path.concat(keyPath).join(".");
400
400
  }
401
401
  #key = null;
@@ -455,7 +455,7 @@
455
455
  * This should be called as part of a Form validation event to provide a customized UX for
456
456
  * the error messages, beyond what the browser would do by default. The keys used are the same
457
457
  * as the attributes of a `ValidityState`, so for example, a range underflow would mean that `validity.rangeUnderflow` would return
458
- * true. Thus, a `<brut-cv>` would be created with `key="general.cv.fe.rangeUnderflow"`.
458
+ * true. Thus, a `<brut-cv>` would be created with `key="cv.fe.rangeUnderflow"`.
459
459
  *
460
460
  * The `cv.fe` is hard-coded to be consistent with Brut's server-side translation management.
461
461
  *
@@ -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";
@@ -1101,6 +1139,84 @@
1101
1139
  };
1102
1140
  var Form_default = Form;
1103
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
+
1104
1220
  // src/Message.js
1105
1221
  var Message = class _Message extends BaseCustomElement_default {
1106
1222
  static tagName = "brut-message";
@@ -1219,121 +1335,174 @@
1219
1335
  };
1220
1336
  var Tabs_default = Tabs;
1221
1337
 
1222
- // src/LocaleDetection.js
1223
- var LocaleDetection = class extends BaseCustomElement_default {
1224
- static tagName = "brut-locale-detection";
1338
+ // src/Tracing.js
1339
+ var Tracing = class extends BaseCustomElement_default {
1340
+ static tagName = "brut-tracing";
1225
1341
  static observedAttributes = [
1226
- "locale-from-server",
1227
- "timezone-from-server",
1228
1342
  "url",
1229
- "timeout-before-ping-ms",
1230
1343
  "show-warnings"
1231
1344
  ];
1232
- #localeFromServer = null;
1233
- #timezoneFromServer = null;
1234
- #reportingURL = null;
1235
- #timeoutBeforePing = 1e3;
1236
- #serverContacted = false;
1237
- localeFromServerChangedCallback({ newValue }) {
1238
- this.#localeFromServer = newValue;
1239
- }
1240
- timezoneFromServerChangedCallback({ newValue }) {
1241
- this.#timezoneFromServer = newValue;
1242
- }
1243
- urlChangedCallback({ newValue }) {
1244
- if (this.#serverContacted) {
1245
- 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);
1246
1354
  }
1247
- 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
+ });
1248
1378
  }
1249
- timeoutBeforePingMsChangedCallback({ newValue }) {
1250
- this.#timeoutBeforePing = newValue;
1379
+ urlChangedCallback({ newValue }) {
1380
+ this.#url = newValue;
1251
1381
  }
1252
1382
  update() {
1253
- if (this.#timeoutBeforePing == 0) {
1254
- this.#pingServerWithLocaleInfo();
1255
- } else {
1256
- setTimeout(this.#pingServerWithLocaleInfo.bind(this), this.#timeoutBeforePing);
1257
- }
1383
+ this.#supportedTypes.forEach((type) => {
1384
+ this.#performanceObserver.observe({ type, buffered: true });
1385
+ });
1258
1386
  }
1259
- #pingServerWithLocaleInfo() {
1260
- if (!this.#reportingURL) {
1261
- this.logger.info("no url= set, so nowhere to report to");
1387
+ #sendSpans() {
1388
+ const headers = this.#initializerHeadersIfCanContinue();
1389
+ if (!headers) {
1262
1390
  return;
1263
1391
  }
1264
- if (this.#localeFromServer && this.#timezoneFromServer) {
1265
- this.logger.info("locale and timezone both set, not contacting server");
1266
- 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
+ });
1267
1398
  }
1268
- if (this.#serverContacted) {
1269
- this.logger.info("server has already been contacted at the given url, not doing it again");
1270
- 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
+ });
1271
1410
  }
1272
- this.#serverContacted = true;
1273
- const formatOptions = Intl.DateTimeFormat().resolvedOptions();
1411
+ this.#sent[this.#url] = true;
1412
+ headers.append("tracestate", `brut=${window.btoa(JSON.stringify(span))}`);
1274
1413
  const request = new Request(
1275
- this.#reportingURL,
1414
+ this.#url,
1276
1415
  {
1277
- headers: {
1278
- "Content-Type": "application/json"
1279
- },
1280
- method: "POST",
1281
- body: JSON.stringify({
1282
- locale: formatOptions.locale,
1283
- timeZone: formatOptions.timeZone
1284
- })
1416
+ headers,
1417
+ method: "GET"
1285
1418
  }
1286
1419
  );
1287
- window.fetch(request).then((response) => {
1288
- if (response.ok) {
1289
- this.logger.info("Server gave us the OK");
1290
- } else {
1291
- console.warn(response);
1420
+ fetch(request).then((response) => {
1421
+ if (!response.ok) {
1422
+ console.warn("Problem sending instrumentation: %s/%s", response.status, response.statusText);
1292
1423
  }
1293
- }).catch((e) => {
1294
- console.warn(e);
1424
+ }).catch((error) => {
1425
+ console.warn("Problem sending instrumentation: %o", error);
1295
1426
  });
1296
1427
  }
1297
- };
1298
- var LocaleDetection_default = LocaleDetection;
1299
-
1300
- // src/Autosubmit.js
1301
- var Autosubmit = class extends BaseCustomElement_default {
1302
- static tagName = "brut-autosubmit";
1303
- static observedAttributes = [
1304
- "show-warnings"
1305
- ];
1306
- #submitForm = (event) => {
1307
- const form2 = this.closest("form");
1308
- if (!form2) {
1309
- 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");
1310
1486
  return;
1311
1487
  }
1312
- if (event.target.form != form2) {
1313
- 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");
1314
1491
  return;
1315
1492
  }
1316
- form2.requestSubmit();
1317
- };
1318
- update() {
1319
- const form2 = this.closest("form");
1320
- if (!form2) {
1321
- 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);
1322
1495
  return;
1323
1496
  }
1324
- const inputs = Array.from(this.querySelectorAll("input, textarea, select")).filter((element) => {
1325
- return element.form == form2;
1326
- });
1327
- if (inputs.length == 0) {
1328
- 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);
1329
1500
  return;
1330
1501
  }
1331
- inputs.forEach((input) => {
1332
- input.addEventListener("change", this.#submitForm);
1333
- });
1502
+ return new Headers({ traceparent });
1334
1503
  }
1335
1504
  };
1336
- var Autosubmit_default = Autosubmit;
1505
+ var Tracing_default = Tracing;
1337
1506
 
1338
1507
  // src/index.js
1339
1508
  var BrutCustomElements = class {
@@ -1360,7 +1529,8 @@
1360
1529
  ConstraintViolationMessage_default,
1361
1530
  Tabs_default,
1362
1531
  LocaleDetection_default,
1363
- Autosubmit_default
1532
+ Autosubmit_default,
1533
+ Tracing_default
1364
1534
  );
1365
1535
 
1366
1536
  // src/appForTestingOnly.js