@furystack/shades 3.6.3

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.
Files changed (108) hide show
  1. package/LICENSE +339 -0
  2. package/README.md +42 -0
  3. package/dist/component-factory.spec.d.ts +2 -0
  4. package/dist/component-factory.spec.d.ts.map +1 -0
  5. package/dist/component-factory.spec.js +20 -0
  6. package/dist/component-factory.spec.js.map +1 -0
  7. package/dist/components/index.d.ts +4 -0
  8. package/dist/components/index.d.ts.map +1 -0
  9. package/dist/components/index.js +7 -0
  10. package/dist/components/index.js.map +1 -0
  11. package/dist/components/lazy-load.d.ts +11 -0
  12. package/dist/components/lazy-load.d.ts.map +1 -0
  13. package/dist/components/lazy-load.js +42 -0
  14. package/dist/components/lazy-load.js.map +1 -0
  15. package/dist/components/route-link.d.ts +3 -0
  16. package/dist/components/route-link.d.ts.map +1 -0
  17. package/dist/components/route-link.js +17 -0
  18. package/dist/components/route-link.js.map +1 -0
  19. package/dist/components/router.d.ts +24 -0
  20. package/dist/components/router.d.ts.map +1 -0
  21. package/dist/components/router.js +50 -0
  22. package/dist/components/router.js.map +1 -0
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +11 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/initialize.d.ts +8 -0
  28. package/dist/initialize.d.ts.map +1 -0
  29. package/dist/initialize.js +9 -0
  30. package/dist/initialize.js.map +1 -0
  31. package/dist/is-jsx-element.spec.d.ts +2 -0
  32. package/dist/is-jsx-element.spec.d.ts.map +1 -0
  33. package/dist/is-jsx-element.spec.js +18 -0
  34. package/dist/is-jsx-element.spec.js.map +1 -0
  35. package/dist/jsx.d.ts +528 -0
  36. package/dist/jsx.d.ts.map +1 -0
  37. package/dist/jsx.js +9 -0
  38. package/dist/jsx.js.map +1 -0
  39. package/dist/models/children-list.d.ts +2 -0
  40. package/dist/models/children-list.d.ts.map +1 -0
  41. package/dist/models/children-list.js +3 -0
  42. package/dist/models/children-list.js.map +1 -0
  43. package/dist/models/index.d.ts +6 -0
  44. package/dist/models/index.d.ts.map +1 -0
  45. package/dist/models/index.js +9 -0
  46. package/dist/models/index.js.map +1 -0
  47. package/dist/models/partial-element.d.ts +6 -0
  48. package/dist/models/partial-element.d.ts.map +1 -0
  49. package/dist/models/partial-element.js +3 -0
  50. package/dist/models/partial-element.js.map +1 -0
  51. package/dist/models/render-options.d.ts +13 -0
  52. package/dist/models/render-options.d.ts.map +1 -0
  53. package/dist/models/render-options.js +3 -0
  54. package/dist/models/render-options.js.map +1 -0
  55. package/dist/models/selection-state.d.ts +10 -0
  56. package/dist/models/selection-state.d.ts.map +1 -0
  57. package/dist/models/selection-state.js +3 -0
  58. package/dist/models/selection-state.js.map +1 -0
  59. package/dist/models/shade-component.d.ts +13 -0
  60. package/dist/models/shade-component.d.ts.map +1 -0
  61. package/dist/models/shade-component.js +14 -0
  62. package/dist/models/shade-component.js.map +1 -0
  63. package/dist/services/index.d.ts +3 -0
  64. package/dist/services/index.d.ts.map +1 -0
  65. package/dist/services/index.js +6 -0
  66. package/dist/services/index.js.map +1 -0
  67. package/dist/services/location-service.d.ts +10 -0
  68. package/dist/services/location-service.d.ts.map +1 -0
  69. package/dist/services/location-service.js +41 -0
  70. package/dist/services/location-service.js.map +1 -0
  71. package/dist/services/location-service.spec.d.ts +2 -0
  72. package/dist/services/location-service.spec.d.ts.map +1 -0
  73. package/dist/services/location-service.spec.js +18 -0
  74. package/dist/services/location-service.spec.js.map +1 -0
  75. package/dist/services/screen-service.d.ts +28 -0
  76. package/dist/services/screen-service.d.ts.map +1 -0
  77. package/dist/services/screen-service.js +52 -0
  78. package/dist/services/screen-service.js.map +1 -0
  79. package/dist/shade-component.d.ts +26 -0
  80. package/dist/shade-component.d.ts.map +1 -0
  81. package/dist/shade-component.js +72 -0
  82. package/dist/shade-component.js.map +1 -0
  83. package/dist/shade.d.ts +40 -0
  84. package/dist/shade.d.ts.map +1 -0
  85. package/dist/shade.js +138 -0
  86. package/dist/shade.js.map +1 -0
  87. package/package.json +41 -0
  88. package/src/component-factory.spec.tsx +22 -0
  89. package/src/components/index.ts +3 -0
  90. package/src/components/lazy-load.tsx +47 -0
  91. package/src/components/route-link.tsx +21 -0
  92. package/src/components/router.tsx +66 -0
  93. package/src/index.ts +7 -0
  94. package/src/initialize.ts +11 -0
  95. package/src/is-jsx-element.spec.ts +18 -0
  96. package/src/jsx.ts +528 -0
  97. package/src/models/children-list.ts +1 -0
  98. package/src/models/index.ts +5 -0
  99. package/src/models/partial-element.ts +3 -0
  100. package/src/models/render-options.ts +16 -0
  101. package/src/models/selection-state.ts +9 -0
  102. package/src/models/shade-component.ts +16 -0
  103. package/src/services/index.ts +2 -0
  104. package/src/services/location-service.spec.ts +16 -0
  105. package/src/services/location-service.tsx +40 -0
  106. package/src/services/screen-service.ts +59 -0
  107. package/src/shade-component.ts +69 -0
  108. package/src/shade.ts +201 -0
@@ -0,0 +1 @@
1
+ export type ChildrenList = Array<string | HTMLElement | JSX.Element | string[] | HTMLElement[] | JSX.Element[]>
@@ -0,0 +1,5 @@
1
+ export * from './children-list'
2
+ export * from './partial-element'
3
+ export * from './render-options'
4
+ export * from './selection-state'
5
+ export * from './shade-component'
@@ -0,0 +1,3 @@
1
+ export type PartialElement<T extends { style?: CSSStyleDeclaration }> = Omit<Partial<T>, 'style'> & {
2
+ style?: Partial<CSSStyleDeclaration>
3
+ }
@@ -0,0 +1,16 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { PartialElement } from './partial-element'
3
+ import { ChildrenList } from './children-list'
4
+
5
+ export type RenderOptions<TProps, TState> = {
6
+ readonly props: TProps
7
+
8
+ injector: Injector
9
+ children: ChildrenList
10
+ element: JSX.Element<TProps, TState>
11
+ } & (unknown extends TState
12
+ ? {}
13
+ : {
14
+ getState: () => TState
15
+ updateState: (newState: PartialElement<TState>, skipRender?: boolean) => void
16
+ })
@@ -0,0 +1,9 @@
1
+ export interface SelectionState {
2
+ focusedPath?: number[]
3
+ selectionRange?: {
4
+ startOffset: number
5
+ startContainerPath: number[]
6
+ endOffset: number
7
+ endContainerPath: number[]
8
+ }
9
+ }
@@ -0,0 +1,16 @@
1
+ import { ChildrenList } from './children-list'
2
+
3
+ /**
4
+ * Type definition for a Shade component
5
+ */
6
+ export type ShadeComponent<TProps = {}> = (arg: TProps, children?: ChildrenList) => JSX.Element
7
+
8
+ /**
9
+ * Type guard that checks if an object is a stateless component
10
+ *
11
+ * @param obj The object to check
12
+ * @returns a value that indicates if the object is a Shade component
13
+ */
14
+ export const isShadeComponent = (obj: any): obj is ShadeComponent<any> => {
15
+ return typeof obj === 'function'
16
+ }
@@ -0,0 +1,2 @@
1
+ export * from './location-service'
2
+ export * from './screen-service'
@@ -0,0 +1,16 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { usingAsync } from '@furystack/utils'
3
+ import { LocationService } from './'
4
+ import { JSDOM } from 'jsdom'
5
+
6
+ describe('LocationService', () => {
7
+ it('Shuld be constructed', async () => {
8
+ await usingAsync(new Injector(), async (i) => {
9
+ const dom = new JSDOM()
10
+ ;(global as any).window = dom.window
11
+ i.setExplicitInstance(dom.window, Window)
12
+ const s = i.getInstance(LocationService)
13
+ expect(s).toBeInstanceOf(LocationService)
14
+ })
15
+ })
16
+ })
@@ -0,0 +1,40 @@
1
+ import { Disposable, ObservableValue, Trace } from '@furystack/utils'
2
+ import { Injectable } from '@furystack/inject'
3
+ @Injectable({ lifetime: 'singleton' })
4
+ export class LocationService implements Disposable {
5
+ public dispose() {
6
+ window.removeEventListener('popstate', this.updateState)
7
+ window.removeEventListener('hashchange', this.updateState)
8
+ this.pushStateTracer.dispose()
9
+ this.replaceStateTracer.dispose()
10
+ }
11
+
12
+ public onLocationChanged = new ObservableValue<URL>(new URL(location.href))
13
+
14
+ public updateState() {
15
+ const newUrl = new URL(location.href)
16
+ this.onLocationChanged.setValue(newUrl)
17
+ }
18
+
19
+ private pushStateTracer: Disposable
20
+ private replaceStateTracer: Disposable
21
+
22
+ constructor() {
23
+ window.addEventListener('popstate', () => this.updateState())
24
+ window.addEventListener('hashchange', () => this.updateState())
25
+
26
+ this.pushStateTracer = Trace.method({
27
+ object: history,
28
+ method: history.pushState,
29
+ isAsync: false,
30
+ onFinished: () => this.updateState(),
31
+ })
32
+
33
+ this.replaceStateTracer = Trace.method({
34
+ object: history,
35
+ method: history.replaceState,
36
+ isAsync: false,
37
+ onFinished: () => this.updateState(),
38
+ })
39
+ }
40
+ }
@@ -0,0 +1,59 @@
1
+ import { Injectable } from '@furystack/inject'
2
+ import { Disposable, ObservableValue } from '@furystack/utils'
3
+
4
+ export const ScreenSizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const
5
+
6
+ export type ScreenSize = typeof ScreenSizes[number]
7
+
8
+ export type Breakpoint = { name: ScreenSize; minSize: number; maxSize?: number }
9
+
10
+ @Injectable({ lifetime: 'singleton' })
11
+ export class Screen implements Disposable {
12
+ private getOrientation = () => (window.matchMedia('(orientation:landscape').matches ? 'landscape' : 'portrait')
13
+
14
+ public readonly breakpoints: { [K in ScreenSize]: { minSize: number } } = {
15
+ xl: { minSize: 1920 },
16
+ lg: { minSize: 1280 },
17
+ md: { minSize: 960 },
18
+ sm: { minSize: 600 },
19
+ xs: { minSize: 0 },
20
+ }
21
+
22
+ public dispose() {
23
+ window.removeEventListener('resize', this.onResizeListener)
24
+ }
25
+
26
+ public readonly screenSize: {
27
+ atLeast: { [K in ScreenSize]: ObservableValue<boolean> }
28
+ } = {
29
+ atLeast: {
30
+ xs: new ObservableValue<boolean>(this.screenSizeAtLeast('xs')),
31
+ sm: new ObservableValue<boolean>(this.screenSizeAtLeast('sm')),
32
+ md: new ObservableValue<boolean>(this.screenSizeAtLeast('md')),
33
+ lg: new ObservableValue<boolean>(this.screenSizeAtLeast('lg')),
34
+ xl: new ObservableValue<boolean>(this.screenSizeAtLeast('xl')),
35
+ },
36
+ }
37
+
38
+ private screenSizeAtLeast(size: ScreenSize) {
39
+ return window.innerWidth >= this.breakpoints[size].minSize
40
+ }
41
+
42
+ public orientation = new ObservableValue<'landscape' | 'portrait'>(this.getOrientation())
43
+
44
+ private onResizeListener = () => {
45
+ this.refreshValues()
46
+ }
47
+
48
+ private refreshValues() {
49
+ this.orientation.setValue(this.getOrientation())
50
+ ScreenSizes.forEach((size) => {
51
+ this.screenSize.atLeast[size].setValue(this.screenSizeAtLeast(size))
52
+ })
53
+ }
54
+
55
+ constructor() {
56
+ window.addEventListener('resize', this.onResizeListener)
57
+ this.refreshValues()
58
+ }
59
+ }
@@ -0,0 +1,69 @@
1
+ import { ChildrenList, ShadeComponent, isShadeComponent } from './models'
2
+
3
+ /**
4
+ * Appends a list of items to a HTML element
5
+ *
6
+ * @param el the root element
7
+ * @param children array of items to append
8
+ */
9
+ export const appendChild = (el: HTMLElement, children: ChildrenList) => {
10
+ for (const child of children) {
11
+ if (typeof child === 'string') {
12
+ el.appendChild(document.createTextNode(child))
13
+ } else {
14
+ if (child instanceof HTMLElement) {
15
+ el.appendChild(child)
16
+ } else if (child instanceof Array) {
17
+ appendChild(el, child)
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ export const hasStyle = (props: any): props is { style: Partial<CSSStyleDeclaration> } => {
24
+ return props?.style !== undefined
25
+ }
26
+
27
+ /**
28
+ * @param el The Target HTML Element
29
+ * @param props The Properties to fetch The Styles Object
30
+ */
31
+ export const attachStyles = (el: HTMLElement, props: any) => {
32
+ if (hasStyle(props))
33
+ for (const key in props.style) {
34
+ if (Object.prototype.hasOwnProperty.call(props.style, key)) {
35
+ ;(el.style as any)[key] = props.style[key]
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Factory method that creates a component. This should be configured as a default JSX Factory in tsconfig.
42
+ *
43
+ * @param elementType The type of the element (component or stateless component factory method)
44
+ * @param props The props for the component
45
+ * @param children additional rest parameters will be parsed as children objects
46
+ * @returns the created JSX element
47
+ */
48
+ export const createComponent = <TProps>(
49
+ elementType: string | ShadeComponent<TProps>,
50
+ props: TProps,
51
+ ...children: ChildrenList
52
+ ) => {
53
+ if (typeof elementType === 'string') {
54
+ const el = document.createElement(elementType)
55
+ Object.assign(el, props)
56
+
57
+ if (props && (props as any).style) {
58
+ attachStyles(el, props)
59
+ }
60
+ if (children) {
61
+ appendChild(el, children)
62
+ }
63
+ return el
64
+ } else if (isShadeComponent(elementType)) {
65
+ const el = (elementType as ShadeComponent<TProps>)(props, children)
66
+ attachStyles(el, props)
67
+ return el
68
+ }
69
+ }
package/src/shade.ts ADDED
@@ -0,0 +1,201 @@
1
+ import { ObservableValue } from '@furystack/utils'
2
+ import { v4 } from 'uuid'
3
+ import { Injector } from '@furystack/inject'
4
+ import { ChildrenList, RenderOptions } from './models'
5
+
6
+ export type ShadeOptions<TProps, TState> = {
7
+ /**
8
+ * Explicit shadow dom name. Will fall back to 'shade-{guid}' if not provided
9
+ */
10
+ shadowDomName?: string
11
+ /**
12
+ * Render hook, this method will be executed on each and every render.
13
+ */
14
+ render: (options: RenderOptions<TProps, TState>) => JSX.Element
15
+
16
+ /**
17
+ * Construct hook. Will be executed once when the element has been constructed and initialized
18
+ */
19
+ constructed?: (
20
+ options: RenderOptions<TProps, TState>,
21
+ ) => void | undefined | (() => void) | Promise<void | undefined | (() => void)>
22
+
23
+ /**
24
+ * Will be executed when the element is attached to the DOM.
25
+ */
26
+ onAttach?: (options: RenderOptions<TProps, TState>) => void
27
+
28
+ /**
29
+ * Will be executed when the element is detached from the DOM.
30
+ */
31
+ onDetach?: (options: RenderOptions<TProps, TState>) => void
32
+ } & (unknown extends TState
33
+ ? {}
34
+ : {
35
+ /**
36
+ * The initial state of the component
37
+ */
38
+ getInitialState: (options: { injector: Injector; props: TProps }) => TState
39
+ })
40
+
41
+ /**
42
+ * Factory method for creating Shade components
43
+ *
44
+ * @param o for component creation
45
+ * @returns the JSX element
46
+ */
47
+ export const Shade = <TProps, TState = unknown>(o: ShadeOptions<TProps, TState>) => {
48
+ // register shadow-dom element
49
+ const customElementName = o.shadowDomName || `shade-${v4()}`
50
+
51
+ const existing = customElements.get(customElementName)
52
+ if (!existing) {
53
+ customElements.define(
54
+ customElementName,
55
+ class extends HTMLElement implements JSX.Element {
56
+ public connectedCallback() {
57
+ o.onAttach && o.onAttach(this.getRenderOptions())
58
+ this.callConstructed()
59
+ }
60
+
61
+ public disconnectedCallback() {
62
+ o.onDetach && o.onDetach(this.getRenderOptions())
63
+ this.props.dispose()
64
+ this.state.dispose()
65
+ this.shadeChildren.dispose()
66
+ this.cleanup && this.cleanup()
67
+ }
68
+
69
+ /**
70
+ * Will be triggered when updating the external props object
71
+ */
72
+ public props: ObservableValue<TProps & { children?: JSX.Element[] }>
73
+
74
+ /**
75
+ * Will be triggered on state update
76
+ */
77
+ public state: ObservableValue<TState>
78
+
79
+ /**
80
+ * Will be updated when on children change
81
+ */
82
+ public shadeChildren = new ObservableValue<ChildrenList>([])
83
+
84
+ /**
85
+ * @param options Options for rendering the component
86
+ * @returns the JSX element
87
+ */
88
+ public render = (options: RenderOptions<TProps, TState>) => o.render(options)
89
+
90
+ /**
91
+ * @returns values for the current render options
92
+ */
93
+ private getRenderOptions = () => {
94
+ const props = this.props.getValue() || {}
95
+ const getState = () => this.state.getValue()
96
+ return {
97
+ props,
98
+ getState,
99
+ injector: this.injector,
100
+ updateState: (newState: TState, skipRender: boolean) => {
101
+ this.state.setValue({ ...this.state.getValue(), ...newState })
102
+ !skipRender && this.updateComponent()
103
+ },
104
+ children: this.shadeChildren.getValue(),
105
+ element: this,
106
+ } as any as RenderOptions<TProps, TState>
107
+ }
108
+
109
+ /**
110
+ * Updates the component in the DOM.
111
+ */
112
+ public async updateComponent() {
113
+ const newJsx = this.render(this.getRenderOptions())
114
+
115
+ // const selectionState = this.getSelectionState()
116
+
117
+ if (this.hasChildNodes()) {
118
+ this.replaceChild(newJsx, this.firstChild as Node)
119
+ // selectionState && this.restoreSelectionState(selectionState)
120
+ } else {
121
+ this.append(newJsx)
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Finialize the component initialization after it gets the Props. Called by the framework internally
127
+ */
128
+ public callConstructed() {
129
+ ;(o as any).getInitialState &&
130
+ this.state.setValue((o as any).getInitialState({ props: this.props.getValue(), injector: this.injector }))
131
+
132
+ this.updateComponent()
133
+ const cleanupResult = o.constructed && o.constructed(this.getRenderOptions())
134
+ if (cleanupResult instanceof Promise) {
135
+ cleanupResult.then((cleanup) => (this.cleanup = cleanup))
136
+ } else {
137
+ // construct is not async
138
+ this.cleanup = cleanupResult
139
+ }
140
+ }
141
+
142
+ private cleanup: void | (() => void) = undefined
143
+
144
+ private _injector?: Injector
145
+
146
+ private getInjectorFromParent(): Injector | void {
147
+ let parent = this.parentElement
148
+ while (parent) {
149
+ if ((parent as JSX.Element).injector) {
150
+ return (parent as JSX.Element).injector
151
+ }
152
+ parent = parent.parentElement
153
+ }
154
+ }
155
+
156
+ public get injector(): Injector {
157
+ if (this._injector) {
158
+ return this._injector
159
+ }
160
+
161
+ const fromState = (this.state.getValue() as any)?.injector
162
+ if (fromState && fromState instanceof Injector) {
163
+ return fromState
164
+ }
165
+
166
+ const fromProps = (this.props.getValue() as any)?.injector
167
+ if (fromProps && fromProps instanceof Injector) {
168
+ return fromProps
169
+ }
170
+
171
+ const fromParent = this.getInjectorFromParent()
172
+ if (fromParent) {
173
+ this._injector = fromParent
174
+ return fromParent
175
+ }
176
+ throw Error('Injector not set explicitly and not found on parents!')
177
+ }
178
+
179
+ public set injector(i: Injector) {
180
+ this._injector = i
181
+ }
182
+
183
+ constructor(_props: TProps) {
184
+ super()
185
+ this.props = new ObservableValue()
186
+ this.state = new ObservableValue()
187
+ }
188
+ } as any as CustomElementConstructor,
189
+ )
190
+ }
191
+
192
+ return (props: TProps, children: ChildrenList) => {
193
+ const el = document.createElement(customElementName, {
194
+ ...props,
195
+ }) as JSX.Element<TProps, TState>
196
+ el.props.setValue(props)
197
+
198
+ el.shadeChildren.setValue(children)
199
+ return el as JSX.Element
200
+ }
201
+ }