brew-js-react 0.6.6 → 0.6.7
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 +58 -1
- package/dialog.js +157 -58
- package/dist/brew-js-react.js +173 -73
- package/dist/brew-js-react.js.map +1 -1
- package/dist/brew-js-react.min.js +2 -2
- package/dist/brew-js-react.min.js.map +1 -1
- package/mixin.d.ts +13 -0
- package/mixin.js +8 -8
- package/package.json +1 -1
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 = Pick<DialogOptions<any>, 'container' | 'className' | 'focus' | 'modal'>;
|
|
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
|
|
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>>;
|
package/dialog.js
CHANGED
|
@@ -2,102 +2,201 @@ 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, 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
8
|
import { lock, preventLeave, runAsync, subscribeAsync } from "zeta-dom/domLock";
|
|
9
|
-
import { closeFlyout, openFlyout } from "brew-js/domAction";
|
|
9
|
+
import { closeFlyout, isFlyoutOpen, openFlyout } from "brew-js/domAction";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 openDialog(element, props, container) {
|
|
44
|
+
if (!containsOrEquals(dom.root, element)) {
|
|
45
|
+
element.className = props.className || '';
|
|
46
|
+
(container || props.container || document.body).appendChild(element);
|
|
47
|
+
if (props.modal) {
|
|
48
|
+
element.setAttribute('is-modal', '');
|
|
49
|
+
}
|
|
50
|
+
setImmediate(function () {
|
|
51
|
+
dom.retainFocus(dom.activeElement, element);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
var promise = openFlyout(element, null, pick(props, ['focus', 'closeOnBlur']));
|
|
55
|
+
if (props.preventLeave) {
|
|
56
|
+
preventLeave(element, promise);
|
|
57
|
+
} else if (props.preventNavigation) {
|
|
58
|
+
lock(element, promise);
|
|
59
|
+
}
|
|
60
|
+
return promise;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {Partial<import("./dialog").DialogOptions<any>>} props
|
|
65
|
+
*/
|
|
66
|
+
export function createDialog(props) {
|
|
67
|
+
var controller = _(props.controller) || {};
|
|
68
|
+
var shared = controller.mode === 'shared';
|
|
69
|
+
var state = shared ? controller : {};
|
|
70
|
+
var root = state.root || (state.root = createDialogElement(props, function () {
|
|
71
|
+
reactRoot.unmount();
|
|
72
|
+
}));
|
|
73
|
+
var reactRoot = state.reactRoot || (state.reactRoot = ReactDOMClient.createRoot(root));
|
|
74
|
+
var scope = state.scope || (state.scope = createAsyncScope(root));
|
|
75
|
+
var closeDialog = shared ? noop : closeFlyout.bind(0, root);
|
|
76
|
+
|
|
77
|
+
function render(closeDialog, props, container) {
|
|
78
|
+
var commitDialog = props.onCommit ? function (value) {
|
|
79
|
+
return runAsync(dom.activeElement, props.onCommit.bind(this, value)).then(closeDialog);
|
|
80
|
+
} : closeDialog;
|
|
81
|
+
var dialogProps = extend({}, props, {
|
|
82
|
+
errorHandler: scope.errorHandler,
|
|
83
|
+
closeDialog: commitDialog,
|
|
84
|
+
commitDialog: commitDialog,
|
|
85
|
+
dismissDialog: closeDialog
|
|
86
|
+
});
|
|
87
|
+
var content = createElement(props.onRender, dialogProps);
|
|
88
|
+
if (props.wrapper) {
|
|
89
|
+
content = createElement(props.wrapper, dialogProps, content);
|
|
90
|
+
}
|
|
91
|
+
reactRoot.render(createElement(StrictMode, null, createElement(scope.Provider, null, content)));
|
|
92
|
+
return shared ? { then: noop } : openDialog(root, props, container);
|
|
93
|
+
}
|
|
36
94
|
|
|
37
95
|
return {
|
|
38
96
|
root: root,
|
|
39
|
-
close:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
97
|
+
close: function (value) {
|
|
98
|
+
return closeDialog(value);
|
|
99
|
+
},
|
|
100
|
+
open: debounceAsync(function () {
|
|
101
|
+
if (controller.enqueue) {
|
|
102
|
+
return controller.enqueue(function (next) {
|
|
103
|
+
closeDialog = shared ? next : closeDialog;
|
|
104
|
+
render(closeDialog, extend({}, controller.props, props), controller.root).then(next);
|
|
58
105
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
106
|
+
}
|
|
107
|
+
return render(closeDialog, props);
|
|
108
|
+
})
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {import("./dialog").DialogControllerOptions | undefined} props
|
|
114
|
+
*/
|
|
115
|
+
export function createDialogQueue(props) {
|
|
116
|
+
var mode = props && props.mode;
|
|
117
|
+
var root = mode && createDialogElement(props);
|
|
118
|
+
var multiple = mode === 'multiple';
|
|
119
|
+
var childProps;
|
|
120
|
+
var queue = [];
|
|
121
|
+
var active = [];
|
|
122
|
+
var controller = {};
|
|
123
|
+
var setPendingCount = defineObservableProperty(controller, 'pendingCount', 0, true);
|
|
124
|
+
|
|
125
|
+
function dismissPending() {
|
|
126
|
+
combineFn(queue.splice(0))();
|
|
127
|
+
setPendingCount(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dismissAll(value) {
|
|
131
|
+
combineFn(active.splice(0))(multiple ? undefined : value);
|
|
132
|
+
dismissPending();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function render(callback) {
|
|
136
|
+
return new Promise(function (resolvePromise) {
|
|
137
|
+
var next = function (value) {
|
|
138
|
+
if (arrRemove(active, resolvePromise)) {
|
|
139
|
+
resolvePromise(value);
|
|
140
|
+
setImmediate(function () {
|
|
141
|
+
(queue.shift() || noop)(true);
|
|
142
|
+
});
|
|
62
143
|
}
|
|
63
|
-
|
|
144
|
+
return root && !queue[0] && !active[0] ? closeFlyout(root) : resolve();
|
|
145
|
+
};
|
|
146
|
+
active.push(resolvePromise);
|
|
147
|
+
setPendingCount(queue.length);
|
|
148
|
+
callback(next);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (multiple) {
|
|
153
|
+
childProps = { closeOnBlur: false };
|
|
154
|
+
props = extend({}, props, childProps);
|
|
155
|
+
} else {
|
|
156
|
+
childProps = props && pick(props, ['className', 'focus', 'modal', 'container']);
|
|
157
|
+
}
|
|
158
|
+
_(controller, {
|
|
159
|
+
root: root,
|
|
160
|
+
mode: mode,
|
|
161
|
+
props: childProps,
|
|
162
|
+
enqueue: function (callback) {
|
|
163
|
+
if (root && !isFlyoutOpen(root)) {
|
|
164
|
+
openDialog(root, props).then(dismissAll);
|
|
64
165
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
lock(root, promise);
|
|
166
|
+
if (queue.length || active.length >= (multiple ? props.concurrent || Infinity : 1)) {
|
|
167
|
+
return new Promise(function (resolve) {
|
|
168
|
+
queue.push(function (renderNext) {
|
|
169
|
+
resolve(renderNext && render(callback));
|
|
170
|
+
});
|
|
171
|
+
setPendingCount(queue.length);
|
|
172
|
+
});
|
|
73
173
|
}
|
|
74
|
-
return
|
|
174
|
+
return render(callback);
|
|
75
175
|
}
|
|
76
|
-
};
|
|
176
|
+
});
|
|
177
|
+
return extend(controller, { dismissAll, dismissPending });
|
|
77
178
|
}
|
|
78
179
|
|
|
79
180
|
/**
|
|
80
181
|
* @param {import("./dialog").DialogProps} props
|
|
81
182
|
*/
|
|
82
183
|
export function Dialog(props) {
|
|
83
|
-
const _props = useState({})[0];
|
|
84
|
-
const
|
|
85
|
-
return
|
|
184
|
+
const _props = extend(useState({})[0], props);
|
|
185
|
+
const element = useState(function () {
|
|
186
|
+
return createDialogElement(_props);
|
|
86
187
|
})[0];
|
|
87
|
-
extend(_props, props);
|
|
88
|
-
|
|
89
188
|
useEffect(function () {
|
|
90
|
-
var opened =
|
|
189
|
+
var opened = isFlyoutOpen(element);
|
|
91
190
|
if (either(opened, _props.isOpen)) {
|
|
92
191
|
if (!opened) {
|
|
93
|
-
|
|
192
|
+
openDialog(element, _props);
|
|
94
193
|
} else {
|
|
95
|
-
|
|
194
|
+
closeFlyout(element);
|
|
96
195
|
}
|
|
97
196
|
}
|
|
98
197
|
}, [_props.isOpen])
|
|
99
198
|
useEffect(function () {
|
|
100
|
-
return
|
|
101
|
-
}, [
|
|
102
|
-
return ReactDOM.createPortal(props.children,
|
|
199
|
+
return closeFlyout.bind(0, element);
|
|
200
|
+
}, []);
|
|
201
|
+
return ReactDOM.createPortal(props.children, element);
|
|
103
202
|
}
|