ember-primitives 0.63.1 → 0.64.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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Matches a file against an `accept` list, as `<input type="file">` reads it.
3
+ *
4
+ * The file picker filters by `accept` itself, but a drop does not,
5
+ * so dropped files need this check.
6
+ */
7
+ export declare function accepts(file: File, accept: string | undefined): boolean;
8
+ /**
9
+ * A drag can carry text or a link instead of files.
10
+ * During the drag, only the types are readable, not the files.
11
+ */
12
+ export declare function hasFiles(event: DragEvent): boolean;
13
+ /**
14
+ * The files of a pick or a drop that match `accept`, and only the first one
15
+ * unless `multiple` is set.
16
+ */
17
+ export declare function filesFrom(list: FileList | null | undefined, accept: string | undefined, multiple: boolean | undefined): File[];
18
+ /**
19
+ * Whether files are dragged over an element.
20
+ *
21
+ * `dragenter` and `dragleave` fire for every child that the pointer crosses,
22
+ * so a single flag would flicker. The depth counts the enters that did not leave yet.
23
+ */
24
+ export declare class DragState {
25
+ #private;
26
+ isDragging: boolean;
27
+ enter(event: DragEvent): void;
28
+ leave(event: DragEvent): void;
29
+ end(): void;
30
+ }
31
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../src/components/file-drop/files.ts"],"names":[],"mappings":"AAkBA;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAYvE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAUlD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EACjC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAC5B,IAAI,EAAE,CAcR;AAED;;;;;GAKG;AACH,qBAAa,SAAS;;IACX,UAAU,UAAS;IAI5B,KAAK,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IAO7B,KAAK,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IAQ7B,GAAG,IAAI,IAAI;CAIZ"}
@@ -0,0 +1,122 @@
1
+ import "./visually-hidden.css";
2
+ import Component from "@glimmer/component";
3
+ import { DragState } from "./file-drop/files.ts";
4
+ import type Owner from "@ember/owner";
5
+ interface FileArgs {
6
+ /**
7
+ * Called with the files that match `@accept`.
8
+ * It is not called when no file matches.
9
+ */
10
+ onFiles: (files: File[]) => void;
11
+ /**
12
+ * Which files to take, in the format of the `accept` attribute
13
+ * of `<input type="file">`, for example `"video/*,.mkv"`.
14
+ *
15
+ * The file picker shows only these files.
16
+ * A drop of other files does nothing.
17
+ */
18
+ accept?: string;
19
+ /**
20
+ * Take more than one file. Without it, only the first file that matches.
21
+ */
22
+ multiple?: boolean;
23
+ }
24
+ export interface Signature {
25
+ /**
26
+ * A `<label>` for a hidden `<input type="file">`,
27
+ * so a click or a key press on the zone opens the file picker.
28
+ *
29
+ * It has `data-dragging` while files are dragged over it.
30
+ */
31
+ Element: HTMLLabelElement;
32
+ Args: FileArgs;
33
+ Blocks: {
34
+ default: [
35
+ {
36
+ /**
37
+ * Files are dragged over the zone.
38
+ */
39
+ isDragging: boolean;
40
+ }
41
+ ];
42
+ };
43
+ }
44
+ /**
45
+ * A zone to drop files on, or to click to pick files.
46
+ *
47
+ * ```gjs
48
+ * import { FileDrop } from 'ember-primitives/components/file-drop';
49
+ *
50
+ * const load = (files) => console.log(files);
51
+ *
52
+ * <template>
53
+ * <FileDrop @accept="video/*" @onFiles={{load}}>
54
+ * Drop a video here, or click to choose one
55
+ * </FileDrop>
56
+ * </template>
57
+ * ```
58
+ */
59
+ export declare class FileDrop extends Component<Signature> {
60
+ drag: DragState;
61
+ inputId: string;
62
+ onDragEnter: (event: DragEvent) => void;
63
+ onDragLeave: (event: DragEvent) => void;
64
+ /**
65
+ * Without this, the browser does not allow the drop.
66
+ */
67
+ onDragOver: (event: DragEvent) => void;
68
+ /**
69
+ * `preventDefault` also tells a `DocumentFileDrop` that this zone took the drop.
70
+ */
71
+ onDrop: (event: DragEvent) => void;
72
+ /**
73
+ * The input is cleared after each pick, so that picking the same file again
74
+ * is a change too.
75
+ */
76
+ onPick: (event: Event) => void;
77
+ take(list: FileList | null | undefined): void;
78
+ }
79
+ export interface DocumentSignature {
80
+ Args: FileArgs;
81
+ Blocks: {
82
+ default: [
83
+ {
84
+ /**
85
+ * Files are dragged over the page.
86
+ */
87
+ isDragging: boolean;
88
+ }
89
+ ];
90
+ };
91
+ }
92
+ /**
93
+ * Takes files dropped anywhere on the page. It renders no element.
94
+ *
95
+ * - The block gets `isDragging`, for example to show where a drop goes.
96
+ * - A drop outside every `FileDrop` does not open the file in the browser tab.
97
+ * - A drop on a `FileDrop` goes only to that `FileDrop`.
98
+ *
99
+ * ```gjs
100
+ * import { DocumentFileDrop } from 'ember-primitives/components/file-drop';
101
+ *
102
+ * const load = (files) => console.log(files);
103
+ *
104
+ * <template>
105
+ * <DocumentFileDrop @accept="video/*" @onFiles={{load}} as |page|>
106
+ * {{#if page.isDragging}}
107
+ * <div class="overlay">Drop to load</div>
108
+ * {{/if}}
109
+ * </DocumentFileDrop>
110
+ * </template>
111
+ * ```
112
+ */
113
+ export declare class DocumentFileDrop extends Component<DocumentSignature> {
114
+ drag: DragState;
115
+ constructor(owner: Owner, args: DocumentSignature["Args"]);
116
+ onDragEnter: (event: DragEvent) => void;
117
+ onDragLeave: (event: DragEvent) => void;
118
+ onDragOver: (event: DragEvent) => void;
119
+ onDrop: (event: DragEvent) => void;
120
+ }
121
+ export {};
122
+ //# sourceMappingURL=file-drop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-drop.d.ts","sourceRoot":"","sources":["../../src/components/file-drop.gts"],"names":[],"mappings":"AAmNA,OAAO,uBAAuB,CAAC;AAE/B,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAM3C,OAAO,EAAE,SAAS,EAAuB,MAAM,sBAAsB,CAAC;AAEtE,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,UAAU,QAAQ;IAChB;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IAEjC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB;;;;;OAKG;IACH,OAAO,EAAE,gBAAgB,CAAC;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE;QACN,OAAO,EAAE;YACP;gBACE;;mBAEG;gBACH,UAAU,EAAE,OAAO,CAAC;aACrB;SACF,CAAC;KACH,CAAC;CACH;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,QAAS,SAAQ,SAAS,CAAC,SAAS,CAAC;IAChD,IAAI,YAAmB;IACvB,OAAO,SAAc;IAErB,WAAW,GAAI,OAAO,SAAS,UAA4B;IAC3D,WAAW,GAAI,OAAO,SAAS,UAA4B;IAE3D;;OAEG;IACH,UAAU,GAAI,OAAO,SAAS,UAE5B;IAEF;;OAEG;IACH,MAAM,GAAI,OAAO,SAAS,UAOxB;IAEF;;;OAGG;IACH,MAAM,GAAI,OAAO,KAAK,UAKpB;IAEF,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS;CA4CvC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE;QACN,OAAO,EAAE;YACP;gBACE;;mBAEG;gBACH,UAAU,EAAE,OAAO,CAAC;aACrB;SACF,CAAC;KACH,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAiB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAChE,IAAI,YAAmB;gBAEX,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC;IAgBzD,WAAW,GAAI,OAAO,SAAS,UAA4B;IAC3D,WAAW,GAAI,OAAO,SAAS,UAA4B;IAE3D,UAAU,GAAI,OAAO,SAAS,UAE5B;IAEF,MAAM,GAAI,OAAO,SAAS,UAUxB;CAQH"}
@@ -6,6 +6,7 @@ export { CommandPalette } from './components/command-palette';
6
6
  export { Dialog, Modal } from './components/dialog';
7
7
  export { Drawer } from './components/drawer';
8
8
  export { ExternalLink } from './components/external-link';
9
+ export { DocumentFileDrop, FileDrop } from './components/file-drop';
9
10
  export { Form } from './components/form';
10
11
  export { IncrementalEach } from './components/incremental-each';
11
12
  export { Key, KeyCombo } from './components/keys';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EACV,iCAAiC,EACjC,gCAAgC,EAChC,8BAA8B,EAC9B,iCAAiC,GAClC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EACL,SAAS,EACT,MAAM,IAAI,eAAe,EACzB,KAAK,IAAI,cAAc,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EACV,iCAAiC,EACjC,gCAAgC,EAChC,8BAA8B,EAC9B,iCAAiC,GAClC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EACL,SAAS,EACT,MAAM,IAAI,eAAe,EACzB,KAAK,IAAI,cAAc,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,cAAc,cAAc,CAAC"}
@@ -0,0 +1,89 @@
1
+ import { tracked } from '@glimmer/tracking';
2
+ import { g, i } from 'decorator-transforms/runtime';
3
+
4
+ function acceptsOne(file, entry) {
5
+ if (entry.startsWith('.')) {
6
+ return file.name.toLowerCase().endsWith(entry.toLowerCase());
7
+ }
8
+ if (entry.endsWith('/*')) {
9
+ return file.type.startsWith(entry.slice(0, -1));
10
+ }
11
+ return file.type === entry;
12
+ }
13
+
14
+ /**
15
+ * Matches a file against an `accept` list, as `<input type="file">` reads it.
16
+ *
17
+ * The file picker filters by `accept` itself, but a drop does not,
18
+ * so dropped files need this check.
19
+ */
20
+ function accepts(file, accept) {
21
+ if (!accept) return true;
22
+ const entries = accept.split(',');
23
+ for (const entry of entries) {
24
+ const trimmed = entry.trim();
25
+ if (trimmed && acceptsOne(file, trimmed)) return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ /**
31
+ * A drag can carry text or a link instead of files.
32
+ * During the drag, only the types are readable, not the files.
33
+ */
34
+ function hasFiles(event) {
35
+ const types = event.dataTransfer?.types;
36
+ if (!types) return false;
37
+ for (const type of types) {
38
+ if (type === 'Files') return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ /**
44
+ * The files of a pick or a drop that match `accept`, and only the first one
45
+ * unless `multiple` is set.
46
+ */
47
+ function filesFrom(list, accept, multiple) {
48
+ const files = [];
49
+ if (!list) return files;
50
+ for (const file of list) {
51
+ if (!accepts(file, accept)) continue;
52
+ files.push(file);
53
+ if (!multiple) break;
54
+ }
55
+ return files;
56
+ }
57
+
58
+ /**
59
+ * Whether files are dragged over an element.
60
+ *
61
+ * `dragenter` and `dragleave` fire for every child that the pointer crosses,
62
+ * so a single flag would flicker. The depth counts the enters that did not leave yet.
63
+ */
64
+ class DragState {
65
+ static {
66
+ g(this.prototype, "isDragging", [tracked], function () {
67
+ return false;
68
+ });
69
+ }
70
+ #isDragging = (i(this, "isDragging"), void 0);
71
+ #depth = 0;
72
+ enter(event) {
73
+ if (!hasFiles(event)) return;
74
+ this.#depth++;
75
+ this.isDragging = true;
76
+ }
77
+ leave(event) {
78
+ if (!hasFiles(event)) return;
79
+ this.#depth = Math.max(0, this.#depth - 1);
80
+ if (this.#depth === 0) this.isDragging = false;
81
+ }
82
+ end() {
83
+ this.#depth = 0;
84
+ this.isDragging = false;
85
+ }
86
+ }
87
+
88
+ export { DragState, accepts, filesFrom, hasFiles };
89
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sources":["../../../src/components/file-drop/files.ts"],"sourcesContent":["import { tracked } from '@glimmer/tracking';\n\n/**\n * One entry of an `accept` list: an extension (`.mp4`),\n * a type with a wildcard (`video/*`), or an exact type (`video/webm`).\n */\nfunction acceptsOne(file: File, entry: string): boolean {\n if (entry.startsWith('.')) {\n return file.name.toLowerCase().endsWith(entry.toLowerCase());\n }\n\n if (entry.endsWith('/*')) {\n return file.type.startsWith(entry.slice(0, -1));\n }\n\n return file.type === entry;\n}\n\n/**\n * Matches a file against an `accept` list, as `<input type=\"file\">` reads it.\n *\n * The file picker filters by `accept` itself, but a drop does not,\n * so dropped files need this check.\n */\nexport function accepts(file: File, accept: string | undefined): boolean {\n if (!accept) return true;\n\n const entries = accept.split(',');\n\n for (const entry of entries) {\n const trimmed = entry.trim();\n\n if (trimmed && acceptsOne(file, trimmed)) return true;\n }\n\n return false;\n}\n\n/**\n * A drag can carry text or a link instead of files.\n * During the drag, only the types are readable, not the files.\n */\nexport function hasFiles(event: DragEvent): boolean {\n const types = event.dataTransfer?.types;\n\n if (!types) return false;\n\n for (const type of types) {\n if (type === 'Files') return true;\n }\n\n return false;\n}\n\n/**\n * The files of a pick or a drop that match `accept`, and only the first one\n * unless `multiple` is set.\n */\nexport function filesFrom(\n list: FileList | null | undefined,\n accept: string | undefined,\n multiple: boolean | undefined\n): File[] {\n const files: File[] = [];\n\n if (!list) return files;\n\n for (const file of list) {\n if (!accepts(file, accept)) continue;\n\n files.push(file);\n\n if (!multiple) break;\n }\n\n return files;\n}\n\n/**\n * Whether files are dragged over an element.\n *\n * `dragenter` and `dragleave` fire for every child that the pointer crosses,\n * so a single flag would flicker. The depth counts the enters that did not leave yet.\n */\nexport class DragState {\n @tracked isDragging = false;\n\n #depth = 0;\n\n enter(event: DragEvent): void {\n if (!hasFiles(event)) return;\n\n this.#depth++;\n this.isDragging = true;\n }\n\n leave(event: DragEvent): void {\n if (!hasFiles(event)) return;\n\n this.#depth = Math.max(0, this.#depth - 1);\n\n if (this.#depth === 0) this.isDragging = false;\n }\n\n end(): void {\n this.#depth = 0;\n this.isDragging = false;\n }\n}\n"],"names":["acceptsOne","file","entry","startsWith","name","toLowerCase","endsWith","type","slice","accepts","accept","entries","split","trimmed","trim","hasFiles","event","types","dataTransfer","filesFrom","list","multiple","files","push","DragState","g","prototype","tracked","i","enter","isDragging","leave","Math","max","end"],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import '../visually-hidden-CGP1FSjt.js';
2
+ import Component from '@glimmer/component';
3
+ import { registerDestructor } from '@ember/destroyable';
4
+ import { hash } from '@ember/helper';
5
+ import { on } from '@ember/modifier';
6
+ import { uniqueId } from '../utils.js';
7
+ import { DragState, hasFiles, filesFrom } from './file-drop/files.js';
8
+ import { precompileTemplate } from '@ember/template-compilation';
9
+ import { setComponentTemplate } from '@ember/component';
10
+
11
+ /**
12
+ * A zone to drop files on, or to click to pick files.
13
+ *
14
+ * ```gjs
15
+ * import { FileDrop } from 'ember-primitives/components/file-drop';
16
+ *
17
+ * const load = (files) => console.log(files);
18
+ *
19
+ * <template>
20
+ * <FileDrop @accept="video/*" @onFiles={{load}}>
21
+ * Drop a video here, or click to choose one
22
+ * </FileDrop>
23
+ * </template>
24
+ * ```
25
+ */
26
+ class FileDrop extends Component {
27
+ drag = new DragState();
28
+ inputId = uniqueId();
29
+ onDragEnter = event => this.drag.enter(event);
30
+ onDragLeave = event => this.drag.leave(event);
31
+ /**
32
+ * Without this, the browser does not allow the drop.
33
+ */
34
+ onDragOver = event => {
35
+ if (hasFiles(event)) event.preventDefault();
36
+ };
37
+ /**
38
+ * `preventDefault` also tells a `DocumentFileDrop` that this zone took the drop.
39
+ */
40
+ onDrop = event => {
41
+ this.drag.end();
42
+ if (!hasFiles(event)) return;
43
+ event.preventDefault();
44
+ this.take(event.dataTransfer?.files);
45
+ };
46
+ /**
47
+ * The input is cleared after each pick, so that picking the same file again
48
+ * is a change too.
49
+ */
50
+ onPick = event => {
51
+ const input = event.currentTarget;
52
+ this.take(input.files);
53
+ input.value = "";
54
+ };
55
+ take(list) {
56
+ const files = filesFrom(list, this.args.accept, this.args.multiple);
57
+ if (files.length > 0) this.args.onFiles(files);
58
+ }
59
+ static {
60
+ setComponentTemplate(precompileTemplate("<label for={{this.inputId}} data-dragging={{if this.drag.isDragging \"\"}} {{on \"dragenter\" this.onDragEnter}} {{on \"dragover\" this.onDragOver}} {{on \"dragleave\" this.onDragLeave}} {{on \"drop\" this.onDrop}} ...attributes>\n {{yield (hash isDragging=this.drag.isDragging)}}\n <input id={{this.inputId}} class=\"ember-primitives__visually-hidden\" type=\"file\" accept={{@accept}} multiple={{@multiple}} {{on \"change\" this.onPick}} />\n</label>", {
61
+ strictMode: true,
62
+ scope: () => ({
63
+ on,
64
+ hash
65
+ })
66
+ }), this);
67
+ }
68
+ }
69
+ /**
70
+ * Takes files dropped anywhere on the page. It renders no element.
71
+ *
72
+ * - The block gets `isDragging`, for example to show where a drop goes.
73
+ * - A drop outside every `FileDrop` does not open the file in the browser tab.
74
+ * - A drop on a `FileDrop` goes only to that `FileDrop`.
75
+ *
76
+ * ```gjs
77
+ * import { DocumentFileDrop } from 'ember-primitives/components/file-drop';
78
+ *
79
+ * const load = (files) => console.log(files);
80
+ *
81
+ * <template>
82
+ * <DocumentFileDrop @accept="video/*" @onFiles={{load}} as |page|>
83
+ * {{#if page.isDragging}}
84
+ * <div class="overlay">Drop to load</div>
85
+ * {{/if}}
86
+ * </DocumentFileDrop>
87
+ * </template>
88
+ * ```
89
+ */
90
+ class DocumentFileDrop extends Component {
91
+ drag = new DragState();
92
+ constructor(owner, args) {
93
+ super(owner, args);
94
+ document.addEventListener("dragenter", this.onDragEnter);
95
+ document.addEventListener("dragover", this.onDragOver);
96
+ document.addEventListener("dragleave", this.onDragLeave);
97
+ document.addEventListener("drop", this.onDrop);
98
+ registerDestructor(this, () => {
99
+ document.removeEventListener("dragenter", this.onDragEnter);
100
+ document.removeEventListener("dragover", this.onDragOver);
101
+ document.removeEventListener("dragleave", this.onDragLeave);
102
+ document.removeEventListener("drop", this.onDrop);
103
+ });
104
+ }
105
+ onDragEnter = event => this.drag.enter(event);
106
+ onDragLeave = event => this.drag.leave(event);
107
+ onDragOver = event => {
108
+ if (hasFiles(event)) event.preventDefault();
109
+ };
110
+ onDrop = event => {
111
+ this.drag.end();
112
+ if (!hasFiles(event) || event.defaultPrevented) return;
113
+ event.preventDefault();
114
+ const files = filesFrom(event.dataTransfer?.files, this.args.accept, this.args.multiple);
115
+ if (files.length > 0) this.args.onFiles(files);
116
+ };
117
+ static {
118
+ setComponentTemplate(precompileTemplate("{{yield (hash isDragging=this.drag.isDragging)}}", {
119
+ strictMode: true,
120
+ scope: () => ({
121
+ hash
122
+ })
123
+ }), this);
124
+ }
125
+ }
126
+
127
+ export { DocumentFileDrop, FileDrop };
128
+ //# sourceMappingURL=file-drop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-drop.js","sources":["../../src/components/file-drop.gts"],"sourcesContent":["import \"./visually-hidden.css\";\n\nimport Component from \"@glimmer/component\";\nimport { registerDestructor } from \"@ember/destroyable\";\nimport { hash } from \"@ember/helper\";\nimport { on } from \"@ember/modifier\";\n\nimport { uniqueId } from \"../utils.ts\";\nimport { DragState, filesFrom, hasFiles } from \"./file-drop/files.ts\";\n\nimport type Owner from \"@ember/owner\";\n\ninterface FileArgs {\n /**\n * Called with the files that match `@accept`.\n * It is not called when no file matches.\n */\n onFiles: (files: File[]) => void;\n\n /**\n * Which files to take, in the format of the `accept` attribute\n * of `<input type=\"file\">`, for example `\"video/*,.mkv\"`.\n *\n * The file picker shows only these files.\n * A drop of other files does nothing.\n */\n accept?: string;\n\n /**\n * Take more than one file. Without it, only the first file that matches.\n */\n multiple?: boolean;\n}\n\nexport interface Signature {\n /**\n * A `<label>` for a hidden `<input type=\"file\">`,\n * so a click or a key press on the zone opens the file picker.\n *\n * It has `data-dragging` while files are dragged over it.\n */\n Element: HTMLLabelElement;\n Args: FileArgs;\n Blocks: {\n default: [\n {\n /**\n * Files are dragged over the zone.\n */\n isDragging: boolean;\n },\n ];\n };\n}\n\n/**\n * A zone to drop files on, or to click to pick files.\n *\n * ```gjs\n * import { FileDrop } from 'ember-primitives/components/file-drop';\n *\n * const load = (files) => console.log(files);\n *\n * <template>\n * <FileDrop @accept=\"video/*\" @onFiles={{load}}>\n * Drop a video here, or click to choose one\n * </FileDrop>\n * </template>\n * ```\n */\nexport class FileDrop extends Component<Signature> {\n drag = new DragState();\n inputId = uniqueId();\n\n onDragEnter = (event: DragEvent) => this.drag.enter(event);\n onDragLeave = (event: DragEvent) => this.drag.leave(event);\n\n /**\n * Without this, the browser does not allow the drop.\n */\n onDragOver = (event: DragEvent) => {\n if (hasFiles(event)) event.preventDefault();\n };\n\n /**\n * `preventDefault` also tells a `DocumentFileDrop` that this zone took the drop.\n */\n onDrop = (event: DragEvent) => {\n this.drag.end();\n\n if (!hasFiles(event)) return;\n\n event.preventDefault();\n this.take(event.dataTransfer?.files);\n };\n\n /**\n * The input is cleared after each pick, so that picking the same file again\n * is a change too.\n */\n onPick = (event: Event) => {\n const input = event.currentTarget as HTMLInputElement;\n\n this.take(input.files);\n input.value = \"\";\n };\n\n take(list: FileList | null | undefined) {\n const files = filesFrom(list, this.args.accept, this.args.multiple);\n\n if (files.length > 0) this.args.onFiles(files);\n }\n\n <template>\n <label\n for={{this.inputId}}\n data-dragging={{if this.drag.isDragging \"\"}}\n {{on \"dragenter\" this.onDragEnter}}\n {{on \"dragover\" this.onDragOver}}\n {{on \"dragleave\" this.onDragLeave}}\n {{on \"drop\" this.onDrop}}\n ...attributes\n >\n {{yield (hash isDragging=this.drag.isDragging)}}\n <input\n id={{this.inputId}}\n class=\"ember-primitives__visually-hidden\"\n type=\"file\"\n accept={{@accept}}\n multiple={{@multiple}}\n {{on \"change\" this.onPick}}\n />\n </label>\n </template>\n}\n\nexport interface DocumentSignature {\n Args: FileArgs;\n Blocks: {\n default: [\n {\n /**\n * Files are dragged over the page.\n */\n isDragging: boolean;\n },\n ];\n };\n}\n\n/**\n * Takes files dropped anywhere on the page. It renders no element.\n *\n * - The block gets `isDragging`, for example to show where a drop goes.\n * - A drop outside every `FileDrop` does not open the file in the browser tab.\n * - A drop on a `FileDrop` goes only to that `FileDrop`.\n *\n * ```gjs\n * import { DocumentFileDrop } from 'ember-primitives/components/file-drop';\n *\n * const load = (files) => console.log(files);\n *\n * <template>\n * <DocumentFileDrop @accept=\"video/*\" @onFiles={{load}} as |page|>\n * {{#if page.isDragging}}\n * <div class=\"overlay\">Drop to load</div>\n * {{/if}}\n * </DocumentFileDrop>\n * </template>\n * ```\n */\nexport class DocumentFileDrop extends Component<DocumentSignature> {\n drag = new DragState();\n\n constructor(owner: Owner, args: DocumentSignature[\"Args\"]) {\n super(owner, args);\n\n document.addEventListener(\"dragenter\", this.onDragEnter);\n document.addEventListener(\"dragover\", this.onDragOver);\n document.addEventListener(\"dragleave\", this.onDragLeave);\n document.addEventListener(\"drop\", this.onDrop);\n\n registerDestructor(this, () => {\n document.removeEventListener(\"dragenter\", this.onDragEnter);\n document.removeEventListener(\"dragover\", this.onDragOver);\n document.removeEventListener(\"dragleave\", this.onDragLeave);\n document.removeEventListener(\"drop\", this.onDrop);\n });\n }\n\n onDragEnter = (event: DragEvent) => this.drag.enter(event);\n onDragLeave = (event: DragEvent) => this.drag.leave(event);\n\n onDragOver = (event: DragEvent) => {\n if (hasFiles(event)) event.preventDefault();\n };\n\n onDrop = (event: DragEvent) => {\n this.drag.end();\n\n if (!hasFiles(event) || event.defaultPrevented) return;\n\n event.preventDefault();\n\n const files = filesFrom(event.dataTransfer?.files, this.args.accept, this.args.multiple);\n\n if (files.length > 0) this.args.onFiles(files);\n };\n\n <template>{{yield (hash isDragging=this.drag.isDragging)}}</template>\n}\n"],"names":["FileDrop","Component","drag","DragState","inputId","uniqueId","onDragEnter","event","enter","onDragLeave","leave","onDragOver","hasFiles","preventDefault","onDrop","end","take","dataTransfer","files","onPick","input","currentTarget","value","list","filesFrom","args","accept","multiple","length","onFiles","setComponentTemplate","precompileTemplate","strictMode","scope","on","hash","DocumentFileDrop","constructor","owner","document","addEventListener","registerDestructor","removeEventListener","defaultPrevented"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { CommandPalette } from './components/command-palette.js';
6
6
  export { Dialog, Modal } from './components/dialog.js';
7
7
  export { Drawer } from './components/drawer.js';
8
8
  export { ExternalLink } from './components/external-link.js';
9
+ export { DocumentFileDrop, FileDrop } from './components/file-drop.js';
9
10
  export { Form } from './components/form.js';
10
11
  export { IncrementalEach } from './components/incremental-each.js';
11
12
  export { Key, KeyCombo } from './components/keys.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * DANGER: this is a *barrel file*\n *\n * It forces the whole library to be loaded and all dependencies.\n *\n * If you have a small app, you probably don't want to import from here -- instead import from each sub-path.\n */\nimport { importSync, isDevelopingApp, macroCondition } from '@embroider/macros';\n\nif (macroCondition(isDevelopingApp())) {\n importSync('./components/violations.css');\n}\n\nexport { Accordion } from './components/accordion.gts';\nexport type {\n AccordionContentExternalSignature,\n AccordionHeaderExternalSignature,\n AccordionItemExternalSignature,\n AccordionTriggerExternalSignature,\n} from './components/accordion/public.ts';\nexport { Avatar } from './components/avatar.gts';\nexport { Breadcrumb } from './components/breadcrumb.gts';\nexport { CommandPalette } from './components/command-palette.gts';\nexport { Dialog, Modal } from './components/dialog.gts';\nexport { Drawer } from './components/drawer.gts';\nexport { ExternalLink } from './components/external-link.gts';\nexport { Form } from './components/form.gts';\nexport { IncrementalEach } from './components/incremental-each.gts';\nexport { Key, KeyCombo } from './components/keys.gts';\nexport { StickyFooter } from './components/layout/sticky-footer.gts';\nexport { Link } from './components/link.gts';\nexport { Menu } from './components/menu.gts';\nexport { OTP, OTPInput } from './components/one-time-password.gts';\nexport { Popover } from './components/popover.gts';\nexport { Portal } from './components/portal.gts';\nexport { PortalTargets } from './components/portal-targets.gts';\nexport { TARGETS as PORTALS } from './components/portal-targets.gts';\nexport { Progress } from './components/progress.gts';\nexport { Rating } from './components/rating.gts';\nexport {\n Resizable,\n Handle as ResizableHandle,\n Panel as ResizablePanel,\n} from './components/resizable.gts';\nexport { Scroller } from './components/scroller.gts';\nexport { Separator } from './components/separator.gts';\nexport { Shadowed } from './components/shadowed.gts';\nexport { Slider } from './components/slider.gts';\nexport { Switch } from './components/switch.gts';\nexport { Toggle } from './components/toggle.gts';\nexport { ToggleGroup } from './components/toggle-group.gts';\nexport { VisuallyHidden } from './components/visually-hidden.gts';\nexport { Zoetrope } from './components/zoetrope.ts';\nexport * from './helpers.ts';\n"],"names":["macroCondition","isDevelopingApp","importSync"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA,IAAIA,cAAc,CAACC,eAAe,EAAE,CAAC,EAAE;EACrCC,UAAU,CAAC,6BAA6B,CAAC;AAC3C"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * DANGER: this is a *barrel file*\n *\n * It forces the whole library to be loaded and all dependencies.\n *\n * If you have a small app, you probably don't want to import from here -- instead import from each sub-path.\n */\nimport { importSync, isDevelopingApp, macroCondition } from '@embroider/macros';\n\nif (macroCondition(isDevelopingApp())) {\n importSync('./components/violations.css');\n}\n\nexport { Accordion } from './components/accordion.gts';\nexport type {\n AccordionContentExternalSignature,\n AccordionHeaderExternalSignature,\n AccordionItemExternalSignature,\n AccordionTriggerExternalSignature,\n} from './components/accordion/public.ts';\nexport { Avatar } from './components/avatar.gts';\nexport { Breadcrumb } from './components/breadcrumb.gts';\nexport { CommandPalette } from './components/command-palette.gts';\nexport { Dialog, Modal } from './components/dialog.gts';\nexport { Drawer } from './components/drawer.gts';\nexport { ExternalLink } from './components/external-link.gts';\nexport { DocumentFileDrop, FileDrop } from './components/file-drop.gts';\nexport { Form } from './components/form.gts';\nexport { IncrementalEach } from './components/incremental-each.gts';\nexport { Key, KeyCombo } from './components/keys.gts';\nexport { StickyFooter } from './components/layout/sticky-footer.gts';\nexport { Link } from './components/link.gts';\nexport { Menu } from './components/menu.gts';\nexport { OTP, OTPInput } from './components/one-time-password.gts';\nexport { Popover } from './components/popover.gts';\nexport { Portal } from './components/portal.gts';\nexport { PortalTargets } from './components/portal-targets.gts';\nexport { TARGETS as PORTALS } from './components/portal-targets.gts';\nexport { Progress } from './components/progress.gts';\nexport { Rating } from './components/rating.gts';\nexport {\n Resizable,\n Handle as ResizableHandle,\n Panel as ResizablePanel,\n} from './components/resizable.gts';\nexport { Scroller } from './components/scroller.gts';\nexport { Separator } from './components/separator.gts';\nexport { Shadowed } from './components/shadowed.gts';\nexport { Slider } from './components/slider.gts';\nexport { Switch } from './components/switch.gts';\nexport { Toggle } from './components/toggle.gts';\nexport { ToggleGroup } from './components/toggle-group.gts';\nexport { VisuallyHidden } from './components/visually-hidden.gts';\nexport { Zoetrope } from './components/zoetrope.ts';\nexport * from './helpers.ts';\n"],"names":["macroCondition","isDevelopingApp","importSync"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA,IAAIA,cAAc,CAACC,eAAe,EAAE,CAAC,EAAE;EACrCC,UAAU,CAAC,6BAA6B,CAAC;AAC3C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-primitives",
3
- "version": "0.63.1",
3
+ "version": "0.64.0",
4
4
  "description": "Making apps easier to build",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -0,0 +1,109 @@
1
+ import { tracked } from '@glimmer/tracking';
2
+
3
+ /**
4
+ * One entry of an `accept` list: an extension (`.mp4`),
5
+ * a type with a wildcard (`video/*`), or an exact type (`video/webm`).
6
+ */
7
+ function acceptsOne(file: File, entry: string): boolean {
8
+ if (entry.startsWith('.')) {
9
+ return file.name.toLowerCase().endsWith(entry.toLowerCase());
10
+ }
11
+
12
+ if (entry.endsWith('/*')) {
13
+ return file.type.startsWith(entry.slice(0, -1));
14
+ }
15
+
16
+ return file.type === entry;
17
+ }
18
+
19
+ /**
20
+ * Matches a file against an `accept` list, as `<input type="file">` reads it.
21
+ *
22
+ * The file picker filters by `accept` itself, but a drop does not,
23
+ * so dropped files need this check.
24
+ */
25
+ export function accepts(file: File, accept: string | undefined): boolean {
26
+ if (!accept) return true;
27
+
28
+ const entries = accept.split(',');
29
+
30
+ for (const entry of entries) {
31
+ const trimmed = entry.trim();
32
+
33
+ if (trimmed && acceptsOne(file, trimmed)) return true;
34
+ }
35
+
36
+ return false;
37
+ }
38
+
39
+ /**
40
+ * A drag can carry text or a link instead of files.
41
+ * During the drag, only the types are readable, not the files.
42
+ */
43
+ export function hasFiles(event: DragEvent): boolean {
44
+ const types = event.dataTransfer?.types;
45
+
46
+ if (!types) return false;
47
+
48
+ for (const type of types) {
49
+ if (type === 'Files') return true;
50
+ }
51
+
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * The files of a pick or a drop that match `accept`, and only the first one
57
+ * unless `multiple` is set.
58
+ */
59
+ export function filesFrom(
60
+ list: FileList | null | undefined,
61
+ accept: string | undefined,
62
+ multiple: boolean | undefined
63
+ ): File[] {
64
+ const files: File[] = [];
65
+
66
+ if (!list) return files;
67
+
68
+ for (const file of list) {
69
+ if (!accepts(file, accept)) continue;
70
+
71
+ files.push(file);
72
+
73
+ if (!multiple) break;
74
+ }
75
+
76
+ return files;
77
+ }
78
+
79
+ /**
80
+ * Whether files are dragged over an element.
81
+ *
82
+ * `dragenter` and `dragleave` fire for every child that the pointer crosses,
83
+ * so a single flag would flicker. The depth counts the enters that did not leave yet.
84
+ */
85
+ export class DragState {
86
+ @tracked isDragging = false;
87
+
88
+ #depth = 0;
89
+
90
+ enter(event: DragEvent): void {
91
+ if (!hasFiles(event)) return;
92
+
93
+ this.#depth++;
94
+ this.isDragging = true;
95
+ }
96
+
97
+ leave(event: DragEvent): void {
98
+ if (!hasFiles(event)) return;
99
+
100
+ this.#depth = Math.max(0, this.#depth - 1);
101
+
102
+ if (this.#depth === 0) this.isDragging = false;
103
+ }
104
+
105
+ end(): void {
106
+ this.#depth = 0;
107
+ this.isDragging = false;
108
+ }
109
+ }
@@ -0,0 +1,211 @@
1
+ import "./visually-hidden.css";
2
+
3
+ import Component from "@glimmer/component";
4
+ import { registerDestructor } from "@ember/destroyable";
5
+ import { hash } from "@ember/helper";
6
+ import { on } from "@ember/modifier";
7
+
8
+ import { uniqueId } from "../utils.ts";
9
+ import { DragState, filesFrom, hasFiles } from "./file-drop/files.ts";
10
+
11
+ import type Owner from "@ember/owner";
12
+
13
+ interface FileArgs {
14
+ /**
15
+ * Called with the files that match `@accept`.
16
+ * It is not called when no file matches.
17
+ */
18
+ onFiles: (files: File[]) => void;
19
+
20
+ /**
21
+ * Which files to take, in the format of the `accept` attribute
22
+ * of `<input type="file">`, for example `"video/*,.mkv"`.
23
+ *
24
+ * The file picker shows only these files.
25
+ * A drop of other files does nothing.
26
+ */
27
+ accept?: string;
28
+
29
+ /**
30
+ * Take more than one file. Without it, only the first file that matches.
31
+ */
32
+ multiple?: boolean;
33
+ }
34
+
35
+ export interface Signature {
36
+ /**
37
+ * A `<label>` for a hidden `<input type="file">`,
38
+ * so a click or a key press on the zone opens the file picker.
39
+ *
40
+ * It has `data-dragging` while files are dragged over it.
41
+ */
42
+ Element: HTMLLabelElement;
43
+ Args: FileArgs;
44
+ Blocks: {
45
+ default: [
46
+ {
47
+ /**
48
+ * Files are dragged over the zone.
49
+ */
50
+ isDragging: boolean;
51
+ },
52
+ ];
53
+ };
54
+ }
55
+
56
+ /**
57
+ * A zone to drop files on, or to click to pick files.
58
+ *
59
+ * ```gjs
60
+ * import { FileDrop } from 'ember-primitives/components/file-drop';
61
+ *
62
+ * const load = (files) => console.log(files);
63
+ *
64
+ * <template>
65
+ * <FileDrop @accept="video/*" @onFiles={{load}}>
66
+ * Drop a video here, or click to choose one
67
+ * </FileDrop>
68
+ * </template>
69
+ * ```
70
+ */
71
+ export class FileDrop extends Component<Signature> {
72
+ drag = new DragState();
73
+ inputId = uniqueId();
74
+
75
+ onDragEnter = (event: DragEvent) => this.drag.enter(event);
76
+ onDragLeave = (event: DragEvent) => this.drag.leave(event);
77
+
78
+ /**
79
+ * Without this, the browser does not allow the drop.
80
+ */
81
+ onDragOver = (event: DragEvent) => {
82
+ if (hasFiles(event)) event.preventDefault();
83
+ };
84
+
85
+ /**
86
+ * `preventDefault` also tells a `DocumentFileDrop` that this zone took the drop.
87
+ */
88
+ onDrop = (event: DragEvent) => {
89
+ this.drag.end();
90
+
91
+ if (!hasFiles(event)) return;
92
+
93
+ event.preventDefault();
94
+ this.take(event.dataTransfer?.files);
95
+ };
96
+
97
+ /**
98
+ * The input is cleared after each pick, so that picking the same file again
99
+ * is a change too.
100
+ */
101
+ onPick = (event: Event) => {
102
+ const input = event.currentTarget as HTMLInputElement;
103
+
104
+ this.take(input.files);
105
+ input.value = "";
106
+ };
107
+
108
+ take(list: FileList | null | undefined) {
109
+ const files = filesFrom(list, this.args.accept, this.args.multiple);
110
+
111
+ if (files.length > 0) this.args.onFiles(files);
112
+ }
113
+
114
+ <template>
115
+ <label
116
+ for={{this.inputId}}
117
+ data-dragging={{if this.drag.isDragging ""}}
118
+ {{on "dragenter" this.onDragEnter}}
119
+ {{on "dragover" this.onDragOver}}
120
+ {{on "dragleave" this.onDragLeave}}
121
+ {{on "drop" this.onDrop}}
122
+ ...attributes
123
+ >
124
+ {{yield (hash isDragging=this.drag.isDragging)}}
125
+ <input
126
+ id={{this.inputId}}
127
+ class="ember-primitives__visually-hidden"
128
+ type="file"
129
+ accept={{@accept}}
130
+ multiple={{@multiple}}
131
+ {{on "change" this.onPick}}
132
+ />
133
+ </label>
134
+ </template>
135
+ }
136
+
137
+ export interface DocumentSignature {
138
+ Args: FileArgs;
139
+ Blocks: {
140
+ default: [
141
+ {
142
+ /**
143
+ * Files are dragged over the page.
144
+ */
145
+ isDragging: boolean;
146
+ },
147
+ ];
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Takes files dropped anywhere on the page. It renders no element.
153
+ *
154
+ * - The block gets `isDragging`, for example to show where a drop goes.
155
+ * - A drop outside every `FileDrop` does not open the file in the browser tab.
156
+ * - A drop on a `FileDrop` goes only to that `FileDrop`.
157
+ *
158
+ * ```gjs
159
+ * import { DocumentFileDrop } from 'ember-primitives/components/file-drop';
160
+ *
161
+ * const load = (files) => console.log(files);
162
+ *
163
+ * <template>
164
+ * <DocumentFileDrop @accept="video/*" @onFiles={{load}} as |page|>
165
+ * {{#if page.isDragging}}
166
+ * <div class="overlay">Drop to load</div>
167
+ * {{/if}}
168
+ * </DocumentFileDrop>
169
+ * </template>
170
+ * ```
171
+ */
172
+ export class DocumentFileDrop extends Component<DocumentSignature> {
173
+ drag = new DragState();
174
+
175
+ constructor(owner: Owner, args: DocumentSignature["Args"]) {
176
+ super(owner, args);
177
+
178
+ document.addEventListener("dragenter", this.onDragEnter);
179
+ document.addEventListener("dragover", this.onDragOver);
180
+ document.addEventListener("dragleave", this.onDragLeave);
181
+ document.addEventListener("drop", this.onDrop);
182
+
183
+ registerDestructor(this, () => {
184
+ document.removeEventListener("dragenter", this.onDragEnter);
185
+ document.removeEventListener("dragover", this.onDragOver);
186
+ document.removeEventListener("dragleave", this.onDragLeave);
187
+ document.removeEventListener("drop", this.onDrop);
188
+ });
189
+ }
190
+
191
+ onDragEnter = (event: DragEvent) => this.drag.enter(event);
192
+ onDragLeave = (event: DragEvent) => this.drag.leave(event);
193
+
194
+ onDragOver = (event: DragEvent) => {
195
+ if (hasFiles(event)) event.preventDefault();
196
+ };
197
+
198
+ onDrop = (event: DragEvent) => {
199
+ this.drag.end();
200
+
201
+ if (!hasFiles(event) || event.defaultPrevented) return;
202
+
203
+ event.preventDefault();
204
+
205
+ const files = filesFrom(event.dataTransfer?.files, this.args.accept, this.args.multiple);
206
+
207
+ if (files.length > 0) this.args.onFiles(files);
208
+ };
209
+
210
+ <template>{{yield (hash isDragging=this.drag.isDragging)}}</template>
211
+ }
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export { CommandPalette } from './components/command-palette.gts';
24
24
  export { Dialog, Modal } from './components/dialog.gts';
25
25
  export { Drawer } from './components/drawer.gts';
26
26
  export { ExternalLink } from './components/external-link.gts';
27
+ export { DocumentFileDrop, FileDrop } from './components/file-drop.gts';
27
28
  export { Form } from './components/form.gts';
28
29
  export { IncrementalEach } from './components/incremental-each.gts';
29
30
  export { Key, KeyCombo } from './components/keys.gts';