@shortfuse/materialdesignweb 0.5.0 → 0.7.0
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 +155 -77
- package/bin/generate-css.js +12 -0
- package/components/Badge.css +30 -0
- package/components/Badge.js +15 -0
- package/components/Body.css +14 -0
- package/components/Body.js +7 -0
- package/components/BottomAppBar.css +23 -0
- package/components/BottomAppBar.js +25 -0
- package/components/Box.css +31 -0
- package/components/Box.js +24 -0
- package/components/Button.css +146 -0
- package/components/Button.js +95 -0
- package/components/Button.md +61 -0
- package/components/Card.css +109 -0
- package/components/Card.js +82 -0
- package/components/Checkbox.css +77 -0
- package/components/Checkbox.js +59 -0
- package/components/CheckboxIcon.css +89 -0
- package/components/CheckboxIcon.js +41 -0
- package/components/Chip.css +35 -0
- package/components/Chip.js +22 -0
- package/components/Dialog.css +235 -0
- package/components/Dialog.js +327 -0
- package/components/DialogActions.js +13 -0
- package/components/Divider.css +41 -0
- package/components/Divider.js +13 -0
- package/components/ExtendedFab.css +24 -0
- package/components/ExtendedFab.js +11 -0
- package/components/Fab.css +23 -0
- package/components/Fab.js +26 -0
- package/components/FilterChip.css +80 -0
- package/components/FilterChip.js +51 -0
- package/components/Headline.css +14 -0
- package/components/Headline.js +33 -0
- package/components/Icon.css +76 -0
- package/components/Icon.js +174 -0
- package/components/IconButton.css +151 -0
- package/components/IconButton.js +65 -0
- package/components/Input.js +16 -0
- package/components/Label.css +14 -0
- package/components/Label.js +7 -0
- package/components/Layout.css +19 -0
- package/components/Layout.js +12 -0
- package/components/List.css +12 -0
- package/components/List.js +17 -0
- package/components/ListItem.css +224 -0
- package/components/ListItem.js +112 -0
- package/components/ListOption.css +34 -0
- package/components/ListOption.js +122 -0
- package/components/ListSelect.css +9 -0
- package/components/ListSelect.js +206 -0
- package/components/Menu.css +171 -0
- package/components/Menu.js +470 -0
- package/components/MenuItem.css +53 -0
- package/components/MenuItem.js +215 -0
- package/components/Nav.css +17 -0
- package/components/Nav.js +23 -0
- package/components/NavBar.css +34 -0
- package/components/NavBar.js +88 -0
- package/components/NavBarItem.css +41 -0
- package/components/NavBarItem.js +7 -0
- package/components/NavDrawer.css +31 -0
- package/components/NavDrawer.js +13 -0
- package/components/NavDrawerItem.css +42 -0
- package/components/NavDrawerItem.js +12 -0
- package/components/NavItem.css +181 -0
- package/components/NavItem.js +83 -0
- package/components/NavRail.css +47 -0
- package/components/NavRail.js +17 -0
- package/components/NavRailItem.css +25 -0
- package/components/NavRailItem.js +7 -0
- package/components/Option.js +91 -0
- package/components/Outline.css +138 -0
- package/components/Pane.css +261 -0
- package/components/Pane.js +21 -0
- package/components/Progress.css +74 -0
- package/components/Progress.js +67 -0
- package/components/ProgressCircle.css +226 -0
- package/components/ProgressLine.css +155 -0
- package/components/Radio.css +83 -0
- package/components/Radio.js +42 -0
- package/components/RadioIcon.css +73 -0
- package/components/RadioIcon.js +37 -0
- package/components/Ripple.css +74 -0
- package/components/Ripple.js +114 -0
- package/components/SegmentedButton.css +94 -0
- package/components/SegmentedButton.js +49 -0
- package/components/SegmentedButtonGroup.css +12 -0
- package/components/SegmentedButtonGroup.js +44 -0
- package/components/Select.css +52 -0
- package/components/Select.js +71 -0
- package/components/Shape.css +132 -0
- package/components/Shape.js +25 -0
- package/components/Slider.css +306 -0
- package/components/Slider.js +206 -0
- package/components/Snackbar.css +80 -0
- package/components/Snackbar.js +75 -0
- package/components/Surface.css +10 -0
- package/components/Surface.js +23 -0
- package/components/Switch.css +63 -0
- package/components/Switch.js +127 -0
- package/components/SwitchIcon.css +177 -0
- package/components/SwitchIcon.js +89 -0
- package/components/SwitchIconAnimations.css +89 -0
- package/components/Tab.css +85 -0
- package/components/Tab.js +103 -0
- package/components/TabContent.js +151 -0
- package/components/TabList.css +129 -0
- package/components/TabList.js +309 -0
- package/components/TabPanel.js +37 -0
- package/components/TextArea.css +93 -0
- package/components/TextArea.js +229 -0
- package/components/Title.css +14 -0
- package/components/Title.js +15 -0
- package/components/Tooltip.css +40 -0
- package/components/Tooltip.js +22 -0
- package/components/TopAppBar.css +209 -0
- package/components/TopAppBar.js +201 -0
- package/core/Composition.js +988 -0
- package/core/CustomElement.js +844 -0
- package/core/ICustomElement.d.ts +288 -0
- package/core/ICustomElement.js +1 -0
- package/core/css.js +51 -0
- package/core/customTypes.js +125 -0
- package/core/dom.js +56 -154
- package/core/identify.js +40 -0
- package/core/observe.js +410 -0
- package/core/template.js +121 -0
- package/core/typings.d.ts +135 -0
- package/core/typings.js +1 -0
- package/mixins/AriaReflectorMixin.js +42 -0
- package/mixins/AriaToolbarMixin.js +13 -0
- package/mixins/ControlMixin.css +57 -0
- package/mixins/ControlMixin.js +212 -0
- package/mixins/DensityMixin.css +40 -0
- package/mixins/DensityMixin.js +11 -0
- package/mixins/FlexableMixin.css +79 -0
- package/mixins/FlexableMixin.js +32 -0
- package/mixins/FormAssociatedMixin.js +170 -0
- package/mixins/InputMixin.js +335 -0
- package/mixins/KeyboardNavMixin.js +244 -0
- package/mixins/RTLObserverMixin.js +35 -0
- package/mixins/ResizeObserverMixin.js +38 -0
- package/mixins/RippleMixin.css +12 -0
- package/mixins/RippleMixin.js +115 -0
- package/mixins/ScrollListenerMixin.js +100 -0
- package/mixins/ShapeMixin.css +135 -0
- package/mixins/ShapeMixin.js +31 -0
- package/mixins/StateMixin.css +82 -0
- package/mixins/StateMixin.js +114 -0
- package/mixins/SurfaceMixin.css +150 -0
- package/mixins/SurfaceMixin.js +32 -0
- package/mixins/TextFieldMixin.css +657 -0
- package/mixins/TextFieldMixin.js +121 -0
- package/mixins/ThemableMixin.css +204 -0
- package/mixins/ThemableMixin.js +16 -0
- package/mixins/TooltipTriggerMixin.css +27 -0
- package/mixins/TooltipTriggerMixin.js +366 -0
- package/mixins/TouchTargetMixin.css +26 -0
- package/mixins/TouchTargetMixin.js +9 -0
- package/package.json +54 -49
- package/theming/index.js +594 -0
- package/theming/loader.js +24 -0
- package/utils/cli.js +11 -0
- package/utils/color_keywords.js +151 -0
- package/utils/hct/Cam16.js +298 -0
- package/utils/hct/CorePalette.js +84 -0
- package/utils/hct/Hct.js +172 -0
- package/utils/hct/Scheme.js +587 -0
- package/utils/hct/TonalPalette.js +68 -0
- package/utils/hct/ViewingConditions.js +136 -0
- package/utils/hct/blend.js +93 -0
- package/utils/hct/colorUtils.js +302 -0
- package/utils/hct/hctSolver.js +559 -0
- package/utils/hct/helper.js +182 -0
- package/utils/hct/mathUtils.js +153 -0
- package/utils/jsonMergePatch.js +100 -0
- package/utils/jsx-runtime.js +101 -0
- package/utils/popup.js +117 -0
- package/utils/svg.js +129 -0
- package/.browserslistrc +0 -4
- package/.eslintrc.json +0 -204
- package/.stylelintrc.json +0 -645
- package/.vscode/launch.json +0 -31
- package/.vscode/settings.json +0 -3
- package/.vscode/tasks.json +0 -32
- package/CHANGELOG.md +0 -36
- package/CODE_OF_CONDUCT.md +0 -46
- package/adapters/datatable/column.js +0 -176
- package/adapters/datatable/index.js +0 -960
- package/adapters/dom/index.js +0 -586
- package/adapters/list/index.js +0 -69
- package/adapters/search/index.js +0 -495
- package/components/appbar/_spec.scss +0 -165
- package/components/appbar/_theme.scss +0 -0
- package/components/appbar/index.scss +0 -2
- package/components/banner/_spec.scss +0 -83
- package/components/banner/_theme.scss +0 -0
- package/components/banner/index.scss +0 -2
- package/components/bottomnav/README.md +0 -85
- package/components/bottomnav/_spec.scss +0 -149
- package/components/bottomnav/_theme.scss +0 -0
- package/components/bottomnav/index.js +0 -117
- package/components/bottomnav/index.scss +0 -2
- package/components/bottomnav/item.js +0 -88
- package/components/button/README.md +0 -61
- package/components/button/_spec.scss +0 -162
- package/components/button/_theme.scss +0 -42
- package/components/button/index.eta +0 -32
- package/components/button/index.js +0 -43
- package/components/button/index.pug +0 -18
- package/components/button/index.scss +0 -2
- package/components/card/_spec.scss +0 -241
- package/components/card/_theme.scss +0 -0
- package/components/card/index.scss +0 -2
- package/components/chip/_spec.scss +0 -111
- package/components/chip/_theme.scss +0 -105
- package/components/chip/index.js +0 -23
- package/components/chip/index.scss +0 -2
- package/components/chip/item.js +0 -20
- package/components/datatable/_spec.scss +0 -225
- package/components/datatable/_theme.scss +0 -128
- package/components/datatable/cell.js +0 -44
- package/components/datatable/columnheader.js +0 -46
- package/components/datatable/index.js +0 -374
- package/components/datatable/index.scss +0 -2
- package/components/datatable/row.js +0 -48
- package/components/datatable/rowheader.js +0 -18
- package/components/dialog/_spec.scss +0 -203
- package/components/dialog/_theme.scss +0 -7
- package/components/dialog/index.js +0 -601
- package/components/dialog/index.scss +0 -2
- package/components/divider/_spec.scss +0 -11
- package/components/divider/_theme.scss +0 -0
- package/components/divider/index.scss +0 -2
- package/components/elevation/_spec.scss +0 -9
- package/components/elevation/_theme.scss +0 -0
- package/components/elevation/index.scss +0 -2
- package/components/fab/_spec.scss +0 -210
- package/components/fab/_theme.scss +0 -0
- package/components/fab/index.js +0 -99
- package/components/fab/index.scss +0 -2
- package/components/grid/_spec.scss +0 -169
- package/components/grid/_theme.scss +0 -0
- package/components/grid/index.scss +0 -2
- package/components/layout/_mixins.scss +0 -11
- package/components/layout/_spec.scss +0 -916
- package/components/layout/_theme.scss +0 -19
- package/components/layout/index.js +0 -454
- package/components/layout/index.scss +0 -2
- package/components/list/_spec.scss +0 -363
- package/components/list/_theme.scss +0 -102
- package/components/list/content.js +0 -106
- package/components/list/index.js +0 -256
- package/components/list/index.scss +0 -2
- package/components/list/item.js +0 -167
- package/components/list/secondary.js +0 -45
- package/components/menu/_spec.scss +0 -329
- package/components/menu/_theme.scss +0 -0
- package/components/menu/index.js +0 -705
- package/components/menu/index.scss +0 -2
- package/components/menu/item.js +0 -231
- package/components/progress/_spec.scss +0 -156
- package/components/progress/_theme.scss +0 -0
- package/components/progress/index.js +0 -36
- package/components/progress/index.scss +0 -2
- package/components/selection/_spec.scss +0 -376
- package/components/selection/_theme.scss +0 -134
- package/components/selection/index.eta +0 -60
- package/components/selection/index.js +0 -70
- package/components/selection/index.pug +0 -30
- package/components/selection/index.scss +0 -2
- package/components/selection/input.js +0 -54
- package/components/selection/radiogroup.js +0 -40
- package/components/slider/_spec.scss +0 -59
- package/components/slider/_theme.scss +0 -0
- package/components/slider/index.scss +0 -2
- package/components/snackbar/_spec.scss +0 -150
- package/components/snackbar/_theme.scss +0 -0
- package/components/snackbar/index.js +0 -338
- package/components/snackbar/index.scss +0 -2
- package/components/tab/_spec.scss +0 -220
- package/components/tab/_theme.scss +0 -0
- package/components/tab/content.js +0 -210
- package/components/tab/index.js +0 -257
- package/components/tab/index.scss +0 -2
- package/components/tab/item.js +0 -88
- package/components/tab/list.js +0 -196
- package/components/tab/panel.js +0 -54
- package/components/textfield/README.md +0 -179
- package/components/textfield/_spec.scss +0 -763
- package/components/textfield/_theme.scss +0 -264
- package/components/textfield/index.eta +0 -74
- package/components/textfield/index.js +0 -160
- package/components/textfield/index.pug +0 -30
- package/components/textfield/index.scss +0 -2
- package/components/tooltip/_spec.scss +0 -185
- package/components/tooltip/_theme.scss +0 -0
- package/components/tooltip/index.scss +0 -2
- package/components/type/_spec.scss +0 -227
- package/components/type/_theme.scss +0 -0
- package/components/type/index.scss +0 -2
- package/core/_breakpoint.scss +0 -189
- package/core/_elevation.scss +0 -78
- package/core/_length.scss +0 -8
- package/core/_motion.scss +0 -31
- package/core/_platform.scss +0 -12
- package/core/_type.scss +0 -128
- package/core/aria/attributes.js +0 -141
- package/core/aria/button.js +0 -49
- package/core/aria/keyboard.js +0 -92
- package/core/aria/rovingtabindex.js +0 -175
- package/core/aria/tab.js +0 -59
- package/core/document/index.js +0 -39
- package/core/overlay/_spec.scss +0 -28
- package/core/overlay/_theme.scss +0 -147
- package/core/overlay/index.js +0 -95
- package/core/overlay/index.scss +0 -2
- package/core/ripple/_spec.scss +0 -196
- package/core/ripple/_theme.scss +0 -20
- package/core/ripple/index.js +0 -286
- package/core/ripple/index.scss +0 -2
- package/core/theme/_aliases.scss +0 -15
- package/core/theme/_config.scss +0 -8
- package/core/theme/_functions.scss +0 -22
- package/core/theme/_palettes.scss +0 -405
- package/core/theme/_spec.scss +0 -0
- package/core/theme/_theme.scss +0 -268
- package/core/theme/index.js +0 -50
- package/core/theme/index.scss +0 -4
- package/core/throttler.js +0 -42
- package/core/transition/index.js +0 -465
- package/docs/_flex.scss +0 -28
- package/docs/_menuoptions.js +0 -183
- package/docs/_partials/_androidnavbar.eta +0 -5
- package/docs/_partials/_androidstatusbar.eta +0 -13
- package/docs/_partials/_appbar.eta +0 -27
- package/docs/_partials/_buttontest.eta +0 -31
- package/docs/_partials/_header.eta +0 -146
- package/docs/_partials/_navlistitem.eta +0 -16
- package/docs/_partials/_target.eta +0 -1
- package/docs/_sample-utils.js +0 -88
- package/docs/_storage.js +0 -33
- package/docs/docs.scss +0 -331
- package/docs/framework.scss +0 -26
- package/docs/index.eta +0 -12
- package/docs/index.js +0 -7
- package/docs/pages/appbar.eta +0 -108
- package/docs/pages/appbar.js +0 -0
- package/docs/pages/bottomnav.eta +0 -188
- package/docs/pages/bottomnav.js +0 -118
- package/docs/pages/button.eta +0 -124
- package/docs/pages/button.js +0 -224
- package/docs/pages/card.eta +0 -90
- package/docs/pages/card.js +0 -175
- package/docs/pages/chip.eta +0 -122
- package/docs/pages/chip.js +0 -80
- package/docs/pages/color.eta +0 -143
- package/docs/pages/color.js +0 -261
- package/docs/pages/datatable.eta +0 -323
- package/docs/pages/datatable.js +0 -160
- package/docs/pages/dialog.eta +0 -184
- package/docs/pages/dialog.js +0 -174
- package/docs/pages/dom.eta +0 -26
- package/docs/pages/dom.js +0 -140
- package/docs/pages/elevation.eta +0 -35
- package/docs/pages/elevation.js +0 -0
- package/docs/pages/fab.eta +0 -99
- package/docs/pages/fab.js +0 -43
- package/docs/pages/grid.eta +0 -135
- package/docs/pages/grid.js +0 -128
- package/docs/pages/layout.eta +0 -8
- package/docs/pages/layout.js +0 -0
- package/docs/pages/list.eta +0 -465
- package/docs/pages/list.js +0 -8
- package/docs/pages/menu.eta +0 -274
- package/docs/pages/menu.js +0 -213
- package/docs/pages/overlay.eta +0 -69
- package/docs/pages/overlay.js +0 -3
- package/docs/pages/progress.eta +0 -23
- package/docs/pages/progress.js +0 -12
- package/docs/pages/ripple.eta +0 -27
- package/docs/pages/ripple.js +0 -3
- package/docs/pages/search.eta +0 -242
- package/docs/pages/search.js +0 -226
- package/docs/pages/selection.eta +0 -107
- package/docs/pages/selection.js +0 -12
- package/docs/pages/slider.eta +0 -23
- package/docs/pages/slider.js +0 -0
- package/docs/pages/snackbar.eta +0 -83
- package/docs/pages/snackbar.js +0 -157
- package/docs/pages/tab.eta +0 -407
- package/docs/pages/tab.js +0 -152
- package/docs/pages/textfield.eta +0 -487
- package/docs/pages/textfield.js +0 -257
- package/docs/pages/tooltip.eta +0 -92
- package/docs/pages/tooltip.js +0 -0
- package/docs/pages/transition.eta +0 -117
- package/docs/pages/transition.js +0 -52
- package/docs/pages/type.eta +0 -31
- package/docs/pages/type.js +0 -0
- package/docs/postrender.js +0 -41
- package/docs/prerender.js +0 -16
- package/docs/pwa/_dialogs.eta +0 -143
- package/docs/pwa/_menus.eta +0 -16
- package/docs/pwa/pwa-prerender.js +0 -3
- package/docs/pwa/pwa.eta +0 -478
- package/docs/pwa/pwa.js +0 -298
- package/docs/pwa/pwa.scss +0 -31
- package/docs/themes/theme-colored.scss +0 -15
- package/docs/themes/theme-default.scss +0 -3
- package/index.scss +0 -27
- package/jsconfig.json +0 -16
- package/scripts/deploy-docs.sh +0 -9
- package/templates/index.eta +0 -2
- package/templates/index.pug +0 -3
- package/tsconfig.json +0 -16
- package/webpack.config.js +0 -304
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
/* eslint-disable sort-class-members/sort-class-members */
|
|
2
|
+
import { generateCSSStyleSheets, generateHTMLStyleElements } from './css.js';
|
|
3
|
+
import { identifierFromElement } from './identify.js';
|
|
4
|
+
import { observeFunction } from './observe.js';
|
|
5
|
+
import { generateFragment, inlineFunctions } from './template.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @template T
|
|
9
|
+
* @typedef {Composition<?>|HTMLStyleElement|CSSStyleSheet|DocumentFragment|((this:T, changes:T) => any)|string} CompositionPart
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @template {any} T
|
|
14
|
+
* @callback Compositor
|
|
15
|
+
* @param {...(CompositionPart<T>)} parts source for interpolation (not mutated)
|
|
16
|
+
* @return {Composition<T>}
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @template T
|
|
21
|
+
* @typedef {Object} WatcherBindEntry
|
|
22
|
+
* @prop {Function} fn
|
|
23
|
+
* @prop {Set<keyof T & string>} props
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @template {any} T
|
|
28
|
+
* @typedef {Object} NodeBindEntry
|
|
29
|
+
* @prop {string} id
|
|
30
|
+
* @prop {number} nodeType
|
|
31
|
+
* @prop {string} node
|
|
32
|
+
* @prop {boolean} [negate]
|
|
33
|
+
* @prop {boolean} [doubleNegate]
|
|
34
|
+
* @prop {Function} [fn]
|
|
35
|
+
* @prop {Set<keyof T & string>} props
|
|
36
|
+
* @prop {T} defaultValue
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** Splits: `{template}text{template}` as `['', 'template', 'text', 'template', '']` */
|
|
40
|
+
const STRING_INTERPOLATION_REGEX = /{([^}]*)}/g;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns event listener bound to shadow root host.
|
|
44
|
+
* Use this function to avoid generating extra closures
|
|
45
|
+
* @this {HTMLElement}
|
|
46
|
+
* @param {Function} fn
|
|
47
|
+
*/
|
|
48
|
+
function buildShadowRootChildListener(fn) {
|
|
49
|
+
/** @param {Event & {currentTarget:{getRootNode: () => ShadowRoot}}} event */
|
|
50
|
+
return function onShadowRootChildEvent(event) {
|
|
51
|
+
const host = event.currentTarget.getRootNode().host;
|
|
52
|
+
fn.call(host, event);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} object
|
|
59
|
+
* @param {'dot'|'bracket'} [syntax]
|
|
60
|
+
* @param {Object} [target]
|
|
61
|
+
* @param {string} [scope]
|
|
62
|
+
* @return {Object}
|
|
63
|
+
*/
|
|
64
|
+
function flattenObject(object, syntax = 'dot', target = {}, scope = '') {
|
|
65
|
+
for (const [key, value] of Object.entries(object)) {
|
|
66
|
+
if (!key) continue; // Blank keys are not supported;
|
|
67
|
+
const scopedKey = scope ? `${scope}.${key}` : key;
|
|
68
|
+
target[scopedKey] = value;
|
|
69
|
+
if (value != null && typeof value === 'object') {
|
|
70
|
+
flattenObject(value, syntax, target, scopedKey);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(object)) {
|
|
74
|
+
const scopedKey = scope ? `${scope}.length` : 'length';
|
|
75
|
+
target[scopedKey] = object.length;
|
|
76
|
+
}
|
|
77
|
+
return target;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @example
|
|
82
|
+
* entryFromPropName(
|
|
83
|
+
* 'address.home.houseNumber',
|
|
84
|
+
* {
|
|
85
|
+
* address: {
|
|
86
|
+
* home: {
|
|
87
|
+
* houseNumber:35,
|
|
88
|
+
* },
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
* ) === {value:35}
|
|
92
|
+
* @param {string} prop
|
|
93
|
+
* @param {any} source
|
|
94
|
+
* @return {null|[string, any]}
|
|
95
|
+
*/
|
|
96
|
+
function entryFromPropName(prop, source) {
|
|
97
|
+
let value = source;
|
|
98
|
+
let child;
|
|
99
|
+
for (child of prop.split('.')) {
|
|
100
|
+
if (!child) throw new Error(`Invalid property: ${prop}`);
|
|
101
|
+
if (child in value === false) return null;
|
|
102
|
+
// @ts-ignore Skip cast
|
|
103
|
+
value = value[child];
|
|
104
|
+
}
|
|
105
|
+
if (value === source) return null;
|
|
106
|
+
return [child, value];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} prop
|
|
111
|
+
* @param {any} source
|
|
112
|
+
* @return {any}
|
|
113
|
+
*/
|
|
114
|
+
function valueFromPropName(prop, source) {
|
|
115
|
+
let value = source;
|
|
116
|
+
for (const child of prop.split('.')) {
|
|
117
|
+
if (!child) return null;
|
|
118
|
+
// @ts-ignore Skip cast
|
|
119
|
+
value = value[child];
|
|
120
|
+
if (value == null) return null;
|
|
121
|
+
}
|
|
122
|
+
if (value === source) return null;
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @template T */
|
|
127
|
+
export default class Composition {
|
|
128
|
+
/**
|
|
129
|
+
* Collection of property bindings.
|
|
130
|
+
* @type {Map<keyof T & string, Set<NodeBindEntry<?>>>}
|
|
131
|
+
*/
|
|
132
|
+
bindings = new Map();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Data of arrays used in templates
|
|
136
|
+
* Usage of a [_for] will create an ArrayLike expectation based on key
|
|
137
|
+
* Only store metadata, not actual data. Currently only needs length.
|
|
138
|
+
* TBD if more is needed later
|
|
139
|
+
* Referenced by property key (string)
|
|
140
|
+
* @type {Map<keyof T & string, ArrayMetadata<T>}
|
|
141
|
+
*/
|
|
142
|
+
arrayMetadata = new Map();
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Collection of events to bind.
|
|
146
|
+
* Indexed by ID
|
|
147
|
+
* @type {Map<string, Set<import('./typings.js').CompositionEventListener<any>>>}
|
|
148
|
+
*/
|
|
149
|
+
events = new Map();
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Snapshot of composition at initial state.
|
|
153
|
+
* This fragment can be cloned for first rendering, instead of calling
|
|
154
|
+
* of using `render()` to construct the initial DOM tree.
|
|
155
|
+
* @type {DocumentFragment}
|
|
156
|
+
*/
|
|
157
|
+
cloneable;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Result of interpolation of the composition template.
|
|
161
|
+
* Includes all DOM elements, which is used to reference for adding and
|
|
162
|
+
* removing DOM elements during render.
|
|
163
|
+
* @type {DocumentFragment}
|
|
164
|
+
*/
|
|
165
|
+
interpolation;
|
|
166
|
+
|
|
167
|
+
/** @type {(HTMLStyleElement|CSSStyleSheet)[]} */
|
|
168
|
+
styles = [];
|
|
169
|
+
|
|
170
|
+
/** @type {CSSStyleSheet[]} */
|
|
171
|
+
adoptedStyleSheets = [];
|
|
172
|
+
|
|
173
|
+
/** @type {DocumentFragment} */
|
|
174
|
+
stylesFragment;
|
|
175
|
+
|
|
176
|
+
/** @type {((this:T, changes:T) => any)[]} */
|
|
177
|
+
watchers = [];
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Maintains a reference list of elements used by render target (root).
|
|
181
|
+
* When root is garbage collected, references are released.
|
|
182
|
+
* This includes disconnected elements.
|
|
183
|
+
* @type {WeakMap<Element|DocumentFragment, Map<string,HTMLElement>>}
|
|
184
|
+
*/
|
|
185
|
+
referenceCache = new WeakMap();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Part of interpolation phase.
|
|
189
|
+
* Maintains a reference list of conditional elements that were removed from
|
|
190
|
+
* `cloneable` due to default state. Used to reconstruct conditional elements
|
|
191
|
+
* with conditional children in default state as well (unlike `interpolation`).
|
|
192
|
+
* @type {Map<string, {element: Element, id: string, parentId: string, commentCache: WeakMap<Element|DocumentFragment,Comment>}>}
|
|
193
|
+
*/
|
|
194
|
+
conditionalElementMetadata = new Map();
|
|
195
|
+
|
|
196
|
+
/** Flag set when template and styles have been interpolated */
|
|
197
|
+
interpolated = false;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {(CompositionPart<T>)[]} parts
|
|
201
|
+
*/
|
|
202
|
+
constructor(...parts) {
|
|
203
|
+
/**
|
|
204
|
+
* Template used to build interpolation and cloneable
|
|
205
|
+
*/
|
|
206
|
+
this.template = generateFragment();
|
|
207
|
+
this.append(...parts);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
* [Symbol.iterator]() {
|
|
211
|
+
for (const part of this.styles) {
|
|
212
|
+
yield part;
|
|
213
|
+
}
|
|
214
|
+
yield this.template;
|
|
215
|
+
for (const part of this.watchers) {
|
|
216
|
+
yield part;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {CompositionPart<T>[]} parts
|
|
222
|
+
*/
|
|
223
|
+
append(...parts) {
|
|
224
|
+
for (const part of parts) {
|
|
225
|
+
if (typeof part === 'string') {
|
|
226
|
+
this.append(generateFragment(part.trim()));
|
|
227
|
+
} else if (typeof part === 'function') {
|
|
228
|
+
this.watchers.push(part);
|
|
229
|
+
} else if (part instanceof Composition) {
|
|
230
|
+
this.append(...part);
|
|
231
|
+
} else if (part instanceof DocumentFragment) {
|
|
232
|
+
this.template.append(part);
|
|
233
|
+
} else if (part instanceof CSSStyleSheet || part instanceof HTMLStyleElement) {
|
|
234
|
+
this.styles.push(part);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Allow chaining
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** @param {import('./typings.js').CompositionEventListener<T>} listener */
|
|
242
|
+
addCompositionEventListener(listener) {
|
|
243
|
+
const key = listener.id ?? '';
|
|
244
|
+
let set = this.events.get(key);
|
|
245
|
+
if (!set) {
|
|
246
|
+
set = new Set();
|
|
247
|
+
this.events.set(key, set);
|
|
248
|
+
}
|
|
249
|
+
set.add(listener);
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Updates component nodes based on data.
|
|
255
|
+
* Expects data in JSON Merge Patch format
|
|
256
|
+
* @see https://www.rfc-editor.org/rfc/rfc7386
|
|
257
|
+
* @param {DocumentFragment|ShadowRoot} root where
|
|
258
|
+
* @param {Partial<?>} changes what
|
|
259
|
+
* @param {any} [context] who
|
|
260
|
+
* @param {Partial<?>} [store] If needed, where to grab extra props
|
|
261
|
+
* @return {void}
|
|
262
|
+
*/
|
|
263
|
+
render(root, changes, context, store) {
|
|
264
|
+
if (!this.initiallyRendered) this.initialRender(root, changes);
|
|
265
|
+
|
|
266
|
+
if (!changes) return;
|
|
267
|
+
|
|
268
|
+
const fnResults = new WeakMap();
|
|
269
|
+
/** @type {WeakMap<Element, Set<string>>} */
|
|
270
|
+
const modifiedNodes = new WeakMap();
|
|
271
|
+
|
|
272
|
+
// Iterate data instead of bindings.
|
|
273
|
+
// TODO: Avoid double iteration and flatten on-the-fly
|
|
274
|
+
const flattened = flattenObject(changes);
|
|
275
|
+
|
|
276
|
+
for (const [key, rawValue] of Object.entries(flattened)) {
|
|
277
|
+
const entries = this.bindings.get(key);
|
|
278
|
+
if (!entries) continue;
|
|
279
|
+
for (const { id, node, nodeType, fn, props, negate, doubleNegate } of entries) {
|
|
280
|
+
/* 1. Find Element */
|
|
281
|
+
|
|
282
|
+
// TODO: Avoid unnecessary element creation.
|
|
283
|
+
// If element can be fully reconstructed with internal properties,
|
|
284
|
+
// skip recreation of element unless it actually needs to added to DOM.
|
|
285
|
+
// Requires tracing of all properties used by conditional elements.
|
|
286
|
+
const ref = this.getElement(root, id);
|
|
287
|
+
if (!ref) {
|
|
288
|
+
console.warn('Non existent id', id);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!ref) continue;
|
|
292
|
+
if (modifiedNodes.get(ref)?.has(node)) {
|
|
293
|
+
// console.warn('Node already modified. Skipping', id, node);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// if (!ref.parentElement && node !== '_if') {
|
|
298
|
+
// if (ref.parentNode === root) {
|
|
299
|
+
// console.debug('Offscreen? root? rendering', ref, node, ref.id, root.host.outerHTML);
|
|
300
|
+
// } else {
|
|
301
|
+
// console.debug('Offscreen rendering', ref, node, ref.id, root.host.outerHTML);
|
|
302
|
+
// }
|
|
303
|
+
// }
|
|
304
|
+
|
|
305
|
+
/* 2. Compute value */
|
|
306
|
+
let value;
|
|
307
|
+
if (fn) {
|
|
308
|
+
if (fnResults.has(fn)) {
|
|
309
|
+
value = fnResults.get(fn);
|
|
310
|
+
} else {
|
|
311
|
+
const args = structuredClone(changes);
|
|
312
|
+
for (const prop of props) {
|
|
313
|
+
if (prop in flattened) continue;
|
|
314
|
+
let lastIndexOfDot = prop.lastIndexOf('.');
|
|
315
|
+
if (lastIndexOfDot === -1) {
|
|
316
|
+
// console.debug('injected shallow', prop);
|
|
317
|
+
args[prop] = store[prop];
|
|
318
|
+
} else {
|
|
319
|
+
// Relying on props being sorted...
|
|
320
|
+
console.debug('need deep', prop);
|
|
321
|
+
let entry;
|
|
322
|
+
let propSearchKey = prop;
|
|
323
|
+
let lastPropSearchKey = prop;
|
|
324
|
+
while (!entry) {
|
|
325
|
+
entry = entryFromPropName(propSearchKey, args);
|
|
326
|
+
if (entry) {
|
|
327
|
+
const propName = lastPropSearchKey.slice(propSearchKey.length + 1);
|
|
328
|
+
entry[1][propName] = valueFromPropName(lastPropSearchKey, store);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
if (lastIndexOfDot === -1) break;
|
|
332
|
+
lastPropSearchKey = prop;
|
|
333
|
+
propSearchKey = prop.slice(0, lastIndexOfDot);
|
|
334
|
+
lastIndexOfDot = propSearchKey.lastIndexOf(',');
|
|
335
|
+
}
|
|
336
|
+
if (!entry) {
|
|
337
|
+
console.warn('what do?');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
value = fn.call(context, args);
|
|
342
|
+
fnResults.set(fn, value);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
value = rawValue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* 3. Operate on value */
|
|
349
|
+
if (doubleNegate) {
|
|
350
|
+
value = !!value;
|
|
351
|
+
} else if (negate) {
|
|
352
|
+
value = !value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* 4. Find Target Node */
|
|
356
|
+
if (nodeType === Node.TEXT_NODE) {
|
|
357
|
+
const index = (node === '#text')
|
|
358
|
+
? 0
|
|
359
|
+
: Number.parseInt(node.slice('#text'.length), 10);
|
|
360
|
+
let nodesFound = 0;
|
|
361
|
+
for (const childNode of ref.childNodes) {
|
|
362
|
+
if (childNode.nodeType !== Node.TEXT_NODE) continue;
|
|
363
|
+
if (index !== nodesFound++) continue;
|
|
364
|
+
childNode.nodeValue = value ?? '';
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
if (index > nodesFound) {
|
|
368
|
+
console.warn('Node not found, adding?');
|
|
369
|
+
ref.append(value);
|
|
370
|
+
}
|
|
371
|
+
} else if (node === '_if') {
|
|
372
|
+
const attached = root.contains(ref);
|
|
373
|
+
const orphaned = ref.parentElement == null && ref.parentNode !== root;
|
|
374
|
+
const shouldShow = value !== null && value !== false;
|
|
375
|
+
if (orphaned && ref.parentNode) {
|
|
376
|
+
console.warn('Orphaned with parent node?', id, { attached, orphaned, shouldShow }, ref.parentNode);
|
|
377
|
+
}
|
|
378
|
+
if (attached !== !orphaned) {
|
|
379
|
+
console.warn('Conditional state', id, { attached, orphaned, shouldShow });
|
|
380
|
+
console.warn('Not attached and not orphaned. Should do nothing?', ref, ref.parentElement);
|
|
381
|
+
}
|
|
382
|
+
if (shouldShow) {
|
|
383
|
+
if (orphaned) {
|
|
384
|
+
const metadata = this.conditionalElementMetadata.get(id);
|
|
385
|
+
if (!metadata) {
|
|
386
|
+
console.error(id);
|
|
387
|
+
throw new Error('Could not find conditional element metadata');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let comment = metadata.commentCache.get(root);
|
|
391
|
+
if (!comment) {
|
|
392
|
+
console.debug('Composition: Comment not cached, building first time');
|
|
393
|
+
const parent = metadata.parentId
|
|
394
|
+
? this.getElement(root, metadata.parentId)
|
|
395
|
+
: root;
|
|
396
|
+
if (!parent) {
|
|
397
|
+
console.error(id);
|
|
398
|
+
throw new Error('Could not find reference parent!');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const commentText = `{#${id}}`;
|
|
402
|
+
for (const child of parent.childNodes) {
|
|
403
|
+
if (child.nodeType !== Node.COMMENT_NODE) continue;
|
|
404
|
+
if ((/** @type {Comment} */child).nodeValue === commentText) {
|
|
405
|
+
comment = child;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
metadata.commentCache.set(this, comment);
|
|
410
|
+
}
|
|
411
|
+
if (comment) {
|
|
412
|
+
console.debug('Composition: Add', id, 'back', ref.outerHTML);
|
|
413
|
+
comment.replaceWith(ref);
|
|
414
|
+
} else {
|
|
415
|
+
console.warn('Could not add', id, 'back to parent');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} else if (!orphaned) {
|
|
419
|
+
const metadata = this.conditionalElementMetadata.get(id);
|
|
420
|
+
if (!metadata) {
|
|
421
|
+
console.error(id);
|
|
422
|
+
throw new Error(`Could not find conditional element metadata for ${id}`);
|
|
423
|
+
}
|
|
424
|
+
let comment = metadata.commentCache.get(root);
|
|
425
|
+
if (!comment) {
|
|
426
|
+
comment = new Comment(`{#${id}}`);
|
|
427
|
+
metadata.commentCache.set(this, comment);
|
|
428
|
+
}
|
|
429
|
+
console.debug('Composition: Remove', id, ref.outerHTML);
|
|
430
|
+
ref.replaceWith(comment);
|
|
431
|
+
}
|
|
432
|
+
} else if (value === false || value == null) {
|
|
433
|
+
ref.removeAttribute(node);
|
|
434
|
+
} else {
|
|
435
|
+
ref.setAttribute(node, value === true ? '' : value);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* 5. Mark Node as modified */
|
|
439
|
+
let set = modifiedNodes.get(ref);
|
|
440
|
+
if (!set) {
|
|
441
|
+
set = new Set();
|
|
442
|
+
modifiedNodes.set(ref, set);
|
|
443
|
+
}
|
|
444
|
+
set.add(node);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* @param {Attr|Text} node
|
|
451
|
+
* @param {Element} element
|
|
452
|
+
* @param {Object} [defaults]
|
|
453
|
+
* @param {string} [parsedValue]
|
|
454
|
+
* @return {boolean} Remove node
|
|
455
|
+
*/
|
|
456
|
+
#interpolateNode(node, element, defaults, parsedValue) {
|
|
457
|
+
const { nodeValue, nodeName, nodeType } = node;
|
|
458
|
+
|
|
459
|
+
if (parsedValue == null) {
|
|
460
|
+
if (!nodeValue) return false;
|
|
461
|
+
const trimmed = nodeValue.trim();
|
|
462
|
+
if (!trimmed) return false;
|
|
463
|
+
if (nodeType === Node.ATTRIBUTE_NODE) {
|
|
464
|
+
if (trimmed[0] !== '{') return false;
|
|
465
|
+
const { length } = trimmed;
|
|
466
|
+
if (trimmed[length - 1] !== '}') return false;
|
|
467
|
+
parsedValue = trimmed.slice(1, -1);
|
|
468
|
+
} else {
|
|
469
|
+
// Split text node into segments
|
|
470
|
+
// TODO: Benchmark indexOf pre-check vs regex
|
|
471
|
+
|
|
472
|
+
const segments = trimmed.split(STRING_INTERPOLATION_REGEX);
|
|
473
|
+
if (segments.length < 3) return false;
|
|
474
|
+
if (segments.length === 3 && !segments[0] && !segments[2]) {
|
|
475
|
+
parsedValue = segments[1];
|
|
476
|
+
} else {
|
|
477
|
+
segments.forEach((segment, index) => {
|
|
478
|
+
// is even = is template string
|
|
479
|
+
if (index % 2) {
|
|
480
|
+
const newNode = new Text();
|
|
481
|
+
node.before(newNode);
|
|
482
|
+
this.#interpolateNode(newNode, element, defaults, segment);
|
|
483
|
+
} else {
|
|
484
|
+
if (!segment) return; // blank
|
|
485
|
+
node.before(segment);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
// node.remove();
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const negate = parsedValue[0] === '!';
|
|
495
|
+
let doubleNegate = false;
|
|
496
|
+
if (negate) {
|
|
497
|
+
parsedValue = parsedValue.slice(1);
|
|
498
|
+
doubleNegate = parsedValue[0] === '!';
|
|
499
|
+
if (doubleNegate) {
|
|
500
|
+
parsedValue = parsedValue.slice(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let isEvent;
|
|
505
|
+
let textNodeIndex;
|
|
506
|
+
|
|
507
|
+
if (nodeType === Node.TEXT_NODE) {
|
|
508
|
+
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
509
|
+
if (element !== node.parentElement) {
|
|
510
|
+
console.warn('mismatch?');
|
|
511
|
+
element = node.parentElement;
|
|
512
|
+
}
|
|
513
|
+
textNodeIndex = 0;
|
|
514
|
+
let prev = node;
|
|
515
|
+
while ((prev = prev.previousSibling)) {
|
|
516
|
+
if (prev.nodeType === Node.TEXT_NODE) {
|
|
517
|
+
textNodeIndex++;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
// @ts-ignore Skip cast
|
|
522
|
+
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
523
|
+
if (element !== node.ownerElement) {
|
|
524
|
+
console.warn('mismatch?');
|
|
525
|
+
element = node.ownerElement;
|
|
526
|
+
}
|
|
527
|
+
if (nodeName.startsWith('on')) {
|
|
528
|
+
// Do not interpolate inline event listeners
|
|
529
|
+
if (nodeName[2] !== '-') return false;
|
|
530
|
+
isEvent = true;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const id = identifierFromElement(element, true);
|
|
535
|
+
|
|
536
|
+
if (isEvent) {
|
|
537
|
+
const eventType = nodeName.slice(3);
|
|
538
|
+
const [, flags, type] = eventType.match(/^([*1~]+)?(.*)$/);
|
|
539
|
+
const options = {
|
|
540
|
+
once: flags?.includes('1'),
|
|
541
|
+
passive: flags?.includes('~'),
|
|
542
|
+
capture: flags?.includes('*'),
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
element.removeAttribute(nodeName);
|
|
546
|
+
|
|
547
|
+
let set = this.events.get(id);
|
|
548
|
+
if (!set) {
|
|
549
|
+
set = new Set();
|
|
550
|
+
this.events.set(id, set);
|
|
551
|
+
}
|
|
552
|
+
if (parsedValue.startsWith('#')) {
|
|
553
|
+
set.add({ type, handleEvent: inlineFunctions.get(parsedValue).fn, ...options });
|
|
554
|
+
} else {
|
|
555
|
+
set.add({ type, prop: parsedValue, ...options });
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** @type {Function} */
|
|
561
|
+
let fn;
|
|
562
|
+
/** @type {Set<string>} */
|
|
563
|
+
let props;
|
|
564
|
+
|
|
565
|
+
/** @type {any} */
|
|
566
|
+
let defaultValue;
|
|
567
|
+
let inlineFunctionOptions;
|
|
568
|
+
// Is Inline Function?
|
|
569
|
+
if (parsedValue.startsWith('#')) {
|
|
570
|
+
inlineFunctionOptions = inlineFunctions.get(parsedValue);
|
|
571
|
+
if (!inlineFunctionOptions) {
|
|
572
|
+
console.warn(`Invalid interpolation value: ${parsedValue}`);
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
if (inlineFunctionOptions.props) {
|
|
576
|
+
console.log('This function has already been called. Reuse props', inlineFunctionOptions, this);
|
|
577
|
+
props = inlineFunctionOptions.props;
|
|
578
|
+
defaultValue = inlineFunctionOptions.defaultValue ?? null;
|
|
579
|
+
} else {
|
|
580
|
+
defaultValue = inlineFunctionOptions.fn;
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
defaultValue = valueFromPropName(parsedValue, defaults);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (!props) {
|
|
587
|
+
if (typeof defaultValue === 'function') {
|
|
588
|
+
// Value must be reinterpolated and function observed
|
|
589
|
+
const observeResult = observeFunction.call(this, defaultValue, defaults);
|
|
590
|
+
fn = defaultValue;
|
|
591
|
+
defaultValue = observeResult.defaultValue;
|
|
592
|
+
props = observeResult.props;
|
|
593
|
+
// console.log(this.static.name, fn.name || parsedValue, combinedSet);
|
|
594
|
+
} else {
|
|
595
|
+
props = new Set([parsedValue]);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (typeof defaultValue === 'symbol') {
|
|
600
|
+
console.warn(': Invalid binding:', parsedValue);
|
|
601
|
+
defaultValue = null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (doubleNegate) {
|
|
605
|
+
defaultValue = !!defaultValue;
|
|
606
|
+
} else if (negate) {
|
|
607
|
+
defaultValue = !defaultValue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (inlineFunctionOptions) {
|
|
611
|
+
inlineFunctionOptions.defaultValue = defaultValue;
|
|
612
|
+
inlineFunctionOptions.props = props;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Bind
|
|
616
|
+
const parsedNodeName = textNodeIndex ? nodeName + textNodeIndex : nodeName;
|
|
617
|
+
const entry = { id, node: parsedNodeName, fn, props, nodeType, defaultValue, negate, doubleNegate };
|
|
618
|
+
for (const prop of props) {
|
|
619
|
+
let set = this.bindings.get(prop);
|
|
620
|
+
if (!set) {
|
|
621
|
+
set = new Set();
|
|
622
|
+
this.bindings.set(prop, set);
|
|
623
|
+
}
|
|
624
|
+
set.add(entry);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Mutate
|
|
628
|
+
|
|
629
|
+
if (nodeType === Node.TEXT_NODE) {
|
|
630
|
+
node.nodeValue = defaultValue ?? '';
|
|
631
|
+
} else if (nodeName === '_if') {
|
|
632
|
+
element.removeAttribute(nodeName);
|
|
633
|
+
if (defaultValue == null || defaultValue === false) {
|
|
634
|
+
// If default state is removed, mark for removal
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
} else if (defaultValue == null || defaultValue === false) {
|
|
638
|
+
element.removeAttribute(nodeName);
|
|
639
|
+
} else {
|
|
640
|
+
element.setAttribute(nodeName, defaultValue === true ? '' : defaultValue);
|
|
641
|
+
}
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* @param {Object} [defaults]
|
|
647
|
+
*/
|
|
648
|
+
interpolate(defaults) {
|
|
649
|
+
// console.log('Template', [...this.template.children].map((child) => child.outerHTML).join('\n'));
|
|
650
|
+
|
|
651
|
+
// Copy template before working on it
|
|
652
|
+
// Store into `cloneable` to split later into `interpolation`
|
|
653
|
+
this.cloneable = /** @type {DocumentFragment} */ (this.template.cloneNode(true));
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Track elements to be removed before using for cloning
|
|
657
|
+
* @type {Element[]}
|
|
658
|
+
*/
|
|
659
|
+
const removalList = [];
|
|
660
|
+
|
|
661
|
+
const TREE_WALKER_FILTER = 5; /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT */
|
|
662
|
+
|
|
663
|
+
const treeWalker = document.createTreeWalker(this.cloneable, TREE_WALKER_FILTER);
|
|
664
|
+
let node = treeWalker.nextNode();
|
|
665
|
+
while (node) {
|
|
666
|
+
/** @type {Element} */
|
|
667
|
+
let element = null;
|
|
668
|
+
let removeElement = false;
|
|
669
|
+
switch (node.nodeType) {
|
|
670
|
+
case Node.ELEMENT_NODE:
|
|
671
|
+
element = node;
|
|
672
|
+
if (element instanceof HTMLTemplateElement) {
|
|
673
|
+
node = treeWalker.nextSibling();
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (node instanceof HTMLStyleElement) {
|
|
677
|
+
// Move style elements out of cloneable
|
|
678
|
+
if (node.parentNode === this.cloneable) {
|
|
679
|
+
this.styles.push(node);
|
|
680
|
+
node.remove();
|
|
681
|
+
node = treeWalker.nextSibling();
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
console.warn('<style> element not moved');
|
|
685
|
+
}
|
|
686
|
+
if (node instanceof HTMLScriptElement) {
|
|
687
|
+
console.warn('<script> element found.');
|
|
688
|
+
node.remove();
|
|
689
|
+
node = treeWalker.nextSibling();
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
for (const attr of [...element.attributes].reverse()) {
|
|
693
|
+
if (attr.nodeName === '_if') {
|
|
694
|
+
// Ensure elements to be removed has identifiable parent
|
|
695
|
+
const id = identifierFromElement(element, true);
|
|
696
|
+
const parentId = element.parentElement
|
|
697
|
+
? identifierFromElement(element.parentElement, true)
|
|
698
|
+
: null;
|
|
699
|
+
this.conditionalElementMetadata.set(id, {
|
|
700
|
+
element,
|
|
701
|
+
id,
|
|
702
|
+
parentId,
|
|
703
|
+
commentCache: new WeakMap(),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
removeElement ||= this.#interpolateNode(attr, element, defaults);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
break;
|
|
710
|
+
case Node.TEXT_NODE:
|
|
711
|
+
element = node.parentNode;
|
|
712
|
+
if (this.#interpolateNode(/** @type {Text} */ (node), element, defaults)) {
|
|
713
|
+
const nextNode = treeWalker.nextNode();
|
|
714
|
+
node.remove();
|
|
715
|
+
node = nextNode;
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
break;
|
|
720
|
+
default:
|
|
721
|
+
throw new Error(`Unexpected node type: ${node.nodeType}`);
|
|
722
|
+
}
|
|
723
|
+
if (removeElement) {
|
|
724
|
+
removalList.push(element);
|
|
725
|
+
}
|
|
726
|
+
node = treeWalker.nextNode();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Split into `interpolation` before removing elements
|
|
730
|
+
/** @type {DocumentFragment} */
|
|
731
|
+
this.interpolation = /** @type {DocumentFragment} */ (this.cloneable.cloneNode(true));
|
|
732
|
+
|
|
733
|
+
// console.debug('Interpolated', [...this.interpolation.children].map((child) => child.outerHTML).join('\n'));
|
|
734
|
+
|
|
735
|
+
// Remove elements from `cloneable` and place comment placeholders
|
|
736
|
+
// Remove in reverse so conditionals within conditionals are properly isolated
|
|
737
|
+
for (const element of [...removalList].reverse()) {
|
|
738
|
+
const { id } = element;
|
|
739
|
+
element.replaceWith(new Comment(`{#${id}}`));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
for (const watcher of this.watchers) {
|
|
743
|
+
this.bindWatcher(watcher, defaults);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if ('adoptedStyleSheets' in document) {
|
|
747
|
+
this.adoptedStyleSheets = [
|
|
748
|
+
...generateCSSStyleSheets(this.styles),
|
|
749
|
+
];
|
|
750
|
+
} else {
|
|
751
|
+
this.stylesFragment = generateFragment();
|
|
752
|
+
this.stylesFragment.append(
|
|
753
|
+
...generateHTMLStyleElements(this.styles),
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
this.interpolated = true;
|
|
758
|
+
|
|
759
|
+
// console.log('Cloneable', [...this.cloneable.children].map((child) => child.outerHTML).join('\n'));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Updates component nodes based on data
|
|
764
|
+
* Expects data in JSON Merge Patch format
|
|
765
|
+
* @see https://www.rfc-editor.org/rfc/rfc7386
|
|
766
|
+
* @param {DocumentFragment|ShadowRoot} root where
|
|
767
|
+
* @param {Partial<?>} data what
|
|
768
|
+
* @return {void}
|
|
769
|
+
*/
|
|
770
|
+
initialRender(root, data) {
|
|
771
|
+
if (!this.interpolated) this.interpolate(data);
|
|
772
|
+
|
|
773
|
+
if ('adoptedStyleSheets' in root) {
|
|
774
|
+
root.adoptedStyleSheets = [
|
|
775
|
+
...root.adoptedStyleSheets,
|
|
776
|
+
...this.adoptedStyleSheets,
|
|
777
|
+
];
|
|
778
|
+
} else if (root instanceof ShadowRoot) {
|
|
779
|
+
root.append(this.stylesFragment.cloneNode(true));
|
|
780
|
+
} else {
|
|
781
|
+
// TODO: Support document styles
|
|
782
|
+
// console.warn('Cannot apply styles to singular element');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
root.append(this.cloneable.cloneNode(true));
|
|
786
|
+
|
|
787
|
+
// console.log('Initial render', [...root.children].map((child) => child.outerHTML).join('\n'));
|
|
788
|
+
|
|
789
|
+
/** @type {EventTarget} */
|
|
790
|
+
const rootEventTarget = root instanceof ShadowRoot ? root.host : root;
|
|
791
|
+
// Bind events in reverse order to support stopImmediatePropagation
|
|
792
|
+
for (const [id, events] of [...this.events].reverse()) {
|
|
793
|
+
// Prepare all event listeners first
|
|
794
|
+
for (const entry of [...events].reverse()) {
|
|
795
|
+
let listener = entry.listener;
|
|
796
|
+
if (!listener) {
|
|
797
|
+
if (root instanceof ShadowRoot) {
|
|
798
|
+
listener = entry.handleEvent ?? valueFromPropName(entry.prop, data);
|
|
799
|
+
if (id) {
|
|
800
|
+
// Wrap to retarget this
|
|
801
|
+
listener = buildShadowRootChildListener(listener);
|
|
802
|
+
}
|
|
803
|
+
// Cache and reuse
|
|
804
|
+
entry.listener = listener;
|
|
805
|
+
// console.log('caching listener', entry);
|
|
806
|
+
} else {
|
|
807
|
+
throw new TypeError('Anonymous event listeners cannot be used in templates');
|
|
808
|
+
// console.warn('creating new listener', entry);
|
|
809
|
+
// listener = entry.handleEvent ?? ((event) => {
|
|
810
|
+
// valueFromPropName(entry.prop, data)(event);
|
|
811
|
+
// });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const eventTarget = id ? root.getElementById(id) : rootEventTarget;
|
|
815
|
+
if (!eventTarget) {
|
|
816
|
+
// Element is not available yet. Bind on reference
|
|
817
|
+
console.debug('Composition: Skip bind events for', id);
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
eventTarget.addEventListener(entry.type, listener, entry);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
this.initiallyRendered = true;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* @param {string} id
|
|
829
|
+
* @param {Element} element
|
|
830
|
+
*/
|
|
831
|
+
attachEventListeners(id, element) {
|
|
832
|
+
const events = this.events.get(id);
|
|
833
|
+
if (events) {
|
|
834
|
+
console.debug('attaching events for', id);
|
|
835
|
+
} else {
|
|
836
|
+
// console.log('no events for', id);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
for (const entry of [...this.events.get(id)].reverse()) {
|
|
840
|
+
const { listener } = entry;
|
|
841
|
+
if (!listener) {
|
|
842
|
+
throw new Error('Template must be interpolated before attaching events');
|
|
843
|
+
}
|
|
844
|
+
element.addEventListener(entry.type, listener, entry);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* @param {Element|DocumentFragment} root
|
|
850
|
+
* @return {Map<string,Element>}
|
|
851
|
+
*/
|
|
852
|
+
getReferences(root) {
|
|
853
|
+
let references = this.referenceCache.get(root);
|
|
854
|
+
if (!references) {
|
|
855
|
+
references = new Map();
|
|
856
|
+
this.referenceCache.set(root, references);
|
|
857
|
+
}
|
|
858
|
+
return references;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* @param {ShadowRoot|DocumentFragment} root
|
|
863
|
+
* @param {string} id
|
|
864
|
+
* @return {Element}
|
|
865
|
+
*/
|
|
866
|
+
getElement(root, id) {
|
|
867
|
+
const references = this.getReferences(root);
|
|
868
|
+
let element = references.get(id);
|
|
869
|
+
if (element) {
|
|
870
|
+
// console.log('Returning from cache', id);
|
|
871
|
+
return element;
|
|
872
|
+
}
|
|
873
|
+
if (element === null) return null; // Cached null response
|
|
874
|
+
|
|
875
|
+
// Undefined
|
|
876
|
+
|
|
877
|
+
// console.log('Search in DOM', id);
|
|
878
|
+
element = root.getElementById(id);
|
|
879
|
+
|
|
880
|
+
if (element) {
|
|
881
|
+
// console.log('Found in DOM', id);
|
|
882
|
+
references.set(id, element);
|
|
883
|
+
return element;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Element not in DOM means child of conditional element
|
|
887
|
+
/** @type {Element} */
|
|
888
|
+
let anchorElement;
|
|
889
|
+
|
|
890
|
+
// Check if element is conditional
|
|
891
|
+
let cloneTarget = this.conditionalElementMetadata.get(id)?.element;
|
|
892
|
+
|
|
893
|
+
if (!cloneTarget) {
|
|
894
|
+
// Check if element even exists in interpolation
|
|
895
|
+
// Check interpolation (full-tree) first
|
|
896
|
+
const interpolatedElement = this.interpolation.getElementById(id);
|
|
897
|
+
if (!interpolatedElement) {
|
|
898
|
+
console.warn('Not in full-tree', id);
|
|
899
|
+
// Cache not in full composition
|
|
900
|
+
references.set(id, null);
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
// Iterate backgrounds until closest conditional element
|
|
904
|
+
// const anchorElementId = this.template.getElementById(id).closest('[_if]').id;
|
|
905
|
+
// anchorElement = this.references.get(anchorElementId) this.interpolation.getElementById(anchorElementId).cloneNode(true);
|
|
906
|
+
let parentElement = interpolatedElement;
|
|
907
|
+
while ((parentElement = parentElement.parentElement) != null) {
|
|
908
|
+
const parentId = parentElement.id;
|
|
909
|
+
if (!parentId) {
|
|
910
|
+
console.warn('Parent does not have ID!');
|
|
911
|
+
cloneTarget = parentElement;
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Parent already referenced
|
|
916
|
+
const referencedParent = references.get(parentId);
|
|
917
|
+
if (referencedParent) {
|
|
918
|
+
// Element may have been removed without ever tree-walking
|
|
919
|
+
console.debug('Parent already referenced', parentId, '>', id);
|
|
920
|
+
anchorElement = referencedParent;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const liveElement = root.getElementById(parentId);
|
|
925
|
+
if (liveElement) {
|
|
926
|
+
console.warn('Parent in DOM and not referenced', parentId, '>', id);
|
|
927
|
+
// Parent already in DOM. Cache reference
|
|
928
|
+
references.set(parentId, liveElement);
|
|
929
|
+
anchorElement = referencedParent;
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const conditionalParent = this.conditionalElementMetadata.get(parentId)?.element;
|
|
934
|
+
if (conditionalParent) {
|
|
935
|
+
console.debug('Found parent conditional element', parentId, '>', id);
|
|
936
|
+
cloneTarget = conditionalParent;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
cloneTarget = parentElement;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
anchorElement ??= /** @type {Element} */ (cloneTarget.cloneNode(true));
|
|
945
|
+
|
|
946
|
+
// Iterate downwards and cache all references
|
|
947
|
+
let node = anchorElement;
|
|
948
|
+
const iterator = document.createTreeWalker(anchorElement, NodeFilter.SHOW_ELEMENT);
|
|
949
|
+
do {
|
|
950
|
+
const nodeIdentifier = node.id;
|
|
951
|
+
if (!element && nodeIdentifier === id) {
|
|
952
|
+
element = node;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (nodeIdentifier) {
|
|
956
|
+
// console.debug('Caching element', nodeIdentifier);
|
|
957
|
+
references.set(nodeIdentifier, node);
|
|
958
|
+
if (cloneTarget) {
|
|
959
|
+
// Attach events regardless of DOM state.
|
|
960
|
+
// EventTargets should still fire even if not part of live document
|
|
961
|
+
this.attachEventListeners(id, element);
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
console.warn('Could not cache node', node);
|
|
965
|
+
}
|
|
966
|
+
} while ((node = iterator.nextNode()));
|
|
967
|
+
return element;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* @param {*} fn
|
|
972
|
+
* @param {any} defaults
|
|
973
|
+
* @return {boolean} reusable
|
|
974
|
+
*/
|
|
975
|
+
bindWatcher(fn, defaults) {
|
|
976
|
+
const { props, defaultValue, reusable } = observeFunction(fn, defaults);
|
|
977
|
+
const entry = { fn, props, defaultValue };
|
|
978
|
+
for (const prop of props) {
|
|
979
|
+
let set = this.bindings.get(prop);
|
|
980
|
+
if (!set) {
|
|
981
|
+
set = new Set();
|
|
982
|
+
this.bindings.set(prop, set);
|
|
983
|
+
}
|
|
984
|
+
set.add(entry);
|
|
985
|
+
}
|
|
986
|
+
return reusable;
|
|
987
|
+
}
|
|
988
|
+
}
|