@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/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, zStackElementStyle } 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
- initializeState() {
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
- onDispose() {
119
- super.onDispose();
120
- this.unfollow();
121
- }
122
-
123
- placement() {
124
- const anchor = this.anchorRect();
125
- const view = document.defaultView;
126
- const width = view ? view.innerWidth : 1024;
127
- const height = view ? view.innerHeight : 768;
128
- this.unobservable.placedViewport = width + "x" + height;
129
- const placement = {};
130
- if ((anchor.top + anchor.bottom) / 2 < height / 2) placement.top = anchor.bottom + GAP + "px";
131
- else placement.bottom = height - anchor.top + GAP + "px";
132
- if ((anchor.left + anchor.right) / 2 < width / 2) placement.left = anchor.left + "px";
133
- else placement.right = width - anchor.right + "px";
134
- return placement;
135
- }
136
-
137
- build() {
138
- this.moved; // Placed again when its anchor element has moved - see follow().
139
- const followsElement = this.anchor !== null && typeof(this.anchor.getBoundingClientRect) === "function";
140
- if (this.showing && followsElement) this.follow();
141
- else this.unfollow();
142
- return overlay(
143
- { key: "overlay", showing: this.showing },
144
- zStack(
145
- { key: "layer", style: { ...fitContainerStyle, pointerEvents: "none" } },
146
- div({
147
- key: "outside",
148
- onmousedown: callback("outside", () => this.close && this.close()),
149
- style: { ...zStackElementStyle, pointerEvents: "auto" },
150
- }),
151
- wrapper(
152
- { key: "content", style: { position: "fixed", pointerEvents: "auto", maxWidth: "min(800px, 90vw)", ...this.placement(), ...this.style } },
153
- this.popoverChildren,
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 { portal, portalContents, Portal, PortalContents } from "./Portal.js";
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 an icon.
27
- * - card: `key`, `variant` ("elevated" - the default - "filled" or
28
- * "outlined"), `style`, `children` - a surface to group content on.
29
- * - controlPanel: `key`, `style`, `children` - controls in a row (buttons,
30
- * fields, a label), on a surface of their own, spaced evenly and
31
- * wrapping when there isn't room: what a page puts its controls in, so
32
- * they sit apart from what they control.
33
- * - alert: `key`, `severity` ("info" - the default - "success",
34
- * "warning" or "error"), `style`, `children` - a message with its
35
- * severity's icon and colors (see alertSeverities).
36
- * - listItem: `key`, `active`, `onClick` (or a loose function),
37
- * `style`, `children` - one clickable row of a list, marked when active.
38
- * - dialog: `key`, `title`, `close` (called by its close button),
39
- * `fullScreen`, `style`, `children` - a dialog's own box: title bar,
40
- * close button and body. Full screen, it fills whatever it's placed in,
41
- * as a phone app's screen does: no corners, no shadow, and a back arrow
42
- * at the top left (calling `close`) instead of the close button. Where it's shown (docked, or modal over the app) is up to
43
- * whoever places it - see cascade.ui's overlay().
44
- * - textField: `key`, `label` (optional), `placeholder`, `value`, `onInput` (called with the new
45
- * value, as text), `type` ("text" - the default - or "number"), `unit`
46
- * (shown after the value: "kg"), `error` (a message, shown under it,
47
- * marking it as wrong), `style` - a labelled input field.
48
- * - checkbox: `key`, `label`, `checked`, `onChange` (called with whether
49
- * it's now checked), `style`.
50
- * - colorField: `key`, `label`, `value` (a hex color: "#34495e"),
51
- * `onInput` (called with the new one, as it's being picked), `style` - a
52
- * color, and the browser's own picker for it.
53
- * - tabBar: `key`, `tabs` (`[{ key, title }]`), `selected` (a tab's key),
54
- * `onSelect` (called with the key of the tab clicked), `style` - a row of
55
- * tabs, one of them selected. What each shows is up to whoever places it.
56
- *
57
- * Widgets that are more than one element (an alert: icon plus message) are
58
- * components of their own in each theme, so the keys inside them are
59
- * scoped to them rather than to whoever builds them - two alerts on one
60
- * page don't collide.
61
- *
62
- * No theme in the context providing a widget falls back to the DOM
63
- * default, which ends in a visible, warning placeholder (see
64
- * DOMDebugServiceLocator) rather than an error.
65
- */
66
-
67
- export function toButtonProperties(parameters) {
68
- const properties = toProperties(parameters);
69
- const content = properties.componentContent;
70
- if (content) {
71
- const handlers = content.filter((item) => typeof(item) === "function");
72
- if (handlers.length > 1) throw new Error("button(): only one loose onClick function allowed.");
73
- if (handlers.length === 1) {
74
- if (properties.onClick) throw new Error("button(): onClick given both loosely and in properties.");
75
- properties.onClick = handlers[0];
76
- }
77
- const rest = content.filter((item) => typeof(item) !== "function");
78
- if (rest.length > 0) properties.componentContent = rest;
79
- else delete properties.componentContent;
80
- }
81
- findImplicitChildren(properties);
82
- return properties;
83
- }
84
-
85
- export function widget(name, properties) {
86
- return locateService({ type: "widget", name, properties }, defaultDOMServiceLocator);
87
- }
88
-
89
- export const button = (...parameters) => widget("button", toButtonProperties(parameters));
90
- export const icon = (...parameters) => widget("icon", toProperties(parameters));
91
- export const iconButton = (...parameters) => widget("iconButton", toButtonProperties(parameters));
92
- export const card = (...parameters) => widget("card", toPropertiesWithChildren(parameters));
93
- export const controlPanel = (...parameters) => widget("controlPanel", toPropertiesWithChildren(parameters));
94
- export const alert = (...parameters) => widget("alert", toPropertiesWithChildren(parameters));
95
- export const listItem = (...parameters) => widget("listItem", toButtonProperties(parameters));
96
- export const dialog = (...parameters) => widget("dialog", toPropertiesWithChildren(parameters));
97
- export const textField = (...parameters) => widget("textField", toProperties(parameters));
98
- export const checkbox = (...parameters) => widget("checkbox", toProperties(parameters));
99
- export const colorField = (...parameters) => widget("colorField", toProperties(parameters));
100
- export const tabBar = (...parameters) => widget("tabBar", toProperties(parameters));
101
-
102
- // Each alert severity's icon and colors - the same in every theme, so a
103
- // warning reads as a warning whichever one is in use.
104
- export const alertSeverities = {
105
- info: { icon: "info", background: "#f0f8ff", iconColor: "rgb(2, 136, 209)", color: "rgb(1, 67, 97)" },
106
- success: { icon: "check_circle", background: "#edf7ed", iconColor: "rgb(46, 125, 50)", color: "rgb(30, 70, 32)" },
107
- warning: { icon: "warning", background: "#fff4e5", iconColor: "rgb(237, 108, 2)", color: "rgb(102, 60, 0)" },
108
- error: { icon: "error", background: "#fdeded", iconColor: "rgb(211, 47, 47)", color: "rgb(95, 33, 32)" },
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
+ };