brew-js-react 0.6.6 → 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/dialog.d.ts CHANGED
@@ -14,6 +14,41 @@ export type DialogBaseProps<T, V = T> = Omit<DialogOptions<T, V | undefined>, 'o
14
14
  /** @deprecated */
15
15
  export type DialogRenderComponentProps<T, V = T> = DialogOptions<T, V | undefined> & DialogContext<V | undefined>;
16
16
 
17
+ export type DialogControllerOptions = Omit<DialogOptions<any>, 'onRender' | 'onCommit' | 'onOpen' | 'onClose' | 'controller'>;
18
+
19
+ export interface DialogControllerAdvancedOptions extends DialogControllerOptions, Pick<DialogOptions<any>, 'onClose' | 'onOpen' | 'preventLeave' | 'preventNavigation'> {
20
+ /**
21
+ * Specifies how dialogs are queued and displayed.
22
+ *
23
+ * - `shared`: content of subsequent dialogs are rendered in the same dialog element;
24
+ * - `multiple`: multiple dialogs can be shown at the same time, as child elements of root dialog element associated with the controller.
25
+ */
26
+ mode: 'shared' | 'multiple';
27
+ /**
28
+ * Specifies number of dialogs can be shown at the same time.
29
+ * When limit is exceeded, dialog will be pended to open until any active dialog is closed.
30
+ *
31
+ * By default there is no limit, and has no effect when {@link DialogControllerAdvancedOptions.mode} is `shared`.
32
+ */
33
+ concurrent?: number;
34
+ }
35
+
36
+ export interface DialogController {
37
+ /**
38
+ * Gets the number of dialogs pending to be shown.
39
+ */
40
+ readonly pendingCount: number;
41
+ /**
42
+ * Cancels pending dialogs, while currently open dialog will not be dismissed.
43
+ */
44
+ dismissPending(): void;
45
+ /**
46
+ * Cancels active and pending dialogs.
47
+ * @param value Value send to active dialog. It is ignored when `mode` is `multiple`.
48
+ */
49
+ dismissAll(value?: any): void;
50
+ }
51
+
17
52
  export interface DialogState<T> {
18
53
  /**
19
54
  * Gets the root element of the dialog.
@@ -35,6 +70,18 @@ export interface DialogState<T> {
35
70
  }
36
71
 
37
72
  export interface DialogOptions<T, V = T | undefined> {
73
+ /**
74
+ * Specifies a controller to allow queueing similar dialogs.
75
+ *
76
+ * When a controller with shared mode is specified, since there is not a designated element for individual dialog,
77
+ * the following options will have no effects: `container`, `className`, `focus`, `modal`, `preventLeave` and `preventNavigation`.
78
+ */
79
+ controller?: DialogController;
80
+ /**
81
+ * Specifies container element where dialog's root element will be inserted to.
82
+ * Default to document's body.
83
+ */
84
+ container?: HTMLElement;
38
85
  /**
39
86
  * Specifies dialog title.
40
87
  * This property is intended to be handled by {@link DialogOptions.onRender} or {@link DialogOptions.wrapper}.
@@ -133,7 +180,17 @@ export interface DialogProps<T, V = T> extends React.PropsWithChildren<DialogBas
133
180
  }
134
181
 
135
182
  /**
136
- * Creates a controller to render dialog.
183
+ * Creates a controller that manage multiple dialogs.
184
+ *
185
+ * When specified as {@link DialogOptions.controller} option for {@link createDialog},
186
+ * dialogs will be queued and be shown one after other.
187
+ *
188
+ * @param props A dictionary containing options.
189
+ */
190
+ export function createDialogQueue(options?: DialogControllerOptions | DialogControllerAdvancedOptions): DialogController;
191
+
192
+ /**
193
+ * Creates a dialog instance.
137
194
  * @param props A dictionary containing options.
138
195
  */
139
196
  export function createDialog<T, V>(props: DialogOptionsStrict<T, V>): DialogState<VoidOrOptional<T>>;
@@ -146,6 +203,20 @@ export function createDialog<P extends DialogOptionsStrict<any, any>>(props: P):
146
203
 
147
204
  export function createDialog<P extends DialogOptions<any, any>>(props: P): DialogState<VoidOrOptional<P extends DialogOptions<infer T, infer V> ? T | V : unknown>>;
148
205
 
206
+ /**
207
+ * Opens a dialog.
208
+ * @param props A dictionary containing options.
209
+ */
210
+ export function openDialog<T, V>(props: DialogOptionsStrict<T, V>): Promise<DialogResult<VoidOrOptional<T>>>;
211
+
212
+ export function openDialog<T, V>(props: DialogOptions<T, V>): Promise<DialogResult<VoidOrOptional<T | V>>>;
213
+
214
+ export function openDialog<T, V = T>(props: DialogOptions<T, V | undefined>): Promise<DialogResult<VoidOrOptional<T | V>>>;
215
+
216
+ export function openDialog<P extends DialogOptionsStrict<any, any>>(props: P): Promise<DialogResult<VoidOrOptional<P extends DialogOptionsStrict<infer T, any> ? T : unknown>>>;
217
+
218
+ export function openDialog<P extends DialogOptions<any, any>>(props: P): Promise<DialogResult<VoidOrOptional<P extends DialogOptions<infer T, infer V> ? T | V : unknown>>>;
219
+
149
220
  /**
150
221
  * Renders a dialog declaratively.
151
222
  */
package/dialog.js CHANGED
@@ -2,102 +2,196 @@ import { createElement, StrictMode, useEffect, useState } from "react";
2
2
  import ReactDOM from "react-dom";
3
3
  import ReactDOMClient from "@misonou/react-dom-client";
4
4
  import { createAsyncScope } from "zeta-dom-react";
5
- import { either, extend, noop, pick, resolve } from "zeta-dom/util";
5
+ import { always, arrRemove, combineFn, createPrivateStore, defineObservableProperty, either, exclude, extend, noop, pick, resolve, setImmediate } from "zeta-dom/util";
6
6
  import { containsOrEquals, removeNode } from "zeta-dom/domUtil";
7
7
  import dom from "zeta-dom/dom";
8
- import { lock, preventLeave, runAsync, subscribeAsync } from "zeta-dom/domLock";
9
- import { closeFlyout, openFlyout } from "brew-js/domAction";
8
+ import { runAsync, subscribeAsync } from "zeta-dom/domLock";
9
+ import { closeFlyout, isFlyoutOpen, openFlyout } from "brew-js/domAction";
10
10
 
11
- /**
12
- * @param {Partial<import("./dialog").DialogOptions<any>>} props
13
- */
14
- export function createDialog(props) {
15
- var root = document.createElement('div');
16
- var reactRoot = ReactDOMClient.createRoot(root);
17
- var scope = createAsyncScope(root);
18
- var closeDialog = closeFlyout.bind(0, root);
11
+ const _ = createPrivateStore();
12
+
13
+ function debounceAsync(callback) {
19
14
  var promise;
15
+ return function () {
16
+ if (!promise) {
17
+ promise = callback.apply(this, arguments);
18
+ always(promise, function () {
19
+ promise = null;
20
+ });
21
+ }
22
+ return promise;
23
+ };
24
+ }
20
25
 
26
+ function createDialogElement(props, unmountAfterUse) {
27
+ var root = document.createElement('div');
21
28
  dom.on(root, {
22
29
  flyoutshow: function () {
23
30
  (props.onOpen || noop)(root);
24
31
  },
25
32
  flyouthide: function () {
26
- promise = null;
27
33
  removeNode(root);
28
34
  (props.onClose || noop)(root);
29
- if (props.onRender) {
30
- reactRoot.unmount();
31
- }
35
+ (unmountAfterUse || noop)();
32
36
  }
33
37
  });
34
38
  root.setAttribute('loading-class', '');
35
39
  subscribeAsync(root, true);
40
+ return root;
41
+ }
42
+
43
+ function showDialog(element, props, container) {
44
+ if (!containsOrEquals(dom.root, element)) {
45
+ element.className = props.className || '';
46
+ if (props.modal) {
47
+ element.setAttribute('is-modal', '');
48
+ }
49
+ (container || props.container || document.body).appendChild(element);
50
+ }
51
+ return openFlyout(element, null, pick(props, ['focus', 'closeOnBlur', 'preventLeave', 'preventNavigation']));
52
+ }
53
+
54
+ export function openDialog(props) {
55
+ return createDialog(props).open();
56
+ }
57
+
58
+ /**
59
+ * @param {Partial<import("./dialog").DialogOptions<any>>} props
60
+ */
61
+ export function createDialog(props) {
62
+ var controller = _(props.controller) || {};
63
+ var shared = controller.mode === 'shared';
64
+ var state = shared ? controller : {};
65
+ var root = state.root || (state.root = createDialogElement(props, function () {
66
+ reactRoot.unmount();
67
+ }));
68
+ var reactRoot = state.reactRoot || (state.reactRoot = ReactDOMClient.createRoot(root));
69
+ var scope = state.scope || (state.scope = createAsyncScope(root));
70
+ var closeDialog = shared ? noop : closeFlyout.bind(0, root);
71
+
72
+ function render(closeDialog, props, container) {
73
+ var commitDialog = props.onCommit ? function (value) {
74
+ return runAsync(dom.activeElement, props.onCommit.bind(this, value)).then(closeDialog);
75
+ } : closeDialog;
76
+ var dialogProps = extend({}, props, {
77
+ errorHandler: scope.errorHandler,
78
+ closeDialog: commitDialog,
79
+ commitDialog: commitDialog,
80
+ dismissDialog: closeDialog
81
+ });
82
+ var content = createElement(props.onRender, dialogProps);
83
+ if (props.wrapper) {
84
+ content = createElement(props.wrapper, dialogProps, content);
85
+ }
86
+ reactRoot.render(createElement(StrictMode, null, createElement(scope.Provider, null, content)));
87
+ return shared ? { then: noop } : showDialog(root, props, container);
88
+ }
36
89
 
37
90
  return {
38
91
  root: root,
39
- close: closeDialog,
40
- open: function () {
41
- if (promise) {
42
- return promise;
43
- }
44
- root.className = props.className || '';
45
- document.body.appendChild(root);
46
- if (props.modal) {
47
- root.setAttribute('is-modal', '');
48
- }
49
- if (props.onRender) {
50
- var commitDialog = props.onCommit ? function (value) {
51
- return runAsync(dom.activeElement, props.onCommit.bind(this, value)).then(closeDialog);
52
- } : closeDialog;
53
- var dialogProps = extend({}, props, {
54
- errorHandler: scope.errorHandler,
55
- closeDialog: commitDialog,
56
- commitDialog: commitDialog,
57
- dismissDialog: closeDialog
92
+ close: function (value) {
93
+ return closeDialog(value);
94
+ },
95
+ open: debounceAsync(function () {
96
+ if (controller.enqueue) {
97
+ return controller.enqueue(function (next) {
98
+ closeDialog = shared ? next : closeDialog;
99
+ render(closeDialog, extend({}, controller.props, props), controller.root).then(next);
58
100
  });
59
- var content = createElement(props.onRender, dialogProps);
60
- if (props.wrapper) {
61
- content = createElement(props.wrapper, dialogProps, content);
101
+ }
102
+ return render(closeDialog, props);
103
+ })
104
+ };
105
+ }
106
+
107
+ /**
108
+ * @param {import("./dialog").DialogControllerOptions | undefined} props
109
+ */
110
+ export function createDialogQueue(props) {
111
+ var mode = props && props.mode;
112
+ var root = mode && createDialogElement(props);
113
+ var multiple = mode === 'multiple';
114
+ var childProps;
115
+ var queue = [];
116
+ var active = [];
117
+ var controller = {};
118
+ var setPendingCount = defineObservableProperty(controller, 'pendingCount', 0, true);
119
+
120
+ function dismissPending() {
121
+ combineFn(queue.splice(0))();
122
+ setPendingCount(0);
123
+ }
124
+
125
+ function dismissAll(value) {
126
+ combineFn(active.splice(0))(multiple ? undefined : value);
127
+ dismissPending();
128
+ }
129
+
130
+ function render(callback) {
131
+ return new Promise(function (resolvePromise) {
132
+ var next = function (value) {
133
+ if (arrRemove(active, resolvePromise)) {
134
+ resolvePromise(value);
135
+ setImmediate(function () {
136
+ (queue.shift() || noop)(true);
137
+ });
62
138
  }
63
- reactRoot.render(createElement(StrictMode, null, createElement(scope.Provider, null, content)));
139
+ return root && !queue[0] && !active[0] ? closeFlyout(root) : resolve();
140
+ };
141
+ active.push(resolvePromise);
142
+ setPendingCount(queue.length);
143
+ callback(next);
144
+ });
145
+ }
146
+
147
+ if (multiple) {
148
+ childProps = { closeOnBlur: false };
149
+ props = extend({}, props, childProps);
150
+ } else {
151
+ childProps = props && exclude(props, ['onCommit', 'onRender', 'onOpen', 'onClose']);
152
+ }
153
+ _(controller, {
154
+ root: root,
155
+ mode: mode,
156
+ props: childProps,
157
+ enqueue: function (callback) {
158
+ if (root && !isFlyoutOpen(root)) {
159
+ showDialog(root, props).then(dismissAll);
64
160
  }
65
- promise = resolve().then(function () {
66
- dom.retainFocus(dom.activeElement, root);
67
- return openFlyout(root, null, pick(props, ['focus']));
68
- });
69
- if (props.preventLeave) {
70
- preventLeave(root, promise);
71
- } else if (props.preventNavigation) {
72
- lock(root, promise);
161
+ if (queue.length || active.length >= (multiple ? props.concurrent || Infinity : 1)) {
162
+ return new Promise(function (resolve) {
163
+ queue.push(function (renderNext) {
164
+ resolve(renderNext && render(callback));
165
+ });
166
+ setPendingCount(queue.length);
167
+ });
73
168
  }
74
- return promise;
169
+ return render(callback);
75
170
  }
76
- };
171
+ });
172
+ return extend(controller, { dismissAll, dismissPending });
77
173
  }
78
174
 
79
175
  /**
80
176
  * @param {import("./dialog").DialogProps} props
81
177
  */
82
178
  export function Dialog(props) {
83
- const _props = useState({})[0];
84
- const dialog = useState(function () {
85
- return createDialog(_props);
179
+ const _props = extend(useState({})[0], props);
180
+ const element = useState(function () {
181
+ return createDialogElement(_props);
86
182
  })[0];
87
- extend(_props, props);
88
-
89
183
  useEffect(function () {
90
- var opened = containsOrEquals(dom.root, dialog.root);
184
+ var opened = isFlyoutOpen(element);
91
185
  if (either(opened, _props.isOpen)) {
92
186
  if (!opened) {
93
- dialog.open();
187
+ showDialog(element, _props);
94
188
  } else {
95
- dialog.close();
189
+ closeFlyout(element);
96
190
  }
97
191
  }
98
192
  }, [_props.isOpen])
99
193
  useEffect(function () {
100
- return dialog.close;
101
- }, [dialog]);
102
- return ReactDOM.createPortal(props.children, dialog.root);
194
+ return closeFlyout.bind(0, element);
195
+ }, []);
196
+ return ReactDOM.createPortal(props.children, element);
103
197
  }