viur-ui 0.0.1 → 0.0.2
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/README.md +67 -41
- package/index.d.ts +18 -9
- package/index.js +6 -3
- package/package.json +3 -5
- package/src/components/Browser/Browser.vue +58 -58
- package/src/components/Browser/TreeNav.vue +32 -32
- package/src/components/Browser/ViurBrowser.d.vue.ts +21 -21
- package/src/components/Browser/ViurBrowser.vue +47 -44
- package/src/components/Browser/dragSession.ts +27 -27
- package/src/components/Browser/index.ts +3 -3
- package/src/components/Browser/types.ts +9 -9
- package/src/components/DataTable/ColumnFilterPanel.vue +7 -7
- package/src/components/DataTable/DataTable.vue +346 -346
- package/src/components/DataTable/ViurDataTable.d.vue.ts +26 -26
- package/src/components/DataTable/ViurDataTable.vue +51 -50
- package/src/components/DataTable/columns.ts +30 -30
- package/src/components/DataTable/index.ts +12 -12
- package/src/components/DataTable/rows.ts +71 -72
- package/src/components/DataTable/types.ts +55 -55
- package/src/components/DataTable/useColumnLayout.ts +28 -28
- package/src/components/DataTable/useGridNavigation.ts +15 -15
- package/src/components/DataTable/useVirtualRows.ts +10 -10
- package/src/components/DataTable/viurColumns.ts +69 -69
- package/src/forms/ViForm.d.vue.ts +32 -32
- package/src/forms/ViForm.vue +17 -16
- package/src/forms/ViFormCategory.vue +3 -1
- package/src/forms/bones/Bone.vue +125 -108
- package/src/forms/bones/BoneActions.vue +21 -21
- package/src/forms/bones/BoneLabel.vue +1 -1
- package/src/forms/bones/BoneMultipleWrapper.vue +3 -3
- package/src/forms/bones/BoneNestedWrapper.vue +3 -3
- package/src/forms/bones/BoneSingleEntry.vue +2 -2
- package/src/forms/bones/FilterBone.vue +3 -3
- package/src/forms/bones/actions/ActionDialog.vue +2 -2
- package/src/forms/bones/actions/Debug.vue +6 -6
- package/src/forms/bones/actions/Delete.vue +2 -2
- package/src/forms/bones/actions/DescribeImage.vue +4 -3
- package/src/forms/bones/actions/Download.vue +4 -4
- package/src/forms/bones/actions/Placeholder.vue +2 -2
- package/src/forms/bones/actions/index.js +72 -74
- package/src/forms/bones/bone.d.ts +19 -19
- package/src/forms/bones/uploadAdapter.js +3 -3
- package/src/forms/bones/utils.js +40 -41
- package/src/forms/bones/view/base_item.vue +1 -1
- package/src/forms/bones/view/boneLogic.js +1 -1
- package/src/forms/bones/widgets/booleanBone.vue +19 -19
- package/src/forms/bones/widgets/booleanBoneChoose.vue +2 -2
- package/src/forms/bones/widgets/booleanBoneSelect.vue +3 -3
- package/src/forms/bones/widgets/codeBone.vue +22 -22
- package/src/forms/bones/widgets/colorBone.vue +18 -18
- package/src/forms/bones/widgets/dateBone.vue +22 -22
- package/src/forms/bones/widgets/emailBone.vue +20 -20
- package/src/forms/bones/widgets/fileBone.vue +32 -31
- package/src/forms/bones/widgets/keyBone.vue +21 -21
- package/src/forms/bones/widgets/numericBone.vue +21 -21
- package/src/forms/bones/widgets/passwordBone.vue +34 -31
- package/src/forms/bones/widgets/phoneBone.vue +20 -20
- package/src/forms/bones/widgets/rawBone.vue +19 -19
- package/src/forms/bones/widgets/recordBone.vue +16 -16
- package/src/forms/bones/widgets/relationalBone.vue +26 -26
- package/src/forms/bones/widgets/selectBone.vue +23 -23
- package/src/forms/bones/widgets/selectBoneChoose.vue +2 -2
- package/src/forms/bones/widgets/spatialBone.vue +21 -21
- package/src/forms/bones/widgets/stringBone.vue +27 -27
- package/src/forms/bones/widgets/textBone.vue +28 -26
- package/src/forms/install.d.ts +1 -1
- package/src/forms/install.js +7 -0
- package/src/forms/layouts/LayloutList2Columns.vue +1 -1
- package/src/forms/layouts/LayoutCategory.vue +1 -1
- package/src/forms/layouts/LayoutList.vue +1 -1
- package/src/forms/utils.js +22 -22
- package/src/request/configStore.js +4 -4
- package/src/request/envelope.js +6 -6
- package/src/request/request.js +22 -22
- package/src/translations/de.js +31 -0
- package/src/translations/en.js +31 -0
- package/src/user/LoginScreen.d.vue.ts +24 -24
- package/src/user/LoginScreen.vue +6 -7
- package/src/user/store.d.ts +16 -16
- package/src/vite/safeHtml.js +73 -0
- package/src/vite/sanitizeVHtml.d.ts +4 -0
- package/src/vite/sanitizeVHtml.js +15 -0
- package/src/webawesomeCompat.css +0 -40
package/README.md
CHANGED
|
@@ -1,79 +1,105 @@
|
|
|
1
1
|
# viur-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vue 3 components and request helpers for [ViUR](https://www.viur.dev).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install viur-ui
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Peers: `vue` 3.3+, `pinia`, `vue-i18n`, `@viur/webawesome`, `@vueuse/core`, `vue-router`; for code/text bones also `codemirror`, `@codemirror/language-data`, `ckeditor5`, `@ckeditor/ckeditor5-vue`.
|
|
12
12
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
### Histoire (Component Stories)
|
|
13
|
+
## Vite
|
|
16
14
|
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
npm run story:dev
|
|
15
|
+
```js
|
|
16
|
+
import vue from "@vitejs/plugin-vue"
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
export default {
|
|
19
|
+
plugins: [vue()],
|
|
20
|
+
optimizeDeps: {
|
|
21
|
+
exclude: ["viur-ui"], // ships raw SFCs; pre-bundling duplicates module state in dev
|
|
22
|
+
},
|
|
23
|
+
}
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Env: `VITE_API_URL`, `VITE_DEFAULT_RENDERER` (`json` | `vi`).
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
# Unit-Tests (Vitest, watch mode)
|
|
29
|
-
npm run test
|
|
28
|
+
## Vitest
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
npm run test:contract
|
|
30
|
+
```js
|
|
31
|
+
test: {
|
|
32
|
+
server: { deps: { inline: ["viur-ui"] } }, // package .js files import .vue — Node can't load those
|
|
33
|
+
}
|
|
36
34
|
```
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
## Setup
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
```js
|
|
39
|
+
import "viur-ui/src/webawesomeConfig.js"
|
|
40
|
+
import { deTranslations, enTranslations } from "viur-ui"
|
|
41
|
+
import { installViurForms } from "viur-ui/src/forms/install.js"
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
app.use(createPinia())
|
|
44
|
+
app.use(i18n)
|
|
45
|
+
installViurForms(app) // also registers v-safe-html (DOMPurify) and $sanitizeHtml
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
Translations: merge the package messages before your app's own (app keys win):
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
i18n.global.mergeLocaleMessage("de", deTranslations)
|
|
52
|
+
i18n.global.mergeLocaleMessage("en", enTranslations)
|
|
53
|
+
```
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
`installViurForms` registers these global names (async), skipping names that are already taken: `ViForm`, `vi-form`, `vi-form-category`, `Bone`, `bone`, `bone-label`, `bone-actions`, `bone-single-entry`, `bone-multiple-list`, `bone-multiple-wrapper`, `bone-nested-wrapper`. The package imports its wrappers locally, so running alongside `@viur/vue-utils` (e.g. a global `Bone` from there) doesn't affect viur-ui forms — but your own templates resolve whichever component owns the name.
|
|
56
|
+
|
|
57
|
+
## Safe HTML (recommended)
|
|
58
|
+
|
|
59
|
+
Route every `v-html` through DOMPurify at compile time — your templates and viur-ui's, without changes:
|
|
49
60
|
|
|
50
61
|
```js
|
|
51
|
-
import vue from
|
|
62
|
+
import vue from "@vitejs/plugin-vue"
|
|
63
|
+
import { sanitizeVHtml } from "viur-ui/src/vite/sanitizeVHtml.js"
|
|
52
64
|
|
|
53
65
|
export default {
|
|
54
|
-
plugins: [vue()],
|
|
55
|
-
optimizeDeps: {
|
|
56
|
-
exclude: ['viur-ui']
|
|
57
|
-
}
|
|
66
|
+
plugins: [vue({ template: { compilerOptions: { directiveTransforms: { html: sanitizeVHtml } } } })],
|
|
58
67
|
}
|
|
59
68
|
```
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
`installViurForms(app)` provides the runtime sanitizer (`window.__sanitizeHtml`); call it before mounting. Without it, rendering throws rather than outputting unsanitized HTML. `v-safe-html` is then redundant.
|
|
62
71
|
|
|
63
|
-
|
|
72
|
+
Not covered: SSR, `innerHTML` in JS, `h("div", { innerHTML })` — use `sanitizeHtml()` there. Don't decode HTML entities before sanitizing.
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
## ViForm
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<ViForm module="user" action="edit" :skelkey="key" />
|
|
67
78
|
```
|
|
68
79
|
|
|
69
|
-
|
|
80
|
+
Props, state and exposed API: [`src/forms/ViForm.d.vue.ts`](./src/forms/ViForm.d.vue.ts).
|
|
81
|
+
|
|
82
|
+
## API
|
|
70
83
|
|
|
71
84
|
```js
|
|
72
|
-
import
|
|
85
|
+
import { useListFetch, useViewFetch, useEditFetch, useConfigStore, notify } from "viur-ui"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Full list: [`index.js`](./index.js), types: [`index.d.ts`](./index.d.ts).
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install && npm install --prefix example
|
|
94
|
+
npm run story:dev # Histoire, :6006
|
|
95
|
+
npm run test:run # Vitest
|
|
96
|
+
npm run lint
|
|
73
97
|
```
|
|
74
98
|
|
|
75
|
-
##
|
|
99
|
+
## Release
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm version patch && git push --follow-tags
|
|
103
|
+
```
|
|
76
104
|
|
|
77
|
-
|
|
78
|
-
|-----------|-------------|
|
|
79
|
-
| `HelloWorld` | Renders "Hello World" |
|
|
105
|
+
Published by [`release.yml`](./.github/workflows/release.yml) via npm Trusted Publishing. `-alpha` / `-beta` / `-rc` → matching dist-tag, else `latest`.
|
package/index.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ export interface ListFetchOptions {
|
|
|
31
31
|
immediate?: boolean
|
|
32
32
|
cached?: boolean
|
|
33
33
|
ttl?: number
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
34
|
+
/** Extra headers for the LIST requests (not the structure) — object or
|
|
35
|
+
* function, evaluated per request (e.g. X-VIUR-BONELIST). */
|
|
36
36
|
headers?: Record<string, string> | (() => Record<string, string> | null) | null
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -83,9 +83,9 @@ type ControllerFetchFn = (
|
|
|
83
83
|
) => UseFetchReturn<any>
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
* `use*Fetch
|
|
88
|
-
* (
|
|
86
|
+
* Build a custom query against an endpoint of a module — the basis of the
|
|
87
|
+
* `use*Fetch` helpers. `ctx` accepts, among others, `renderer`, `version`, `query`
|
|
88
|
+
* (additional query string), `headers`, `immediate`.
|
|
89
89
|
*/
|
|
90
90
|
export declare function createControllerFetch(options?: {
|
|
91
91
|
endpoint?: string
|
|
@@ -97,9 +97,9 @@ export declare function createControllerFetch(options?: {
|
|
|
97
97
|
|
|
98
98
|
export declare const useViewFetch: ControllerFetchFn
|
|
99
99
|
export declare const useStructureFetch: ControllerFetchFn
|
|
100
|
-
/**
|
|
100
|
+
/** Unwrap envelope v2 (v1 responses are returned unchanged). */
|
|
101
101
|
export declare function normalizeEnvelope(body: unknown): unknown
|
|
102
|
-
/**
|
|
102
|
+
/** Flatten a skel structure — accepts both dict and pair-list form. */
|
|
103
103
|
export declare function normalizeStructure(structure: unknown): Record<string, Record<string, unknown>>
|
|
104
104
|
export declare const useAddFetch: ControllerFetchFn
|
|
105
105
|
export declare const useEditFetch: ControllerFetchFn
|
|
@@ -345,7 +345,16 @@ export declare function skelDataToFormData(
|
|
|
345
345
|
|
|
346
346
|
// ─── Translations ─────────────────────────────────────────────────────────────
|
|
347
347
|
|
|
348
|
-
/** Message
|
|
349
|
-
* (vue-i18n
|
|
348
|
+
/** Message trees of the package components — merge into the app's vue-i18n messages
|
|
349
|
+
* (vue-i18n is required; the components translate with t() without fallbacks). */
|
|
350
350
|
export declare const deTranslations: Record<string, any>
|
|
351
351
|
export declare const enTranslations: Record<string, any>
|
|
352
|
+
|
|
353
|
+
// ─── Safe HTML ────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
/** Sanitizes an HTML string with DOMPurify (scripts, event handlers and javascript: URLs removed). */
|
|
356
|
+
export declare function sanitizeHtml(value: unknown): string
|
|
357
|
+
/** Vue plugin: registers the `v-safe-html` directive and `$sanitizeHtml`. */
|
|
358
|
+
export declare const safeHtml: import("vue").Plugin
|
|
359
|
+
/** The `v-safe-html` directive for local registration (`import { vSafeHtml }` in `<script setup>`). */
|
|
360
|
+
export declare const vSafeHtml: import("vue").ObjectDirective<HTMLElement, unknown>
|
package/index.js
CHANGED
|
@@ -37,10 +37,13 @@ export {
|
|
|
37
37
|
// Theme (brand color from the configuration)
|
|
38
38
|
export { useColorStore, hexToHsl, BRAND_LIGHTNESS } from "./src/theme/colorStore.js"
|
|
39
39
|
|
|
40
|
-
// Translations (
|
|
41
|
-
// vue-i18n
|
|
40
|
+
// Translations (components of this package; merge into the app's vue-i18n instance —
|
|
41
|
+
// vue-i18n is required, the components translate with t() without fallbacks)
|
|
42
42
|
export { default as deTranslations } from "./src/translations/de.js"
|
|
43
43
|
export { default as enTranslations } from "./src/translations/en.js"
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// Display of bone values
|
|
46
46
|
export { default as boneLogic } from "./src/forms/bones/view/boneLogic.js"
|
|
47
|
+
|
|
48
|
+
// Safe HTML rendering (DOMPurify): v-safe-html directive + sanitizeHtml()
|
|
49
|
+
export { default as safeHtml, sanitizeHtml, vSafeHtml } from "./src/vite/safeHtml.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viur-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/viur-framework/viur-ui"
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"index.d.ts"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@vue/compiler-dom": "^3.3.0",
|
|
17
18
|
"destr": "^2.0.5",
|
|
19
|
+
"dompurify": "^3.4.16",
|
|
18
20
|
"json-editor-vue": "^0.18.1",
|
|
19
21
|
"libphonenumber-js": "^1.12.41",
|
|
20
22
|
"logics-js": "^0.4.1",
|
|
@@ -24,7 +26,6 @@
|
|
|
24
26
|
"peerDependencies": {
|
|
25
27
|
"@ckeditor/ckeditor5-vue": "^7.3.0",
|
|
26
28
|
"@codemirror/language-data": "^6.5.2",
|
|
27
|
-
"@viur/shoelace": ">=1.0.0-0",
|
|
28
29
|
"@viur/webawesome": ">=0.10.0",
|
|
29
30
|
"@vueuse/core": "^12.4.0",
|
|
30
31
|
"ckeditor5": "^45.2.0",
|
|
@@ -40,15 +41,12 @@
|
|
|
40
41
|
"test:run": "vitest run",
|
|
41
42
|
"story:dev": "histoire dev",
|
|
42
43
|
"story:build": "histoire build",
|
|
43
|
-
"histoire:legacy": "histoire dev --config histoire.legacy.config.js",
|
|
44
|
-
"test:contract": "playwright test --config tests/contract/playwright.config.ts",
|
|
45
44
|
"lint": "eslint .",
|
|
46
45
|
"format:check": "prettier --check .",
|
|
47
46
|
"prepare": "lefthook install"
|
|
48
47
|
},
|
|
49
48
|
"devDependencies": {
|
|
50
49
|
"@histoire/plugin-vue": "^1.0.0-beta.1",
|
|
51
|
-
"@playwright/test": "^1.59.1",
|
|
52
50
|
"@vitejs/plugin-vue": "^6.0.6",
|
|
53
51
|
"@vueuse/core": "^14.2.1",
|
|
54
52
|
"eslint": "^10.2.1",
|
|
@@ -125,46 +125,46 @@
|
|
|
125
125
|
<script setup>
|
|
126
126
|
import { useI18n } from "vue-i18n"
|
|
127
127
|
import { ref, computed, provide, watch } from "vue"
|
|
128
|
-
import { DataTable } from "../DataTable"
|
|
128
|
+
import { DataTable } from "../DataTable/index.ts"
|
|
129
129
|
import TreeNav from "./TreeNav.vue"
|
|
130
|
-
import { createDragSession, dropModeFor, DRAG_SESSION } from "./dragSession"
|
|
130
|
+
import { createDragSession, dropModeFor, DRAG_SESSION } from "./dragSession.ts"
|
|
131
131
|
|
|
132
132
|
// --------------------------------------------------------------------------- //
|
|
133
|
-
// Browser —
|
|
133
|
+
// Browser — tree for navigation + DataTable as list //
|
|
134
134
|
// --------------------------------------------------------------------------- //
|
|
135
135
|
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// navigate —
|
|
140
|
-
// open —
|
|
141
|
-
// move —
|
|
142
|
-
// reorder —
|
|
143
|
-
// upload —
|
|
144
|
-
//
|
|
145
|
-
//
|
|
136
|
+
// GENERIC core: knows nothing about ViUR. The app (or ViurBrowser.vue) supplies
|
|
137
|
+
// rootnodes, the contents of the current location (`rows`, folders first) and a
|
|
138
|
+
// loader for the tree levels — and gets intents back as events:
|
|
139
|
+
// navigate — another location chosen (tree, breadcrumb, double-click on folder)
|
|
140
|
+
// open — double-click on an entry (leaf)
|
|
141
|
+
// move — drag & drop between list and tree, or into a folder row
|
|
142
|
+
// reorder — reordering within the list (DataTable row-reorder, 1:1)
|
|
143
|
+
// upload — files dragged from the OS into the list
|
|
144
|
+
// The browser loads nothing itself and only moves the VIEW — saving and
|
|
145
|
+
// reloading are up to the app (see ViurBrowser for the ViUR variant).
|
|
146
146
|
|
|
147
147
|
const props = defineProps({
|
|
148
|
-
/**
|
|
148
|
+
/** Roots/repositories: { key, name, icon? } */
|
|
149
149
|
rootNodes: { type: Array, default: () => [] },
|
|
150
|
-
/**
|
|
150
|
+
/** Contents of the current location — folders first (see rowKind). */
|
|
151
151
|
rows: { type: Array, default: () => [] },
|
|
152
152
|
columns: { type: Array, default: () => [] },
|
|
153
153
|
rowKey: { type: String, default: "key" },
|
|
154
|
-
/**
|
|
154
|
+
/** Classifies a row: "node" (folder) or "leaf". */
|
|
155
155
|
rowKind: { type: Function, default: (row) => (row?.skelType === "node" ? "node" : "leaf") },
|
|
156
|
-
/**
|
|
156
|
+
/** Children of a tree node: (node) => Promise<BrowserNode[]> */
|
|
157
157
|
loadNodeChildren: { type: Function, default: null },
|
|
158
158
|
orderField: { type: String, default: "sortindex" },
|
|
159
159
|
selectable: { type: [String, Boolean], default: "multiple" },
|
|
160
160
|
loading: { type: Boolean, default: false },
|
|
161
|
-
/** OS
|
|
161
|
+
/** Accept OS files (only the `upload` event, no upload code here). */
|
|
162
162
|
uploads: { type: Boolean, default: false },
|
|
163
163
|
uploadHint: { type: [String, null], default: null },
|
|
164
164
|
label: { type: String, default: "Browser" },
|
|
165
|
-
/** Cursor
|
|
165
|
+
/** Cursor loading: null = off, otherwise "is there more?" (DataTable bar). */
|
|
166
166
|
hasMore: { type: [Boolean, null], default: null },
|
|
167
|
-
/**
|
|
167
|
+
/** Further DataTable props (withSearch, size, …). */
|
|
168
168
|
tableProps: { type: Object, default: () => ({}) },
|
|
169
169
|
})
|
|
170
170
|
|
|
@@ -178,9 +178,9 @@ const treeEl = ref(null)
|
|
|
178
178
|
const tableEl = ref(null)
|
|
179
179
|
const listEl = ref(null)
|
|
180
180
|
|
|
181
|
-
// ── Rootnodes &
|
|
182
|
-
//
|
|
183
|
-
//
|
|
181
|
+
// ── Rootnodes & location ──────────────────────────────────────────────────────
|
|
182
|
+
// The active root follows from the location (breadcrumb root); without a
|
|
183
|
+
// location the first rootnode applies. A change in the select navigates to its root.
|
|
184
184
|
const nodeIndex = ref(new Map()) // key → { node, parent }
|
|
185
185
|
|
|
186
186
|
watch(
|
|
@@ -237,7 +237,7 @@ function onRootChange(value) {
|
|
|
237
237
|
|
|
238
238
|
function onRowDoubleClick({ row, key }) {
|
|
239
239
|
if (props.rowKind(row) === "node") {
|
|
240
|
-
//
|
|
240
|
+
// Remember the folder from the list so the breadcrumb knows its name.
|
|
241
241
|
rememberNodes([{ key, name: rowName(row), data: row }], location.value)
|
|
242
242
|
navigateTo(key)
|
|
243
243
|
} else {
|
|
@@ -250,11 +250,11 @@ function rowName(row) {
|
|
|
250
250
|
return typeof value === "string" ? value : (value?.name ?? String(row?.[props.rowKey] ?? ""))
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
// ──
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
253
|
+
// ── Local view of the list ────────────────────────────────────────────────────
|
|
254
|
+
// A drag is purely client-side: rows that LEAVE the current location are
|
|
255
|
+
// hidden; nodes dragged HERE from the tree are inserted. `rows` (the app's
|
|
256
|
+
// data) stay untouched — new data or a change of location discards the
|
|
257
|
+
// overlay.
|
|
258
258
|
const movedAway = ref(new Set())
|
|
259
259
|
const movedIn = ref([])
|
|
260
260
|
|
|
@@ -277,9 +277,9 @@ function resetLocalMoves() {
|
|
|
277
277
|
watch(() => props.rows, resetLocalMoves)
|
|
278
278
|
watch(location, resetLocalMoves)
|
|
279
279
|
|
|
280
|
-
// ── Drag
|
|
280
|
+
// ── Drag session (list ↔ tree) ────────────────────────────────────────────────
|
|
281
281
|
const session = createDragSession((payload, target) => {
|
|
282
|
-
//
|
|
282
|
+
// Nothing drops into itself or its own subtree: discard the target.
|
|
283
283
|
if (payload.keys.some((key) => String(key) === String(target.key))) return
|
|
284
284
|
let parent = target.key != null ? nodeIndex.value.get(String(target.key))?.parent : null
|
|
285
285
|
const seen = new Set()
|
|
@@ -289,14 +289,14 @@ const session = createDragSession((payload, target) => {
|
|
|
289
289
|
parent = nodeIndex.value.get(String(parent))?.parent ?? null
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
//
|
|
292
|
+
// Update the view immediately — without reload.
|
|
293
293
|
const targetIsCurrent = location.value != null && String(target.key) === String(location.value)
|
|
294
294
|
if (targetIsCurrent) {
|
|
295
295
|
const away = new Set(movedAway.value)
|
|
296
296
|
for (const key of payload.keys) away.delete(String(key))
|
|
297
297
|
movedAway.value = away
|
|
298
298
|
if (payload.source === "tree") {
|
|
299
|
-
//
|
|
299
|
+
// Tree node dragged into the open level: insert as a folder row.
|
|
300
300
|
const rows = payload.rows.map((row) => row?.data ?? row).filter(Boolean)
|
|
301
301
|
movedIn.value = [...movedIn.value, ...rows]
|
|
302
302
|
}
|
|
@@ -305,15 +305,15 @@ const session = createDragSession((payload, target) => {
|
|
|
305
305
|
movedIn.value = movedIn.value.filter(
|
|
306
306
|
(row) => !payload.keys.some((key) => String(key) === String(row[props.rowKey]))
|
|
307
307
|
)
|
|
308
|
-
//
|
|
308
|
+
// Whatever left the location is no longer selected either.
|
|
309
309
|
selectedKeys.value = selectedKeys.value.filter(
|
|
310
310
|
(selected) => !payload.keys.some((key) => String(key) === String(selected))
|
|
311
311
|
)
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
314
|
+
// The TREE also updates locally: moved FOLDERS change (or leave) their
|
|
315
|
+
// loaded level — without reload. Leaves don't appear in the navigation
|
|
316
|
+
// tree and need nothing.
|
|
317
317
|
for (const row of payload.rows) {
|
|
318
318
|
const raw = row?.data ?? row
|
|
319
319
|
const isNode = payload.source === "tree" || props.rowKind(raw) === "node"
|
|
@@ -321,7 +321,7 @@ const session = createDragSession((payload, target) => {
|
|
|
321
321
|
const key = raw?.[props.rowKey] ?? row?.key
|
|
322
322
|
const node = { key, name: nodeIndex.value.get(String(key))?.node?.name ?? rowName(raw), data: raw }
|
|
323
323
|
treeEl.value?.moveNode?.(node, target.key)
|
|
324
|
-
rememberNodes([node], target.key) //
|
|
324
|
+
rememberNodes([node], target.key) // keep the breadcrumb parent chain up to date
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
emit("move", {
|
|
@@ -334,11 +334,11 @@ const session = createDragSession((payload, target) => {
|
|
|
334
334
|
})
|
|
335
335
|
provide(DRAG_SESSION, session)
|
|
336
336
|
|
|
337
|
-
//
|
|
338
|
-
// •
|
|
339
|
-
//
|
|
340
|
-
// •
|
|
341
|
-
//
|
|
337
|
+
// The list itself is a drop target — in both directions:
|
|
338
|
+
// • The MIDDLE of a folder row means "into"; the edges (before/after) are
|
|
339
|
+
// left to the table's internal reordering.
|
|
340
|
+
// • If the drag comes from the TREE, the rest of the list (leaf rows,
|
|
341
|
+
// empty area) is a target too: "into the currently open folder".
|
|
342
342
|
session.register({
|
|
343
343
|
id: "list",
|
|
344
344
|
el: () => listEl.value,
|
|
@@ -355,7 +355,7 @@ session.register({
|
|
|
355
355
|
},
|
|
356
356
|
})
|
|
357
357
|
|
|
358
|
-
/**
|
|
358
|
+
/** Drag from the tree targets the open level → highlight the whole list. */
|
|
359
359
|
const listIsDropTarget = computed(
|
|
360
360
|
() =>
|
|
361
361
|
session.state.over?.container === "list" &&
|
|
@@ -363,7 +363,7 @@ const listIsDropTarget = computed(
|
|
|
363
363
|
String(session.state.over?.key) === String(location.value)
|
|
364
364
|
)
|
|
365
365
|
|
|
366
|
-
// ── Upload
|
|
366
|
+
// ── Upload dropzone (native, only for OS files) ──────────────────────────────
|
|
367
367
|
const fileDragDepth = ref(0)
|
|
368
368
|
const hasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files")
|
|
369
369
|
function onFileDragEnter(event) {
|
|
@@ -396,15 +396,15 @@ defineExpose({ table: tableEl, tree: treeEl, session, navigateTo })
|
|
|
396
396
|
flex-direction: column;
|
|
397
397
|
min-block-size: 0;
|
|
398
398
|
block-size: 100%;
|
|
399
|
-
/*
|
|
400
|
-
|
|
401
|
-
(appearance="plain"
|
|
399
|
+
/* Framed like the list widget (DataTable `appearance="outlined"`): ONE
|
|
400
|
+
box around tree and list. The table inside therefore stays frameless
|
|
401
|
+
(appearance="plain" below), otherwise there would be two nested frames. */
|
|
402
402
|
background-color: var(--wa-color-surface-default, #fff);
|
|
403
403
|
border: var(--wa-border-width-s, 1px) solid var(--wa-color-surface-border, #ddd);
|
|
404
404
|
border-radius: var(--wa-border-radius-m, 0.5rem);
|
|
405
405
|
overflow: hidden;
|
|
406
|
-
/*
|
|
407
|
-
(.vi-dt),
|
|
406
|
+
/* One text size for the whole box — the same as in the table
|
|
407
|
+
(.vi-dt), so breadcrumb and tree don't look larger than the list. */
|
|
408
408
|
font-size: var(--wa-font-size-s, 0.875rem);
|
|
409
409
|
}
|
|
410
410
|
.vi-browser-head {
|
|
@@ -413,7 +413,7 @@ defineExpose({ table: tableEl, tree: treeEl, session, navigateTo })
|
|
|
413
413
|
gap: var(--wa-space-s, 0.5rem);
|
|
414
414
|
padding: var(--wa-space-2xs, 0.25rem) var(--wa-space-xs, 0.5rem);
|
|
415
415
|
border-block-end: 1px solid var(--wa-color-surface-border, #ddd);
|
|
416
|
-
/*
|
|
416
|
+
/* Set off like the table header (--dt-header-background). */
|
|
417
417
|
background-color: var(--wa-color-surface-lowered, #f4f4f5);
|
|
418
418
|
}
|
|
419
419
|
.vi-browser-roots {
|
|
@@ -447,14 +447,14 @@ defineExpose({ table: tableEl, tree: treeEl, session, navigateTo })
|
|
|
447
447
|
.vi-browser-body {
|
|
448
448
|
flex: 1;
|
|
449
449
|
min-block-size: 0;
|
|
450
|
-
/* <wa-split-panel>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
450
|
+
/* <wa-split-panel> IS the grid (display: grid on the host). Its row is
|
|
451
|
+
content-sized by itself and thus overflows the fixed height — the
|
|
452
|
+
table would then be as tall as its rows and lengthen the page instead
|
|
453
|
+
of scrolling within itself. A single full-height row binds the panels
|
|
454
|
+
to the area. Applies to the horizontal split, which is fixed
|
|
455
|
+
here. */
|
|
456
456
|
grid-template-rows: 100%;
|
|
457
|
-
/*
|
|
457
|
+
/* Sidebar no smaller than 10rem, list keeps at least half */
|
|
458
458
|
--min: 10rem;
|
|
459
459
|
--max: 50%;
|
|
460
460
|
}
|