@liquefy/cascade.ui 1.0.0 → 2.0.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 +23 -4
- package/package.json +5 -5
- package/src/BasicTheme.js +14 -10
- package/src/Dropdown.js +84 -0
- package/src/Layout.js +153 -120
- package/src/Modal.js +136 -0
- package/src/Overlay.js +95 -85
- package/src/OverlayFrame.js +77 -39
- package/src/Popover.js +174 -158
- package/src/index.js +3 -2
- package/src/widgets.js +127 -109
- package/src/Portal.js +0 -132
package/src/Popover.js
CHANGED
|
@@ -1,158 +1,174 @@
|
|
|
1
|
-
import { Component, frozen, callback } from "@liquefy/cascade.component";
|
|
2
|
-
import { div } from "@liquefy/cascade.dom";
|
|
3
|
-
import { overlay } from "./Overlay.js";
|
|
4
|
-
import { zStack, wrapper, fitContainerStyle
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Popover - ported from flow.ui/basic/src/popover.js: content shown next to
|
|
8
|
-
* something on the page (an information button, say), over everything
|
|
9
|
-
* else, closed by clicking anywhere outside it.
|
|
10
|
-
*
|
|
11
|
-
* - anchor: what it points to - an element (typically what was clicked:
|
|
12
|
-
* `event.currentTarget`), or a rectangle in viewport coordinates. An
|
|
13
|
-
* element it follows: while it's shown, a resize of the window, or a
|
|
14
|
-
* scroll, places it again, where the element now is - watched in the
|
|
15
|
-
* frames after, for as long as it keeps moving, so whatever lays out
|
|
16
|
-
* again on the same resize has done so, however late (a toolbar moving
|
|
17
|
-
* the button it was opened from, say). A rectangle stays put.
|
|
18
|
-
* - showing, close: whether it's shown, and what a click outside it calls.
|
|
19
|
-
* - style, children: the popover's own content (usually a card or an
|
|
20
|
-
* alert), and extra style for the box it's placed in.
|
|
21
|
-
*
|
|
22
|
-
* Flow measured the popover's own size to place it; here it's placed by
|
|
23
|
-
* which quarter of the window its anchor is in instead - below the anchor
|
|
24
|
-
* when that's in the upper half, above it otherwise, and aligned to the
|
|
25
|
-
* anchor's left or right edge, whichever is further from the window's
|
|
26
|
-
* edge - so it never needs measuring, and so never needs a second pass.
|
|
27
|
-
* Shown through overlay() - the nearest OverlayFrame shows it.
|
|
28
|
-
*/
|
|
29
|
-
export function popover(...parameters) {
|
|
30
|
-
return new Popover(...parameters);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const GAP = 6;
|
|
34
|
-
|
|
35
|
-
export class Popover extends Component {
|
|
36
|
-
setProperties({ anchor, showing, close, style, children }) {
|
|
37
|
-
this.anchor = frozen(anchor || null);
|
|
38
|
-
this.showing = !!showing;
|
|
39
|
-
this.close = close || null;
|
|
40
|
-
this.style = frozen(style || null);
|
|
41
|
-
this.popoverChildren = frozen(children || []);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// `moved`: counts the frames after a resize or a scroll, while it's
|
|
45
|
-
// shown and anchored to an element - so it's built, and placed, again
|
|
46
|
-
// (see follow()).
|
|
47
|
-
|
|
48
|
-
return { moved: 0 };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
initialUnobservables() {
|
|
52
|
-
return { following: null };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Where the anchor is now - or, for an element no longer in the page (a
|
|
56
|
-
// theme switch replacing the button, say), where it last was.
|
|
57
|
-
anchorRect() {
|
|
58
|
-
const anchor = this.anchor;
|
|
59
|
-
if (!anchor) return { left: 0, top: 0, right: 0, bottom: 0 };
|
|
60
|
-
if (typeof(anchor.getBoundingClientRect) !== "function") return anchor;
|
|
61
|
-
const u = this.unobservable;
|
|
62
|
-
if (anchor.isConnected || !u.lastAnchorRect) u.lastAnchorRect = anchor.getBoundingClientRect();
|
|
63
|
-
return u.lastAnchorRect;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Following an element: while shown, on a resize or a scroll, place again
|
|
67
|
-
// - in the frames after, whenever the element has moved.
|
|
68
|
-
follow() {
|
|
69
|
-
const u = this.unobservable;
|
|
70
|
-
if (u.following) return;
|
|
71
|
-
const view = document.defaultView;
|
|
72
|
-
const later = view.requestAnimationFrame ? (action) => view.requestAnimationFrame(action) : (action) => setTimeout(action, 0);
|
|
73
|
-
// Watched frame by frame, until it has held still for a few: what moves
|
|
74
|
-
// it may lay out again later than the next frame - a bounds provider's
|
|
75
|
-
// ResizeObserver, say, which runs after animation frames.
|
|
76
|
-
const sameRect = (a, b) => a && b && a.left === b.left && a.top === b.top && a.right === b.right && a.bottom === b.bottom;
|
|
77
|
-
let watching = false;
|
|
78
|
-
let stillFrames = 0;
|
|
79
|
-
const watch = () => {
|
|
80
|
-
if (!u.following) { watching = false; return; }
|
|
81
|
-
const anchor = this.anchor;
|
|
82
|
-
const rect = anchor && anchor.isConnected && typeof(anchor.getBoundingClientRect) === "function" ? anchor.getBoundingClientRect() : null;
|
|
83
|
-
// Placed again if the element has moved - or the window has changed
|
|
84
|
-
// size: a placement from its right or bottom edge moves along with it.
|
|
85
|
-
const viewportChanged = u.placedViewport !== view.innerWidth + "x" + view.innerHeight;
|
|
86
|
-
if ((rect && !sameRect(rect, u.lastAnchorRect)) || viewportChanged) {
|
|
87
|
-
stillFrames = 0;
|
|
88
|
-
this.moved = this.moved + 1;
|
|
89
|
-
} else stillFrames++;
|
|
90
|
-
if (stillFrames < 3) later(watch);
|
|
91
|
-
else watching = false;
|
|
92
|
-
};
|
|
93
|
-
const moved = () => {
|
|
94
|
-
stillFrames = 0;
|
|
95
|
-
if (watching) return;
|
|
96
|
-
watching = true;
|
|
97
|
-
later(watch);
|
|
98
|
-
};
|
|
99
|
-
view.addEventListener("resize", moved);
|
|
100
|
-
document.addEventListener("scroll", moved, true);
|
|
101
|
-
u.following = () => {
|
|
102
|
-
view.removeEventListener("resize", moved);
|
|
103
|
-
document.removeEventListener("scroll", moved, true);
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
unfollow() {
|
|
108
|
-
const u = this.unobservable;
|
|
109
|
-
if (!u.following) return;
|
|
110
|
-
u.following();
|
|
111
|
-
u.following = null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
onHide() {
|
|
115
|
-
this.unfollow();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
1
|
+
import { Component, frozen, callback, withoutRecording } from "@liquefy/cascade.component";
|
|
2
|
+
import { div } from "@liquefy/cascade.dom";
|
|
3
|
+
import { overlay } from "./Overlay.js";
|
|
4
|
+
import { zStack, wrapper, fitContainerStyle } from "./Layout.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Popover - ported from flow.ui/basic/src/popover.js: content shown next to
|
|
8
|
+
* something on the page (an information button, say), over everything
|
|
9
|
+
* else, closed by clicking anywhere outside it.
|
|
10
|
+
*
|
|
11
|
+
* - anchor: what it points to - an element (typically what was clicked:
|
|
12
|
+
* `event.currentTarget`), or a rectangle in viewport coordinates. An
|
|
13
|
+
* element it follows: while it's shown, a resize of the window, or a
|
|
14
|
+
* scroll, places it again, where the element now is - watched in the
|
|
15
|
+
* frames after, for as long as it keeps moving, so whatever lays out
|
|
16
|
+
* again on the same resize has done so, however late (a toolbar moving
|
|
17
|
+
* the button it was opened from, say). A rectangle stays put.
|
|
18
|
+
* - showing, close: whether it's shown, and what a click outside it calls.
|
|
19
|
+
* - style, children: the popover's own content (usually a card or an
|
|
20
|
+
* alert), and extra style for the box it's placed in.
|
|
21
|
+
*
|
|
22
|
+
* Flow measured the popover's own size to place it; here it's placed by
|
|
23
|
+
* which quarter of the window its anchor is in instead - below the anchor
|
|
24
|
+
* when that's in the upper half, above it otherwise, and aligned to the
|
|
25
|
+
* anchor's left or right edge, whichever is further from the window's
|
|
26
|
+
* edge - so it never needs measuring, and so never needs a second pass.
|
|
27
|
+
* Shown through overlay() - the nearest OverlayFrame shows it.
|
|
28
|
+
*/
|
|
29
|
+
export function popover(...parameters) {
|
|
30
|
+
return new Popover(...parameters);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const GAP = 6;
|
|
34
|
+
|
|
35
|
+
export class Popover extends Component {
|
|
36
|
+
setProperties({ anchor, showing, close, style, children }) {
|
|
37
|
+
this.anchor = frozen(anchor || null);
|
|
38
|
+
this.showing = !!showing;
|
|
39
|
+
this.close = close || null;
|
|
40
|
+
this.style = frozen(style || null);
|
|
41
|
+
this.popoverChildren = frozen(children || []);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// `moved`: counts the frames after a resize or a scroll, while it's
|
|
45
|
+
// shown and anchored to an element - so it's built, and placed, again
|
|
46
|
+
// (see follow()).
|
|
47
|
+
initialState() {
|
|
48
|
+
return { moved: 0 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
initialUnobservables() {
|
|
52
|
+
return { following: null };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Where the anchor is now - or, for an element no longer in the page (a
|
|
56
|
+
// theme switch replacing the button, say), where it last was.
|
|
57
|
+
anchorRect() {
|
|
58
|
+
const anchor = this.anchor;
|
|
59
|
+
if (!anchor) return { left: 0, top: 0, right: 0, bottom: 0 };
|
|
60
|
+
if (typeof(anchor.getBoundingClientRect) !== "function") return anchor;
|
|
61
|
+
const u = this.unobservable;
|
|
62
|
+
if (anchor.isConnected || !u.lastAnchorRect) u.lastAnchorRect = anchor.getBoundingClientRect();
|
|
63
|
+
return u.lastAnchorRect;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Following an element: while shown, on a resize or a scroll, place again
|
|
67
|
+
// - in the frames after, whenever the element has moved.
|
|
68
|
+
follow() {
|
|
69
|
+
const u = this.unobservable;
|
|
70
|
+
if (u.following) return;
|
|
71
|
+
const view = document.defaultView;
|
|
72
|
+
const later = view.requestAnimationFrame ? (action) => view.requestAnimationFrame(action) : (action) => setTimeout(action, 0);
|
|
73
|
+
// Watched frame by frame, until it has held still for a few: what moves
|
|
74
|
+
// it may lay out again later than the next frame - a bounds provider's
|
|
75
|
+
// ResizeObserver, say, which runs after animation frames.
|
|
76
|
+
const sameRect = (a, b) => a && b && a.left === b.left && a.top === b.top && a.right === b.right && a.bottom === b.bottom;
|
|
77
|
+
let watching = false;
|
|
78
|
+
let stillFrames = 0;
|
|
79
|
+
const watch = () => {
|
|
80
|
+
if (!u.following) { watching = false; return; }
|
|
81
|
+
const anchor = this.anchor;
|
|
82
|
+
const rect = anchor && anchor.isConnected && typeof(anchor.getBoundingClientRect) === "function" ? anchor.getBoundingClientRect() : null;
|
|
83
|
+
// Placed again if the element has moved - or the window has changed
|
|
84
|
+
// size: a placement from its right or bottom edge moves along with it.
|
|
85
|
+
const viewportChanged = u.placedViewport !== view.innerWidth + "x" + view.innerHeight;
|
|
86
|
+
if ((rect && !sameRect(rect, u.lastAnchorRect)) || viewportChanged) {
|
|
87
|
+
stillFrames = 0;
|
|
88
|
+
this.moved = this.moved + 1;
|
|
89
|
+
} else stillFrames++;
|
|
90
|
+
if (stillFrames < 3) later(watch);
|
|
91
|
+
else watching = false;
|
|
92
|
+
};
|
|
93
|
+
const moved = () => {
|
|
94
|
+
stillFrames = 0;
|
|
95
|
+
if (watching) return;
|
|
96
|
+
watching = true;
|
|
97
|
+
later(watch);
|
|
98
|
+
};
|
|
99
|
+
view.addEventListener("resize", moved);
|
|
100
|
+
document.addEventListener("scroll", moved, true);
|
|
101
|
+
u.following = () => {
|
|
102
|
+
view.removeEventListener("resize", moved);
|
|
103
|
+
document.removeEventListener("scroll", moved, true);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
unfollow() {
|
|
108
|
+
const u = this.unobservable;
|
|
109
|
+
if (!u.following) return;
|
|
110
|
+
u.following();
|
|
111
|
+
u.following = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onHide() {
|
|
115
|
+
this.unfollow();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Shown again (its page switched back to, say): an up-to-date build
|
|
119
|
+
// doesn't rerun, so follow the element again here. Read without
|
|
120
|
+
// recording - this runs inside whoever is rendering.
|
|
121
|
+
onShow() {
|
|
122
|
+
withoutRecording(() => {
|
|
123
|
+
if (this.showing && this.followsElement()) this.follow();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
followsElement() {
|
|
128
|
+
return this.anchor !== null && typeof(this.anchor.getBoundingClientRect) === "function";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onDispose() {
|
|
132
|
+
super.onDispose();
|
|
133
|
+
this.unfollow();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
placement() {
|
|
137
|
+
const anchor = this.anchorRect();
|
|
138
|
+
const view = document.defaultView;
|
|
139
|
+
const width = view ? view.innerWidth : 1024;
|
|
140
|
+
const height = view ? view.innerHeight : 768;
|
|
141
|
+
this.unobservable.placedViewport = width + "x" + height;
|
|
142
|
+
const placement = {};
|
|
143
|
+
if ((anchor.top + anchor.bottom) / 2 < height / 2) placement.top = anchor.bottom + GAP + "px";
|
|
144
|
+
else placement.bottom = height - anchor.top + GAP + "px";
|
|
145
|
+
if ((anchor.left + anchor.right) / 2 < width / 2) placement.left = anchor.left + "px";
|
|
146
|
+
else placement.right = width - anchor.right + "px";
|
|
147
|
+
return placement;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
build() {
|
|
151
|
+
this.moved; // Placed again when its anchor element has moved - see follow().
|
|
152
|
+
const followsElement = this.followsElement();
|
|
153
|
+
if (this.showing && followsElement) this.follow();
|
|
154
|
+
else this.unfollow();
|
|
155
|
+
return overlay(
|
|
156
|
+
{ key: "overlay", showing: this.showing },
|
|
157
|
+
zStack(
|
|
158
|
+
{ key: "layer", style: { ...fitContainerStyle, pointerEvents: "none" } },
|
|
159
|
+
div({
|
|
160
|
+
key: "outside",
|
|
161
|
+
onmousedown: callback("outside", () => this.close && this.close()),
|
|
162
|
+
style: { pointerEvents: "auto" },
|
|
163
|
+
}),
|
|
164
|
+
// Its own size and place - not the whole layer, as the zStack gives
|
|
165
|
+
// its children (see zStackElementStyle): stretched, the invisible
|
|
166
|
+
// rest of the box would catch the clicks meant for outside it.
|
|
167
|
+
wrapper(
|
|
168
|
+
{ key: "content", style: { position: "fixed", top: "auto", left: "auto", width: "auto", height: "auto", pointerEvents: "auto", maxWidth: "min(800px, 90vw)", ...this.placement(), ...this.style } },
|
|
169
|
+
this.popoverChildren,
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { OverlayFrame, overlayFrame } from "./OverlayFrame.js";
|
|
2
2
|
export { Overlay, overlay } from "./Overlay.js";
|
|
3
|
+
export { ModalBackdrop, Modal, ModalAssembly, modalBackdrop, modal, modalAssembly, modalBackdropColor } from "./Modal.js";
|
|
3
4
|
export {
|
|
4
5
|
flexContainerStyle, rowStyle, columnStyle,
|
|
5
6
|
centerStyle, middleStyle, centerMiddleStyle,
|
|
@@ -7,9 +8,9 @@ export {
|
|
|
7
8
|
layoutBorderStyle, overflowVisibleStyle, zStackElementStyle,
|
|
8
9
|
wrapper, row, column, center, middle, centerMiddle, filler, zStack,
|
|
9
10
|
} from "./Layout.js";
|
|
10
|
-
export { widget, button, icon, iconButton, card, controlPanel, alert, listItem, dialog, textField, checkbox, colorField, tabBar, alertSeverities, toButtonProperties } from "./widgets.js";
|
|
11
|
+
export { widget, button, icon, iconButton, card, controlPanel, alert, listItem, dialog, textField, checkbox, colorField, tabBar, alertSeverities, toButtonProperties, keyAsLabel } from "./widgets.js";
|
|
11
12
|
export { popover } from "./Popover.js";
|
|
12
|
-
export {
|
|
13
|
+
export { Dropdown, dropdown } from "./Dropdown.js";
|
|
13
14
|
export { BasicThemeServiceLocator, basicTheme, basicShadow } from "./BasicTheme.js";
|
|
14
15
|
export {
|
|
15
16
|
ColorScheme, ColorSchemeScope, colorSchemeScope, currentColorScheme, themeColor, deriveColors, colorVariables, defaultColors,
|
package/src/widgets.js
CHANGED
|
@@ -1,109 +1,127 @@
|
|
|
1
|
-
import { toProperties, toPropertiesWithChildren, findImplicitChildren, locateService } from "@liquefy/cascade.component";
|
|
2
|
-
import { defaultDOMServiceLocator } from "@liquefy/cascade.dom";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Themed widgets - ported from flow.ui/themed: app code calls `button(...)`
|
|
6
|
-
* and gets whatever the current theme provides for it, found through the
|
|
7
|
-
* service locator in the render context (see cascade.component's
|
|
8
|
-
* ServiceLocator.js) as `{ type: "widget", name: "button", properties }`.
|
|
9
|
-
* Which theme that is, is decided by whoever set up the render context -
|
|
10
|
-
* the app, or a subtree overriding it (serviceProvider()) - never by the
|
|
11
|
-
* component using the widget.
|
|
12
|
-
*
|
|
13
|
-
* Each widget has a property contract every theme implements, so switching
|
|
14
|
-
* themes changes appearance, not meaning:
|
|
15
|
-
*
|
|
16
|
-
* - button: `key`, `onClick`, `disabled`, `variant` ("filled" for the
|
|
17
|
-
* main thing to do - what else a theme knows is up to it), `style`, `children` - with
|
|
18
|
-
* flow's own argument convention, a loose function argument is
|
|
19
|
-
* `onClick` and anything else loose is a child:
|
|
20
|
-
* `button("Save", () => save())`.
|
|
21
|
-
* - icon: `key`, `name` (a Material icon name - "info", "close",
|
|
22
|
-
* "shopping_cart", ...), `style` (fontSize sets its size). Both themes
|
|
23
|
-
* draw icons with Google's icon fonts, which the app links (see
|
|
24
|
-
* cascade.application/demo/index.html).
|
|
25
|
-
* - iconButton: `key`, `icon` (an icon name), `onClick` (or a loose
|
|
26
|
-
* function), `title`, `style` - a round button showing just
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (handlers.length
|
|
74
|
-
|
|
75
|
-
properties.onClick
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
export
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
1
|
+
import { toProperties, toPropertiesWithChildren, findImplicitChildren, locateService } from "@liquefy/cascade.component";
|
|
2
|
+
import { defaultDOMServiceLocator, text } from "@liquefy/cascade.dom";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Themed widgets - ported from flow.ui/themed: app code calls `button(...)`
|
|
6
|
+
* and gets whatever the current theme provides for it, found through the
|
|
7
|
+
* service locator in the render context (see cascade.component's
|
|
8
|
+
* ServiceLocator.js) as `{ type: "widget", name: "button", properties }`.
|
|
9
|
+
* Which theme that is, is decided by whoever set up the render context -
|
|
10
|
+
* the app, or a subtree overriding it (serviceProvider()) - never by the
|
|
11
|
+
* component using the widget.
|
|
12
|
+
*
|
|
13
|
+
* Each widget has a property contract every theme implements, so switching
|
|
14
|
+
* themes changes appearance, not meaning:
|
|
15
|
+
*
|
|
16
|
+
* - button: `key`, `onClick`, `disabled`, `variant` ("filled" for the
|
|
17
|
+
* main thing to do - what else a theme knows is up to it), `style`, `children` - with
|
|
18
|
+
* flow's own argument convention, a loose function argument is
|
|
19
|
+
* `onClick` and anything else loose is a child:
|
|
20
|
+
* `button("Save", () => save())`.
|
|
21
|
+
* - icon: `key`, `name` (a Material icon name - "info", "close",
|
|
22
|
+
* "shopping_cart", ...), `style` (fontSize sets its size). Both themes
|
|
23
|
+
* draw icons with Google's icon fonts, which the app links (see
|
|
24
|
+
* cascade.application/demo/index.html).
|
|
25
|
+
* - iconButton: `key`, `icon` (an icon name), `onClick` (or a loose
|
|
26
|
+
* function), `title`, `disabled`, `style` - a round button showing just
|
|
27
|
+
* an icon.
|
|
28
|
+
* - card: `key`, `variant` ("elevated" - the default - "filled" or
|
|
29
|
+
* "outlined"), `style`, `children` - a surface to group content on.
|
|
30
|
+
* - controlPanel: `key`, `style`, `children` - controls in a row (buttons,
|
|
31
|
+
* fields, a label), on a surface of their own, spaced evenly and
|
|
32
|
+
* wrapping when there isn't room: what a page puts its controls in, so
|
|
33
|
+
* they sit apart from what they control.
|
|
34
|
+
* - alert: `key`, `severity` ("info" - the default - "success",
|
|
35
|
+
* "warning" or "error"), `style`, `children` - a message with its
|
|
36
|
+
* severity's icon and colors (see alertSeverities).
|
|
37
|
+
* - listItem: `key`, `active`, `onClick` (or a loose function),
|
|
38
|
+
* `style`, `children` - one clickable row of a list, marked when active.
|
|
39
|
+
* - dialog: `key`, `title`, `close` (called by its close button),
|
|
40
|
+
* `fullScreen`, `style`, `children` - a dialog's own box: title bar,
|
|
41
|
+
* close button and body. Full screen, it fills whatever it's placed in,
|
|
42
|
+
* as a phone app's screen does: no corners, no shadow, and a back arrow
|
|
43
|
+
* at the top left (calling `close`) instead of the close button. Where it's shown (docked, or modal over the app) is up to
|
|
44
|
+
* whoever places it - see cascade.ui's overlay().
|
|
45
|
+
* - textField: `key`, `label` (optional), `placeholder`, `value`, `onInput` (called with the new
|
|
46
|
+
* value, as text), `type` ("text" - the default - or "number"), `unit`
|
|
47
|
+
* (shown after the value: "kg"), `error` (a message, shown under it,
|
|
48
|
+
* marking it as wrong), `style` - a labelled input field.
|
|
49
|
+
* - checkbox: `key`, `label`, `checked`, `onChange` (called with whether
|
|
50
|
+
* it's now checked), `style`.
|
|
51
|
+
* - colorField: `key`, `label`, `value` (a hex color: "#34495e"),
|
|
52
|
+
* `onInput` (called with the new one, as it's being picked), `style` - a
|
|
53
|
+
* color, and the browser's own picker for it.
|
|
54
|
+
* - tabBar: `key`, `tabs` (`[{ key, title }]`), `selected` (a tab's key),
|
|
55
|
+
* `onSelect` (called with the key of the tab clicked), `style` - a row of
|
|
56
|
+
* tabs, one of them selected. What each shows is up to whoever places it.
|
|
57
|
+
*
|
|
58
|
+
* Widgets that are more than one element (an alert: icon plus message) are
|
|
59
|
+
* components of their own in each theme, so the keys inside them are
|
|
60
|
+
* scoped to them rather than to whoever builds them - two alerts on one
|
|
61
|
+
* page don't collide.
|
|
62
|
+
*
|
|
63
|
+
* No theme in the context providing a widget falls back to the DOM
|
|
64
|
+
* default, which ends in a visible, warning placeholder (see
|
|
65
|
+
* DOMDebugServiceLocator) rather than an error.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
export function toButtonProperties(parameters) {
|
|
69
|
+
const properties = toProperties(parameters);
|
|
70
|
+
const content = properties.componentContent;
|
|
71
|
+
if (content) {
|
|
72
|
+
const handlers = content.filter((item) => typeof(item) === "function");
|
|
73
|
+
if (handlers.length > 1) throw new Error("button(): only one loose onClick function allowed.");
|
|
74
|
+
if (handlers.length === 1) {
|
|
75
|
+
if (properties.onClick) throw new Error("button(): onClick given both loosely and in properties.");
|
|
76
|
+
properties.onClick = handlers[0];
|
|
77
|
+
}
|
|
78
|
+
const rest = content.filter((item) => typeof(item) !== "function");
|
|
79
|
+
if (rest.length > 0) properties.componentContent = rest;
|
|
80
|
+
else delete properties.componentContent;
|
|
81
|
+
}
|
|
82
|
+
findImplicitChildren(properties);
|
|
83
|
+
return properties;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A button given nothing to show - `button("save", () => save())`, where
|
|
87
|
+
// the leading lowercase string is its key (see cascade.component's
|
|
88
|
+
// implicitProperties.js) - shows its key as its label, as words:
|
|
89
|
+
// "save" -> "Save", "openDialog" -> "Open dialog".
|
|
90
|
+
function withKeyAsLabel(properties) {
|
|
91
|
+
const hasContent = (properties.children && properties.children.length > 0) || (properties.componentContent && properties.componentContent.length > 0);
|
|
92
|
+
if (!hasContent && typeof(properties.key) === "string" && properties.key !== "") {
|
|
93
|
+
properties.children = [text({ key: properties.key + ".label", text: keyAsLabel(properties.key) })];
|
|
94
|
+
}
|
|
95
|
+
return properties;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function keyAsLabel(key) {
|
|
99
|
+
const words = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().toLowerCase();
|
|
100
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function widget(name, properties) {
|
|
104
|
+
return locateService({ type: "widget", name, properties }, defaultDOMServiceLocator);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const button = (...parameters) => widget("button", withKeyAsLabel(toButtonProperties(parameters)));
|
|
108
|
+
export const icon = (...parameters) => widget("icon", toProperties(parameters));
|
|
109
|
+
export const iconButton = (...parameters) => widget("iconButton", toButtonProperties(parameters));
|
|
110
|
+
export const card = (...parameters) => widget("card", toPropertiesWithChildren(parameters));
|
|
111
|
+
export const controlPanel = (...parameters) => widget("controlPanel", toPropertiesWithChildren(parameters));
|
|
112
|
+
export const alert = (...parameters) => widget("alert", toPropertiesWithChildren(parameters));
|
|
113
|
+
export const listItem = (...parameters) => widget("listItem", toButtonProperties(parameters));
|
|
114
|
+
export const dialog = (...parameters) => widget("dialog", toPropertiesWithChildren(parameters));
|
|
115
|
+
export const textField = (...parameters) => widget("textField", toProperties(parameters));
|
|
116
|
+
export const checkbox = (...parameters) => widget("checkbox", toProperties(parameters));
|
|
117
|
+
export const colorField = (...parameters) => widget("colorField", toProperties(parameters));
|
|
118
|
+
export const tabBar = (...parameters) => widget("tabBar", toProperties(parameters));
|
|
119
|
+
|
|
120
|
+
// Each alert severity's icon and colors - the same in every theme, so a
|
|
121
|
+
// warning reads as a warning whichever one is in use.
|
|
122
|
+
export const alertSeverities = {
|
|
123
|
+
info: { icon: "info", background: "#f0f8ff", iconColor: "rgb(2, 136, 209)", color: "rgb(1, 67, 97)" },
|
|
124
|
+
success: { icon: "check_circle", background: "#edf7ed", iconColor: "rgb(46, 125, 50)", color: "rgb(30, 70, 32)" },
|
|
125
|
+
warning: { icon: "warning", background: "#fff4e5", iconColor: "rgb(237, 108, 2)", color: "rgb(102, 60, 0)" },
|
|
126
|
+
error: { icon: "error", background: "#fdeded", iconColor: "rgb(211, 47, 47)", color: "rgb(95, 33, 32)" },
|
|
127
|
+
};
|