@expressive/ui 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +48 -2
- package/dist/button/base.d.ts +63 -0
- package/dist/button/base.js +90 -0
- package/dist/button/base.js.map +1 -0
- package/dist/button/button.d.ts +56 -0
- package/dist/button/button.js +138 -0
- package/dist/button/button.js.map +1 -0
- package/dist/calendar/base.d.ts +112 -0
- package/dist/calendar/base.js +235 -0
- package/dist/calendar/base.js.map +1 -0
- package/dist/calendar/calendar.d.ts +78 -0
- package/dist/calendar/calendar.js +145 -0
- package/dist/calendar/calendar.js.map +1 -0
- package/dist/calendar/date.js +56 -0
- package/dist/calendar/date.js.map +1 -0
- package/dist/dialog/base.d.ts +74 -0
- package/dist/dialog/base.js +143 -0
- package/dist/dialog/base.js.map +1 -0
- package/dist/dialog/dialog.d.ts +69 -0
- package/dist/dialog/dialog.js +108 -0
- package/dist/dialog/dialog.js.map +1 -0
- package/dist/dom.d.ts +16 -0
- package/dist/dom.js +17 -0
- package/dist/dropzone/base.d.ts +77 -0
- package/dist/dropzone/base.js +159 -0
- package/dist/dropzone/base.js.map +1 -0
- package/dist/dropzone/dropzone.d.ts +38 -0
- package/dist/dropzone/dropzone.js +92 -0
- package/dist/dropzone/dropzone.js.map +1 -0
- package/dist/form/base.d.ts +157 -0
- package/dist/form/base.js +243 -0
- package/dist/form/base.js.map +1 -0
- package/dist/form/form.d.ts +123 -0
- package/dist/form/form.js +179 -0
- package/dist/form/form.js.map +1 -0
- package/dist/icons/caret.d.ts +8 -0
- package/dist/icons/caret.js +16 -0
- package/dist/icons/caret.js.map +1 -0
- package/dist/icons/check.d.ts +8 -0
- package/dist/icons/check.js +16 -0
- package/dist/icons/check.js.map +1 -0
- package/dist/icons/cross.d.ts +8 -0
- package/dist/icons/cross.js +16 -0
- package/dist/icons/cross.js.map +1 -0
- package/dist/icons/icon.js +16 -0
- package/dist/icons/icon.js.map +1 -0
- package/dist/icons/spinner.d.ts +8 -0
- package/dist/icons/spinner.js +27 -0
- package/dist/icons/spinner.js.map +1 -0
- package/dist/icons/upload.d.ts +8 -0
- package/dist/icons/upload.js +16 -0
- package/dist/icons/upload.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/layout/base.d.ts +88 -0
- package/dist/layout/base.js +158 -0
- package/dist/layout/base.js.map +1 -0
- package/dist/layout/layout.d.ts +53 -0
- package/dist/layout/layout.js +80 -0
- package/dist/layout/layout.js.map +1 -0
- package/dist/piece.js +13 -0
- package/dist/piece.js.map +1 -0
- package/dist/popover/base.d.ts +94 -0
- package/dist/popover/base.js +176 -0
- package/dist/popover/base.js.map +1 -0
- package/dist/popover/place.d.ts +12 -0
- package/dist/popover/place.js +32 -0
- package/dist/popover/place.js.map +1 -0
- package/dist/popover/popover.d.ts +52 -0
- package/dist/popover/popover.js +104 -0
- package/dist/popover/popover.js.map +1 -0
- package/dist/select/base.d.ts +196 -0
- package/dist/select/base.js +409 -0
- package/dist/select/base.js.map +1 -0
- package/dist/select/select.d.ts +119 -0
- package/dist/select/select.js +219 -0
- package/dist/select/select.js.map +1 -0
- package/dist/table/base.d.ts +96 -0
- package/dist/table/base.js +154 -0
- package/dist/table/base.js.map +1 -0
- package/dist/table/column.d.ts +53 -0
- package/dist/table/column.js +29 -0
- package/dist/table/column.js.map +1 -0
- package/dist/table/table.d.ts +41 -0
- package/dist/table/table.js +101 -0
- package/dist/table/table.js.map +1 -0
- package/package.json +76 -4
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { missing } from "../piece.js";
|
|
2
|
+
import { Component } from "@expressive/mvc";
|
|
3
|
+
import { jsx, jsxs } from "@expressive/mvc/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/dialog/base.tsx
|
|
6
|
+
const STACK = [];
|
|
7
|
+
const ARMED = /* @__PURE__ */ new WeakSet();
|
|
8
|
+
const RESTORE = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
const PANEL = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
11
|
+
let ID = 0;
|
|
12
|
+
let LOCKS = 0;
|
|
13
|
+
let overflow = "";
|
|
14
|
+
const Layer = ({ children }) => children;
|
|
15
|
+
/**
|
|
16
|
+
* Modal dialog behavior without a look: focus trap and restore, scroll lock, dismissal and ARIA, assembled from pieces.
|
|
17
|
+
*
|
|
18
|
+
* Every visible piece is a placeholder that throws when rendered - a skin assigns each one it renders. `Layer` renders
|
|
19
|
+
* in place by default; a skin replaces it to move the dialog out of the page. Pieces spread the matching `*Props`
|
|
20
|
+
* getter onto their element to stay accessible.
|
|
21
|
+
*/
|
|
22
|
+
var Dialog = class extends Component {
|
|
23
|
+
open = false;
|
|
24
|
+
onClose = void 0;
|
|
25
|
+
/** Heading, and the dialog's accessible name. */
|
|
26
|
+
title = void 0;
|
|
27
|
+
/** Whether Escape, the backdrop and the close button dismiss. */
|
|
28
|
+
dismissible = true;
|
|
29
|
+
/** Prefixes the heading id. */
|
|
30
|
+
id = `dialog-${++ID}`;
|
|
31
|
+
Layer = Layer;
|
|
32
|
+
Backdrop = missing("Dialog.Backdrop");
|
|
33
|
+
Panel = missing("Dialog.Panel");
|
|
34
|
+
Header = missing("Dialog.Header");
|
|
35
|
+
Title = missing("Dialog.Title");
|
|
36
|
+
Close = missing("Dialog.Close");
|
|
37
|
+
constructor(...args) {
|
|
38
|
+
super(...args, () => {
|
|
39
|
+
this.get((current) => {
|
|
40
|
+
if (current.open) return this.enter();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** For the backdrop: a press that starts and ends on it closes. */
|
|
45
|
+
get backdropProps() {
|
|
46
|
+
return {
|
|
47
|
+
onMouseDown: (event) => {
|
|
48
|
+
if (event.target === event.currentTarget) ARMED.add(this);
|
|
49
|
+
else ARMED.delete(this);
|
|
50
|
+
},
|
|
51
|
+
onClick: (event) => {
|
|
52
|
+
if (ARMED.delete(this) && event.target === event.currentTarget) this.close();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
get panelProps() {
|
|
57
|
+
const { id, title } = this;
|
|
58
|
+
return {
|
|
59
|
+
ref: this.focusIn,
|
|
60
|
+
id: `${id}-panel`,
|
|
61
|
+
role: "dialog",
|
|
62
|
+
"aria-modal": "true",
|
|
63
|
+
"aria-labelledby": title ? `${id}-title` : void 0,
|
|
64
|
+
tabIndex: -1
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
get titleProps() {
|
|
68
|
+
return { id: `${this.id}-title` };
|
|
69
|
+
}
|
|
70
|
+
get closeProps() {
|
|
71
|
+
return {
|
|
72
|
+
type: "button",
|
|
73
|
+
"aria-label": "Close",
|
|
74
|
+
onClick: this.close
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Ask to close; ignored unless `dismissible`. */
|
|
78
|
+
close() {
|
|
79
|
+
if (this.dismissible && this.onClose) this.onClose();
|
|
80
|
+
}
|
|
81
|
+
/** Escape closes; Tab goes to `trap`. Ignored unless this is the topmost open dialog. */
|
|
82
|
+
keyDown(event) {
|
|
83
|
+
if (STACK[STACK.length - 1] !== this) return;
|
|
84
|
+
if (event.key == "Escape") {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
this.close();
|
|
87
|
+
} else if (event.key == "Tab") this.trap(event);
|
|
88
|
+
}
|
|
89
|
+
/** Send keys pressed anywhere to `keyDown` while open; returns the release. Defaults to `document`. */
|
|
90
|
+
listen() {
|
|
91
|
+
document.addEventListener("keydown", this.keyDown);
|
|
92
|
+
return () => document.removeEventListener("keydown", this.keyDown);
|
|
93
|
+
}
|
|
94
|
+
/** Stop the page behind from scrolling while open; returns the release. Defaults to the body's `overflow`. */
|
|
95
|
+
lock() {
|
|
96
|
+
if (!LOCKS++) {
|
|
97
|
+
overflow = document.body.style.overflow;
|
|
98
|
+
document.body.style.overflow = "hidden";
|
|
99
|
+
}
|
|
100
|
+
return () => {
|
|
101
|
+
if (!--LOCKS) document.body.style.overflow = overflow;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Keep Tab focus inside the panel, wrapping at either end. Defaults to the DOM's focusable elements. */
|
|
105
|
+
trap(event) {
|
|
106
|
+
const panel = PANEL.get(this);
|
|
107
|
+
const items = Array.from(panel.querySelectorAll(FOCUSABLE));
|
|
108
|
+
const first = items[0] || panel;
|
|
109
|
+
const last = items[items.length - 1] || panel;
|
|
110
|
+
const active = document.activeElement;
|
|
111
|
+
if (!panel.contains(active) || (event.shiftKey ? active === first || active === panel : active === last)) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
(event.shiftKey ? last : first).focus();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
enter() {
|
|
117
|
+
const unlock = this.lock();
|
|
118
|
+
const release = this.listen();
|
|
119
|
+
STACK.push(this);
|
|
120
|
+
return () => {
|
|
121
|
+
release();
|
|
122
|
+
unlock();
|
|
123
|
+
STACK.splice(STACK.indexOf(this), 1);
|
|
124
|
+
RESTORE.get(this)?.focus();
|
|
125
|
+
RESTORE.delete(this);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Ref for the panel, in `panelProps`; focuses it, or its `autofocus` element. */
|
|
129
|
+
focusIn(panel) {
|
|
130
|
+
if (!panel) return;
|
|
131
|
+
PANEL.set(this, panel);
|
|
132
|
+
RESTORE.set(this, document.activeElement);
|
|
133
|
+
(panel.querySelector("[autofocus]") || panel).focus();
|
|
134
|
+
}
|
|
135
|
+
render(props) {
|
|
136
|
+
const { open, title, dismissible } = this;
|
|
137
|
+
return open && /* @__PURE__ */ jsx(this.Layer, { children: /* @__PURE__ */ jsx(this.Backdrop, { children: /* @__PURE__ */ jsxs(this.Panel, { children: [(title || dismissible) && /* @__PURE__ */ jsxs(this.Header, { children: [title && /* @__PURE__ */ jsx(this.Title, {}), dismissible && /* @__PURE__ */ jsx(this.Close, {})] }), props.children] }) }) });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { Dialog };
|
|
143
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","names":[],"sources":["../../src/dialog/base.tsx"],"sourcesContent":["import { Component, type Model } from '@expressive/mvc';\n\nimport { missing } from '../piece';\n\ndeclare namespace Dialog {\n type Wrap = (props: { children: Component.Node }) => Component.Node;\n type Part = () => Component.Node;\n type Key = Pick<KeyboardEvent, 'key' | 'shiftKey' | 'preventDefault'>;\n}\n\nconst STACK: Dialog[] = [];\n\nconst ARMED = new WeakSet<Dialog>();\n\nconst RESTORE = new WeakMap<Dialog, HTMLElement>();\n\nconst PANEL = new WeakMap<Dialog, HTMLElement>();\n\nconst FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nlet ID = 0;\n\nlet LOCKS = 0;\n\nlet overflow = '';\n\nconst Layer = ({ children }: { children: Component.Node }): Component.Node => children;\n\n/**\n * Modal dialog behavior without a look: focus trap and restore, scroll lock, dismissal and ARIA, assembled from pieces.\n *\n * Every visible piece is a placeholder that throws when rendered - a skin assigns each one it renders. `Layer` renders\n * in place by default; a skin replaces it to move the dialog out of the page. Pieces spread the matching `*Props`\n * getter onto their element to stay accessible.\n */\nclass Dialog extends Component {\n open = false;\n onClose?: () => void = undefined;\n\n /** Heading, and the dialog's accessible name. */\n title?: string = undefined;\n\n /** Whether Escape, the backdrop and the close button dismiss. */\n dismissible = true;\n\n /** Prefixes the heading id. */\n id = `dialog-${++ID}`;\n\n Layer: Dialog.Wrap = Layer;\n Backdrop: Dialog.Wrap = missing('Dialog.Backdrop');\n Panel: Dialog.Wrap = missing('Dialog.Panel');\n Header: Dialog.Wrap = missing('Dialog.Header');\n Title: Dialog.Part = missing('Dialog.Title');\n Close: Dialog.Part = missing('Dialog.Close');\n\n constructor(...args: Model.Args) {\n super(...args, () => {\n this.get((current) => {\n if (current.open) return this.enter();\n });\n });\n }\n\n /** For the backdrop: a press that starts and ends on it closes. */\n get backdropProps() {\n return {\n onMouseDown: (event: MouseEvent) => {\n if (event.target === event.currentTarget) ARMED.add(this);\n else ARMED.delete(this);\n },\n onClick: (event: MouseEvent) => {\n if (ARMED.delete(this) && event.target === event.currentTarget) this.close();\n }\n };\n }\n\n get panelProps() {\n const { id, title } = this;\n\n return {\n ref: this.focusIn,\n id: `${id}-panel`,\n role: 'dialog',\n 'aria-modal': 'true',\n 'aria-labelledby': title ? `${id}-title` : undefined,\n tabIndex: -1\n } as const;\n }\n\n get titleProps() {\n return { id: `${this.id}-title` };\n }\n\n get closeProps() {\n return { type: 'button', 'aria-label': 'Close', onClick: this.close } as const;\n }\n\n /** Ask to close; ignored unless `dismissible`. */\n close() {\n if (this.dismissible && this.onClose) this.onClose();\n }\n\n /** Escape closes; Tab goes to `trap`. Ignored unless this is the topmost open dialog. */\n protected keyDown(event: Dialog.Key) {\n if (STACK[STACK.length - 1] !== this) return;\n\n if (event.key == 'Escape') {\n event.preventDefault();\n this.close();\n } else if (event.key == 'Tab') this.trap(event);\n }\n\n /** Send keys pressed anywhere to `keyDown` while open; returns the release. Defaults to `document`. */\n protected listen(): () => void {\n document.addEventListener('keydown', this.keyDown);\n\n return () => document.removeEventListener('keydown', this.keyDown);\n }\n\n /** Stop the page behind from scrolling while open; returns the release. Defaults to the body's `overflow`. */\n protected lock(): () => void {\n if (!LOCKS++) {\n overflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n return () => {\n if (!--LOCKS) document.body.style.overflow = overflow;\n };\n }\n\n /** Keep Tab focus inside the panel, wrapping at either end. Defaults to the DOM's focusable elements. */\n protected trap(event: Dialog.Key) {\n const panel = PANEL.get(this)!;\n const items = Array.from(panel.querySelectorAll<HTMLElement>(FOCUSABLE));\n const first = items[0] || panel;\n const last = items[items.length - 1] || panel;\n const active = document.activeElement;\n\n if (!panel.contains(active) || (event.shiftKey ? active === first || active === panel : active === last)) {\n event.preventDefault();\n (event.shiftKey ? last : first).focus();\n }\n }\n\n private enter() {\n const unlock = this.lock();\n const release = this.listen();\n\n STACK.push(this);\n\n return () => {\n release();\n unlock();\n STACK.splice(STACK.indexOf(this), 1);\n RESTORE.get(this)?.focus();\n RESTORE.delete(this);\n };\n }\n\n /** Ref for the panel, in `panelProps`; focuses it, or its `autofocus` element. */\n focusIn(panel: HTMLElement | null) {\n if (!panel) return;\n\n PANEL.set(this, panel);\n RESTORE.set(this, document.activeElement as HTMLElement);\n (panel.querySelector<HTMLElement>('[autofocus]') || panel).focus();\n }\n\n render(props: { children?: Component.Node }): Component.Node {\n const { open, title, dismissible } = this;\n\n return (\n open && (\n <this.Layer>\n <this.Backdrop>\n <this.Panel>\n {(title || dismissible) && (\n <this.Header>\n {title && <this.Title />}\n {dismissible && <this.Close />}\n </this.Header>\n )}\n {props.children}\n </this.Panel>\n </this.Backdrop>\n </this.Layer>\n )\n );\n }\n}\n\nexport { Dialog };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Dialog as Dialog$1 } from "./base.js";
|
|
2
|
+
import { Component } from "@expressive/mvc";
|
|
3
|
+
|
|
4
|
+
//#region src/dialog/dialog.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Modal dialog, portalled out of the page while `open`.
|
|
7
|
+
*
|
|
8
|
+
* Focus moves in on open - to an `autofocus` element, else the panel - stays trapped while open, and returns on close.
|
|
9
|
+
* The page behind stops scrolling. Escape and a press on the backdrop call `onClose` unless `dismissible` is false;
|
|
10
|
+
* only the topmost of stacked dialogs answers. Pieces - `Layer`, `Backdrop`, `Panel`, `Header`, `Title`, `Close` - are
|
|
11
|
+
* function components reading `Dialog.get()`, each styled on its own; pass one as a prop or assign it in a subclass to
|
|
12
|
+
* replace it. Stock pieces are statics. A replacement `Panel` spreads `panelProps`; `Layer` portals into `into`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <Dialog open={this.editing} title="Rename" onClose={() => (this.editing = false)}>
|
|
16
|
+
* <input autofocus value={this.name} />
|
|
17
|
+
* </Dialog>
|
|
18
|
+
*/
|
|
19
|
+
declare class Dialog extends Dialog$1 {
|
|
20
|
+
/** Where `Layer` portals: an element or a selector. */
|
|
21
|
+
into: Element | string;
|
|
22
|
+
static Layer: ({
|
|
23
|
+
children
|
|
24
|
+
}: {
|
|
25
|
+
children: Component.Node;
|
|
26
|
+
}) => Component.Node;
|
|
27
|
+
static Backdrop: ({
|
|
28
|
+
children
|
|
29
|
+
}: {
|
|
30
|
+
children: Component.Node;
|
|
31
|
+
}) => Component.Node;
|
|
32
|
+
static Panel: ({
|
|
33
|
+
children
|
|
34
|
+
}: {
|
|
35
|
+
children: Component.Node;
|
|
36
|
+
}) => Component.Node;
|
|
37
|
+
static Header: ({
|
|
38
|
+
children
|
|
39
|
+
}: {
|
|
40
|
+
children: Component.Node;
|
|
41
|
+
}) => Component.Node;
|
|
42
|
+
static Title: () => Component.Node;
|
|
43
|
+
static Close: () => Component.Node;
|
|
44
|
+
Layer: ({
|
|
45
|
+
children
|
|
46
|
+
}: {
|
|
47
|
+
children: Component.Node;
|
|
48
|
+
}) => Component.Node;
|
|
49
|
+
Backdrop: ({
|
|
50
|
+
children
|
|
51
|
+
}: {
|
|
52
|
+
children: Component.Node;
|
|
53
|
+
}) => Component.Node;
|
|
54
|
+
Panel: ({
|
|
55
|
+
children
|
|
56
|
+
}: {
|
|
57
|
+
children: Component.Node;
|
|
58
|
+
}) => Component.Node;
|
|
59
|
+
Header: ({
|
|
60
|
+
children
|
|
61
|
+
}: {
|
|
62
|
+
children: Component.Node;
|
|
63
|
+
}) => Component.Node;
|
|
64
|
+
Title: () => Component.Node;
|
|
65
|
+
Close: () => Component.Node;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { Dialog };
|
|
69
|
+
//# sourceMappingURL=dialog.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Dialog as Dialog$1 } from "./base.js";
|
|
2
|
+
import { Cross } from "../icons/cross.js";
|
|
3
|
+
import { Portal, style } from "@expressive/dom";
|
|
4
|
+
import { jsx } from "@expressive/dom/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/dialog/dialog.tsx
|
|
7
|
+
const Layer = ({ children }) => /* @__PURE__ */ jsx(Portal, {
|
|
8
|
+
into: Dialog.get().into,
|
|
9
|
+
children
|
|
10
|
+
});
|
|
11
|
+
const Backdrop = ({ children }) => /* @__PURE__ */ jsx("div", {
|
|
12
|
+
...Dialog$1.get().backdropProps,
|
|
13
|
+
children
|
|
14
|
+
});
|
|
15
|
+
style(Backdrop, {
|
|
16
|
+
position: "fixed",
|
|
17
|
+
inset: "0",
|
|
18
|
+
zIndex: "1000",
|
|
19
|
+
display: "grid",
|
|
20
|
+
placeItems: "center",
|
|
21
|
+
padding: "16px",
|
|
22
|
+
background: "rgb(0 0 0 / 0.4)"
|
|
23
|
+
});
|
|
24
|
+
const Panel = ({ children }) => /* @__PURE__ */ jsx("div", {
|
|
25
|
+
...Dialog$1.get().panelProps,
|
|
26
|
+
children
|
|
27
|
+
});
|
|
28
|
+
style(Panel, {
|
|
29
|
+
position: "relative",
|
|
30
|
+
boxSizing: "border-box",
|
|
31
|
+
width: "min(480px, 100%)",
|
|
32
|
+
maxHeight: "100%",
|
|
33
|
+
overflow: "auto",
|
|
34
|
+
padding: "20px 24px",
|
|
35
|
+
background: "var(--ui-surface, #fff)",
|
|
36
|
+
color: "inherit",
|
|
37
|
+
borderRadius: "10px",
|
|
38
|
+
boxShadow: "0 10px 40px rgb(0 0 0 / 0.2)",
|
|
39
|
+
outline: "0"
|
|
40
|
+
});
|
|
41
|
+
const Header = ({ children }) => /* @__PURE__ */ jsx("div", { children });
|
|
42
|
+
style(Header, {
|
|
43
|
+
display: "flex",
|
|
44
|
+
alignItems: "center",
|
|
45
|
+
gap: "12px",
|
|
46
|
+
marginBottom: "12px"
|
|
47
|
+
});
|
|
48
|
+
const Title = () => {
|
|
49
|
+
const { title, titleProps } = Dialog$1.get();
|
|
50
|
+
return /* @__PURE__ */ jsx("h2", {
|
|
51
|
+
...titleProps,
|
|
52
|
+
children: title
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
style(Title, {
|
|
56
|
+
flex: "1",
|
|
57
|
+
margin: "0",
|
|
58
|
+
fontSize: "18px"
|
|
59
|
+
});
|
|
60
|
+
const Close = () => /* @__PURE__ */ jsx("button", {
|
|
61
|
+
...Dialog$1.get().closeProps,
|
|
62
|
+
children: /* @__PURE__ */ jsx(Cross, {})
|
|
63
|
+
});
|
|
64
|
+
style(Close, {
|
|
65
|
+
display: "flex",
|
|
66
|
+
marginLeft: "auto",
|
|
67
|
+
padding: "6px",
|
|
68
|
+
border: "0",
|
|
69
|
+
borderRadius: "4px",
|
|
70
|
+
background: "none",
|
|
71
|
+
color: "var(--ui-muted, #999)",
|
|
72
|
+
cursor: "pointer",
|
|
73
|
+
":hover": { background: "var(--ui-highlight, #deebff)" }
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Modal dialog, portalled out of the page while `open`.
|
|
77
|
+
*
|
|
78
|
+
* Focus moves in on open - to an `autofocus` element, else the panel - stays trapped while open, and returns on close.
|
|
79
|
+
* The page behind stops scrolling. Escape and a press on the backdrop call `onClose` unless `dismissible` is false;
|
|
80
|
+
* only the topmost of stacked dialogs answers. Pieces - `Layer`, `Backdrop`, `Panel`, `Header`, `Title`, `Close` - are
|
|
81
|
+
* function components reading `Dialog.get()`, each styled on its own; pass one as a prop or assign it in a subclass to
|
|
82
|
+
* replace it. Stock pieces are statics. A replacement `Panel` spreads `panelProps`; `Layer` portals into `into`.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* <Dialog open={this.editing} title="Rename" onClose={() => (this.editing = false)}>
|
|
86
|
+
* <input autofocus value={this.name} />
|
|
87
|
+
* </Dialog>
|
|
88
|
+
*/
|
|
89
|
+
var Dialog = class extends Dialog$1 {
|
|
90
|
+
/** Where `Layer` portals: an element or a selector. */
|
|
91
|
+
into = "body";
|
|
92
|
+
static Layer = Layer;
|
|
93
|
+
static Backdrop = Backdrop;
|
|
94
|
+
static Panel = Panel;
|
|
95
|
+
static Header = Header;
|
|
96
|
+
static Title = Title;
|
|
97
|
+
static Close = Close;
|
|
98
|
+
Layer = Layer;
|
|
99
|
+
Backdrop = Backdrop;
|
|
100
|
+
Panel = Panel;
|
|
101
|
+
Header = Header;
|
|
102
|
+
Title = Title;
|
|
103
|
+
Close = Close;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
export { Dialog };
|
|
108
|
+
//# sourceMappingURL=dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.js","names":["Base.Dialog"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["/** @jsxImportSource @expressive/dom */\nimport { Component } from '@expressive/mvc';\nimport { Portal, style } from '@expressive/dom';\n\nimport { Cross } from '../icons';\nimport * as Base from './base';\n\nconst Layer = ({ children }: { children: Component.Node }): Component.Node => (\n <Portal into={Dialog.get().into}>{children}</Portal>\n);\n\nconst Backdrop = ({ children }: { children: Component.Node }): Component.Node => (\n <div {...Base.Dialog.get().backdropProps}>{children}</div>\n);\n\nstyle(Backdrop, {\n position: 'fixed',\n inset: '0',\n zIndex: '1000',\n display: 'grid',\n placeItems: 'center',\n padding: '16px',\n background: 'rgb(0 0 0 / 0.4)'\n});\n\nconst Panel = ({ children }: { children: Component.Node }): Component.Node => (\n <div {...Base.Dialog.get().panelProps}>{children}</div>\n);\n\nstyle(Panel, {\n position: 'relative',\n boxSizing: 'border-box',\n width: 'min(480px, 100%)',\n maxHeight: '100%',\n overflow: 'auto',\n padding: '20px 24px',\n background: 'var(--ui-surface, #fff)',\n color: 'inherit',\n borderRadius: '10px',\n boxShadow: '0 10px 40px rgb(0 0 0 / 0.2)',\n outline: '0'\n});\n\nconst Header = ({ children }: { children: Component.Node }): Component.Node => <div>{children}</div>;\n\nstyle(Header, {\n display: 'flex',\n alignItems: 'center',\n gap: '12px',\n marginBottom: '12px'\n});\n\nconst Title = (): Component.Node => {\n const { title, titleProps } = Base.Dialog.get();\n\n return <h2 {...titleProps}>{title}</h2>;\n};\n\nstyle(Title, {\n flex: '1',\n margin: '0',\n fontSize: '18px'\n});\n\nconst Close = (): Component.Node => (\n <button {...Base.Dialog.get().closeProps}>\n <Cross />\n </button>\n);\n\nstyle(Close, {\n display: 'flex',\n marginLeft: 'auto',\n padding: '6px',\n border: '0',\n borderRadius: '4px',\n background: 'none',\n color: 'var(--ui-muted, #999)',\n cursor: 'pointer',\n ':hover': {\n background: 'var(--ui-highlight, #deebff)'\n }\n});\n\n/**\n * Modal dialog, portalled out of the page while `open`.\n *\n * Focus moves in on open - to an `autofocus` element, else the panel - stays trapped while open, and returns on close.\n * The page behind stops scrolling. Escape and a press on the backdrop call `onClose` unless `dismissible` is false;\n * only the topmost of stacked dialogs answers. Pieces - `Layer`, `Backdrop`, `Panel`, `Header`, `Title`, `Close` - are\n * function components reading `Dialog.get()`, each styled on its own; pass one as a prop or assign it in a subclass to\n * replace it. Stock pieces are statics. A replacement `Panel` spreads `panelProps`; `Layer` portals into `into`.\n *\n * @example\n * <Dialog open={this.editing} title=\"Rename\" onClose={() => (this.editing = false)}>\n * <input autofocus value={this.name} />\n * </Dialog>\n */\nclass Dialog extends Base.Dialog {\n /** Where `Layer` portals: an element or a selector. */\n into: Element | string = 'body';\n\n static Layer = Layer;\n static Backdrop = Backdrop;\n static Panel = Panel;\n static Header = Header;\n static Title = Title;\n static Close = Close;\n\n Layer = Layer;\n Backdrop = Backdrop;\n Panel = Panel;\n Header = Header;\n Title = Title;\n Close = Close;\n}\n\nexport { Backdrop, Close, Dialog, Header, Layer, Panel, Title };\n"],"mappings":";;;;;;AAOA,MAAM,SAAS,EAAE,eACf,oBAAC;CAAO,MAAM,OAAO,KAAK,CAAC;CAAO;EAAkB;AAGtD,MAAM,YAAY,EAAE,eAClB,oBAAC;CAAI,YAAgB,KAAK,CAAC;CAAgB;EAAe;AAG5D,MAAM,UAAU;CACd,UAAU;CACV,OAAO;CACP,QAAQ;CACR,SAAS;CACT,YAAY;CACZ,SAAS;CACT,YAAY;CACb,CAAC;AAEF,MAAM,SAAS,EAAE,eACf,oBAAC;CAAI,YAAgB,KAAK,CAAC;CAAa;EAAe;AAGzD,MAAM,OAAO;CACX,UAAU;CACV,WAAW;CACX,OAAO;CACP,WAAW;CACX,UAAU;CACV,SAAS;CACT,YAAY;CACZ,OAAO;CACP,cAAc;CACd,WAAW;CACX,SAAS;CACV,CAAC;AAEF,MAAM,UAAU,EAAE,eAA6D,oBAAC,SAAK,WAAe;AAEpG,MAAM,QAAQ;CACZ,SAAS;CACT,YAAY;CACZ,KAAK;CACL,cAAc;CACf,CAAC;AAEF,MAAM,cAA8B;CAClC,MAAM,EAAE,OAAO,wBAA2B,KAAK;AAE/C,QAAO,oBAAC;EAAG,GAAI;YAAa;GAAW;;AAGzC,MAAM,OAAO;CACX,MAAM;CACN,QAAQ;CACR,UAAU;CACX,CAAC;AAEF,MAAM,cACJ,oBAAC;CAAO,YAAgB,KAAK,CAAC;WAC5B,oBAAC,UAAQ;EACF;AAGX,MAAM,OAAO;CACX,SAAS;CACT,YAAY;CACZ,SAAS;CACT,QAAQ;CACR,cAAc;CACd,YAAY;CACZ,OAAO;CACP,QAAQ;CACR,UAAU,EACR,YAAY,gCACb;CACF,CAAC;;;;;;;;;;;;;;;AAgBF,IAAM,SAAN,cAAqBA,SAAY;;CAE/B,OAAyB;CAEzB,OAAO,QAAQ;CACf,OAAO,WAAW;CAClB,OAAO,QAAQ;CACf,OAAO,SAAS;CAChB,OAAO,QAAQ;CACf,OAAO,QAAQ;CAEf,QAAQ;CACR,WAAW;CACX,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,QAAQ"}
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Button } from "./button/button.js";
|
|
2
|
+
import { Calendar } from "./calendar/calendar.js";
|
|
3
|
+
import { Dialog } from "./dialog/dialog.js";
|
|
4
|
+
import { Dropzone } from "./dropzone/dropzone.js";
|
|
5
|
+
import { Field, Form, Submit } from "./form/form.js";
|
|
6
|
+
import { Caret } from "./icons/caret.js";
|
|
7
|
+
import { Check } from "./icons/check.js";
|
|
8
|
+
import { Cross } from "./icons/cross.js";
|
|
9
|
+
import { Spinner } from "./icons/spinner.js";
|
|
10
|
+
import { Upload } from "./icons/upload.js";
|
|
11
|
+
import { Layout } from "./layout/layout.js";
|
|
12
|
+
import { Popover } from "./popover/popover.js";
|
|
13
|
+
import { Select } from "./select/select.js";
|
|
14
|
+
import { CellProps, CellSlot, Column, HeadProps, HeadSlot } from "./table/column.js";
|
|
15
|
+
import { Table } from "./table/table.js";
|
|
16
|
+
export { Button, Calendar, Caret, type CellProps, type CellSlot, Check, Column, Cross, Dialog, Dropzone, Field, Form, type HeadProps, type HeadSlot, Layout, Popover, Select, Spinner, Submit, Table, Upload };
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Column } from "./table/column.js";
|
|
2
|
+
import { Caret } from "./icons/caret.js";
|
|
3
|
+
import { Check } from "./icons/check.js";
|
|
4
|
+
import { Cross } from "./icons/cross.js";
|
|
5
|
+
import { Spinner } from "./icons/spinner.js";
|
|
6
|
+
import { Upload } from "./icons/upload.js";
|
|
7
|
+
import { Button } from "./button/button.js";
|
|
8
|
+
import { Calendar } from "./calendar/calendar.js";
|
|
9
|
+
import { Dialog } from "./dialog/dialog.js";
|
|
10
|
+
import { Dropzone } from "./dropzone/dropzone.js";
|
|
11
|
+
import { Field, Form, Submit } from "./form/form.js";
|
|
12
|
+
import { Layout } from "./layout/layout.js";
|
|
13
|
+
import { Popover } from "./popover/popover.js";
|
|
14
|
+
import { Select } from "./select/select.js";
|
|
15
|
+
import { Table } from "./table/table.js";
|
|
16
|
+
|
|
17
|
+
export { Button, Calendar, Caret, Check, Column, Cross, Dialog, Dropzone, Field, Form, Layout, Popover, Select, Spinner, Submit, Table, Upload };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Component, Model, ref } from "@expressive/mvc";
|
|
2
|
+
|
|
3
|
+
//#region src/dropzone/base.d.ts
|
|
4
|
+
declare namespace Dropzone {
|
|
5
|
+
type Drag = Pick<DragEvent, 'dataTransfer' | 'preventDefault' | 'stopPropagation'>;
|
|
6
|
+
type Wrap = (props: {
|
|
7
|
+
children: Component.Node;
|
|
8
|
+
}) => Component.Node;
|
|
9
|
+
type Part = () => Component.Node;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* File drop target without a look: drag state, accept filtering, the hidden file input and messages, from pieces.
|
|
13
|
+
*
|
|
14
|
+
* `Root`, `Prompt` and `Message` are placeholders that throw when rendered - a skin assigns each, spreading
|
|
15
|
+
* `rootProps` and `promptProps`; `Message` writes `role="alert"`. `Input` is the hidden file input and needs no skin.
|
|
16
|
+
*/
|
|
17
|
+
declare class Dropzone extends Component {
|
|
18
|
+
onDrop?: (files: File[]) => void;
|
|
19
|
+
onReject?: (files: File[]) => void;
|
|
20
|
+
accept?: string;
|
|
21
|
+
multiple: boolean;
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
label: string;
|
|
24
|
+
hint?: string;
|
|
25
|
+
/** Nested drag-enter count; above zero while files hover. */
|
|
26
|
+
depth: number;
|
|
27
|
+
/** Files turned away by the last drop or pick. */
|
|
28
|
+
rejected: File[];
|
|
29
|
+
input: ref.Object<HTMLInputElement>;
|
|
30
|
+
Root: Dropzone.Wrap;
|
|
31
|
+
Input: Dropzone.Part;
|
|
32
|
+
Prompt: Dropzone.Part;
|
|
33
|
+
Message: Dropzone.Part;
|
|
34
|
+
constructor(...args: Model.Args);
|
|
35
|
+
/** Why the last drop or pick skipped files; empty when none were. */
|
|
36
|
+
get message(): string;
|
|
37
|
+
/** For the drop target: drag handlers. */
|
|
38
|
+
get rootProps(): {
|
|
39
|
+
onDragEnter: (event: Dropzone.Drag) => void;
|
|
40
|
+
onDragOver: (event: Dropzone.Drag) => void;
|
|
41
|
+
onDragLeave: (event: Dropzone.Drag) => void;
|
|
42
|
+
onDrop: (event: Dropzone.Drag) => void;
|
|
43
|
+
};
|
|
44
|
+
/** For the hidden file input. */
|
|
45
|
+
get inputProps(): {
|
|
46
|
+
readonly ref: ref.Object<HTMLInputElement>;
|
|
47
|
+
readonly type: "file";
|
|
48
|
+
readonly tabIndex: -1;
|
|
49
|
+
readonly 'aria-hidden': "true";
|
|
50
|
+
readonly accept: string | undefined;
|
|
51
|
+
readonly multiple: boolean;
|
|
52
|
+
readonly onChange: (event: Event) => void;
|
|
53
|
+
};
|
|
54
|
+
/** For the browse button, with `active` while files hover. */
|
|
55
|
+
get promptProps(): {
|
|
56
|
+
active: boolean;
|
|
57
|
+
props: {
|
|
58
|
+
readonly type: "button";
|
|
59
|
+
readonly disabled: boolean;
|
|
60
|
+
readonly onClick: () => void;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/** Keep files dropped beside the target from opening in the page; returns the release. Defaults to `document`. */
|
|
64
|
+
protected listen(): () => void;
|
|
65
|
+
/** Open the file picker. */
|
|
66
|
+
browse(): void;
|
|
67
|
+
/** Sort files into accepted and rejected, and report each. */
|
|
68
|
+
receive(files: File[]): void;
|
|
69
|
+
protected enter(event: Dropzone.Drag): void;
|
|
70
|
+
protected over(event: Dropzone.Drag): void;
|
|
71
|
+
protected leave(event: Dropzone.Drag): void;
|
|
72
|
+
protected drop(event: Dropzone.Drag): void;
|
|
73
|
+
render(): Component.Node;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { Dropzone };
|
|
77
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { missing } from "../piece.js";
|
|
2
|
+
import { Component, ref } from "@expressive/mvc";
|
|
3
|
+
import { jsx, jsxs } from "@expressive/mvc/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/dropzone/base.tsx
|
|
6
|
+
/**
|
|
7
|
+
* File drop target without a look: drag state, accept filtering, the hidden file input and messages, from pieces.
|
|
8
|
+
*
|
|
9
|
+
* `Root`, `Prompt` and `Message` are placeholders that throw when rendered - a skin assigns each, spreading
|
|
10
|
+
* `rootProps` and `promptProps`; `Message` writes `role="alert"`. `Input` is the hidden file input and needs no skin.
|
|
11
|
+
*/
|
|
12
|
+
var Dropzone = class extends Component {
|
|
13
|
+
onDrop = void 0;
|
|
14
|
+
onReject = void 0;
|
|
15
|
+
accept = void 0;
|
|
16
|
+
multiple = true;
|
|
17
|
+
disabled = false;
|
|
18
|
+
label = "Drop files here or click to browse";
|
|
19
|
+
hint = void 0;
|
|
20
|
+
/** Nested drag-enter count; above zero while files hover. */
|
|
21
|
+
depth = 0;
|
|
22
|
+
/** Files turned away by the last drop or pick. */
|
|
23
|
+
rejected = [];
|
|
24
|
+
input = ref();
|
|
25
|
+
Root = missing("Dropzone.Root");
|
|
26
|
+
Input = Input;
|
|
27
|
+
Prompt = missing("Dropzone.Prompt");
|
|
28
|
+
Message = missing("Dropzone.Message");
|
|
29
|
+
constructor(...args) {
|
|
30
|
+
super(...args, () => {
|
|
31
|
+
this.get(() => this.listen());
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Why the last drop or pick skipped files; empty when none were. */
|
|
35
|
+
get message() {
|
|
36
|
+
const { rejected, accept, multiple } = this;
|
|
37
|
+
const count = rejected.length;
|
|
38
|
+
if (!count) return "";
|
|
39
|
+
const reason = !multiple && count > 1 ? "only one file at a time" : `expected ${accept}`;
|
|
40
|
+
return `Skipped ${count} ${count == 1 ? "file" : "files"} - ${reason}.`;
|
|
41
|
+
}
|
|
42
|
+
/** For the drop target: drag handlers. */
|
|
43
|
+
get rootProps() {
|
|
44
|
+
return {
|
|
45
|
+
onDragEnter: this.enter,
|
|
46
|
+
onDragOver: this.over,
|
|
47
|
+
onDragLeave: this.leave,
|
|
48
|
+
onDrop: this.drop
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** For the hidden file input. */
|
|
52
|
+
get inputProps() {
|
|
53
|
+
const { input, accept, multiple } = this;
|
|
54
|
+
return {
|
|
55
|
+
ref: input,
|
|
56
|
+
type: "file",
|
|
57
|
+
tabIndex: -1,
|
|
58
|
+
"aria-hidden": "true",
|
|
59
|
+
accept,
|
|
60
|
+
multiple,
|
|
61
|
+
onChange: (event) => {
|
|
62
|
+
const picker = event.currentTarget;
|
|
63
|
+
const files = Array.from(picker.files || []);
|
|
64
|
+
picker.value = "";
|
|
65
|
+
this.receive(files);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** For the browse button, with `active` while files hover. */
|
|
70
|
+
get promptProps() {
|
|
71
|
+
const { disabled, depth } = this;
|
|
72
|
+
return {
|
|
73
|
+
active: depth > 0,
|
|
74
|
+
props: {
|
|
75
|
+
type: "button",
|
|
76
|
+
disabled,
|
|
77
|
+
onClick: this.browse
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Keep files dropped beside the target from opening in the page; returns the release. Defaults to `document`. */
|
|
82
|
+
listen() {
|
|
83
|
+
const outside = (event) => {
|
|
84
|
+
if (carriesFiles(event)) event.preventDefault();
|
|
85
|
+
if (event.type == "drop") this.depth = 0;
|
|
86
|
+
};
|
|
87
|
+
document.addEventListener("dragover", outside);
|
|
88
|
+
document.addEventListener("drop", outside);
|
|
89
|
+
return () => {
|
|
90
|
+
document.removeEventListener("dragover", outside);
|
|
91
|
+
document.removeEventListener("drop", outside);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Open the file picker. */
|
|
95
|
+
browse() {
|
|
96
|
+
if (!this.disabled) this.input.current?.click();
|
|
97
|
+
}
|
|
98
|
+
/** Sort files into accepted and rejected, and report each. */
|
|
99
|
+
receive(files) {
|
|
100
|
+
if (this.disabled || !files.length) return;
|
|
101
|
+
const accepted = this.multiple || files.length == 1 ? files.filter((file) => matches(file, this.accept)) : [];
|
|
102
|
+
const rejected = files.filter((file) => !accepted.includes(file));
|
|
103
|
+
this.rejected = rejected;
|
|
104
|
+
if (rejected.length && this.onReject) this.onReject(rejected);
|
|
105
|
+
if (accepted.length && this.onDrop) this.onDrop(accepted);
|
|
106
|
+
}
|
|
107
|
+
enter(event) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
if (carriesFiles(event) && !this.disabled) this.depth++;
|
|
110
|
+
}
|
|
111
|
+
over(event) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = this.disabled ? "none" : "copy";
|
|
114
|
+
}
|
|
115
|
+
leave(event) {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
this.depth = Math.max(0, this.depth - 1);
|
|
118
|
+
}
|
|
119
|
+
drop(event) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
this.depth = 0;
|
|
123
|
+
this.receive(Array.from(event.dataTransfer?.files || []));
|
|
124
|
+
}
|
|
125
|
+
render() {
|
|
126
|
+
const { rejected } = this;
|
|
127
|
+
return /* @__PURE__ */ jsxs(this.Root, { children: [
|
|
128
|
+
/* @__PURE__ */ jsx(this.Input, {}),
|
|
129
|
+
/* @__PURE__ */ jsx(this.Prompt, {}),
|
|
130
|
+
rejected.length > 0 && /* @__PURE__ */ jsx(this.Message, {})
|
|
131
|
+
] });
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const hidden = {
|
|
135
|
+
position: "absolute",
|
|
136
|
+
width: "1px",
|
|
137
|
+
height: "1px",
|
|
138
|
+
overflow: "hidden",
|
|
139
|
+
clip: "rect(0 0 0 0)",
|
|
140
|
+
whiteSpace: "nowrap"
|
|
141
|
+
};
|
|
142
|
+
const Input = () => /* @__PURE__ */ jsx("input", {
|
|
143
|
+
...Dropzone.get().inputProps,
|
|
144
|
+
style: hidden
|
|
145
|
+
});
|
|
146
|
+
function carriesFiles(event) {
|
|
147
|
+
return !!event.dataTransfer && Array.from(event.dataTransfer.types).includes("Files");
|
|
148
|
+
}
|
|
149
|
+
function matches(file, accept) {
|
|
150
|
+
const patterns = (accept || "").split(",").map((pattern) => pattern.trim().toLowerCase()).filter(Boolean);
|
|
151
|
+
if (!patterns.length) return true;
|
|
152
|
+
const name = file.name.toLowerCase();
|
|
153
|
+
const type = file.type.toLowerCase();
|
|
154
|
+
return patterns.some((pattern) => pattern.startsWith(".") ? name.endsWith(pattern) : pattern.endsWith("/*") ? type.startsWith(pattern.slice(0, -1)) : type == pattern);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
export { Dropzone };
|
|
159
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","names":[],"sources":["../../src/dropzone/base.tsx"],"sourcesContent":["import { Component, ref, type Model } from '@expressive/mvc';\n\nimport { missing } from '../piece';\n\ndeclare namespace Dropzone {\n type Drag = Pick<DragEvent, 'dataTransfer' | 'preventDefault' | 'stopPropagation'>;\n\n type Wrap = (props: { children: Component.Node }) => Component.Node;\n type Part = () => Component.Node;\n}\n\n/**\n * File drop target without a look: drag state, accept filtering, the hidden file input and messages, from pieces.\n *\n * `Root`, `Prompt` and `Message` are placeholders that throw when rendered - a skin assigns each, spreading\n * `rootProps` and `promptProps`; `Message` writes `role=\"alert\"`. `Input` is the hidden file input and needs no skin.\n */\nclass Dropzone extends Component {\n onDrop?: (files: File[]) => void = undefined;\n onReject?: (files: File[]) => void = undefined;\n\n accept?: string = undefined;\n multiple = true;\n disabled = false;\n\n label = 'Drop files here or click to browse';\n hint?: string = undefined;\n\n /** Nested drag-enter count; above zero while files hover. */\n depth = 0;\n\n /** Files turned away by the last drop or pick. */\n rejected: File[] = [];\n\n input = ref<HTMLInputElement>();\n\n Root: Dropzone.Wrap = missing('Dropzone.Root');\n Input: Dropzone.Part = Input;\n Prompt: Dropzone.Part = missing('Dropzone.Prompt');\n Message: Dropzone.Part = missing('Dropzone.Message');\n\n constructor(...args: Model.Args) {\n super(...args, () => {\n this.get(() => this.listen());\n });\n }\n\n /** Why the last drop or pick skipped files; empty when none were. */\n get message(): string {\n const { rejected, accept, multiple } = this;\n const count = rejected.length;\n\n if (!count) return '';\n\n const reason = !multiple && count > 1 ? 'only one file at a time' : `expected ${accept}`;\n\n return `Skipped ${count} ${count == 1 ? 'file' : 'files'} - ${reason}.`;\n }\n\n /** For the drop target: drag handlers. */\n get rootProps() {\n return {\n onDragEnter: this.enter,\n onDragOver: this.over,\n onDragLeave: this.leave,\n onDrop: this.drop\n };\n }\n\n /** For the hidden file input. */\n get inputProps() {\n const { input, accept, multiple } = this;\n\n return {\n ref: input,\n type: 'file',\n tabIndex: -1,\n 'aria-hidden': 'true',\n accept,\n multiple,\n onChange: (event: Event) => {\n const picker = event.currentTarget as HTMLInputElement;\n const files = Array.from(picker.files || []);\n\n picker.value = '';\n this.receive(files);\n }\n } as const;\n }\n\n /** For the browse button, with `active` while files hover. */\n get promptProps() {\n const { disabled, depth } = this;\n\n return {\n active: depth > 0,\n props: { type: 'button', disabled, onClick: this.browse } as const\n };\n }\n\n /** Keep files dropped beside the target from opening in the page; returns the release. Defaults to `document`. */\n protected listen(): () => void {\n const outside = (event: DragEvent) => {\n if (carriesFiles(event)) event.preventDefault();\n if (event.type == 'drop') this.depth = 0;\n };\n\n document.addEventListener('dragover', outside);\n document.addEventListener('drop', outside);\n\n return () => {\n document.removeEventListener('dragover', outside);\n document.removeEventListener('drop', outside);\n };\n }\n\n /** Open the file picker. */\n browse() {\n if (!this.disabled) this.input.current?.click();\n }\n\n /** Sort files into accepted and rejected, and report each. */\n receive(files: File[]) {\n if (this.disabled || !files.length) return;\n\n const fits = this.multiple || files.length == 1;\n const accepted = fits ? files.filter((file) => matches(file, this.accept)) : [];\n const rejected = files.filter((file) => !accepted.includes(file));\n\n this.rejected = rejected;\n\n if (rejected.length && this.onReject) this.onReject(rejected);\n if (accepted.length && this.onDrop) this.onDrop(accepted);\n }\n\n protected enter(event: Dropzone.Drag) {\n event.preventDefault();\n\n if (carriesFiles(event) && !this.disabled) this.depth++;\n }\n\n protected over(event: Dropzone.Drag) {\n event.preventDefault();\n\n if (event.dataTransfer) event.dataTransfer.dropEffect = this.disabled ? 'none' : 'copy';\n }\n\n protected leave(event: Dropzone.Drag) {\n event.preventDefault();\n this.depth = Math.max(0, this.depth - 1);\n }\n\n protected drop(event: Dropzone.Drag) {\n event.preventDefault();\n event.stopPropagation();\n this.depth = 0;\n this.receive(Array.from(event.dataTransfer?.files || []));\n }\n\n render(): Component.Node {\n const { rejected } = this;\n\n return (\n <this.Root>\n <this.Input />\n <this.Prompt />\n {rejected.length > 0 && <this.Message />}\n </this.Root>\n );\n }\n}\n\nconst hidden = {\n position: 'absolute',\n width: '1px',\n height: '1px',\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap'\n} as const;\n\nconst Input = (): Component.Node => <input {...Dropzone.get().inputProps} style={hidden} />;\n\nfunction carriesFiles(event: Pick<DragEvent, 'dataTransfer'>) {\n return !!event.dataTransfer && Array.from(event.dataTransfer.types).includes('Files');\n}\n\nfunction matches(file: File, accept?: string) {\n const patterns = (accept || '')\n .split(',')\n .map((pattern) => pattern.trim().toLowerCase())\n .filter(Boolean);\n\n if (!patterns.length) return true;\n\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n\n return patterns.some((pattern) =>\n pattern.startsWith('.') ? name.endsWith(pattern) : pattern.endsWith('/*') ? type.startsWith(pattern.slice(0, -1)) : type == pattern\n );\n}\n\nexport { Dropzone, Input };\n"],"mappings":"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"}
|