@brightspace-ui/core 3.32.0 → 3.33.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -318,3 +318,65 @@ export const blockquoteStyles = css`
318
318
  }
319
319
  }
320
320
  `;
321
+
322
+ const importUrl = 'https://s.brightspace.com/lib/fonts/0.6.1/assets/';
323
+ const fonts = {
324
+ LatoRegular: 'Lato-400',
325
+ LatoBold: 'Lato-700',
326
+ BCSansLight: 'BCSans-Light',
327
+ BCSansRegular: 'BCSans-Regular',
328
+ BCSansBold: 'BCSans-Bold',
329
+ BCSansLightItalic: 'BCSans-LightItalic',
330
+ BCSansItalic: 'BCSans-Italic',
331
+ BCSansBoldItalic: 'BCSans-BoldItalic'
332
+ };
333
+ export const fontFacesCss = `
334
+ @font-face {
335
+ font-family: 'Lato';
336
+ font-style: normal;
337
+ font-weight: 400;
338
+ src: local('Lato Regular'), local('Lato-Regular'), url(${new URL(`${fonts.LatoRegular}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.LatoRegular}.woff`, importUrl)}) format('woff'), url(${new URL(`${fonts.LatoRegular}.ttf`, importUrl)}) format('truetype');
339
+ }
340
+ @font-face {
341
+ font-family: 'Lato';
342
+ font-style: normal;
343
+ font-weight: 700;
344
+ src: local('Lato Bold'), local('Lato-Bold'), url(${new URL(`${fonts.LatoBold}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.LatoBold}.woff`, importUrl)}) format('woff'), url(${new URL(`${fonts.LatoBold}.ttf`, importUrl)}) format('truetype');
345
+ }
346
+ @font-face {
347
+ font-family: 'BC Sans';
348
+ font-style: normal;
349
+ font-weight: 300;
350
+ src: url(${new URL(`${fonts.BCSansLight}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansLight}.woff`, importUrl)}) format('woff');
351
+ }
352
+ @font-face {
353
+ font-family: 'BC Sans';
354
+ font-style: normal;
355
+ font-weight: 400;
356
+ src: url(${new URL(`${fonts.BCSansRegular}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansRegular}.woff`, importUrl)}) format('woff');
357
+ }
358
+ @font-face {
359
+ font-family: 'BC Sans';
360
+ font-style: normal;
361
+ font-weight: 700;
362
+ src: url(${new URL(`${fonts.BCSansBold}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansBold}.woff`, importUrl)}) format('woff');
363
+ }
364
+ @font-face {
365
+ font-family: 'BC Sans';
366
+ font-style: italic;
367
+ font-weight: 300;
368
+ src: url(${new URL(`${fonts.BCSansLightItalic}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansLightItalic}.woff`, importUrl)}) format('woff');
369
+ }
370
+ @font-face {
371
+ font-family: 'BC Sans';
372
+ font-style: italic;
373
+ font-weight: 400;
374
+ src: url(${new URL(`${fonts.BCSansItalic}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansItalic}.woff`, importUrl)}) format('woff');
375
+ }
376
+ @font-face {
377
+ font-family: 'BC Sans';
378
+ font-style: italic;
379
+ font-weight: 700;
380
+ src: url(${new URL(`${fonts.BCSansBoldItalic}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansBoldItalic}.woff`, importUrl)}) format('woff');
381
+ }
382
+ `;
@@ -1,16 +1,5 @@
1
1
  import '../colors/colors.js';
2
-
3
- export const importUrl = 'https://s.brightspace.com/lib/fonts/0.6.1/assets/';
4
- export const fonts = {
5
- LatoRegular: 'Lato-400',
6
- LatoBold: 'Lato-700',
7
- BCSansLight: 'BCSans-Light',
8
- BCSansRegular: 'BCSans-Regular',
9
- BCSansBold: 'BCSans-Bold',
10
- BCSansLightItalic: 'BCSans-LightItalic',
11
- BCSansItalic: 'BCSans-Italic',
12
- BCSansBoldItalic: 'BCSans-BoldItalic'
13
- };
2
+ import { fontFacesCss } from './styles.js';
14
3
 
15
4
  if (!document.head.querySelector('#d2l-typography-font-face')) {
16
5
  const style = document.createElement('style');
@@ -24,54 +13,7 @@ if (!document.head.querySelector('#d2l-typography-font-face')) {
24
13
  --d2l-document-direction: rtl;
25
14
  }
26
15
 
27
- @font-face {
28
- font-family: 'Lato';
29
- font-style: normal;
30
- font-weight: 400;
31
- src: local('Lato Regular'), local('Lato-Regular'), url(${new URL(`${fonts.LatoRegular}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.LatoRegular}.woff`, importUrl)}) format('woff'), url(${new URL(`${fonts.LatoRegular}.ttf`, importUrl)}) format('truetype');
32
- }
33
- @font-face {
34
- font-family: 'Lato';
35
- font-style: normal;
36
- font-weight: 700;
37
- src: local('Lato Bold'), local('Lato-Bold'), url(${new URL(`${fonts.LatoBold}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.LatoBold}.woff`, importUrl)}) format('woff'), url(${new URL(`${fonts.LatoBold}.ttf`, importUrl)}) format('truetype');
38
- }
39
- @font-face {
40
- font-family: 'BC Sans';
41
- font-style: normal;
42
- font-weight: 300;
43
- src: url(${new URL(`${fonts.BCSansLight}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansLight}.woff`, importUrl)}) format('woff');
44
- }
45
- @font-face {
46
- font-family: 'BC Sans';
47
- font-style: normal;
48
- font-weight: 400;
49
- src: url(${new URL(`${fonts.BCSansRegular}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansRegular}.woff`, importUrl)}) format('woff');
50
- }
51
- @font-face {
52
- font-family: 'BC Sans';
53
- font-style: normal;
54
- font-weight: 700;
55
- src: url(${new URL(`${fonts.BCSansBold}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansBold}.woff`, importUrl)}) format('woff');
56
- }
57
- @font-face {
58
- font-family: 'BC Sans';
59
- font-style: italic;
60
- font-weight: 300;
61
- src: url(${new URL(`${fonts.BCSansLightItalic}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansLightItalic}.woff`, importUrl)}) format('woff');
62
- }
63
- @font-face {
64
- font-family: 'BC Sans';
65
- font-style: italic;
66
- font-weight: 400;
67
- src: url(${new URL(`${fonts.BCSansItalic}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansItalic}.woff`, importUrl)}) format('woff');
68
- }
69
- @font-face {
70
- font-family: 'BC Sans';
71
- font-style: italic;
72
- font-weight: 700;
73
- src: url(${new URL(`${fonts.BCSansBoldItalic}.woff2`, importUrl)}) format('woff2'), url(${new URL(`${fonts.BCSansBoldItalic}.woff`, importUrl)}) format('woff');
74
- }
16
+ ${fontFacesCss}
75
17
 
76
18
  .d2l-typography {
77
19
  color: var(--d2l-color-ferrite);
@@ -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';
@@ -45,7 +45,6 @@ class HtmlBlockMathRenderer {
45
45
 
46
46
  const mathJaxConfig = {
47
47
  deferTypeset: true,
48
- enableMML3Support: context.enableMML3Support,
49
48
  renderLatex: context.renderLatex,
50
49
  outputScale: context.outputScale || 1,
51
50
  window: window
@@ -62,24 +61,6 @@ class HtmlBlockMathRenderer {
62
61
  elm.style.height = '0.5rem';
63
62
  });
64
63
 
65
- if (context.enableMML3Support) {
66
- // There's a bug in the experimental MML3 plugin that causes mi and mo elements containing non-breaking
67
- // spaces to break MathJax's math processing (e.g. <mo>&nbsp;</mo>, <mi>Some&nbsp;Identifier</mi>).
68
- // Unfortunately, WIRIS tends to add a lot of these in chemistry equations, so they break math processing.
69
- //
70
- // In order to address this, we can just remove any non-breaking spaces entirely, replacing them with
71
- // empty strings. MathJax will ignore any empty elements as a result, and while this may mean intended
72
- // whitespace is occasionally removed, it's necessary for MathJax to render anything at all.
73
- //
74
- // NOTE: MathJax evidently has a fix for this in MathJax 4, so we should consider trying to remove this when
75
- // the update comes out of beta and we decide to take it on.
76
- //
77
- // See https://github.com/mathjax/MathJax/issues/3030 for some related discussion.
78
- elem.querySelectorAll('mo, mi').forEach(elm => {
79
- elm.innerHTML = elm.innerHTML.replace(/&nbsp;/g, '');
80
- });
81
- }
82
-
83
64
  // If we're using deferred rendering, we need to create a document structure
84
65
  // within the element so MathJax can appropriately process math.
85
66
  if (!options.noDeferredRendering) elem.innerHTML = `<mjx-doc><mjx-head></mjx-head><mjx-body>${elem.innerHTML}</mjx-body></mjx-doc>`;
@@ -120,13 +101,7 @@ export function loadMathJax(mathJaxConfig) {
120
101
  }
121
102
  },
122
103
  loader: {
123
- load: mathJaxConfig && mathJaxConfig.enableMML3Support
124
- ? [
125
- '[mml]/mml3',
126
- 'ui/menu'
127
- ] : [
128
- 'ui/menu'
129
- ]
104
+ load: ['ui/menu']
130
105
  },
131
106
  startup: {
132
107
  ready: () => {
@@ -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.32.0",
3
+ "version": "3.33.1",
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"
@@ -4,6 +4,5 @@ console.warn('Using mathjax test context, this is intended for demo pages and te
4
4
 
5
5
  addContext('d2l-mathjax', {
6
6
  outputScale: 1.1,
7
- renderLatex: !(window.location.search.indexOf('latex=false') !== -1),
8
- enableMML3Support: true
7
+ renderLatex: !(window.location.search.indexOf('latex=false') !== -1)
9
8
  });
@@ -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
- }