@api-client/ui 0.0.10 → 0.0.12

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.
Files changed (176) hide show
  1. package/.eslintrc +16 -1
  2. package/demo/elements/index.html +3 -0
  3. package/demo/elements/store/file-picker.html +15 -0
  4. package/demo/elements/store/file-picker.ts +134 -0
  5. package/demo/elements/store/index.html +19 -0
  6. package/demo/index.html +3 -0
  7. package/demo/layout/index.html +91 -0
  8. package/demo/layout/index.ts +182 -0
  9. package/demo/store/StorePlugin.js +1 -0
  10. package/dist/bindings/base/StoreBindings.d.ts +5 -0
  11. package/dist/bindings/base/StoreBindings.d.ts.map +1 -1
  12. package/dist/bindings/base/StoreBindings.js +15 -1
  13. package/dist/bindings/base/StoreBindings.js.map +1 -1
  14. package/dist/define/store/file-picker.d.ts +9 -0
  15. package/dist/define/store/file-picker.d.ts.map +1 -0
  16. package/dist/define/store/file-picker.js +10 -0
  17. package/dist/define/store/file-picker.js.map +1 -0
  18. package/dist/define/{files → store}/share-file.d.ts +1 -1
  19. package/dist/define/store/share-file.d.ts.map +1 -0
  20. package/dist/define/{files → store}/share-file.js +2 -2
  21. package/dist/define/store/share-file.js.map +1 -0
  22. package/dist/elements/layout/SplitItem.d.ts +1 -9
  23. package/dist/elements/layout/SplitItem.d.ts.map +1 -1
  24. package/dist/elements/layout/SplitItem.js +27 -20
  25. package/dist/elements/layout/SplitItem.js.map +1 -1
  26. package/dist/elements/layout/SplitLayout.d.ts +16 -14
  27. package/dist/elements/layout/SplitLayout.d.ts.map +1 -1
  28. package/dist/elements/layout/SplitLayout.js +47 -42
  29. package/dist/elements/layout/SplitLayout.js.map +1 -1
  30. package/dist/elements/layout/SplitPanel.d.ts +7 -2
  31. package/dist/elements/layout/SplitPanel.d.ts.map +1 -1
  32. package/dist/elements/layout/SplitPanel.js +130 -52
  33. package/dist/elements/layout/SplitPanel.js.map +1 -1
  34. package/dist/elements/layout/SplitView.d.ts.map +1 -1
  35. package/dist/elements/layout/SplitView.js +18 -14
  36. package/dist/elements/layout/SplitView.js.map +1 -1
  37. package/dist/elements/layout/type.d.ts +3 -3
  38. package/dist/elements/layout/type.d.ts.map +1 -1
  39. package/dist/elements/layout/type.js.map +1 -1
  40. package/dist/elements/store/FilePicker.element.d.ts +87 -0
  41. package/dist/elements/store/FilePicker.element.d.ts.map +1 -0
  42. package/dist/elements/store/FilePicker.element.js +263 -0
  43. package/dist/elements/store/FilePicker.element.js.map +1 -0
  44. package/dist/elements/store/FilePicker.styles.d.ts +3 -0
  45. package/dist/elements/store/FilePicker.styles.d.ts.map +1 -0
  46. package/dist/elements/store/FilePicker.styles.js +72 -0
  47. package/dist/elements/store/FilePicker.styles.js.map +1 -0
  48. package/dist/elements/store/FilesLib.d.ts +10 -0
  49. package/dist/elements/store/FilesLib.d.ts.map +1 -0
  50. package/dist/elements/store/FilesLib.js +38 -0
  51. package/dist/elements/store/FilesLib.js.map +1 -0
  52. package/dist/elements/{files/ShareFile.d.ts → store/ShareFile.element.d.ts} +1 -1
  53. package/dist/elements/store/ShareFile.element.d.ts.map +1 -0
  54. package/dist/elements/{files/ShareFile.js → store/ShareFile.element.js} +1 -1
  55. package/dist/elements/store/ShareFile.element.js.map +1 -0
  56. package/dist/elements/store/ShareFile.styles.d.ts.map +1 -0
  57. package/dist/elements/{files → store}/ShareFile.styles.js.map +1 -1
  58. package/dist/events/EventTypes.d.ts +1 -0
  59. package/dist/events/EventTypes.d.ts.map +1 -1
  60. package/dist/events/EventTypes.js +1 -0
  61. package/dist/events/EventTypes.js.map +1 -1
  62. package/dist/events/Events.d.ts +1 -0
  63. package/dist/events/Events.d.ts.map +1 -1
  64. package/dist/events/StoreEvents.d.ts +8 -1
  65. package/dist/events/StoreEvents.d.ts.map +1 -1
  66. package/dist/events/StoreEvents.js +19 -0
  67. package/dist/events/StoreEvents.js.map +1 -1
  68. package/dist/pages/ApplicationScreen.d.ts +1 -1
  69. package/dist/pages/ApplicationScreen.d.ts.map +1 -1
  70. package/dist/pages/ApplicationScreen.js +4 -2
  71. package/dist/pages/ApplicationScreen.js.map +1 -1
  72. package/dist/pages/api-client/ApiClient.screen.d.ts +0 -6
  73. package/dist/pages/api-client/ApiClient.screen.d.ts.map +1 -1
  74. package/dist/pages/api-client/ApiClient.screen.js +16 -29
  75. package/dist/pages/api-client/ApiClient.screen.js.map +1 -1
  76. package/dist/pages/api-client/Authenticate.screen.d.ts +1 -1
  77. package/dist/pages/api-client/Authenticate.screen.d.ts.map +1 -1
  78. package/dist/pages/api-client/Authenticate.screen.js +2 -2
  79. package/dist/pages/api-client/Authenticate.screen.js.map +1 -1
  80. package/dist/pages/api-client/pages/Files.page.d.ts +6 -35
  81. package/dist/pages/api-client/pages/Files.page.d.ts.map +1 -1
  82. package/dist/pages/api-client/pages/Files.page.js +32 -141
  83. package/dist/pages/api-client/pages/Files.page.js.map +1 -1
  84. package/dist/pages/api-client/pages/Shared.page.d.ts +1 -5
  85. package/dist/pages/api-client/pages/Shared.page.d.ts.map +1 -1
  86. package/dist/pages/api-client/pages/Shared.page.js +1 -40
  87. package/dist/pages/api-client/pages/Shared.page.js.map +1 -1
  88. package/dist/pages/demo/DemoPage.d.ts +7 -0
  89. package/dist/pages/demo/DemoPage.d.ts.map +1 -1
  90. package/dist/pages/demo/DemoPage.js +14 -0
  91. package/dist/pages/demo/DemoPage.js.map +1 -1
  92. package/dist/pages/http-project/HttpClientCommands.d.ts.map +1 -1
  93. package/dist/pages/http-project/HttpClientCommands.js +28 -12
  94. package/dist/pages/http-project/HttpClientCommands.js.map +1 -1
  95. package/dist/store/FileSystem.d.ts +90 -0
  96. package/dist/store/FileSystem.d.ts.map +1 -0
  97. package/dist/store/FileSystem.js +260 -0
  98. package/dist/store/FileSystem.js.map +1 -0
  99. package/dist/styles/global-styles.d.ts.map +1 -1
  100. package/dist/styles/global-styles.js +7 -0
  101. package/dist/styles/global-styles.js.map +1 -1
  102. package/dist/ui/icons/Icons.d.ts +2 -1
  103. package/dist/ui/icons/Icons.d.ts.map +1 -1
  104. package/dist/ui/icons/Icons.js +1 -0
  105. package/dist/ui/icons/Icons.js.map +1 -1
  106. package/dist/ui/list/UiDropdownList.d.ts +9 -1
  107. package/dist/ui/list/UiDropdownList.d.ts.map +1 -1
  108. package/dist/ui/list/UiDropdownList.js +39 -17
  109. package/dist/ui/list/UiDropdownList.js.map +1 -1
  110. package/dist/ui/list/UiList.d.ts +6 -1
  111. package/dist/ui/list/UiList.d.ts.map +1 -1
  112. package/dist/ui/list/UiList.js +24 -9
  113. package/dist/ui/list/UiList.js.map +1 -1
  114. package/dist/ui/table/DataTable.d.ts +4 -0
  115. package/dist/ui/table/DataTable.d.ts.map +1 -1
  116. package/dist/ui/table/DataTable.js +23 -1
  117. package/dist/ui/table/DataTable.js.map +1 -1
  118. package/package.json +2 -1
  119. package/src/bindings/base/StoreBindings.ts +16 -1
  120. package/src/define/store/file-picker.ts +12 -0
  121. package/src/define/{files → store}/share-file.ts +2 -2
  122. package/src/elements/layout/SplitItem.ts +29 -21
  123. package/src/elements/layout/SplitLayout.ts +53 -43
  124. package/src/elements/layout/SplitPanel.ts +140 -57
  125. package/src/elements/layout/SplitView.ts +18 -15
  126. package/src/elements/layout/type.ts +3 -4
  127. package/src/elements/store/FilePicker.element.ts +297 -0
  128. package/src/elements/store/FilePicker.styles.ts +72 -0
  129. package/src/elements/store/FilesLib.ts +32 -0
  130. package/src/events/EventTypes.ts +1 -0
  131. package/src/events/StoreEvents.ts +21 -1
  132. package/src/pages/ApplicationScreen.ts +5 -3
  133. package/src/pages/api-client/ApiClient.screen.ts +16 -31
  134. package/src/pages/api-client/Authenticate.screen.ts +2 -2
  135. package/src/pages/api-client/pages/Files.page.ts +37 -164
  136. package/src/pages/api-client/pages/Shared.page.ts +2 -40
  137. package/src/pages/demo/DemoPage.ts +17 -0
  138. package/src/pages/http-project/HttpClientCommands.ts +28 -12
  139. package/src/store/FileSystem.ts +325 -0
  140. package/src/styles/global-styles.ts +7 -0
  141. package/src/ui/icons/Icons.ts +2 -1
  142. package/src/ui/list/UiDropdownList.ts +44 -17
  143. package/src/ui/list/UiList.ts +26 -10
  144. package/src/ui/table/DataTable.ts +29 -3
  145. package/test/elements/layout/SplitItem.test.ts +76 -75
  146. package/test/elements/layout/SplitLayoutManager.test.ts +70 -69
  147. package/test/elements/layout/SplitPanel.test.ts +10 -7
  148. package/test/elements/store/FilePicker.test.ts +241 -0
  149. package/test/env.js +3 -0
  150. package/test/helpers/StoreHelper.ts +390 -0
  151. package/tsconfig.eslint.json +10 -0
  152. package/web-test-runner.config.mjs +51 -2
  153. package/dist/define/files/share-file.d.ts.map +0 -1
  154. package/dist/define/files/share-file.js.map +0 -1
  155. package/dist/define/layout/layout-panel.d.ts +0 -7
  156. package/dist/define/layout/layout-panel.d.ts.map +0 -1
  157. package/dist/define/layout/layout-panel.js +0 -3
  158. package/dist/define/layout/layout-panel.js.map +0 -1
  159. package/dist/elements/files/ShareFile.d.ts.map +0 -1
  160. package/dist/elements/files/ShareFile.js.map +0 -1
  161. package/dist/elements/files/ShareFile.styles.d.ts.map +0 -1
  162. package/dist/elements/layout/LayoutManager.d.ts +0 -327
  163. package/dist/elements/layout/LayoutManager.d.ts.map +0 -1
  164. package/dist/elements/layout/LayoutManager.js +0 -747
  165. package/dist/elements/layout/LayoutManager.js.map +0 -1
  166. package/dist/elements/layout/LayoutPanelElement.d.ts +0 -62
  167. package/dist/elements/layout/LayoutPanelElement.d.ts.map +0 -1
  168. package/dist/elements/layout/LayoutPanelElement.js +0 -628
  169. package/dist/elements/layout/LayoutPanelElement.js.map +0 -1
  170. package/src/define/layout/layout-panel.ts +0 -9
  171. package/src/elements/layout/LayoutManager.ts +0 -930
  172. package/src/elements/layout/LayoutPanelElement.ts +0 -651
  173. /package/dist/elements/{files → store}/ShareFile.styles.d.ts +0 -0
  174. /package/dist/elements/{files → store}/ShareFile.styles.js +0 -0
  175. /package/src/elements/{files/ShareFile.ts → store/ShareFile.element.ts} +0 -0
  176. /package/src/elements/{files → store}/ShareFile.styles.ts +0 -0
@@ -8,6 +8,7 @@ import CheckboxElement from '../../ui/input/CheckboxElement.js';
8
8
  import typography from "../../styles/m3/typography.module.js";
9
9
  import surface from "../../styles/m3/surface.module.js";
10
10
  import '../../define/ui/ui-icon.js';
11
+ import '../../define/ui/ui-button.js';
11
12
 
12
13
  /**
13
14
  * A base class for demo pages in the API Client ecosystem.
@@ -33,6 +34,11 @@ export abstract class DemoPage extends RouteMixin(RenderableMixin(EventTarget))
33
34
  */
34
35
  @reactive() darkThemeActive = false;
35
36
 
37
+ /**
38
+ * For some demo pages, whether the user is authenticated in the store.
39
+ */
40
+ authenticated = false;
41
+
36
42
  constructor() {
37
43
  super();
38
44
  this.handleMediaQuery = this.handleMediaQuery.bind(this);
@@ -151,4 +157,15 @@ export abstract class DemoPage extends RouteMixin(RenderableMixin(EventTarget))
151
157
  * ```
152
158
  */
153
159
  abstract contentTemplate(): TemplateResult;
160
+
161
+ handleAuthenticate(): void {
162
+ // ...
163
+ }
164
+
165
+ authenticateTemplate(): TemplateResult {
166
+ return html`
167
+ <p>Store authorization required.</p>
168
+ <ui-button @click="${this.handleAuthenticate}">Authenticate</ui-button>
169
+ `;
170
+ }
154
171
  }
@@ -85,12 +85,16 @@ const commands: IContextMenuCommand<HttpProjectContextualStore>[] = [
85
85
  label: 'Close',
86
86
  icon: close,
87
87
  execute: (init): void => {
88
- const { key } = init.target.dataset;
89
- if (!key) {
88
+ const { key, panel } = init.target.dataset;
89
+ if (!key || !panel) {
90
90
  return;
91
91
  }
92
92
  const layout = init.store.get('layout');
93
- layout.removeItem(key);
93
+ const parent = layout.findPanel(panel);
94
+ if (!parent) {
95
+ return;
96
+ }
97
+ parent.removeItem(key);
94
98
  }
95
99
  },
96
100
  {
@@ -105,12 +109,16 @@ const commands: IContextMenuCommand<HttpProjectContextualStore>[] = [
105
109
  return true;
106
110
  },
107
111
  execute: (init): void => {
108
- const { key } = init.target.dataset;
109
- if (!key) {
112
+ const { key, panel } = init.target.dataset;
113
+ if (!key || !panel) {
110
114
  return;
111
115
  }
112
116
  const layout = init.store.get('layout');
113
- layout.removeRelative(key, SplitCloseDirection.both);
117
+ const parent = layout.findPanel(panel);
118
+ if (!parent) {
119
+ return;
120
+ }
121
+ parent.removeRelative(key, SplitCloseDirection.both);
114
122
  },
115
123
  },
116
124
  {
@@ -124,12 +132,16 @@ const commands: IContextMenuCommand<HttpProjectContextualStore>[] = [
124
132
  return true;
125
133
  },
126
134
  execute: (init): void => {
127
- const { key } = init.target.dataset;
128
- if (!key) {
135
+ const { key, panel } = init.target.dataset;
136
+ if (!key || !panel) {
129
137
  return;
130
138
  }
131
139
  const layout = init.store.get('layout');
132
- layout.removeRelative(key, SplitCloseDirection.right);
140
+ const parent = layout.findPanel(panel);
141
+ if (!parent) {
142
+ return;
143
+ }
144
+ parent.removeRelative(key, SplitCloseDirection.right);
133
145
  },
134
146
  },
135
147
  {
@@ -143,12 +155,16 @@ const commands: IContextMenuCommand<HttpProjectContextualStore>[] = [
143
155
  return true;
144
156
  },
145
157
  execute: (init): void => {
146
- const { key } = init.target.dataset;
147
- if (!key) {
158
+ const { key, panel } = init.target.dataset;
159
+ if (!key || !panel) {
148
160
  return;
149
161
  }
150
162
  const layout = init.store.get('layout');
151
- layout.removeRelative(key, SplitCloseDirection.left);
163
+ const parent = layout.findPanel(panel);
164
+ if (!parent) {
165
+ return;
166
+ }
167
+ parent.removeRelative(key, SplitCloseDirection.left);
152
168
  },
153
169
  },
154
170
 
@@ -0,0 +1,325 @@
1
+ import {
2
+ BroadcastEvent,
3
+ BroadcastFileData,
4
+ ContextListResult,
5
+ ContextSpaceListOptions,
6
+ DeletedBroadcastEvent,
7
+ FileAccessBroadcastEvent,
8
+ FileBreadcrumb,
9
+ FileMetaCreatedBroadcastEvent,
10
+ FilePatchBroadcastEvent,
11
+ IFile,
12
+ ListFileKind,
13
+ } from "@api-client/core/build/browser.js";
14
+ import { Patch } from "@api-client/json";
15
+ import { filesSortFunction } from "../elements/store/FilesLib.js";
16
+ import { Events } from "../events/Events.js";
17
+ import { StoreBroadcast } from "../http-client/store/StoreBroadcast.js";
18
+
19
+ type FilesSource = 'own' | 'shared';
20
+
21
+ /**
22
+ * Contains a logic to read files from the store.
23
+ *
24
+ * It uses app events to query for the data.
25
+ */
26
+ export class FileSystem extends EventTarget {
27
+ source: FilesSource = 'own';
28
+
29
+ /**
30
+ * The list of files to render.
31
+ * Do not overwrite this property or when you must then update the DataTable's `items` as well.
32
+ */
33
+ files: IFile[] = [];
34
+
35
+ /**
36
+ * The pagination cursor for files.
37
+ */
38
+ cursor?: string;
39
+
40
+ /**
41
+ * This is set when the last files query did not yeld results.
42
+ */
43
+ queryEnded?: boolean;
44
+
45
+ /**
46
+ * The currently rendered breadcrumbs.
47
+ */
48
+ breadcrumbs?: FileBreadcrumb[];
49
+
50
+ /**
51
+ * Whether the UI is reading files
52
+ */
53
+ reading?: boolean;
54
+
55
+ /**
56
+ * The list of file kinds to list.
57
+ * Folders are always included.
58
+ */
59
+ kinds?: ListFileKind[];
60
+
61
+ /**
62
+ * The timeout for the query debouncer.
63
+ * When any property change this is the time the element will wait
64
+ * until the actual query is made.
65
+ */
66
+ debounceTimeout = 100;
67
+
68
+ debouncerValue?: number;
69
+
70
+ /**
71
+ * The variable parent folder key.
72
+ * This is set when interacting with the UI or when the `folder` attribute change.
73
+ */
74
+ parent?: string;
75
+
76
+ /**
77
+ * The page limit. Defaults to the store defaults.
78
+ * @attribute
79
+ */
80
+ limit?: number;
81
+
82
+ filesChannel = new BroadcastChannel(StoreBroadcast.files);
83
+
84
+ eventsTarget: EventTarget = document.body;
85
+
86
+ constructor() {
87
+ super();
88
+ this.fileMetaMessageHandler = this.fileMetaMessageHandler.bind(this);
89
+ }
90
+
91
+ observe(): void {
92
+ this.filesChannel.addEventListener('message', this.fileMetaMessageHandler);
93
+ }
94
+
95
+ unobserve(): void {
96
+ this.filesChannel.removeEventListener('message', this.fileMetaMessageHandler);
97
+ }
98
+
99
+ fileMetaMessageHandler(e: MessageEvent): void {
100
+ const event = e.data as BroadcastEvent & BroadcastFileData;
101
+ if (event.parent !== this.parent) {
102
+ return;
103
+ }
104
+ if (event.alt !== 'meta') {
105
+ return;
106
+ }
107
+ switch (event.operation) {
108
+ case 'created': this.handleFileCreated((event as FileMetaCreatedBroadcastEvent).data); break;
109
+ case 'deleted': this.handleFileDeleted(event as DeletedBroadcastEvent); break;
110
+ case 'patch': this.handleFilePatch(event as FilePatchBroadcastEvent); break;
111
+ case 'access-granted': this.handleFileAccessGranted(event as FileAccessBroadcastEvent); break;
112
+ case 'access-removed': this.handleFileAccessRemoved(event as FileAccessBroadcastEvent); break;
113
+ default:
114
+ }
115
+ }
116
+
117
+ handleFileCreated(file: IFile): void {
118
+ this.files.push(file);
119
+ // We don't sort here so files that appear at the end which makes sense because the list is scrolling.
120
+ // this.files.sort(filesSortFunction);
121
+ this.notifyUpdate();
122
+ }
123
+
124
+ handleFileDeleted(event: DeletedBroadcastEvent): void {
125
+ this.removeFileFromList(event.key);
126
+ }
127
+
128
+ async handleFileAccessGranted(event: FileAccessBroadcastEvent): Promise<void> {
129
+ if (this.source !== 'shared') {
130
+ return;
131
+ }
132
+ const { key } = event;
133
+ try {
134
+ const file = await Events.Store.File.read(key, false, this.eventsTarget);
135
+ const index = this.files.findIndex(i => i.key === key);
136
+ if (index >= 0) {
137
+ this.files.splice(index, 1);
138
+ }
139
+ this.handleFileCreated(file);
140
+ } catch (e) {
141
+ // eslint-disable-next-line no-console
142
+ console.warn(e);
143
+ }
144
+ }
145
+
146
+ handleFileAccessRemoved(event: FileAccessBroadcastEvent): void {
147
+ if (this.source !== 'shared') {
148
+ return;
149
+ }
150
+ const { key } = event;
151
+ this.removeFileFromList(key);
152
+ }
153
+
154
+ removeFileFromList(key: string): void {
155
+ const index = this.files.findIndex(i => i.key === key);
156
+ if (index === -1) {
157
+ return;
158
+ }
159
+ this.files.splice(index, 1);
160
+ this.notifyUpdate();
161
+ this.dispatchEvent(new CustomEvent('delete', { detail: key }));
162
+ }
163
+
164
+ handleFilePatch(event: FilePatchBroadcastEvent): void {
165
+ const { data, key } = event;
166
+ const index = this.files.findIndex(i => i.key === key);
167
+ if (index === -1) {
168
+ return;
169
+ }
170
+ const file = this.files[index];
171
+ const result = Patch.apply(file, data.patch);
172
+ this.files[index] = result.doc as IFile;
173
+ this.notifyUpdate();
174
+ }
175
+
176
+ debounceQuery(): void {
177
+ if (this.debouncerValue) {
178
+ clearTimeout(this.debouncerValue);
179
+ }
180
+ this.debouncerValue = window.setTimeout(() => {
181
+ this.queryPage();
182
+ this.queryBreadcrumbs();
183
+ }, this.debounceTimeout);
184
+ }
185
+
186
+ resetList(): void {
187
+ this.files = [];
188
+ this.cursor = undefined;
189
+ this.breadcrumbs = undefined;
190
+ this.queryEnded = false;
191
+ }
192
+
193
+ async queryPage(): Promise<void> {
194
+ if (this.reading || this.queryEnded) {
195
+ return;
196
+ }
197
+ this.reading = true;
198
+ this.notifyUpdate();
199
+ try {
200
+ const result = await this.listFiles();
201
+ if (!result) {
202
+ throw new Error(`Files event not handled.`);
203
+ }
204
+ const { items, cursor } = result;
205
+ if (!items.length) {
206
+ this.queryEnded = true;
207
+ } else {
208
+ if (!this.files.length) {
209
+ // we only sort the first page as later the user scrolls and sorting would change the order.
210
+ items.sort(filesSortFunction);
211
+ }
212
+ this.files = this.files.concat(items);
213
+ }
214
+ if (cursor) {
215
+ this.cursor = cursor;
216
+ }
217
+ } catch(e) {
218
+ const err = e as Error;
219
+ this.notifyError(`Unable to load files list. ${err.message}`);
220
+ // eslint-disable-next-line no-console
221
+ console.error(e);
222
+ } finally {
223
+ this.reading = false;
224
+ }
225
+ this.dispatchEvent(new Event('querycomplete'));
226
+ this.notifyUpdate();
227
+ }
228
+
229
+ async queryBreadcrumbs(): Promise<void> {
230
+ if (this.parent) {
231
+ const br = await this.listBreadcrumbs(this.parent);
232
+ this.breadcrumbs = br.items;
233
+ } else {
234
+ this.breadcrumbs = undefined;
235
+ }
236
+ }
237
+
238
+ getPageQueryOptions(): ContextSpaceListOptions {
239
+ const { cursor: filesCursor, parent, limit } = this;
240
+ const opts: ContextSpaceListOptions = {
241
+ space: '', // this is filled by the bindings.
242
+ descending: true,
243
+ };
244
+ if (filesCursor) {
245
+ opts.cursor = filesCursor;
246
+ }
247
+ if (parent) {
248
+ opts.parent = parent;
249
+ }
250
+ if (typeof limit === 'number' && limit) {
251
+ opts.limit = limit;
252
+ }
253
+ return opts;
254
+ }
255
+
256
+ async listFiles(): Promise<ContextListResult<IFile>> {
257
+ const { kinds } = this;
258
+ const opts = this.getPageQueryOptions();
259
+ if (this.source === 'shared') {
260
+ return Events.Store.File.listShared(kinds, opts, this.eventsTarget);
261
+ }
262
+ return Events.Store.File.list(kinds, opts, this.eventsTarget);
263
+ }
264
+
265
+ async listBreadcrumbs(folder: string): Promise<ContextListResult<FileBreadcrumb>> {
266
+ return Events.Store.File.breadcrumbs(folder, this.eventsTarget);
267
+ }
268
+
269
+ notifyError(error: string): void {
270
+ this.dispatchEvent(new CustomEvent<string>('error', {
271
+ detail: error,
272
+ }));
273
+ }
274
+
275
+ notifyUpdate(): void {
276
+ this.dispatchEvent(new Event('change'));
277
+ }
278
+
279
+ /**
280
+ * Sets a parent and makes the query.
281
+ * @param key The key of the parent to query. Empty for root folder.
282
+ */
283
+ selectFolder(key?: string): void {
284
+ this.parent = key;
285
+ this.resetList();
286
+ this.debounceQuery();
287
+ // this.queryPage();
288
+ this.queryBreadcrumbs();
289
+ }
290
+
291
+ /**
292
+ * Moves to a parent folder, if any.
293
+ */
294
+ async parentUp(): Promise<void> {
295
+ const { breadcrumbs = [] } = this;
296
+ const [, parent] = breadcrumbs;
297
+ if (!parent) {
298
+ if (!this.parent) {
299
+ return;
300
+ }
301
+ this.parent = undefined;
302
+ } else {
303
+ if (this.parent === parent.key) {
304
+ return;
305
+ }
306
+ this.parent = parent.key;
307
+ }
308
+ this.resetList();
309
+ await Promise.all([
310
+ this.queryPage(),
311
+ this.queryBreadcrumbs(),
312
+ ]);
313
+ this.notifyUpdate();
314
+ }
315
+
316
+ /**
317
+ * @param target The HTML element that is a scrollable list if files.
318
+ * @returns True when the scroll position indicates that the user scrolled near the end of the list.
319
+ */
320
+ isListEnd(target: HTMLElement): boolean {
321
+ const { scrollTop, offsetHeight, scrollHeight } = target;
322
+ // 20 is the offset which qualifies as the end. An arbitrary number.
323
+ return scrollTop + offsetHeight >= scrollHeight - 20;
324
+ }
325
+ }
@@ -65,6 +65,13 @@ body {
65
65
  justify-content: center;
66
66
  position: absolute;
67
67
  inset: 0;
68
+ background-color: var(--md-sys-color-surface);
69
+ }
70
+
71
+ .message,
72
+ .sub-message,
73
+ .auth-required-screen {
74
+ color: var(--md-sys-color-on-surface);
68
75
  }
69
76
 
70
77
  .full-error {
@@ -8,7 +8,7 @@ import { svg, SVGTemplateResult } from 'lit';
8
8
  export const iconWrapper = (tpl: SVGTemplateResult, width = 24, height = 24, top = 0, left = 0): SVGTemplateResult => svg`<svg viewBox="${top} ${left} ${width} ${height}" preserveAspectRatio="xMidYMid meet" focusable="false" style="pointer-events: none; display: block; width: 100%; height: 100%;">${tpl}</svg>`;
9
9
 
10
10
  export type IconType =
11
- 'add' | 'api' | 'apps' | 'arrowBack' | 'arrowDropDown' | 'cancel' | 'cancelFilled' | 'check' | 'checkBox' | 'checkBoxBlank' | 'checkIndeterminate' | 'chevronLeft' | 'chevronRight' | 'close' | 'cloud' | 'cloudFilled' | 'collectionsBookmark' | 'deleteFile' | 'deleteOutline' | 'edit' | 'environment' | 'expandMore' |
11
+ 'add' | 'api' | 'apps' | 'arrowBack' | 'arrowDropDown' | 'cancel' | 'cancelFilled' | 'certificate' | 'check' | 'checkBox' | 'checkBoxBlank' | 'checkIndeterminate' | 'chevronLeft' | 'chevronRight' | 'close' | 'cloud' | 'cloudFilled' | 'collectionsBookmark' | 'deleteFile' | 'deleteOutline' | 'edit' | 'environment' | 'expandMore' |
12
12
  'fileDownload' | 'folder' | 'folderFilled' | 'folderShared' | 'help' | 'history' | 'info' | 'key' | 'menu' | 'leaderBoard' |
13
13
  'moreVert' | 'openInNew' | 'personAdd' | 'playArrow' | 'project' | 'remove' | 'rename' | 'request' | 'restoreFromTrash' | 'save' |
14
14
  'schema' | 'schemaEntity' | 'schemaModel' | 'schemaNamespace' | 'search' | 'send' | 'settings' | 'space' | 'taskAlt' | 'timeline' | 'tune' | 'viewGrid' | 'viewList' | 'visibility' | 'visibilityOff' | 'warning';
@@ -20,6 +20,7 @@ export const arrowBack = iconWrapper(svg`<path d="M20 11H7.83l5.59-5.59L12 4l-8
20
20
  export const arrowDropDown = iconWrapper(svg`<path d="M7 10l5 5 5-5z"/>`);
21
21
  export const cancel = iconWrapper(svg`<path d="M0 0h24v24H0V0z" fill="none" opacity=".87"/><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.59-13L12 10.59 8.41 7 7 8.41 10.59 12 7 15.59 8.41 17 12 13.41 15.59 17 17 15.59 13.41 12 17 8.41z"/>`);
22
22
  export const cancelFilled = iconWrapper(svg`<path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/>`);
23
+ export const certificate = iconWrapper(svg`<path d="M4 3c-1.11 0-2 .89-2 2v10a2 2 0 0 0 2 2h8v5l3-3l3 3v-5h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4m8 2l3 2l3-2v3.5l3 1.5l-3 1.5V15l-3-2l-3 2v-3.5L9 10l3-1.5V5M4 5h5v2H4V5m0 4h3v2H4V9m0 4h5v2H4v-2Z"/>`);
23
24
  export const check = iconWrapper(svg`<path d="m9.55 18-5.7-5.7 1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4Z"/>`);
24
25
  export const checkBox = iconWrapper(svg`<path d="m10.6 16.2 7.05-7.05-1.4-1.4-5.65 5.65-2.85-2.85-1.4 1.4ZM5 21q-.825 0-1.413-.587Q3 19.825 3 19V5q0-.825.587-1.413Q4.175 3 5 3h14q.825 0 1.413.587Q21 4.175 21 5v14q0 .825-.587 1.413Q19.825 21 19 21Zm0-2h14V5H5v14ZM5 5v14V5Z"/>`);
25
26
  export const checkBoxBlank = iconWrapper(svg`<path d="M5 21q-.825 0-1.413-.587Q3 19.825 3 19V5q0-.825.587-1.413Q4.175 3 5 3h14q.825 0 1.413.587Q21 4.175 21 5v14q0 .825-.587 1.413Q19.825 21 19 21Zm0-2h14V5H5v14Z"/>`);
@@ -27,7 +27,7 @@ export interface UiDropdownListSelection {
27
27
  *
28
28
  * @slot - The default slot for the dropdown trigger (button)
29
29
  * @slot dropdown - The slot for the list.
30
- * @fires select - Custom event with the selected item on the `detail.item` when the user selected an item.
30
+ * @fires select - Custom event with the selected item on the `detail.item` when the user selected an item. When the event is cancelled then there's no side effects (closing the dropdown)
31
31
  * @fires dropdownopen - An event informing other dropdowns that this one was opened and other should close.
32
32
  * @fires open - An event dispatched when the open state change through a user interaction
33
33
  */
@@ -91,6 +91,13 @@ export default class UiDropdownList extends LitElement {
91
91
  */
92
92
  @property({ type: Boolean }) matchTriggerWidth?: boolean;
93
93
 
94
+ /**
95
+ * When set it closes the drop-down when `tab` button is pressed.
96
+ * This is not a default behavior since the drop-down content can have its own logic
97
+ * related to tab index.
98
+ */
99
+ @property({ type: Boolean }) closeOnTab?: boolean;
100
+
94
101
  /**
95
102
  * The first element located in the default slot.
96
103
  */
@@ -145,9 +152,7 @@ export default class UiDropdownList extends LitElement {
145
152
  if (e.composedPath()[0] === this) {
146
153
  return;
147
154
  }
148
- this.open = false;
149
- this.updateExpanded();
150
- this.notifyOpen();
155
+ this.close();
151
156
  }
152
157
 
153
158
  protected updateExpanded(): void {
@@ -212,10 +217,15 @@ export default class UiDropdownList extends LitElement {
212
217
  }
213
218
 
214
219
  protected contentKeyDownHandler(e: KeyboardEvent): void {
215
- if (e.code === 'Escape' || e.code === 'Tab') {
216
- this.open = false;
217
- this.updateExpanded();
218
- this.notifyOpen();
220
+ if (e.defaultPrevented) {
221
+ return;
222
+ }
223
+ if (e.code === 'Escape') {
224
+ this.close();
225
+ } else if (e.code === 'Tab') {
226
+ if (this.closeOnTab) {
227
+ this.close();
228
+ }
219
229
  } else if (['Enter', 'Space'].includes(e.code)) {
220
230
  this.activate(e);
221
231
  }
@@ -225,6 +235,17 @@ export default class UiDropdownList extends LitElement {
225
235
  this.activate(e);
226
236
  }
227
237
 
238
+ close(): void {
239
+ this.open = false;
240
+ this.updateExpanded();
241
+ this.notifyOpen();
242
+ }
243
+
244
+ protected contentCloseHandler(e: Event): void {
245
+ e.stopPropagation();
246
+ this.close();
247
+ }
248
+
228
249
  protected override willUpdate(cp: PropertyValues<this>): void {
229
250
  super.willUpdate(cp);
230
251
  if ((cp.has('noOverlap') || cp.has('verticalAlign') || cp.has('horizontalAlign') || cp.has('open')) && this.open) {
@@ -272,9 +293,7 @@ export default class UiDropdownList extends LitElement {
272
293
  if (inside) {
273
294
  return;
274
295
  }
275
- this.open = false;
276
- this.updateExpanded();
277
- this.notifyOpen();
296
+ this.close();
278
297
  }
279
298
 
280
299
  protected toggleOpened(): void {
@@ -311,6 +330,9 @@ export default class UiDropdownList extends LitElement {
311
330
  }
312
331
 
313
332
  protected activate(e: Event): void {
333
+ if (e.defaultPrevented) {
334
+ return;
335
+ }
314
336
  const path = e.composedPath();
315
337
  let item: HTMLElement | undefined;
316
338
  while (!item) {
@@ -328,14 +350,18 @@ export default class UiDropdownList extends LitElement {
328
350
  if (!item) {
329
351
  return;
330
352
  }
331
- this.dispatchEvent(new CustomEvent<UiDropdownListSelection>('select', {
353
+ const event = new CustomEvent<UiDropdownListSelection>('select', {
354
+ cancelable: true,
355
+ composed: true,
332
356
  detail: {
333
357
  item,
334
- }
335
- }));
336
- this.open = false;
337
- this.updateExpanded();
338
- this.notifyOpen();
358
+ },
359
+ })
360
+ this.dispatchEvent(event);
361
+ if (event.defaultPrevented) {
362
+ return;
363
+ }
364
+ this.close();
339
365
  }
340
366
 
341
367
  protected notifyOpen(): void {
@@ -385,6 +411,7 @@ export default class UiDropdownList extends LitElement {
385
411
  @slotchange="${this.dropdownChanged}"
386
412
  @keydown="${this.contentKeyDownHandler}"
387
413
  @click="${this.contentClickHandler}"
414
+ @close="${this.contentCloseHandler}"
388
415
  ></slot>
389
416
  </div>
390
417
  `;
@@ -58,6 +58,15 @@ export default class UiList extends LitElement {
58
58
  }
59
59
  }
60
60
 
61
+ override focus(options?: FocusOptions): void {
62
+ const { activeListItem } = this;
63
+ if (activeListItem) {
64
+ activeListItem.focus(options);
65
+ return;
66
+ }
67
+ this.activateFirstItem();
68
+ }
69
+
61
70
  override firstUpdated(changedProperties: PropertyValues): void {
62
71
  super.firstUpdated(changedProperties);
63
72
 
@@ -66,7 +75,7 @@ export default class UiList extends LitElement {
66
75
 
67
76
  activateFirstItem(): void {
68
77
  this.activeListItem = this.getFirstItem();
69
- this.activeListItem.activate();
78
+ this.activeListItem?.activate();
70
79
  }
71
80
 
72
81
  activateLastItem(): void {
@@ -318,30 +327,37 @@ export default class UiList extends LitElement {
318
327
  this.manageSelection(item);
319
328
  item.activate();
320
329
  this.activeListItem = item;
321
- this.notifySelect(item);
330
+ if (this.notifySelect(item)) {
331
+ e.preventDefault();
332
+ }
322
333
  }
323
334
 
324
- notifySelect(item: UiListItem): void {
335
+ /**
336
+ * @param item The UiListItem that is selected.
337
+ * @returns True when the event was canceled.
338
+ */
339
+ notifySelect(item: UiListItem): boolean {
325
340
  const index = this.items.indexOf(item);
326
341
  if (index === -1) {
327
- return;
342
+ return false;
328
343
  }
329
- this.dispatchEvent(new CustomEvent<UiListSelection>('select', {
344
+ const event = new CustomEvent<UiListSelection>('select', {
345
+ cancelable: true,
330
346
  detail: {
331
347
  item,
332
348
  index,
333
349
  }
334
- }));
350
+ });
351
+ this.dispatchEvent(event);
352
+ return event.defaultPrevented;
335
353
  }
336
354
 
337
355
  protected manageSelection(item: UiListItem): void {
338
356
  if (!this.selectActive) {
339
357
  return;
340
358
  }
341
- const { activeListItem } = this;
342
- if (activeListItem) {
343
- activeListItem.classList.remove('select');
344
- }
359
+ const { items } = this;
360
+ items.forEach((current) => current.classList.remove('select'));
345
361
  item.classList.add('select');
346
362
  }
347
363