@brightspace-ui/core 3.33.0 → 3.34.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -41,6 +41,7 @@ class TestTable extends RtlMixin(DemoPassthroughMixin(TableWrapper, 'd2l-table-w
41
41
  static get properties() {
42
42
  return {
43
43
  paging: { type: Boolean, reflect: true },
44
+ multiLine: { type: Boolean, attribute: 'multi-line' },
44
45
  showButtons: { type: Boolean, attribute: 'show-buttons' },
45
46
  stickyControls: { attribute: 'sticky-controls', type: Boolean, reflect: true },
46
47
  visibleBackground: { attribute: 'visible-background', type: Boolean, reflect: true },
@@ -72,6 +73,7 @@ class TestTable extends RtlMixin(DemoPassthroughMixin(TableWrapper, 'd2l-table-w
72
73
  constructor() {
73
74
  super();
74
75
 
76
+ this.multiLine = false;
75
77
  this.paging = false;
76
78
  this.showButtons = false;
77
79
  this.stickyControls = false;
@@ -99,11 +101,25 @@ class TestTable extends RtlMixin(DemoPassthroughMixin(TableWrapper, 'd2l-table-w
99
101
 
100
102
  <table class="d2l-table">
101
103
  <thead>
102
- <tr>
103
- <th scope="col" sticky><d2l-selection-select-all></d2l-selection-select-all></th>
104
- ${this._renderDoubleSortButton('Location')}
105
- ${columns.map(columnHeading => this._renderSortButton(columnHeading))}
106
- </tr>
104
+ ${this.multiLine ? html`
105
+ <tr>
106
+ <th scope="col" sticky><d2l-selection-select-all></d2l-selection-select-all></th>
107
+ ${this._renderDoubleSortButton('Location')}
108
+ <th scope="col" colspan="${columns.length}" sticky>
109
+ Metrics
110
+ </th>
111
+ </tr>
112
+ <tr>
113
+ <th scope="col" sticky></th>
114
+ ${columns.map(columnHeading => this._renderSortButton(columnHeading))}
115
+ </tr>
116
+ ` : html`
117
+ <tr>
118
+ <th scope="col" sticky><d2l-selection-select-all></d2l-selection-select-all></th>
119
+ ${this._renderDoubleSortButton('Location')}
120
+ ${columns.map(columnHeading => this._renderSortButton(columnHeading))}
121
+ </tr>
122
+ `}
107
123
  </thead>
108
124
  <tbody>
109
125
  <tr class="d2l-table-header">
@@ -182,7 +198,7 @@ class TestTable extends RtlMixin(DemoPassthroughMixin(TableWrapper, 'd2l-table-w
182
198
  _renderDoubleSortButton(name) {
183
199
  const noSort = this._sortField?.toLowerCase() !== 'city' && this._sortField?.toLowerCase() !== 'country';
184
200
  return html`
185
- <th scope="col">
201
+ <th rowspan="${this.multiLine ? 2 : 1}" scope="col">
186
202
  <d2l-table-col-sort-button
187
203
  ?desc="${this._sortDesc}"
188
204
  ?nosort="${noSort}">${name}
@@ -80,6 +80,13 @@
80
80
  </template>
81
81
  </d2l-demo-snippet>
82
82
 
83
+ <h2>Multi-line with scroll-wrapper + sticky</h2>
84
+ <d2l-demo-snippet>
85
+ <template>
86
+ <d2l-test-table multi-line sticky-headers sticky-controls sticky-headers-scroll-wrapper style="width: 400px;"></d2l-test-table>
87
+ </template>
88
+ </d2l-demo-snippet>
89
+
83
90
  <div style="margin-bottom: 1000px;"></div>
84
91
  </d2l-demo-page>
85
92
  </body>
@@ -599,16 +599,87 @@ export class TableWrapper extends RtlMixin(PageableMixin(SelectionMixin(LitEleme
599
599
  const body = this._table.querySelector('tbody');
600
600
  if (!head || !body) return;
601
601
 
602
- const firstRowHead = head.rows[0];
603
- const firstRowBody = body.rows[0];
604
- if (!firstRowHead || !firstRowBody || firstRowHead.cells.length !== firstRowBody.cells.length) return;
602
+ const candidateRowHeadCells = [];
605
603
 
606
- for (let i = 0; i < firstRowHead.cells.length; i++) {
607
- const headCell = firstRowHead.cells[i];
608
- const bodyCell = firstRowBody.cells[i];
609
- const bodyStyle = getComputedStyle(bodyCell);
604
+ // Max length of one of our body rows, which we'll try to map to our candidate head cells.
605
+ const maxRowBodyLength = Math.max(...([...body.rows].map(row => row.cells.length)));
606
+
607
+ const headCellsMap = [];
608
+ for (let i = 0; i < head.rows.length; i++) {
609
+ headCellsMap[i] = [];
610
+ }
611
+
612
+ // Build a map of which cells "exist" in each head row so we can pick out
613
+ // a candidate in each column to sync widths with.
614
+ for (let rowIndex = 0; rowIndex < head.rows.length; rowIndex++) {
615
+ const rowMap = headCellsMap[rowIndex];
616
+
617
+ let spanOffset = 0;
618
+ for (let colIndex = 0; colIndex < maxRowBodyLength; colIndex++) {
619
+ if (rowMap[colIndex] === null) {
620
+ spanOffset++;
621
+ continue;
622
+ }
623
+
624
+ const cell = head.rows[rowIndex].cells[colIndex - spanOffset];
625
+ rowMap[colIndex] = cell;
626
+
627
+ if (!cell) continue;
628
+
629
+ const colSpan = cell.colSpan;
630
+ const rowSpan = cell.rowSpan;
631
+
632
+ for (let offset = 1; offset < colSpan; offset++) {
633
+ rowMap[colIndex + offset] = null;
634
+ }
635
+
636
+ for (let offset = 1; offset < rowSpan; offset++) {
637
+ headCellsMap[rowIndex + offset][colIndex] = null;
638
+ }
639
+ }
640
+ }
641
+
642
+ // Pick out a single head cell for each column to sync widths.
643
+ for (let i = 0; i < maxRowBodyLength; i++) {
644
+ let candidateCell;
645
+ for (const rowMap of headCellsMap) {
646
+ const cell = rowMap[i];
647
+
648
+ if (cell && cell.colSpan === 1) {
649
+ candidateCell = cell;
650
+ break;
651
+ }
652
+ }
653
+
654
+ // This does not support heads without at least one single-column
655
+ // spanning cell in each column.
656
+ if (!candidateCell) return;
657
+
658
+ candidateRowHeadCells[i] = candidateCell;
659
+ }
660
+
661
+ // Pick the body row with the most cells (and no colspans) to measure against
662
+ const candidateRowBody = [...body.rows].find(row => {
663
+ return row.cells.length === maxRowBodyLength
664
+ && ![...row.cells].find(cell => cell.colSpan > 1);
665
+ });
666
+
667
+ if (candidateRowHeadCells.length === 0 || !candidateRowBody) return;
668
+
669
+ let candidateRowBodyLength = 0;
670
+ for (const cell of candidateRowBody.cells) {
671
+ candidateRowBodyLength += cell.colSpan;
672
+ }
673
+
674
+ if (candidateRowHeadCells.length !== candidateRowBodyLength) return;
675
+
676
+ for (let i = 0; i < candidateRowHeadCells.length; i++) {
677
+ const headCell = candidateRowHeadCells[i];
610
678
  const headStyle = getComputedStyle(headCell);
611
679
 
680
+ const bodyCell = candidateRowBody.cells[i];
681
+ const bodyStyle = getComputedStyle(bodyCell);
682
+
612
683
  if (headCell.clientWidth > bodyCell.clientWidth) {
613
684
  const headOverallWidth = parseFloat(headStyle.width) + parseFloat(headStyle.paddingLeft) + parseFloat(headStyle.paddingRight);
614
685
  bodyCell.style.minWidth = `${headOverallWidth - parseFloat(bodyStyle.paddingLeft) - parseFloat(bodyStyle.paddingRight)}px`;
@@ -11944,6 +11944,11 @@
11944
11944
  "name": "d2l-test-table",
11945
11945
  "path": "./components/table/demo/table-test.js",
11946
11946
  "attributes": [
11947
+ {
11948
+ "name": "multi-line",
11949
+ "type": "boolean",
11950
+ "default": "false"
11951
+ },
11947
11952
  {
11948
11953
  "name": "paging",
11949
11954
  "type": "boolean",
@@ -12001,6 +12006,12 @@
12001
12006
  }
12002
12007
  ],
12003
12008
  "properties": [
12009
+ {
12010
+ "name": "multiLine",
12011
+ "attribute": "multi-line",
12012
+ "type": "boolean",
12013
+ "default": "false"
12014
+ },
12004
12015
  {
12005
12016
  "name": "paging",
12006
12017
  "attribute": "paging",
@@ -1,408 +1 @@
1
- import { getDocumentLocaleSettings } from '@brightspace-ui/intl/lib/common.js';
2
-
3
- const CacheName = 'd2l-oslo';
4
- const ContentTypeHeader = 'Content-Type';
5
- const ContentTypeJson = 'application/json';
6
- const DebounceTime = 150;
7
- const ETagHeader = 'ETag';
8
- const StateFetching = 2;
9
- const StateIdle = 1;
10
-
11
- const BatchFailedReason = new Error('Failed to fetch batch overrides.');
12
- const SingleFailedReason = new Error('Failed to fetch overrides.');
13
-
14
- const blobs = new Map();
15
-
16
- let cache = undefined;
17
- let cachePromise = undefined;
18
- let documentLocaleSettings = undefined;
19
- let queue = [];
20
- let state = StateIdle;
21
- let timer = 0;
22
- let debug = false;
23
-
24
- async function publish(request, response) {
25
-
26
- if (response.ok) {
27
- const overridesJson = await response.json();
28
- request.resolve(overridesJson);
29
- } else {
30
- request.reject(SingleFailedReason);
31
- }
32
- }
33
-
34
- async function flushQueue() {
35
-
36
- timer = 0;
37
- state = StateFetching;
38
-
39
- if (queue.length <= 0) {
40
- state = StateIdle;
41
- return;
42
- }
43
-
44
- const requests = queue;
45
-
46
- queue = [];
47
-
48
- const resources = requests.map(item => item.resource);
49
- const bodyObject = { resources };
50
- const bodyText = JSON.stringify(bodyObject);
51
-
52
- const res = await fetch(documentLocaleSettings.oslo.batch, {
53
- method: 'POST',
54
- body: bodyText,
55
- headers: { [ContentTypeHeader]: ContentTypeJson }
56
- });
57
-
58
- if (res.ok) {
59
-
60
- const responses = (await res.json()).resources;
61
-
62
- const tasks = [];
63
-
64
- for (let i = 0; i < responses.length; ++i) {
65
-
66
- const response = responses[i];
67
- const request = requests[i];
68
-
69
- const responseValue = new Response(response.body, {
70
- status: response.status,
71
- headers: response.headers
72
- });
73
-
74
- // New version might be available since the page loaded, so make a
75
- // record of it.
76
-
77
- const nextVersion = responseValue.headers.get(ETagHeader);
78
- if (nextVersion) {
79
- setVersion(nextVersion);
80
- }
81
-
82
- const cacheKey = new Request(formatCacheKey(request.resource));
83
- const cacheValue = responseValue.clone();
84
-
85
- if (cache === undefined) {
86
- if (cachePromise === undefined) {
87
- cachePromise = caches.open(CacheName);
88
- }
89
- cache = await cachePromise;
90
- }
91
-
92
- debug && console.log(`[Oslo] cache prime: ${request.resource}`);
93
- tasks.push(cache.put(cacheKey, cacheValue));
94
- tasks.push(publish(request, responseValue));
95
- }
96
-
97
- await Promise.all(tasks);
98
-
99
- } else {
100
-
101
- for (const request of requests) {
102
-
103
- request.reject(BatchFailedReason);
104
- }
105
- }
106
-
107
- if (queue.length > 0) {
108
- setTimeout(flushQueue, 0);
109
- } else {
110
- state = StateIdle;
111
- }
112
- }
113
-
114
- function debounceQueue() {
115
-
116
- if (state !== StateIdle) {
117
- return;
118
- }
119
-
120
- if (timer > 0) {
121
- clearTimeout(timer);
122
- }
123
-
124
- timer = setTimeout(flushQueue, DebounceTime);
125
- }
126
-
127
- async function fetchCollection(url) {
128
-
129
- if (blobs.has(url)) {
130
- return Promise.resolve(blobs.get(url));
131
- }
132
-
133
- const res = await fetch(url, { method: 'GET' });
134
-
135
- if (res.ok) {
136
- const resJson = await res.json();
137
- blobs.set(url, resJson);
138
- return Promise.resolve(resJson);
139
- } else {
140
- return Promise.reject(SingleFailedReason);
141
- }
142
- }
143
-
144
- function fetchWithQueuing(resource) {
145
-
146
- const promise = new Promise((resolve, reject) => {
147
-
148
- queue.push({ resource, resolve, reject });
149
- });
150
-
151
- debounceQueue();
152
-
153
- return promise;
154
- }
155
-
156
- function formatCacheKey(resource) {
157
-
158
- return formatOsloRequest(documentLocaleSettings.oslo.collection, resource);
159
- }
160
-
161
- async function fetchWithCaching(resource) {
162
-
163
- if (cache === undefined) {
164
- if (cachePromise === undefined) {
165
- cachePromise = caches.open(CacheName);
166
- }
167
- cache = await cachePromise;
168
- }
169
-
170
- const cacheKey = new Request(formatCacheKey(resource));
171
- const cacheValue = await cache.match(cacheKey);
172
- if (cacheValue === undefined) {
173
- debug && console.log(`[Oslo] cache miss: ${resource}`);
174
- return fetchWithQueuing(resource);
175
- }
176
-
177
- debug && console.log(`[Oslo] cache hit: ${resource}`);
178
- if (!cacheValue.ok) {
179
- fetchWithQueuing(resource).then(url => URL.revokeObjectURL(url));
180
- throw SingleFailedReason;
181
- }
182
-
183
- // Check if the cache response is stale based on either the document init or
184
- // any requests we've made to the LMS since init. We'll still serve stale
185
- // from cache for this page, but we'll update it in the background for the
186
- // next page.
187
-
188
- // We rely on the ETag header to identify if the cache needs to be updated.
189
- // The LMS will provide it in the format: [release].[build].[langModifiedVersion]
190
- // So for example, an ETag in the 20.20.10 release could be: 20.20.10.24605.55520
191
-
192
- const currentVersion = getVersion();
193
- if (currentVersion) {
194
-
195
- const previousVersion = cacheValue.headers.get(ETagHeader);
196
- if (previousVersion !== currentVersion) {
197
-
198
- debug && console.log(`[Oslo] cache stale: ${resource}`);
199
- fetchWithQueuing(resource).then(url => URL.revokeObjectURL(url));
200
- }
201
- }
202
-
203
- return await cacheValue.json();
204
- }
205
-
206
- function fetchWithPooling(resource) {
207
-
208
- // At most one request per resource.
209
-
210
- let promise = blobs.get(resource);
211
- if (promise === undefined) {
212
- promise = fetchWithCaching(resource);
213
- blobs.set(resource, promise);
214
- }
215
- return promise;
216
- }
217
-
218
- async function shouldUseBatchFetch() {
219
-
220
- if (documentLocaleSettings === undefined) {
221
- documentLocaleSettings = getDocumentLocaleSettings();
222
- }
223
-
224
- if (!documentLocaleSettings.oslo) {
225
- return false;
226
- }
227
-
228
- try {
229
-
230
- // try opening CacheStorage, if the session is in a private browser in firefox this throws an exception
231
- await caches.open(CacheName);
232
-
233
- // Only batch if we can do client-side caching, otherwise it's worse on each
234
- // subsequent page navigation.
235
-
236
- return Boolean(documentLocaleSettings.oslo.batch) && 'CacheStorage' in window;
237
- } catch (err) {
238
- return false;
239
- }
240
-
241
- }
242
-
243
- function shouldUseCollectionFetch() {
244
-
245
- if (documentLocaleSettings === undefined) {
246
- documentLocaleSettings = getDocumentLocaleSettings();
247
- }
248
-
249
- if (!documentLocaleSettings.oslo) {
250
- return false;
251
- }
252
-
253
- return Boolean(documentLocaleSettings.oslo.collection);
254
- }
255
-
256
- function setVersion(version) {
257
-
258
- if (documentLocaleSettings === undefined) {
259
- documentLocaleSettings = getDocumentLocaleSettings();
260
- }
261
-
262
- if (!documentLocaleSettings.oslo) {
263
- return;
264
- }
265
-
266
- documentLocaleSettings.oslo.version = version;
267
- }
268
-
269
- function getVersion() {
270
-
271
- if (documentLocaleSettings === undefined) {
272
- documentLocaleSettings = getDocumentLocaleSettings();
273
- }
274
-
275
- const shouldReturnVersion =
276
- documentLocaleSettings.oslo &&
277
- documentLocaleSettings.oslo.version;
278
- if (!shouldReturnVersion) {
279
- return null;
280
- }
281
-
282
- return documentLocaleSettings.oslo.version;
283
- }
284
-
285
- async function shouldFetchOverrides() {
286
-
287
- const isOsloAvailable =
288
- await shouldUseBatchFetch() ||
289
- shouldUseCollectionFetch();
290
-
291
- return isOsloAvailable;
292
- }
293
-
294
- async function fetchOverride(formatFunc) {
295
-
296
- let resource, res, requestURL;
297
-
298
- if (await shouldUseBatchFetch()) {
299
-
300
- // If batching is available, pool requests together.
301
-
302
- resource = formatFunc();
303
- res = fetchWithPooling(resource);
304
-
305
- } else /* shouldUseCollectionFetch() == true */ {
306
-
307
- // Otherwise, fetch it directly and let the LMS manage the cache.
308
-
309
- resource = formatFunc();
310
- requestURL = formatOsloRequest(documentLocaleSettings.oslo.collection, resource);
311
-
312
- res = fetchCollection(requestURL);
313
-
314
- }
315
- res = res.catch(coalesceToNull);
316
- return res;
317
- }
318
-
319
- function coalesceToNull() {
320
-
321
- return null;
322
- }
323
-
324
- function formatOsloRequest(baseUrl, resource) {
325
- return `${baseUrl}/${resource}`;
326
- }
327
-
328
- export function __clearWindowCache() {
329
-
330
- // Used to reset state for tests.
331
-
332
- blobs.clear();
333
- cache = undefined;
334
- cachePromise = undefined;
335
- }
336
-
337
- export function __enableDebugging() {
338
-
339
- // Used to enable debug logging during development.
340
-
341
- debug = true;
342
- }
343
-
344
- export async function getLocalizeOverrideResources(
345
- langCode,
346
- translations,
347
- formatFunc
348
- ) {
349
- const promises = [];
350
-
351
- promises.push(translations);
352
-
353
- if (await shouldFetchOverrides()) {
354
- const overrides = await fetchOverride(formatFunc);
355
- promises.push(overrides);
356
- }
357
-
358
- const results = await Promise.all(promises);
359
-
360
- return {
361
- language: langCode,
362
- resources: Object.assign({}, ...results)
363
- };
364
- }
365
-
366
- export async function getLocalizeResources(
367
- possibleLanguages,
368
- filterFunc,
369
- formatFunc,
370
- fetchFunc
371
- ) {
372
-
373
- const promises = [];
374
- let supportedLanguage;
375
-
376
- if (await shouldFetchOverrides()) {
377
-
378
- const overrides = await fetchOverride(formatFunc, fetchFunc);
379
- promises.push(overrides);
380
- }
381
-
382
- for (const language of possibleLanguages) {
383
-
384
- if (filterFunc(language)) {
385
-
386
- if (supportedLanguage === undefined) {
387
- supportedLanguage = language;
388
- }
389
-
390
- const translations = fetchFunc(formatFunc(language));
391
- promises.push(translations);
392
-
393
- break;
394
- }
395
- }
396
-
397
- const results = await Promise.all(promises);
398
-
399
- // We're fetching in best -> worst, so we'll assign worst -> best, so the
400
- // best overwrite everything else.
401
-
402
- results.reverse();
403
-
404
- return {
405
- language: supportedLanguage,
406
- resources: Object.assign({}, ...results)
407
- };
408
- }
1
+ export { getLocalizeOverrideResources, getLocalizeResources } from '@brightspace-ui/intl/helpers/getLocalizeResources.js';
@@ -1,4 +1,4 @@
1
- import { disallowedTagsRegex, getLocalizeClass, validateMarkup } from './localize.js';
1
+ import { disallowedTagsRegex, getLocalizeClass, validateMarkup } from '@brightspace-ui/intl/lib/localize.js';
2
2
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
3
3
  import { html } from 'lit';
4
4
  import { ifDefined } from 'lit/directives/if-defined.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.33.0",
3
+ "version": "3.34.0",
4
4
  "description": "A collection of accessible, free, open-source web components for building Brightspace applications",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/BrightspaceUI/core.git",
@@ -67,10 +67,8 @@
67
67
  "dependencies": {
68
68
  "@brightspace-ui/intl": "^3",
69
69
  "@brightspace-ui/lms-context-provider": "^1",
70
- "@formatjs/intl-pluralrules": "^1",
71
70
  "@open-wc/dedupe-mixin": "^1",
72
71
  "ifrau": "^0.41",
73
- "intl-messageformat": "^10",
74
72
  "lit": "^3",
75
73
  "prismjs": "^1",
76
74
  "resize-observer-polyfill": "^1"
@@ -1,255 +0,0 @@
1
- import '@formatjs/intl-pluralrules/dist-es6/polyfill-locales.js';
2
- import { defaultLocale as fallbackLang, getDocumentLocaleSettings, supportedLangpacks } from '@brightspace-ui/intl/lib/common.js';
3
- import { getLocalizeOverrideResources } from '../../helpers/getLocalizeResources.js';
4
- import IntlMessageFormat from 'intl-messageformat';
5
-
6
- export const allowedTags = Object.freeze(['d2l-link', 'd2l-tooltip-help', 'p', 'br', 'b', 'strong', 'i', 'em', 'button']);
7
-
8
- const getDisallowedTagsRegex = allowedTags => {
9
- const validTerminators = '([>\\s/]|$)';
10
- const allowedAfterTriangleBracket = `/?(${allowedTags.join('|')})?${validTerminators}`;
11
- return new RegExp(`<(?!${allowedAfterTriangleBracket})`);
12
- };
13
-
14
- export const disallowedTagsRegex = getDisallowedTagsRegex(allowedTags);
15
- const noAllowedTagsRegex = getDisallowedTagsRegex([]);
16
-
17
- export const getLocalizeClass = (superclass = class {}) => class LocalizeClass extends superclass {
18
-
19
- static documentLocaleSettings = getDocumentLocaleSettings();
20
- static #localizeMarkup;
21
-
22
- static setLocalizeMarkup(localizeMarkup) {
23
- this.#localizeMarkup ??= localizeMarkup;
24
- }
25
-
26
- pristine = true;
27
- #connected = false;
28
- #localeChangeCallback;
29
- #resourcesPromise;
30
- #resolveResourcesLoaded;
31
-
32
- async #localeChangeHandler() {
33
- if (!this._hasResources()) return;
34
-
35
- const resourcesPromise = this.constructor._getAllLocalizeResources(this.config);
36
- this.#resourcesPromise = resourcesPromise;
37
- const localizeResources = (await resourcesPromise).flat(Infinity);
38
- // If the locale changed while resources were being fetched, abort
39
- if (this.#resourcesPromise !== resourcesPromise) return;
40
-
41
- const allResources = {};
42
- const resolvedLocales = new Set();
43
- for (const { language, resources } of localizeResources) {
44
- for (const [key, value] of Object.entries(resources)) {
45
- allResources[key] = { language, value };
46
- resolvedLocales.add(language);
47
- }
48
- }
49
- this.localize.resources = allResources;
50
- this.localize.resolvedLocale = [...resolvedLocales][0];
51
- if (resolvedLocales.size > 1) {
52
- console.warn(`Resolved multiple locales in '${this.constructor.name || this.tagName || ''}': ${[...resolvedLocales].join(', ')}`);
53
- }
54
-
55
- if (this.pristine) {
56
- this.pristine = false;
57
- this.#resolveResourcesLoaded();
58
- }
59
-
60
- this.#onResourcesChange();
61
- }
62
-
63
- #onResourcesChange() {
64
- if (this.#connected) {
65
- this.dispatchEvent?.(new CustomEvent('d2l-localize-resources-change'));
66
- this.config?.onResourcesChange?.();
67
- this.onLocalizeResourcesChange?.();
68
- }
69
- }
70
-
71
- connect() {
72
- this.#localeChangeCallback = () => this.#localeChangeHandler();
73
- LocalizeClass.documentLocaleSettings.addChangeListener(this.#localeChangeCallback);
74
- this.#connected = true;
75
- this.#localeChangeCallback();
76
- }
77
-
78
- disconnect() {
79
- LocalizeClass.documentLocaleSettings.removeChangeListener(this.#localeChangeCallback);
80
- this.#connected = false;
81
- }
82
-
83
- localize(key) {
84
-
85
- const { language, value } = this.localize.resources?.[key] ?? {};
86
- if (!value) return '';
87
-
88
- let params = {};
89
- if (arguments.length > 1 && arguments[1]?.constructor === Object) {
90
- // support for key-value replacements as a single arg
91
- params = arguments[1];
92
- } else {
93
- // legacy support for localize-behavior replacements as many args
94
- for (let i = 1; i < arguments.length; i += 2) {
95
- params[arguments[i]] = arguments[i + 1];
96
- }
97
- }
98
-
99
- const translatedMessage = new IntlMessageFormat(value, language);
100
- let formattedMessage = value;
101
- try {
102
- validateMarkup(formattedMessage, noAllowedTagsRegex);
103
- formattedMessage = translatedMessage.format(params);
104
- } catch (e) {
105
- if (e.name === 'MarkupError') {
106
- e = new Error('localize() does not support rich text. For more information, see: https://github.com/BrightspaceUI/core/blob/main/mixins/localize/'); // eslint-disable-line no-ex-assign
107
- formattedMessage = '';
108
- }
109
- console.error(e);
110
- }
111
-
112
- return formattedMessage;
113
- }
114
-
115
- localizeHTML(key, params = {}) {
116
-
117
- const { language, value } = this.localize.resources?.[key] ?? {};
118
- if (!value) return '';
119
-
120
- const translatedMessage = new IntlMessageFormat(value, language);
121
- let formattedMessage = value;
122
- try {
123
- const unvalidated = translatedMessage.format({
124
- b: chunks => LocalizeClass.#localizeMarkup`<b>${chunks}</b>`,
125
- br: () => LocalizeClass.#localizeMarkup`<br>`,
126
- em: chunks => LocalizeClass.#localizeMarkup`<em>${chunks}</em>`,
127
- i: chunks => LocalizeClass.#localizeMarkup`<i>${chunks}</i>`,
128
- p: chunks => LocalizeClass.#localizeMarkup`<p>${chunks}</p>`,
129
- strong: chunks => LocalizeClass.#localizeMarkup`<strong>${chunks}</strong>`,
130
- ...params
131
- });
132
- validateMarkup(unvalidated);
133
- formattedMessage = unvalidated;
134
- } catch (e) {
135
- if (e.name === 'MarkupError') formattedMessage = '';
136
- console.error(e);
137
- }
138
-
139
- return formattedMessage;
140
- }
141
-
142
- __resourcesLoadedPromise = new Promise(r => this.#resolveResourcesLoaded = r);
143
-
144
- static _generatePossibleLanguages(config) {
145
-
146
- if (config?.useBrowserLangs) return navigator.languages.map(e => e.toLowerCase()).concat('en');
147
-
148
- const { language, fallbackLanguage } = this.documentLocaleSettings;
149
- const langs = [ language, fallbackLanguage ]
150
- .filter(e => e)
151
- .map(e => [ e.toLowerCase(), e.split('-')[0] ])
152
- .flat();
153
-
154
- return Array.from(new Set([ ...langs, 'en-us', 'en' ]));
155
- }
156
-
157
- static _getAllLocalizeResources(config = this.localizeConfig) {
158
- const resourcesLoadedPromises = [];
159
- const superCtor = Object.getPrototypeOf(this);
160
- // get imported terms for each config, head up the chain to get them all
161
- if ('_getAllLocalizeResources' in superCtor) {
162
- const superConfig = Object.prototype.hasOwnProperty.call(superCtor, 'localizeConfig') && superCtor.localizeConfig.importFunc ? superCtor.localizeConfig : config;
163
- resourcesLoadedPromises.push(superCtor._getAllLocalizeResources(superConfig));
164
- }
165
- if (Object.prototype.hasOwnProperty.call(this, 'getLocalizeResources') || Object.prototype.hasOwnProperty.call(this, 'resources')) {
166
- const possibleLanguages = this._generatePossibleLanguages(config);
167
- const resourcesPromise = this.getLocalizeResources(possibleLanguages, config);
168
- resourcesLoadedPromises.push(resourcesPromise);
169
- }
170
- return Promise.all(resourcesLoadedPromises);
171
- }
172
-
173
- static async _getLocalizeResources(langs, { importFunc, osloCollection, useBrowserLangs }) {
174
-
175
- // in dev, don't request unsupported langpacks
176
- if (!importFunc.toString().includes('switch') && !useBrowserLangs) {
177
- langs = langs.filter(lang => supportedLangpacks.includes(lang));
178
- }
179
-
180
- for (const lang of [...langs, fallbackLang]) {
181
-
182
- const resources = await Promise.resolve(importFunc(lang)).catch(() => {});
183
-
184
- if (resources) {
185
-
186
- if (osloCollection) {
187
- return await getLocalizeOverrideResources(
188
- lang,
189
- resources,
190
- () => osloCollection
191
- );
192
- }
193
-
194
- return {
195
- language: lang,
196
- resources
197
- };
198
- }
199
- }
200
- }
201
-
202
- _hasResources() {
203
- return this.constructor.localizeConfig ? Boolean(this.constructor.localizeConfig.importFunc) : this.constructor.getLocalizeResources !== undefined;
204
- }
205
-
206
- };
207
-
208
- export const Localize = class extends getLocalizeClass() {
209
-
210
- static getLocalizeResources() {
211
- return super._getLocalizeResources(...arguments);
212
- }
213
-
214
- constructor(config) {
215
- super();
216
- super.constructor.setLocalizeMarkup(localizeMarkup);
217
- this.config = config;
218
- this.connect();
219
- }
220
-
221
- get ready() {
222
- return this.__resourcesLoadedPromise;
223
- }
224
-
225
- connect() {
226
- super.connect();
227
- return this.ready;
228
- }
229
-
230
- };
231
-
232
- class MarkupError extends Error {
233
- name = this.constructor.name;
234
- }
235
-
236
- export function validateMarkup(content, disallowedTagsRegex) {
237
- if (content) {
238
- if (content.forEach) {
239
- content.forEach(item => validateMarkup(item));
240
- return;
241
- }
242
- if (content._localizeMarkup) return;
243
- if (Object.hasOwn(content, '_$litType$')) throw new MarkupError('Rich-text replacements must use localizeMarkup templates. For more information, see: https://github.com/BrightspaceUI/core/blob/main/mixins/localize/');
244
-
245
- if (content.constructor === String && disallowedTagsRegex?.test(content)) throw new MarkupError(`Rich-text replacements may use only the following allowed elements: ${allowedTags}. For more information, see: https://github.com/BrightspaceUI/core/blob/main/mixins/localize/`);
246
- }
247
- }
248
-
249
- export function localizeMarkup(strings, ...expressions) {
250
- strings.forEach(str => validateMarkup(str, disallowedTagsRegex));
251
- expressions.forEach(exp => validateMarkup(exp, disallowedTagsRegex));
252
- return strings.reduce((acc, i, idx) => {
253
- return acc.push(i, expressions[idx] ?? '') && acc;
254
- }, []).join('');
255
- }