@sitecore-feaas/clientside 0.1.19 → 0.1.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/build/basic.js +5 -1
- package/build/basic.js.map +1 -1
- package/build/cloud.js +40 -7
- package/build/cloud.js.map +1 -1
- package/build/components/FEAASBase.js +60 -21
- package/build/components/FEAASBase.js.map +1 -1
- package/build/components/FEAASComponent.js +35 -1
- package/build/components/FEAASComponent.js.map +1 -1
- package/build/components/FEAASElement.js +14 -0
- package/build/components/FEAASElement.js.map +1 -0
- package/build/components/FEAASLoader.js +13 -1
- package/build/components/FEAASLoader.js.map +1 -1
- package/build/components/FEAASStylesheet.js.map +1 -1
- package/build/dom.js +6 -0
- package/build/dom.js.map +1 -1
- package/build/fetch.js +48 -45
- package/build/fetch.js.map +1 -1
- package/build/ui/FEAASContext.js +12 -13
- package/build/ui/FEAASContext.js.map +1 -1
- package/build/ui/FEAASEditor.js +448 -76
- package/build/ui/FEAASEditor.js.map +1 -1
- package/build/ui/FEAASPicker.js +9 -59
- package/build/ui/FEAASPicker.js.map +1 -1
- package/dist/browser/basic.esm.js +5 -5
- package/dist/browser/basic.esm.js.map +1 -1
- package/dist/browser/basic.js +5 -5
- package/dist/browser/basic.js.map +1 -1
- package/dist/browser/index.esm.js +5 -10
- package/dist/browser/index.esm.js.map +1 -1
- package/dist/browser/index.js +5 -10
- package/dist/browser/index.js.map +1 -1
- package/dist/node/basic.esm.js +3 -3
- package/dist/node/basic.esm.js.map +1 -1
- package/dist/node/basic.js +3 -3
- package/dist/node/basic.js.map +1 -1
- package/dist/node/index.esm.js +3 -8
- package/dist/node/index.esm.js.map +1 -1
- package/dist/node/index.js +3 -8
- package/dist/node/index.js.map +1 -1
- package/examples/index.html +8 -49
- package/package.json +1 -6
- package/src/basic.ts +8 -1
- package/src/cloud.ts +43 -13
- package/src/components/FEAASBase.tsx +73 -20
- package/src/components/{FEAASComponent.ts → FEAASComponent.tsx} +37 -1
- package/src/components/FEAASElement.tsx +21 -0
- package/src/components/{FEAASLoader.ts → FEAASLoader.tsx} +14 -1
- package/src/components/{FEAASStylesheet.ts → FEAASStylesheet.tsx} +0 -0
- package/src/dom.ts +5 -0
- package/src/fetch.ts +82 -51
- package/src/ui/FEAASContext.tsx +13 -14
- package/src/ui/FEAASEditor.tsx +598 -144
- package/src/ui/FEAASPicker.tsx +18 -65
- package/test/components.ts +19 -0
- package/types/basic.d.ts +3 -0
- package/types/basic.d.ts.map +1 -1
- package/types/cloud.d.ts +8 -1
- package/types/cloud.d.ts.map +1 -1
- package/types/components/FEAASBase.d.ts +18 -4
- package/types/components/FEAASBase.d.ts.map +1 -1
- package/types/components/FEAASComponent.d.ts +7 -0
- package/types/components/FEAASComponent.d.ts.map +1 -1
- package/types/components/FEAASElement.d.ts +12 -0
- package/types/components/FEAASElement.d.ts.map +1 -0
- package/types/components/FEAASLoader.d.ts +6 -0
- package/types/components/FEAASLoader.d.ts.map +1 -1
- package/types/components/FEAASStylesheet.d.ts +2 -2
- package/types/components/FEAASStylesheet.d.ts.map +1 -1
- package/types/dom.d.ts.map +1 -1
- package/types/fetch.d.ts +42 -1
- package/types/fetch.d.ts.map +1 -1
- package/types/tsbuildinfo +1 -1
- package/types/tsbuildinfo_ui +1 -1
- package/types/ui/FEAASContext.d.ts +4 -4
- package/types/ui/FEAASContext.d.ts.map +1 -1
- package/types/ui/FEAASEditor.d.ts +71 -5
- package/types/ui/FEAASEditor.d.ts.map +1 -1
- package/types/ui/FEAASPicker.d.ts +15 -34
- package/types/ui/FEAASPicker.d.ts.map +1 -1
package/examples/index.html
CHANGED
|
@@ -1,54 +1,13 @@
|
|
|
1
1
|
<script type="module">
|
|
2
|
-
import
|
|
3
|
-
const renderMyComponent = (element, data) =>
|
|
4
|
-
FEAAS.renderComponent(
|
|
5
|
-
{
|
|
6
|
-
componentId: 'our-dominator1',
|
|
7
|
-
versionId: 'dominator1-version2',
|
|
8
|
-
revision: 'staged',
|
|
9
|
-
hostname: 'https://feaascomponentsapidev.blob.core.windows.net',
|
|
10
|
-
data: data
|
|
11
|
-
},
|
|
12
|
-
element
|
|
13
|
-
)
|
|
2
|
+
import FEAASElement from '../dist/browser/basic.esm.js'
|
|
14
3
|
|
|
15
|
-
const stylesheet = FEAAS.renderStylesheet({
|
|
16
|
-
libraryId: 'our-wonderful-library'
|
|
17
|
-
})
|
|
18
|
-
document.head.appendChild(stylesheet)
|
|
19
4
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
'title-text': 'Hello world Hello Thomas 2r23r',
|
|
28
|
-
html: '<div class="-card--solid-color-cards -fill--light-blue"><button class="-button">Hola</button> <p><a href="#">Link</a> Text</div></p>',
|
|
29
|
-
open: true
|
|
30
|
-
},
|
|
31
|
-
{ 'title-text': 'Bye world', html: 'Mystical island' }
|
|
32
|
-
],
|
|
33
|
-
items2: [
|
|
34
|
-
{
|
|
35
|
-
'title-text': 'Cherry pop',
|
|
36
|
-
html: '<div class="-card--solid-color-cards -fill--light-blue"><button class="-button">Hola</button> <p><a href="#">Link</a> Text</div></p>'
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
'title-text': 'Berry',
|
|
40
|
-
html: '<div class="-card--solid-color-cards -fill--light-blue"><button class="-button">Hola</button> <p><a href="#">Link</a> Text</div></p>'
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
'title-text': 'Pear',
|
|
44
|
-
html: '<div class="-card--solid-color-cards -fill--light-blue"><button class="-button">Hola</button> <a href="#">Link</a> Text</div>',
|
|
45
|
-
open: true
|
|
46
|
-
},
|
|
47
|
-
{ 'title-text': 'Coolio', html: 'Mystical island' }
|
|
48
|
-
]
|
|
5
|
+
export class Z extends FEAASElement<{}> {
|
|
6
|
+
renderReact(
|
|
7
|
+
props: { children?: ReactNode },
|
|
8
|
+
payload: { React: typeof import('react'); ReactDOM: typeof import('react-dom') }
|
|
9
|
+
): ReactNode | ReactElement<any, string | JSXElementConstructor<any>> {
|
|
10
|
+
return
|
|
49
11
|
}
|
|
50
|
-
}
|
|
51
|
-
document.body.appendChild(element)
|
|
52
|
-
|
|
53
|
-
// renderMyComponent(element, {"user": {"name": "Bob"}})
|
|
12
|
+
}
|
|
54
13
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sitecore-feaas/clientside",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"start": "webpack serve --open",
|
|
6
6
|
"build": "rm -rf dist;npm run build:types; npm run build:esbuild; npm run build:sourcemaps",
|
|
@@ -14,12 +14,7 @@
|
|
|
14
14
|
"postpublish": "node ../cdn/upload-package.js clientside $npm_package_version"
|
|
15
15
|
},
|
|
16
16
|
"license": "ISC",
|
|
17
|
-
"optionalDependencies": {
|
|
18
|
-
"react": ">=16.8.0",
|
|
19
|
-
"react-dom": ">=16.8.0"
|
|
20
|
-
},
|
|
21
17
|
"peerDependencies": {
|
|
22
|
-
"react": ">=16.8.0",
|
|
23
18
|
"react-dom": ">=16.8.0"
|
|
24
19
|
},
|
|
25
20
|
"main": "./dist/browser/index.esm.js",
|
package/src/basic.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import FEAASElement from './components/FEAASElement.js'
|
|
2
|
+
|
|
1
3
|
export * from './dom.js'
|
|
2
4
|
export * from './fetch.js'
|
|
3
5
|
export * from './components/FEAASBase.js'
|
|
6
|
+
export * from './components/FEAASElement.js'
|
|
4
7
|
export * from './components/FEAASComponent.js'
|
|
5
8
|
export * from './components/FEAASLoader.js'
|
|
6
9
|
export * from './components/FEAASStylesheet.js'
|
|
@@ -8,7 +11,11 @@ export * from './components/FEAASStylesheet.js'
|
|
|
8
11
|
declare global {
|
|
9
12
|
interface Window {
|
|
10
13
|
FEAASLoading: Promise<any>
|
|
14
|
+
FEAASElement: typeof FEAASElement
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
if (typeof window != 'undefined')
|
|
18
|
+
if (typeof window != 'undefined') {
|
|
19
|
+
window.FEAASElement = FEAASElement
|
|
20
|
+
window.FEAASLoading ??= Promise.resolve()
|
|
21
|
+
}
|
package/src/cloud.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchAndRevalidate } from './fetch.js'
|
|
1
|
+
import { fetchAndRevalidate, fetchAndValidate, invalidateRequest, refetchAndValidate } from './fetch.js'
|
|
2
2
|
|
|
3
3
|
export interface CloudTemplate {
|
|
4
4
|
cdn?: string
|
|
@@ -7,6 +7,7 @@ export interface CloudTemplate {
|
|
|
7
7
|
component: string
|
|
8
8
|
version: string
|
|
9
9
|
revision?: string
|
|
10
|
+
instance?: string
|
|
10
11
|
}
|
|
11
12
|
export interface CloudStylesheet {
|
|
12
13
|
cdn?: string
|
|
@@ -36,20 +37,49 @@ export function getStylesheetURL({
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export async function fetchComponent(props: CloudTemplate, callback: (html: string) => void) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const onlyIfNotDeleted = (response: Response) => {
|
|
41
|
+
return response.headers.get('x-ms-meta-versionDeletedAt') == null
|
|
42
|
+
}
|
|
43
|
+
const parseResponse = (response: Response) => {
|
|
44
|
+
response.text().then(callback)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fetch two requests in parallel: One optimistically checking if component was forked,
|
|
48
|
+
// and another checking original definition of a component
|
|
49
|
+
const results = await Promise.all([
|
|
50
|
+
props.instance &&
|
|
51
|
+
fetchAndValidate({
|
|
52
|
+
options: {
|
|
53
|
+
headers: {
|
|
54
|
+
Accept: 'text/html'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
url: getComponentURL({ ...props, version: props.instance }),
|
|
58
|
+
validator: onlyIfNotDeleted
|
|
59
|
+
}).catch((e) => e as Error),
|
|
60
|
+
fetchAndValidate({ url: getComponentURL(props), validator: onlyIfNotDeleted }).catch((e) => e as Error)
|
|
61
|
+
])
|
|
62
|
+
const result = results.find((result) => result && 'isCached' in result && result)
|
|
63
|
+
if (!result || result instanceof Error) {
|
|
64
|
+
const error = results.find((result) => result && result instanceof Error)
|
|
65
|
+
throw error || new Error('Can not fetch component')
|
|
66
|
+
}
|
|
67
|
+
if (results[0] == result) {
|
|
68
|
+
console.log('Using fork!', results)
|
|
69
|
+
}
|
|
70
|
+
parseResponse(result.response)
|
|
71
|
+
return refetchAndValidate({ ...result, callback: parseResponse }).catch((e) => {
|
|
72
|
+
if (results[0] == result) {
|
|
73
|
+
invalidateRequest({ ...result, callback: null, validator: null })
|
|
74
|
+
console.log('Fork was deleted')
|
|
75
|
+
// forked version was cached, but it's now deleted
|
|
76
|
+
if (!(results[1] instanceof Error)) {
|
|
77
|
+
parseResponse(results[1].response)
|
|
78
|
+
return refetchAndValidate({ ...results[1], callback: parseResponse })
|
|
44
79
|
}
|
|
45
|
-
},
|
|
46
|
-
(response) => {
|
|
47
|
-
return response.headers.get('x-ms-meta-versionDeletedAt') == null
|
|
48
|
-
},
|
|
49
|
-
(response) => {
|
|
50
|
-
response.text().then(callback)
|
|
51
80
|
}
|
|
52
|
-
|
|
81
|
+
throw e
|
|
82
|
+
})
|
|
53
83
|
}
|
|
54
84
|
|
|
55
85
|
export async function fetchStylesheet(props: CloudStylesheet, callback: (css: string) => void) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JSXElementConstructor, ReactElement, ReactNode } from 'react'
|
|
2
|
+
import type ReactDOMClient from 'react-dom/client'
|
|
2
3
|
import type ReactDOM from 'react-dom'
|
|
3
4
|
import type React from 'react'
|
|
4
5
|
|
|
@@ -9,6 +10,7 @@ var WebComponent = typeof HTMLElement == 'undefined' ? (class {} as unknown as t
|
|
|
9
10
|
interface ReactPayload {
|
|
10
11
|
React: typeof React
|
|
11
12
|
ReactDOM: typeof ReactDOM
|
|
13
|
+
ReactDOMClient: typeof ReactDOMClient
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
@@ -27,7 +29,7 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
|
27
29
|
nextRender: number
|
|
28
30
|
|
|
29
31
|
/** Is/was component in the dom? */
|
|
30
|
-
connected =
|
|
32
|
+
connected = false
|
|
31
33
|
|
|
32
34
|
/** Result of lazy loading */
|
|
33
35
|
payload: Payload
|
|
@@ -35,6 +37,12 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
|
35
37
|
refs: Record<string, FEAASBase> = {}
|
|
36
38
|
descendants: FEAASBase[] = []
|
|
37
39
|
|
|
40
|
+
constructor() {
|
|
41
|
+
super()
|
|
42
|
+
this.addEventListener('feaasConnected', this)
|
|
43
|
+
this.addEventListener('feaasDisconnected', this)
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
/** Attaches shadow root if getShadowRootOptions any settings, or uses element itself */
|
|
39
47
|
getRoot(): FEAASBase | HTMLElement | ShadowRoot {
|
|
40
48
|
return this
|
|
@@ -47,38 +55,76 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
|
47
55
|
|
|
48
56
|
/** Components parses its attributes when connected, and schedules rendeirng */
|
|
49
57
|
connectedCallback() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
this.initialize()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Web components are initialized depth-first, making child elements render before its parents. This could here
|
|
63
|
+
* attempts to avoid this and inverts the initialization logic, so that children are initialized in the context of its
|
|
64
|
+
* already-initialized parents.
|
|
65
|
+
*
|
|
66
|
+
* FEAAS components dispatch `feaasConnected` event that allows components to store references to each other. In
|
|
67
|
+
* addition it allows external scripts to listen for initialization of FEAAS components
|
|
68
|
+
*/
|
|
69
|
+
initialize() {
|
|
70
|
+
const parent = this.getParent() as FEAASBase
|
|
71
|
+
// Allow parent component to initialize first
|
|
72
|
+
if (parent && !parent.connected) return
|
|
73
|
+
if (this.connected) return
|
|
74
|
+
this.connected = true
|
|
75
|
+
console.log('FEAAS: Connected', this.tagName, this)
|
|
76
|
+
this.dispatchEvent(new Event('feaasConnected', { bubbles: true, composed: true }))
|
|
77
|
+
this.getElements().map((element: FEAASBase) => element.initialize())
|
|
55
78
|
this.set()
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getParent(onlyFeaas = true) {
|
|
83
|
+
for (
|
|
84
|
+
var p: HTMLElement = this;
|
|
85
|
+
(p = p.parentElement || ((p.parentNode instanceof ShadowRoot ? p.parentNode.host : null) as HTMLElement));
|
|
86
|
+
|
|
87
|
+
) {
|
|
88
|
+
if (!onlyFeaas || p.tagName.startsWith('FEAAS-')) return p
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getElements(root: HTMLElement | ShadowRoot = this.getRoot(), onlyFeaas = true): HTMLElement[] {
|
|
93
|
+
return Array.from(root.querySelectorAll('*'))
|
|
94
|
+
.map(
|
|
95
|
+
(el) =>
|
|
96
|
+
[
|
|
97
|
+
!onlyFeaas || el.tagName.startsWith('FEAAS-') ? el : null,
|
|
98
|
+
el.shadowRoot ? this.getElements(el.shadowRoot, onlyFeaas) : null
|
|
99
|
+
] as HTMLElement[]
|
|
100
|
+
)
|
|
101
|
+
.flat()
|
|
102
|
+
.filter(Boolean)
|
|
56
103
|
}
|
|
57
104
|
|
|
58
105
|
/** Nothing special happens on disconnect, except setting a flag */
|
|
59
106
|
disconnectedCallback() {
|
|
107
|
+
if (!this.connected) return
|
|
60
108
|
console.log('FEAAS: Disconnected', this)
|
|
61
|
-
this.removeEventListener('feaasConnected', this)
|
|
62
|
-
this.removeEventListener('feaasDisconnected', this)
|
|
63
109
|
this.dispatchEvent(new Event('feaasDisconnected', { bubbles: true, composed: true }))
|
|
64
110
|
this.connected = null
|
|
65
111
|
}
|
|
66
112
|
|
|
67
113
|
/** Catch-all event handler that dispatches events to callback */
|
|
68
114
|
handleEvent(event: Event) {
|
|
115
|
+
const target = event.composedPath()[0] as FEAASBase
|
|
69
116
|
switch (event.type) {
|
|
70
117
|
case 'feaasConnected':
|
|
71
|
-
this.nestedCallback(
|
|
118
|
+
if (target != this) this.nestedCallback(target)
|
|
72
119
|
break
|
|
73
120
|
case 'feaasDisconnected':
|
|
74
|
-
this.unnestedCallback(
|
|
121
|
+
if (target != this) this.unnestedCallback(target)
|
|
75
122
|
break
|
|
76
123
|
}
|
|
77
124
|
}
|
|
78
125
|
|
|
79
126
|
/** Act on children element being removed */
|
|
80
|
-
unnestedCallback(
|
|
81
|
-
const target = event.composedPath()[0] as FEAASBase
|
|
127
|
+
unnestedCallback(target: FEAASBase) {
|
|
82
128
|
const index = this.descendants.indexOf(target)
|
|
83
129
|
if (index == -1) return
|
|
84
130
|
console.log('FEAAS: Unnest', this, target)
|
|
@@ -91,8 +137,7 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
|
91
137
|
}
|
|
92
138
|
|
|
93
139
|
/** Act on children element being added */
|
|
94
|
-
nestedCallback(
|
|
95
|
-
const target = event.composedPath()[0] as FEAASBase
|
|
140
|
+
nestedCallback(target: FEAASBase): boolean | void {
|
|
96
141
|
const index = this.descendants.indexOf(target)
|
|
97
142
|
if (index != -1) return
|
|
98
143
|
this.descendants.push(target)
|
|
@@ -153,12 +198,12 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
|
153
198
|
render(nested = false): any {
|
|
154
199
|
console.log('FEAAS: React', this.tagName, this.payload)
|
|
155
200
|
if (this.payload == null) return
|
|
156
|
-
const { React, ReactDOM } = (this.payload as ReactPayload) || {}
|
|
201
|
+
const { React, ReactDOM, ReactDOMClient } = (this.payload as ReactPayload) || {}
|
|
157
202
|
if (!React) throw new Error(`${this.tagName}: React is not present in payload`)
|
|
158
203
|
if (!ReactDOM) throw new Error(`${this.tagName}: ReactDOM is not present in payload`)
|
|
159
|
-
|
|
160
204
|
if (!this.refs.topReact && !this.forceUpdateReact) {
|
|
161
|
-
|
|
205
|
+
this.reactRoot ||= ReactDOMClient.createRoot(this.getReactRootElement())
|
|
206
|
+
this.reactRoot.render(React.createElement(this.getReactWrapper(), { z: 'root' }))
|
|
162
207
|
} else if (this.forceUpdateReact) {
|
|
163
208
|
this.forceUpdateReact?.()
|
|
164
209
|
} else {
|
|
@@ -182,13 +227,14 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
|
182
227
|
}
|
|
183
228
|
|
|
184
229
|
reactWrapper: (props?: any) => ReactNode
|
|
230
|
+
reactRoot: ReactDOMClient.Root
|
|
185
231
|
getReactWrapper(target: FEAASBase = this, displayName = target.tagName) {
|
|
186
232
|
const Wrapper = (target.reactWrapper ||= (props?: any) => {
|
|
187
233
|
if (target.renderReact) {
|
|
188
234
|
const { React, ReactDOM } = (target.payload as ReactPayload) || {}
|
|
189
235
|
const [value, forceUpdateReact] = React.useReducer((r) => r + 1, 0)
|
|
190
236
|
target.forceUpdateReact = forceUpdateReact
|
|
191
|
-
return ReactDOM.createPortal(<>{target.invokeReactRendering()}</>, target.
|
|
237
|
+
return ReactDOM.createPortal(<>{target.invokeReactRendering()}</>, target.getReactRootElement())
|
|
192
238
|
} else {
|
|
193
239
|
return target.render()
|
|
194
240
|
}
|
|
@@ -214,7 +260,7 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
|
214
260
|
payload: typeof this.payload
|
|
215
261
|
): ReactNode | ReactElement
|
|
216
262
|
|
|
217
|
-
|
|
263
|
+
getReactRootElement(): HTMLElement | ShadowRoot {
|
|
218
264
|
return this.getRoot()
|
|
219
265
|
}
|
|
220
266
|
|
|
@@ -256,7 +302,14 @@ export class FEAASBase<Props = any, Payload = any> extends WebComponent {
|
|
|
256
302
|
}
|
|
257
303
|
|
|
258
304
|
getContextAttribute(name: string, element = this) {
|
|
259
|
-
|
|
305
|
+
for (
|
|
306
|
+
var p: HTMLElement = this;
|
|
307
|
+
(p = p.parentElement || ((p.parentNode instanceof ShadowRoot ? p.parentNode.host : null) as HTMLElement));
|
|
308
|
+
|
|
309
|
+
) {
|
|
310
|
+
if (p.getAttribute(name) != null) return p.getAttribute(name)
|
|
311
|
+
}
|
|
312
|
+
return this.getAttribute(name)
|
|
260
313
|
}
|
|
261
314
|
|
|
262
315
|
static register(tagName: string) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchComponent } from '../cloud.js'
|
|
2
2
|
import { renderDOMContent, renderDOMElement } from '../dom.js'
|
|
3
3
|
import { FEAASBase, OptionalExcept } from './FEAASBase.js'
|
|
4
|
+
import { FEAASLoader } from './FEAASLoader.js'
|
|
4
5
|
|
|
5
6
|
export function FEAASComponentsProps(element: FEAASBase = this) {
|
|
6
7
|
let data: any
|
|
@@ -19,12 +20,25 @@ export function FEAASComponentsProps(element: FEAASBase = this) {
|
|
|
19
20
|
hostname: element.getAttribute('hostname'),
|
|
20
21
|
cdn: element.getContextAttribute('cdn'),
|
|
21
22
|
template: element.getAttribute('template'),
|
|
23
|
+
instance: element.getAttribute('instance'),
|
|
24
|
+
editable: element.getAttribute('editable') != null,
|
|
22
25
|
suspended: element.getAttribute('suspended') != null
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
export type FEAASComponentsProps = ReturnType<typeof FEAASComponentsProps>
|
|
26
29
|
export class FEAASComponent extends FEAASBase<FEAASComponentsProps, string> {
|
|
27
|
-
static observedAttributes = [
|
|
30
|
+
static observedAttributes = [
|
|
31
|
+
'library',
|
|
32
|
+
'component',
|
|
33
|
+
'version',
|
|
34
|
+
'revision',
|
|
35
|
+
'hostname',
|
|
36
|
+
'cdn',
|
|
37
|
+
'template',
|
|
38
|
+
'data',
|
|
39
|
+
'instance',
|
|
40
|
+
'editable'
|
|
41
|
+
]
|
|
28
42
|
|
|
29
43
|
needsRefresh: boolean
|
|
30
44
|
setData(data: any) {
|
|
@@ -36,11 +50,33 @@ export class FEAASComponent extends FEAASBase<FEAASComponentsProps, string> {
|
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
|
|
53
|
+
if (oldValue === newValue) return
|
|
39
54
|
this.payload = undefined
|
|
40
55
|
this.needsRefresh = true
|
|
41
56
|
super.attributeChangedCallback(name, oldValue, newValue)
|
|
42
57
|
}
|
|
43
58
|
|
|
59
|
+
getEditor(): any {
|
|
60
|
+
return (document.querySelector('feaas-context') as FEAASBase)?.refs.editor
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
super()
|
|
65
|
+
this.addEventListener('click', () => {
|
|
66
|
+
if (this.props.editable) {
|
|
67
|
+
this.edit()
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
edit() {
|
|
73
|
+
const editor = this.getEditor()
|
|
74
|
+
if (editor?.setTarget(this)) {
|
|
75
|
+
editor.open()
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
/* Check if component is a repeated clone, and return the original */
|
|
45
81
|
getOriginal() {
|
|
46
82
|
const scope = this.getAttribute('data-path-scope')
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ReactNode, ReactElement, JSXElementConstructor } from 'react'
|
|
2
|
+
import { FEAASBase } from './FEAASBase.js'
|
|
3
|
+
|
|
4
|
+
export default class FEAASElement<P> extends FEAASBase<
|
|
5
|
+
P,
|
|
6
|
+
{
|
|
7
|
+
React: typeof React
|
|
8
|
+
ReactDOM: typeof ReactDOM
|
|
9
|
+
}
|
|
10
|
+
> {
|
|
11
|
+
load() {
|
|
12
|
+
return Promise.all([
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
import('https://cdn.skypack.dev/react'),
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
import('https://cdn.skypack.dev/react-dom')
|
|
17
|
+
]).then(([React, ReactDOM]) => {
|
|
18
|
+
return { React, ReactDOM }
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -2,6 +2,7 @@ import { FEAASBase } from './FEAASBase.js'
|
|
|
2
2
|
|
|
3
3
|
export function FEAASLoaderProps(element: HTMLElement = this) {
|
|
4
4
|
return {
|
|
5
|
+
opacity: element?.getAttribute('opacity') ? parseFloat(element?.getAttribute('opacity')) : 1,
|
|
5
6
|
hidden: element?.getAttribute('hidden') != null
|
|
6
7
|
}
|
|
7
8
|
}
|
|
@@ -11,9 +12,14 @@ export class FEAASLoader extends FEAASBase<FEAASLoaderProps, string> {
|
|
|
11
12
|
stylesheet: HTMLStyleElement
|
|
12
13
|
loader: HTMLDivElement
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
getProps() {
|
|
16
|
+
return FEAASLoaderProps(this)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static observedAttributes = ['hidden', 'opacity']
|
|
15
20
|
|
|
16
21
|
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
|
|
22
|
+
if (oldValue === newValue) return
|
|
17
23
|
if (name == 'hidden') {
|
|
18
24
|
this.style.setProperty('display', 'block', 'important')
|
|
19
25
|
this.style.setProperty('transition', 'opacity 0.3s', 'important')
|
|
@@ -25,9 +31,12 @@ export class FEAASLoader extends FEAASBase<FEAASLoaderProps, string> {
|
|
|
25
31
|
this.style.pointerEvents = 'all'
|
|
26
32
|
}
|
|
27
33
|
}
|
|
34
|
+
return super.attributeChangedCallback(name, oldValue, newValue)
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
connectedCallback() {
|
|
38
|
+
this.payload = null
|
|
39
|
+
super.connectedCallback()
|
|
31
40
|
this.attributeChangedCallback('hidden', this.getAttribute('hidden'), this.getAttribute('hidden'))
|
|
32
41
|
this.stylesheet = document.createElement('style')
|
|
33
42
|
this.stylesheet.textContent = `
|
|
@@ -101,6 +110,10 @@ export class FEAASLoader extends FEAASBase<FEAASLoaderProps, string> {
|
|
|
101
110
|
this.getRoot().appendChild(this.stylesheet)
|
|
102
111
|
this.getRoot().appendChild(this.loader)
|
|
103
112
|
}
|
|
113
|
+
|
|
114
|
+
render() {
|
|
115
|
+
this.loader.style.opacity = String(this.props.opacity)
|
|
116
|
+
}
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
declare global {
|
|
File without changes
|
package/src/dom.ts
CHANGED
|
@@ -310,6 +310,11 @@ export function renderDOMContent(root: HTMLElement, template: string, data?: Dat
|
|
|
310
310
|
const newTemplate = config?.processTemplate?.(root, template, data) || template
|
|
311
311
|
root.innerHTML = newTemplate
|
|
312
312
|
root.classList.add('-feaas')
|
|
313
|
+
// fixme, jss needs to assign theme classes to component instead
|
|
314
|
+
const anchor = root.closest('.component.feaas') || root
|
|
315
|
+
if (anchor.className.includes('-theme--')) {
|
|
316
|
+
for (const child of root.children) child.className = child.className.replace(/(\s|^)-theme--[^\s]+\s*/g, '$1')
|
|
317
|
+
}
|
|
313
318
|
return renderDOMElement(root, data, config)
|
|
314
319
|
}
|
|
315
320
|
|
package/src/fetch.ts
CHANGED
|
@@ -1,64 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Utility to cache-bust static files. It relies on reading Date header to detect cached response, so it requires CORS
|
|
3
|
+
* settings to be set.
|
|
4
|
+
*
|
|
5
|
+
* It accepts validator function that determines if response is usable. If response was pulled from cache and is valid,
|
|
6
|
+
* the optional callback function is fired right away. Then HEAD request is made bypassing the cache, to see if the
|
|
7
|
+
* response has an newer Last-Modified header. In case it does and it passes validation, third request is fired that
|
|
8
|
+
* actually fetches the new content and callback is invoked the second time.
|
|
9
|
+
*
|
|
10
|
+
* It works as promise, but also accepts callback to use cached blobs mid-flight and have access to headers. Any network
|
|
11
|
+
* errors will not be caught. Function throws when unable to fetch valid response.
|
|
12
|
+
*/
|
|
13
|
+
export interface ValidatableRequest {
|
|
14
|
+
url: Parameters<typeof fetch>[0]
|
|
15
|
+
options?: Parameters<typeof fetch>[1]
|
|
16
|
+
validator?: (resposne: Response) => boolean
|
|
17
|
+
callback?: (response: Response, status: 'initial' | 'updated' | 'check') => void
|
|
18
|
+
isCached?: boolean
|
|
19
|
+
response?: Response
|
|
20
|
+
}
|
|
15
21
|
|
|
16
|
-
*/
|
|
17
22
|
export async function fetchAndRevalidate(
|
|
18
|
-
url:
|
|
19
|
-
options
|
|
20
|
-
validator
|
|
21
|
-
callback
|
|
23
|
+
url: ValidatableRequest['url'],
|
|
24
|
+
options?: ValidatableRequest['options'],
|
|
25
|
+
validator?: ValidatableRequest['validator'],
|
|
26
|
+
callback?: ValidatableRequest['callback']
|
|
22
27
|
) {
|
|
23
28
|
// make request as usual, see if there's cached version of an asset
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
var response = initialResponse
|
|
29
|
-
callback(initialResponse, 'cached')
|
|
29
|
+
return fetchAndValidate({ url, options, validator, callback })
|
|
30
|
+
.then(refetchAndValidate)
|
|
31
|
+
.then((result) => result.response)
|
|
32
|
+
}
|
|
30
33
|
|
|
34
|
+
export async function fetchAndValidate(
|
|
35
|
+
{ url, options, validator, callback }: ValidatableRequest,
|
|
36
|
+
phase: 'initial' | 'updated' | 'check' = 'initial'
|
|
37
|
+
) {
|
|
38
|
+
const date = new Date()
|
|
39
|
+
console.log('fetch', phase, url)
|
|
40
|
+
const response = await fetch(url, options)
|
|
41
|
+
if (!response.ok) throw new Error('Fetch failed')
|
|
42
|
+
if (validator?.(response) === false) throw new Error('Response is not valid')
|
|
31
43
|
// see if response was pulled from cache
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
const isCached = new Date(response.headers.get('date')) < date
|
|
45
|
+
callback?.(response, phase)
|
|
46
|
+
return {
|
|
47
|
+
url,
|
|
48
|
+
options,
|
|
49
|
+
isCached,
|
|
50
|
+
response,
|
|
51
|
+
validator
|
|
52
|
+
}
|
|
53
|
+
}
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
export async function refetchAndValidate(request: ValidatableRequest & Required<Pick<ValidatableRequest, 'response'>>) {
|
|
56
|
+
const { url, isCached, options, validator, response, callback } = request
|
|
57
|
+
if (isCached) {
|
|
58
|
+
// check if response was stale by sending quick HEAD request busting cache
|
|
59
|
+
const { response: head } = await fetchAndValidate(
|
|
60
|
+
{
|
|
61
|
+
url: url + '?' + Math.random(),
|
|
62
|
+
options: {
|
|
63
|
+
...options,
|
|
64
|
+
method: 'HEAD',
|
|
65
|
+
cache: 'no-cache'
|
|
66
|
+
},
|
|
67
|
+
validator
|
|
68
|
+
},
|
|
69
|
+
'check'
|
|
70
|
+
)
|
|
47
71
|
|
|
72
|
+
// there's a newer version of the file that we may fetch
|
|
73
|
+
if (head.headers.get('last-modified') != response.headers.get('last-modified')) {
|
|
48
74
|
// otherwise the new resource has to be re-fetched
|
|
49
|
-
|
|
50
|
-
...options,
|
|
51
|
-
cache: 'reload'
|
|
52
|
-
})
|
|
75
|
+
return invalidateRequest(request)
|
|
53
76
|
}
|
|
54
77
|
}
|
|
78
|
+
return request
|
|
79
|
+
}
|
|
55
80
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
export async function invalidateRequest(request: ValidatableRequest) {
|
|
82
|
+
const { url, options, validator, callback } = request
|
|
83
|
+
return fetchAndValidate(
|
|
84
|
+
{
|
|
85
|
+
url,
|
|
86
|
+
options: {
|
|
87
|
+
...options,
|
|
88
|
+
cache: 'reload'
|
|
89
|
+
},
|
|
90
|
+
validator,
|
|
91
|
+
callback
|
|
92
|
+
},
|
|
93
|
+
'updated'
|
|
94
|
+
)
|
|
64
95
|
}
|