genesys-spark 4.0.0-beta.50 → 4.0.0-beta.52
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/README.md +21 -5
- package/dist/hosts.d.ts +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +109 -16
- package/dist/loading.d.ts +28 -0
- package/jest.config.js +5 -0
- package/package.json +7 -3
- package/rollup.config.js +1 -1
- package/src/hosts.ts +14 -3
- package/src/index.ts +20 -47
- package/src/loading.ts +88 -0
- package/test/loading.spec.ts +146 -0
package/README.md
CHANGED
|
@@ -2,26 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
This package is the default way to use Spark. It provides access to Spark's lazy-loading custom elements via a single shared CDN, as well as utilities for non-rendering UI tasks.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
6
8
|
|
|
7
9
|
Use your package manager of choice to install the package in your project.
|
|
8
10
|
|
|
9
11
|
`npm install genesys-spark`
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
### Localization
|
|
14
|
+
|
|
15
|
+
To ensure components are localized correctly, use the [`lang` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) to set the language of
|
|
16
|
+
your page. Components will respect the language of their closest ancestor with
|
|
17
|
+
a recognized `lang` attribute.
|
|
18
|
+
|
|
19
|
+
### Loading the components
|
|
12
20
|
|
|
13
|
-
When initializing your app/page, call `
|
|
21
|
+
When initializing your app/page, call `registerSparkComponents`, which will inject
|
|
14
22
|
the script and style tags into your page that define the main Spark custom elements:
|
|
15
23
|
|
|
16
24
|
```js
|
|
17
25
|
import { registerSparkComponents } from 'genesys-spark';
|
|
18
26
|
|
|
19
27
|
// It's not _required_ to await component loading, but it can help prevent a flash
|
|
20
|
-
// of unstyled content
|
|
28
|
+
// of unstyled content.
|
|
21
29
|
await registerSparkComponents();
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
### Font Loading
|
|
33
|
+
|
|
34
|
+
While `registerSparkComponents()` will add the required webfonts to your page if
|
|
35
|
+
they are not already present, it is recommended that you link stylesheets to load
|
|
36
|
+
the Urbanist and Noto Sans fonts in your document prior to starting your app. This
|
|
37
|
+
will improve loading performance and prevent a jarring font transition when the
|
|
38
|
+
required fonts load.
|
|
39
|
+
|
|
40
|
+
## Asset loading details
|
|
25
41
|
|
|
26
42
|
By adding a script tag to the document, we ensure that the components are always
|
|
27
43
|
loaded from the same location. This keeps the bundle size of the consuming application
|
package/dist/hosts.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Loads the spark
|
|
2
|
+
* Loads the spark web components, as well as required CSS and fonts from a
|
|
3
|
+
* shared CDN. Performance can be optimized by pre-loading fonts in static HTML.
|
|
4
|
+
*
|
|
3
5
|
* @returns a promise that succeeds if the component script and styles
|
|
4
|
-
* load successfully.
|
|
6
|
+
* load successfully. It is not recommended to wait on this promise or to stop
|
|
7
|
+
* application bootstrap if it rejects. Its primary use should be for logging
|
|
8
|
+
* unexpected failures.
|
|
5
9
|
*/
|
|
6
10
|
export declare function registerSparkComponents(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -23,29 +23,57 @@ var DOMAIN_LIST = [
|
|
|
23
23
|
* Will use the domain of the current window if it matches a Genesys domain.
|
|
24
24
|
*/
|
|
25
25
|
function getAssetsOrigin() {
|
|
26
|
+
var matchedDomain = getRegionDomain();
|
|
27
|
+
return "https://app.".concat(matchedDomain || DEFAULT_DOMAIN);
|
|
28
|
+
}
|
|
29
|
+
function getFontOrigin() {
|
|
30
|
+
return getAssetsOrigin();
|
|
31
|
+
}
|
|
32
|
+
function getRegionDomain() {
|
|
26
33
|
var pageHost = window.location.hostname;
|
|
27
|
-
|
|
34
|
+
return DOMAIN_LIST.find(function (regionDomain) {
|
|
28
35
|
return pageHost.endsWith(regionDomain);
|
|
29
36
|
});
|
|
30
|
-
return "https://app.".concat(matchedDomain || DEFAULT_DOMAIN);
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
/******************************************************************************
|
|
40
|
+
Copyright (c) Microsoft Corporation.
|
|
41
|
+
|
|
42
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
43
|
+
purpose with or without fee is hereby granted.
|
|
44
|
+
|
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
46
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
47
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
48
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
49
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
50
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
51
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
52
|
+
***************************************************************************** */
|
|
53
|
+
/* global Reflect, Promise, SuppressedError, Symbol */
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
var __assign = function() {
|
|
57
|
+
__assign = Object.assign || function __assign(t) {
|
|
58
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
59
|
+
s = arguments[i];
|
|
60
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
|
|
61
|
+
}
|
|
62
|
+
return t;
|
|
63
|
+
};
|
|
64
|
+
return __assign.apply(this, arguments);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
68
|
+
var e = new Error(message);
|
|
69
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
70
|
+
};
|
|
71
|
+
|
|
38
72
|
/**
|
|
39
|
-
*
|
|
40
|
-
* @
|
|
41
|
-
*
|
|
73
|
+
* Add a script tag to the document if it is not already present.
|
|
74
|
+
* @param scriptSrc The src attribute of the script
|
|
75
|
+
* @returns a promise that resolves if the script loads or is already present
|
|
42
76
|
*/
|
|
43
|
-
function registerSparkComponents() {
|
|
44
|
-
return Promise.all([
|
|
45
|
-
checkAndLoadScript(SCRIPT_SRC),
|
|
46
|
-
checkAndLoadStyle(STYLE_HREF)
|
|
47
|
-
]).then();
|
|
48
|
-
}
|
|
49
77
|
function checkAndLoadScript(scriptSrc) {
|
|
50
78
|
var existingTag = document.querySelector("script[src=\"".concat(scriptSrc, "\"]"));
|
|
51
79
|
if (existingTag) {
|
|
@@ -67,6 +95,11 @@ function checkAndLoadScript(scriptSrc) {
|
|
|
67
95
|
return result;
|
|
68
96
|
}
|
|
69
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Add a stylesheet link tag to the document if it is not already present.
|
|
100
|
+
* @param styleHref The href attribute of the stylesheet
|
|
101
|
+
* @returns a promise that resolves if the style loads or is already present
|
|
102
|
+
*/
|
|
70
103
|
function checkAndLoadStyle(styleHref) {
|
|
71
104
|
var existingTag = document.querySelector("link[href=\"".concat(styleHref, "\"]"));
|
|
72
105
|
if (existingTag) {
|
|
@@ -88,6 +121,66 @@ function checkAndLoadStyle(styleHref) {
|
|
|
88
121
|
return result;
|
|
89
122
|
}
|
|
90
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Given an object that maps font-family identifiers to CSS urls e.g:
|
|
126
|
+
* {
|
|
127
|
+
* "Urbanist": "/urbanist.css",
|
|
128
|
+
* "Noto Sans": "/noto-sans.css"
|
|
129
|
+
* }
|
|
130
|
+
* This function checks for loaded fonts with those identifiers. If a font is
|
|
131
|
+
* not found, the corresponding stylesheet is added to the document.
|
|
132
|
+
* @param fonts An object mapping font-family identifiers to CSS file urls
|
|
133
|
+
* @returns A promise that resolves once the script tags has finished loading.
|
|
134
|
+
* It does not fail if the script tags fail to load because we don't want to fail
|
|
135
|
+
* the whole component loading process in that situation.
|
|
136
|
+
*/
|
|
137
|
+
function checkAndLoadFonts(fonts) {
|
|
138
|
+
var fontsToLoad = __assign({}, fonts); //clone our input so we can safely mutate it.
|
|
139
|
+
document.fonts.forEach(function (fontFace) {
|
|
140
|
+
// If the family is defined with quotes in CSS (e.g. `font-family: "Noto Sans"),
|
|
141
|
+
// those quotes may be preserved JS, depending on the browser.
|
|
142
|
+
var normalizedFamily = fontFace.family.replace(/"/g, "");
|
|
143
|
+
if (fontsToLoad[normalizedFamily]) {
|
|
144
|
+
// remove the font from the set to load
|
|
145
|
+
delete fontsToLoad[normalizedFamily];
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return Promise.all(Object.values(fontsToLoad).map(function (href) {
|
|
149
|
+
return checkAndLoadStyle(href)["catch"](function () {
|
|
150
|
+
// Don't fail loading process for fonts, since the components
|
|
151
|
+
// should still be reasonably usable.
|
|
152
|
+
console.info("genesys-spark: couldn't load font style ".concat(href));
|
|
153
|
+
});
|
|
154
|
+
})).then(function () { }); // flatten the promise array
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var ASSET_PREFIX = "/spark-components/build-assets/4.0.0-beta.52-77/genesys-webcomponents/";
|
|
158
|
+
var SCRIPT_PATH = "genesys-webcomponents.esm.js";
|
|
159
|
+
var STYLE_PATH = "genesys-webcomponents.css";
|
|
160
|
+
var assetsOrigin = getAssetsOrigin();
|
|
161
|
+
var SCRIPT_SRC = "".concat(assetsOrigin).concat(ASSET_PREFIX).concat(SCRIPT_PATH);
|
|
162
|
+
var STYLE_HREF = "".concat(assetsOrigin).concat(ASSET_PREFIX).concat(STYLE_PATH);
|
|
163
|
+
var fontOrigin = getFontOrigin();
|
|
164
|
+
var FONTS = {
|
|
165
|
+
"Urbanist": "".concat(fontOrigin, "/webfonts/urbanist.css"),
|
|
166
|
+
"Noto Sans": "".concat(fontOrigin, "/webfonts/noto-sans.css")
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Loads the spark web components, as well as required CSS and fonts from a
|
|
170
|
+
* shared CDN. Performance can be optimized by pre-loading fonts in static HTML.
|
|
171
|
+
*
|
|
172
|
+
* @returns a promise that succeeds if the component script and styles
|
|
173
|
+
* load successfully. It is not recommended to wait on this promise or to stop
|
|
174
|
+
* application bootstrap if it rejects. Its primary use should be for logging
|
|
175
|
+
* unexpected failures.
|
|
176
|
+
*/
|
|
177
|
+
function registerSparkComponents() {
|
|
178
|
+
return Promise.all([
|
|
179
|
+
checkAndLoadScript(SCRIPT_SRC),
|
|
180
|
+
checkAndLoadStyle(STYLE_HREF),
|
|
181
|
+
checkAndLoadFonts(FONTS)
|
|
182
|
+
]).then();
|
|
183
|
+
}
|
|
91
184
|
// TODO: Build out utility functions where components aren't the right solution
|
|
92
185
|
// export function formatDate(...)
|
|
93
186
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add a script tag to the document if it is not already present.
|
|
3
|
+
* @param scriptSrc The src attribute of the script
|
|
4
|
+
* @returns a promise that resolves if the script loads or is already present
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkAndLoadScript(scriptSrc: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Add a stylesheet link tag to the document if it is not already present.
|
|
9
|
+
* @param styleHref The href attribute of the stylesheet
|
|
10
|
+
* @returns a promise that resolves if the style loads or is already present
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkAndLoadStyle(styleHref: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Given an object that maps font-family identifiers to CSS urls e.g:
|
|
15
|
+
* {
|
|
16
|
+
* "Urbanist": "/urbanist.css",
|
|
17
|
+
* "Noto Sans": "/noto-sans.css"
|
|
18
|
+
* }
|
|
19
|
+
* This function checks for loaded fonts with those identifiers. If a font is
|
|
20
|
+
* not found, the corresponding stylesheet is added to the document.
|
|
21
|
+
* @param fonts An object mapping font-family identifiers to CSS file urls
|
|
22
|
+
* @returns A promise that resolves once the script tags has finished loading.
|
|
23
|
+
* It does not fail if the script tags fail to load because we don't want to fail
|
|
24
|
+
* the whole component loading process in that situation.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkAndLoadFonts(fonts: {
|
|
27
|
+
[key: string]: string;
|
|
28
|
+
}): Promise<void>;
|
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genesys-spark",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.52",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "rollup -c",
|
|
11
11
|
"dev": "rollup -c --watch",
|
|
12
|
-
"test": "
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"test.watch": "jest --watch",
|
|
13
14
|
"version-sync": "npm version --no-git-tag-version --allow-same-version"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
@@ -18,6 +19,9 @@
|
|
|
18
19
|
"rollup": "^3.29.4"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
|
-
"@tsconfig/strictest": "^2.0.2"
|
|
22
|
+
"@tsconfig/strictest": "^2.0.2",
|
|
23
|
+
"jest": "^29.7.0",
|
|
24
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
25
|
+
"ts-jest": "^29.1.1"
|
|
22
26
|
}
|
|
23
27
|
}
|
package/rollup.config.js
CHANGED
package/src/hosts.ts
CHANGED
|
@@ -37,10 +37,21 @@ export function getAssetsOrigin(): string {
|
|
|
37
37
|
return "http://localhost:3333";
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const matchedDomain = getRegionDomain();
|
|
41
|
+
return `https://app.${matchedDomain || DEFAULT_DOMAIN}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getFontOrigin(): string {
|
|
45
|
+
if (IS_DEV_MODE == true) {
|
|
46
|
+
// Fonts aren't locally hosted during dev mode
|
|
47
|
+
return "http://app.inindca.com"
|
|
48
|
+
}
|
|
49
|
+
return getAssetsOrigin();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getRegionDomain() {
|
|
40
53
|
const pageHost = window.location.hostname;
|
|
41
|
-
|
|
54
|
+
return DOMAIN_LIST.find(regionDomain =>
|
|
42
55
|
pageHost.endsWith(regionDomain)
|
|
43
56
|
);
|
|
44
|
-
|
|
45
|
-
return `https://app.${matchedDomain || DEFAULT_DOMAIN}`;
|
|
46
57
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,65 +1,38 @@
|
|
|
1
|
-
import { getAssetsOrigin } from "./hosts";
|
|
1
|
+
import { getAssetsOrigin, getFontOrigin } from "./hosts";
|
|
2
|
+
import { checkAndLoadScript, checkAndLoadStyle, checkAndLoadFonts } from "./loading";
|
|
2
3
|
|
|
3
4
|
const ASSET_PREFIX = "__ASSET_PREFIX__";
|
|
4
5
|
const SCRIPT_PATH = "genesys-webcomponents.esm.js";
|
|
5
6
|
const STYLE_PATH = "genesys-webcomponents.css";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
8
|
+
const assetsOrigin = getAssetsOrigin();
|
|
9
|
+
const SCRIPT_SRC = `${assetsOrigin}${ASSET_PREFIX}${SCRIPT_PATH}`;
|
|
10
|
+
const STYLE_HREF = `${assetsOrigin}${ASSET_PREFIX}${STYLE_PATH}`;
|
|
11
|
+
const fontOrigin = getFontOrigin();
|
|
12
|
+
const FONTS = {
|
|
13
|
+
"Urbanist": `${fontOrigin}/webfonts/urbanist.css`,
|
|
14
|
+
"Noto Sans": `${fontOrigin}/webfonts/noto-sans.css`
|
|
15
|
+
}
|
|
9
16
|
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
|
-
* Loads the spark
|
|
19
|
+
* Loads the spark web components, as well as required CSS and fonts from a
|
|
20
|
+
* shared CDN. Performance can be optimized by pre-loading fonts in static HTML.
|
|
21
|
+
*
|
|
13
22
|
* @returns a promise that succeeds if the component script and styles
|
|
14
|
-
* load successfully.
|
|
23
|
+
* load successfully. It is not recommended to wait on this promise or to stop
|
|
24
|
+
* application bootstrap if it rejects. Its primary use should be for logging
|
|
25
|
+
* unexpected failures.
|
|
15
26
|
*/
|
|
16
27
|
export function registerSparkComponents() : Promise<void> {
|
|
17
28
|
return Promise.all([
|
|
18
29
|
checkAndLoadScript(SCRIPT_SRC),
|
|
19
|
-
checkAndLoadStyle(STYLE_HREF)
|
|
30
|
+
checkAndLoadStyle(STYLE_HREF),
|
|
31
|
+
checkAndLoadFonts(FONTS)
|
|
20
32
|
]).then()
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
function checkAndLoadScript(scriptSrc: string): Promise<void> {
|
|
24
|
-
const existingTag = document.querySelector(`script[src="${scriptSrc}"]`);
|
|
25
|
-
if (existingTag) {
|
|
26
|
-
return Promise.resolve();
|
|
27
|
-
} else {
|
|
28
|
-
const scriptTag = document.createElement('script');
|
|
29
|
-
scriptTag.setAttribute("type", "module");
|
|
30
|
-
scriptTag.setAttribute("src", scriptSrc);
|
|
31
|
-
const result = new Promise<void>((resolve, reject) => {
|
|
32
|
-
scriptTag.addEventListener('load', () => {
|
|
33
|
-
resolve();
|
|
34
|
-
})
|
|
35
|
-
scriptTag.addEventListener('error', () => {
|
|
36
|
-
reject(`Spark script failed to load: ${scriptSrc}`);
|
|
37
|
-
})
|
|
38
|
-
});
|
|
39
|
-
document.head.appendChild(scriptTag);
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
35
|
|
|
44
|
-
function checkAndLoadStyle(styleHref: string): Promise<void> {
|
|
45
|
-
const existingTag = document.querySelector(`link[href="${styleHref}"]`);
|
|
46
|
-
if (existingTag) {
|
|
47
|
-
return Promise.resolve();
|
|
48
|
-
} else {
|
|
49
|
-
const styleTag = document.createElement('link');
|
|
50
|
-
styleTag.setAttribute("href", styleHref);
|
|
51
|
-
styleTag.setAttribute("rel", "stylesheet");
|
|
52
|
-
const result = new Promise<void>((resolve, reject) => {
|
|
53
|
-
styleTag.addEventListener('load', () => {
|
|
54
|
-
resolve();
|
|
55
|
-
})
|
|
56
|
-
styleTag.addEventListener('error', () => {
|
|
57
|
-
reject(`Spark styles failed to load: ${styleHref}`);
|
|
58
|
-
})
|
|
59
|
-
});
|
|
60
|
-
document.head.appendChild(styleTag);
|
|
61
|
-
return result;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
36
|
// TODO: Build out utility functions where components aren't the right solution
|
|
65
|
-
// export function formatDate(...)
|
|
37
|
+
// export function formatDate(...)
|
|
38
|
+
|
package/src/loading.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add a script tag to the document if it is not already present.
|
|
3
|
+
* @param scriptSrc The src attribute of the script
|
|
4
|
+
* @returns a promise that resolves if the script loads or is already present
|
|
5
|
+
*/
|
|
6
|
+
export function checkAndLoadScript(scriptSrc: string): Promise<void> {
|
|
7
|
+
const existingTag = document.querySelector(`script[src="${scriptSrc}"]`);
|
|
8
|
+
if (existingTag) {
|
|
9
|
+
return Promise.resolve();
|
|
10
|
+
} else {
|
|
11
|
+
const scriptTag = document.createElement('script');
|
|
12
|
+
scriptTag.setAttribute("type", "module");
|
|
13
|
+
scriptTag.setAttribute("src", scriptSrc);
|
|
14
|
+
const result = new Promise<void>((resolve, reject) => {
|
|
15
|
+
scriptTag.addEventListener('load', () => {
|
|
16
|
+
resolve();
|
|
17
|
+
})
|
|
18
|
+
scriptTag.addEventListener('error', () => {
|
|
19
|
+
reject(`Spark script failed to load: ${scriptSrc}`);
|
|
20
|
+
})
|
|
21
|
+
});
|
|
22
|
+
document.head.appendChild(scriptTag);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add a stylesheet link tag to the document if it is not already present.
|
|
29
|
+
* @param styleHref The href attribute of the stylesheet
|
|
30
|
+
* @returns a promise that resolves if the style loads or is already present
|
|
31
|
+
*/
|
|
32
|
+
export function checkAndLoadStyle(styleHref: string): Promise<void> {
|
|
33
|
+
const existingTag = document.querySelector(`link[href="${styleHref}"]`);
|
|
34
|
+
if (existingTag) {
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
} else {
|
|
37
|
+
const styleTag = document.createElement('link');
|
|
38
|
+
styleTag.setAttribute("href", styleHref);
|
|
39
|
+
styleTag.setAttribute("rel", "stylesheet");
|
|
40
|
+
const result = new Promise<void>((resolve, reject) => {
|
|
41
|
+
styleTag.addEventListener('load', () => {
|
|
42
|
+
resolve();
|
|
43
|
+
})
|
|
44
|
+
styleTag.addEventListener('error', () => {
|
|
45
|
+
reject(`Spark styles failed to load: ${styleHref}`);
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
document.head.appendChild(styleTag);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Given an object that maps font-family identifiers to CSS urls e.g:
|
|
55
|
+
* {
|
|
56
|
+
* "Urbanist": "/urbanist.css",
|
|
57
|
+
* "Noto Sans": "/noto-sans.css"
|
|
58
|
+
* }
|
|
59
|
+
* This function checks for loaded fonts with those identifiers. If a font is
|
|
60
|
+
* not found, the corresponding stylesheet is added to the document.
|
|
61
|
+
* @param fonts An object mapping font-family identifiers to CSS file urls
|
|
62
|
+
* @returns A promise that resolves once the script tags has finished loading.
|
|
63
|
+
* It does not fail if the script tags fail to load because we don't want to fail
|
|
64
|
+
* the whole component loading process in that situation.
|
|
65
|
+
*/
|
|
66
|
+
export function checkAndLoadFonts(fonts: {[key: string]: string}): Promise<void> {
|
|
67
|
+
const fontsToLoad = {...fonts}; //clone our input so we can safely mutate it.
|
|
68
|
+
|
|
69
|
+
document.fonts.forEach((fontFace) => {
|
|
70
|
+
// If the family is defined with quotes in CSS (e.g. `font-family: "Noto Sans"),
|
|
71
|
+
// those quotes may be preserved JS, depending on the browser.
|
|
72
|
+
const normalizedFamily = fontFace.family.replace(/"/g, "");
|
|
73
|
+
if(fontsToLoad[normalizedFamily]) {
|
|
74
|
+
// remove the font from the set to load
|
|
75
|
+
delete fontsToLoad[normalizedFamily];
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return Promise.all(
|
|
80
|
+
Object.values(fontsToLoad).map((href) => {
|
|
81
|
+
return checkAndLoadStyle(href).catch(() => {
|
|
82
|
+
// Don't fail loading process for fonts, since the components
|
|
83
|
+
// should still be reasonably usable.
|
|
84
|
+
console.info(`genesys-spark: couldn't load font style ${href}`)
|
|
85
|
+
});
|
|
86
|
+
})
|
|
87
|
+
).then(() => {}) // flatten the promise array
|
|
88
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { checkAndLoadFonts, checkAndLoadScript, checkAndLoadStyle } from '../src/loading';
|
|
2
|
+
|
|
3
|
+
const SCRIPT_URL = 'https://localhost/script.js';
|
|
4
|
+
const STYLE_URL = 'https://localhost/style.css';
|
|
5
|
+
const FONTS = {
|
|
6
|
+
"Font1": `font1.css`,
|
|
7
|
+
"Font2": `font2.css`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('The loading module', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
document.head.innerHTML = '';
|
|
13
|
+
document.body.innerHTML = '';
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('checkAndLoadScript', () => {
|
|
17
|
+
test('will create a new script tag and wait for it to load', async () => {
|
|
18
|
+
const promise = checkAndLoadScript(SCRIPT_URL);
|
|
19
|
+
const tag = document.querySelector(`script[src="${SCRIPT_URL}"]`);
|
|
20
|
+
|
|
21
|
+
expect(tag).not.toBeNull(); // The tag was created
|
|
22
|
+
tag?.dispatchEvent(new Event('load')); // The promise will resolve
|
|
23
|
+
|
|
24
|
+
expect(promise).resolves.toBe(undefined);
|
|
25
|
+
});
|
|
26
|
+
test('will fail if the script fails to load', async () => {
|
|
27
|
+
const promise = checkAndLoadScript(SCRIPT_URL);
|
|
28
|
+
const tag = document.querySelector(`script[src="${SCRIPT_URL}"]`);
|
|
29
|
+
|
|
30
|
+
expect(tag).not.toBeNull(); // The tag was created
|
|
31
|
+
tag?.dispatchEvent(new Event('error')); // The promise will reject
|
|
32
|
+
|
|
33
|
+
expect(promise).rejects.toContain(SCRIPT_URL);
|
|
34
|
+
});
|
|
35
|
+
test('will not create a tag if one already exists', async () => {
|
|
36
|
+
const script = document.createElement('script');
|
|
37
|
+
script.setAttribute('src', SCRIPT_URL);
|
|
38
|
+
document.body.appendChild(script);
|
|
39
|
+
|
|
40
|
+
await checkAndLoadScript(SCRIPT_URL); // Should resolve immediately
|
|
41
|
+
const tags = document.querySelectorAll(`script[src="${SCRIPT_URL}"]`);
|
|
42
|
+
expect(tags.length).toBe(1); // A second tag was not created
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('checkAndLoadStyle', () => {
|
|
46
|
+
test('will create a new link tag and wait for it to load', async () => {
|
|
47
|
+
const promise = checkAndLoadStyle(STYLE_URL);
|
|
48
|
+
const tag = document.querySelector(
|
|
49
|
+
`link[href="${STYLE_URL}"][rel="stylesheet"]`
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(tag).not.toBeNull(); // The tag was created
|
|
53
|
+
tag?.dispatchEvent(new Event('load')); // The promise will resolve
|
|
54
|
+
|
|
55
|
+
expect(promise).resolves.toBe(undefined);
|
|
56
|
+
});
|
|
57
|
+
test('will fail if the style fails to load', async () => {
|
|
58
|
+
const promise = checkAndLoadStyle(STYLE_URL);
|
|
59
|
+
const tag = document.querySelector(
|
|
60
|
+
`link[href="${STYLE_URL}"][rel="stylesheet"]`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(tag).not.toBeNull(); // The tag was created
|
|
64
|
+
tag?.dispatchEvent(new Event('error')); // The promise will reject
|
|
65
|
+
|
|
66
|
+
expect(promise).rejects.toContain(STYLE_URL);
|
|
67
|
+
});
|
|
68
|
+
test('will not create a tag if one already exists', async () => {
|
|
69
|
+
const link = document.createElement('link');
|
|
70
|
+
link.setAttribute('href', STYLE_URL);
|
|
71
|
+
link.setAttribute('rel', 'stylesheet');
|
|
72
|
+
document.body.appendChild(link);
|
|
73
|
+
|
|
74
|
+
await checkAndLoadStyle(STYLE_URL); // Should resolve immediately
|
|
75
|
+
const tags = document.querySelectorAll(`link[href="${STYLE_URL}"]`);
|
|
76
|
+
expect(tags.length).toBe(1); // A second tag was not created
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("checkAndLoadFonts", () => {
|
|
80
|
+
let documentFonts : Array<any> = [];
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
documentFonts = [];
|
|
84
|
+
//@ts-ignore - needed to be able to stub out font API
|
|
85
|
+
document.fonts = documentFonts;
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('Will load missing fonts', async () => {
|
|
89
|
+
const promise = checkAndLoadFonts(FONTS);
|
|
90
|
+
const tag1 = document.querySelector(`link[href="font1.css"]`);
|
|
91
|
+
const tag2 = document.querySelector(`link[href="font2.css"]`);
|
|
92
|
+
|
|
93
|
+
expect(tag1).not.toBeNull();
|
|
94
|
+
expect(tag2).not.toBeNull();
|
|
95
|
+
|
|
96
|
+
tag1?.dispatchEvent(new Event("load"));
|
|
97
|
+
tag2?.dispatchEvent(new Event("load"));
|
|
98
|
+
|
|
99
|
+
expect(promise).resolves.toBe(undefined);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('Will not load fonts that are already loaded', async () => {
|
|
103
|
+
documentFonts.push({family: "Font2"});
|
|
104
|
+
|
|
105
|
+
const promise = checkAndLoadFonts(FONTS);
|
|
106
|
+
const tag1 = document.querySelector(`link[href="font1.css"]`);
|
|
107
|
+
const tag2 = document.querySelector(`link[href="font2.css"]`);
|
|
108
|
+
|
|
109
|
+
expect(tag1).not.toBeNull();
|
|
110
|
+
expect(tag2).toBeNull();
|
|
111
|
+
|
|
112
|
+
tag1?.dispatchEvent(new Event("load"));
|
|
113
|
+
|
|
114
|
+
expect(promise).resolves.toBe(undefined);
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('Ignores quotes on the family when checking for loaded fonts', async () => {
|
|
118
|
+
documentFonts.push({family: '"Font2"'});
|
|
119
|
+
|
|
120
|
+
const promise = checkAndLoadFonts(FONTS);
|
|
121
|
+
const tag1 = document.querySelector(`link[href="font1.css"]`);
|
|
122
|
+
const tag2 = document.querySelector(`link[href="font2.css"]`);
|
|
123
|
+
|
|
124
|
+
expect(tag1).not.toBeNull();
|
|
125
|
+
expect(tag2).toBeNull();
|
|
126
|
+
|
|
127
|
+
tag1?.dispatchEvent(new Event("load"));
|
|
128
|
+
|
|
129
|
+
expect(promise).resolves.toBe(undefined);
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('Still resolves even if fonts fail to load', async () => {
|
|
133
|
+
const promise = checkAndLoadFonts(FONTS);
|
|
134
|
+
const tag1 = document.querySelector(`link[href="font1.css"]`);
|
|
135
|
+
const tag2 = document.querySelector(`link[href="font2.css"]`);
|
|
136
|
+
|
|
137
|
+
expect(tag1).not.toBeNull();
|
|
138
|
+
expect(tag2).not.toBeNull();
|
|
139
|
+
|
|
140
|
+
tag1?.dispatchEvent(new Event("error"));
|
|
141
|
+
tag2?.dispatchEvent(new Event("error"));
|
|
142
|
+
|
|
143
|
+
expect(promise).resolves.toBe(undefined);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|