@budibase/frontend-core 2.5.6-alpha.5 → 2.5.6-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -4
- package/src/components/grid/cells/AttachmentCell.svelte +153 -0
- package/src/components/grid/cells/BooleanCell.svelte +44 -0
- package/src/components/grid/cells/DataCell.svelte +86 -0
- package/src/components/grid/cells/DateCell.svelte +77 -0
- package/src/components/grid/cells/FormulaCell.svelte +5 -0
- package/src/components/grid/cells/GridCell.svelte +159 -0
- package/src/components/grid/cells/HeaderCell.svelte +237 -0
- package/src/components/grid/cells/JSONCell.svelte +36 -0
- package/src/components/grid/cells/LongFormCell.svelte +117 -0
- package/src/components/grid/cells/MultiSelectCell.svelte +7 -0
- package/src/components/grid/cells/NumberCell.svelte +7 -0
- package/src/components/grid/cells/OptionsCell.svelte +239 -0
- package/src/components/grid/cells/RelationshipCell.svelte +503 -0
- package/src/components/grid/cells/TextCell.svelte +110 -0
- package/src/components/grid/controls/AddColumnButton.svelte +16 -0
- package/src/components/grid/controls/AddRowButton.svelte +18 -0
- package/src/components/grid/controls/BetaButton.svelte +46 -0
- package/src/components/grid/controls/DeleteButton.svelte +72 -0
- package/src/components/grid/controls/HideColumnsButton.svelte +91 -0
- package/src/components/grid/controls/RowHeightButton.svelte +70 -0
- package/src/components/grid/controls/SortButton.svelte +107 -0
- package/src/components/grid/index.js +1 -0
- package/src/components/grid/layout/Avatar.svelte +24 -0
- package/src/components/grid/layout/Grid.svelte +250 -0
- package/src/components/grid/layout/GridBody.svelte +38 -0
- package/src/components/grid/layout/GridRow.svelte +57 -0
- package/src/components/grid/layout/GridScrollWrapper.svelte +84 -0
- package/src/components/grid/layout/HeaderRow.svelte +30 -0
- package/src/components/grid/layout/NewRowTop.svelte +247 -0
- package/src/components/grid/layout/RowCount.svelte +9 -0
- package/src/components/grid/layout/StickyColumn.svelte +245 -0
- package/src/components/grid/layout/UserAvatars.svelte +20 -0
- package/src/components/grid/lib/constants.js +11 -0
- package/src/components/grid/lib/events.js +29 -0
- package/src/components/grid/lib/renderers.js +29 -0
- package/src/components/grid/lib/utils.js +26 -0
- package/src/components/grid/lib/websocket.js +63 -0
- package/src/components/grid/overlays/KeyboardManager.svelte +180 -0
- package/src/components/grid/overlays/MenuOverlay.svelte +95 -0
- package/src/components/grid/overlays/ReorderOverlay.svelte +63 -0
- package/src/components/grid/overlays/ResizeOverlay.svelte +70 -0
- package/src/components/grid/overlays/ScrollOverlay.svelte +121 -0
- package/src/components/grid/stores/bounds.js +16 -0
- package/src/components/grid/stores/clipboard.js +34 -0
- package/src/components/grid/stores/columns.js +206 -0
- package/src/components/grid/stores/index.js +48 -0
- package/src/components/grid/stores/menu.js +49 -0
- package/src/components/grid/stores/pagination.js +24 -0
- package/src/components/grid/stores/reorder.js +155 -0
- package/src/components/grid/stores/resize.js +124 -0
- package/src/components/grid/stores/rows.js +462 -0
- package/src/components/grid/stores/scroll.js +201 -0
- package/src/components/grid/stores/ui.js +168 -0
- package/src/components/grid/stores/users.js +115 -0
- package/src/components/grid/stores/validation.js +45 -0
- package/src/components/grid/stores/viewport.js +141 -0
- package/src/components/index.js +1 -0
- package/src/fetch/DataFetch.js +27 -21
- package/src/fetch/QueryFetch.js +1 -1
- package/src/themes/midnight.css +3 -0
- package/src/themes/nord.css +3 -0
- package/src/utils/utils.js +20 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const getColor = (idx, opacity = 0.3) => {
|
|
2
|
+
if (idx == null || idx === -1) {
|
|
3
|
+
return null
|
|
4
|
+
}
|
|
5
|
+
return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const TypeIconMap = {
|
|
9
|
+
text: "Text",
|
|
10
|
+
options: "Dropdown",
|
|
11
|
+
datetime: "Date",
|
|
12
|
+
barcodeqr: "Camera",
|
|
13
|
+
longform: "TextAlignLeft",
|
|
14
|
+
array: "Dropdown",
|
|
15
|
+
number: "123",
|
|
16
|
+
boolean: "Boolean",
|
|
17
|
+
attachment: "AppleFiles",
|
|
18
|
+
link: "Link",
|
|
19
|
+
formula: "Calculator",
|
|
20
|
+
json: "Brackets",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getColumnIcon = column => {
|
|
24
|
+
const type = column.schema.type
|
|
25
|
+
return TypeIconMap[type] || "Text"
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { get } from "svelte/store"
|
|
2
|
+
import { io } from "socket.io-client"
|
|
3
|
+
|
|
4
|
+
export const createWebsocket = context => {
|
|
5
|
+
const { rows, tableId, users, userId, focusedCellId } = context
|
|
6
|
+
|
|
7
|
+
// Determine connection info
|
|
8
|
+
const tls = location.protocol === "https:"
|
|
9
|
+
const proto = tls ? "wss:" : "ws:"
|
|
10
|
+
const host = location.hostname
|
|
11
|
+
const port = location.port || (tls ? 443 : 80)
|
|
12
|
+
const socket = io(`${proto}//${host}:${port}`, {
|
|
13
|
+
path: "/socket/grid",
|
|
14
|
+
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
|
|
15
|
+
reconnectionAttempts: 3,
|
|
16
|
+
// Delay reconnection attempt by 5 seconds
|
|
17
|
+
reconnectionDelay: 5000,
|
|
18
|
+
reconnectionDelayMax: 5000,
|
|
19
|
+
// Timeout after 4 seconds so we never stack requests
|
|
20
|
+
timeout: 4000,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const connectToTable = tableId => {
|
|
24
|
+
if (!socket.connected) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
// Identify which table we are editing
|
|
28
|
+
socket.emit("select-table", tableId, response => {
|
|
29
|
+
// handle initial connection info
|
|
30
|
+
users.set(response.users)
|
|
31
|
+
userId.set(response.id)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Event handlers
|
|
36
|
+
socket.on("connect", () => {
|
|
37
|
+
connectToTable(get(tableId))
|
|
38
|
+
})
|
|
39
|
+
socket.on("row-update", data => {
|
|
40
|
+
if (data.id) {
|
|
41
|
+
rows.actions.refreshRow(data.id)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
socket.on("user-update", user => {
|
|
45
|
+
users.actions.updateUser(user)
|
|
46
|
+
})
|
|
47
|
+
socket.on("user-disconnect", user => {
|
|
48
|
+
users.actions.removeUser(user)
|
|
49
|
+
})
|
|
50
|
+
socket.on("connect_error", err => {
|
|
51
|
+
console.log("Failed to connect to grid websocket:", err.message)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Change websocket connection when table changes
|
|
55
|
+
tableId.subscribe(connectToTable)
|
|
56
|
+
|
|
57
|
+
// Notify selected cell changes
|
|
58
|
+
focusedCellId.subscribe($focusedCellId => {
|
|
59
|
+
socket.emit("select-cell", $focusedCellId)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return () => socket?.disconnect()
|
|
63
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onMount } from "svelte"
|
|
3
|
+
import { debounce } from "../../../utils/utils"
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
enrichedRows,
|
|
7
|
+
focusedCellId,
|
|
8
|
+
visibleColumns,
|
|
9
|
+
focusedRow,
|
|
10
|
+
stickyColumn,
|
|
11
|
+
focusedCellAPI,
|
|
12
|
+
clipboard,
|
|
13
|
+
} = getContext("grid")
|
|
14
|
+
|
|
15
|
+
// Global key listener which intercepts all key events
|
|
16
|
+
const handleKeyDown = e => {
|
|
17
|
+
// If nothing selected avoid processing further key presses
|
|
18
|
+
if (!$focusedCellId) {
|
|
19
|
+
if (e.key === "Tab") {
|
|
20
|
+
e.preventDefault()
|
|
21
|
+
focusFirstCell()
|
|
22
|
+
}
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Always intercept certain key presses
|
|
27
|
+
const api = $focusedCellAPI
|
|
28
|
+
if (e.key === "Escape") {
|
|
29
|
+
api?.blur?.()
|
|
30
|
+
} else if (e.key === "Tab") {
|
|
31
|
+
api?.blur?.()
|
|
32
|
+
changeFocusedColumn(1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Pass the key event to the selected cell and let it decide whether to
|
|
36
|
+
// capture it or not
|
|
37
|
+
if (!api?.isReadonly()) {
|
|
38
|
+
const handled = api?.onKeyDown?.(e)
|
|
39
|
+
if (handled) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Avoid processing events sourced from modals
|
|
45
|
+
if (e.target?.closest?.(".spectrum-Modal")) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
e.preventDefault()
|
|
49
|
+
|
|
50
|
+
// Handle the key ourselves
|
|
51
|
+
if (e.metaKey || e.ctrlKey) {
|
|
52
|
+
switch (e.key) {
|
|
53
|
+
case "c":
|
|
54
|
+
clipboard.actions.copy()
|
|
55
|
+
break
|
|
56
|
+
case "v":
|
|
57
|
+
clipboard.actions.paste()
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
switch (e.key) {
|
|
62
|
+
case "ArrowLeft":
|
|
63
|
+
changeFocusedColumn(-1)
|
|
64
|
+
break
|
|
65
|
+
case "ArrowRight":
|
|
66
|
+
changeFocusedColumn(1)
|
|
67
|
+
break
|
|
68
|
+
case "ArrowUp":
|
|
69
|
+
changeFocusedRow(-1)
|
|
70
|
+
break
|
|
71
|
+
case "ArrowDown":
|
|
72
|
+
changeFocusedRow(1)
|
|
73
|
+
break
|
|
74
|
+
case "Delete":
|
|
75
|
+
case "Backspace":
|
|
76
|
+
deleteSelectedCell()
|
|
77
|
+
break
|
|
78
|
+
case "Enter":
|
|
79
|
+
focusCell()
|
|
80
|
+
break
|
|
81
|
+
default:
|
|
82
|
+
startEnteringValue(e.key, e.which)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Focuses the first cell in the grid
|
|
88
|
+
const focusFirstCell = () => {
|
|
89
|
+
const firstRow = $enrichedRows[0]
|
|
90
|
+
if (!firstRow) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
const firstColumn = $stickyColumn || $visibleColumns[0]
|
|
94
|
+
if (!firstColumn) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
focusedCellId.set(`${firstRow._id}-${firstColumn.name}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Changes the focused cell by moving it left or right to a different column
|
|
101
|
+
const changeFocusedColumn = delta => {
|
|
102
|
+
if (!$focusedCellId) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const cols = $visibleColumns
|
|
106
|
+
const split = $focusedCellId.split("-")
|
|
107
|
+
const columnName = split[1]
|
|
108
|
+
let newColumnName
|
|
109
|
+
if (columnName === $stickyColumn?.name) {
|
|
110
|
+
const index = delta - 1
|
|
111
|
+
newColumnName = cols[index]?.name
|
|
112
|
+
} else {
|
|
113
|
+
const index = cols.findIndex(col => col.name === columnName) + delta
|
|
114
|
+
if (index === -1) {
|
|
115
|
+
newColumnName = $stickyColumn?.name
|
|
116
|
+
} else {
|
|
117
|
+
newColumnName = cols[index]?.name
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (newColumnName) {
|
|
121
|
+
$focusedCellId = `${split[0]}-${newColumnName}`
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Changes the focused cell by moving it up or down to a new row
|
|
126
|
+
const changeFocusedRow = delta => {
|
|
127
|
+
if (!$focusedRow) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
const newRow = $enrichedRows[$focusedRow.__idx + delta]
|
|
131
|
+
if (newRow) {
|
|
132
|
+
const split = $focusedCellId.split("-")
|
|
133
|
+
$focusedCellId = `${newRow._id}-${split[1]}`
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Debounce to avoid holding down delete and spamming requests
|
|
138
|
+
const deleteSelectedCell = debounce(() => {
|
|
139
|
+
if ($focusedCellAPI?.isReadonly()) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
$focusedCellAPI.setValue(null)
|
|
143
|
+
}, 100)
|
|
144
|
+
|
|
145
|
+
// Focuses the current cell for editing
|
|
146
|
+
const focusCell = () => {
|
|
147
|
+
if ($focusedCellAPI?.isReadonly()) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
$focusedCellAPI?.focus?.()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Utils to identify a key code
|
|
154
|
+
const keyCodeIsNumber = keyCode => keyCode >= 48 && keyCode <= 57
|
|
155
|
+
const keyCodeIsLetter = keyCode => keyCode >= 65 && keyCode <= 90
|
|
156
|
+
|
|
157
|
+
// Focuses the cell and starts entering a new value
|
|
158
|
+
const startEnteringValue = (key, keyCode) => {
|
|
159
|
+
if ($focusedCellAPI) {
|
|
160
|
+
const type = $focusedCellAPI.getType()
|
|
161
|
+
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
|
162
|
+
$focusedCellAPI.setValue(parseInt(key))
|
|
163
|
+
$focusedCellAPI.focus()
|
|
164
|
+
} else if (
|
|
165
|
+
["string", "barcodeqr", "longform"].includes(type) &&
|
|
166
|
+
(keyCodeIsLetter(keyCode) || keyCodeIsNumber(keyCode))
|
|
167
|
+
) {
|
|
168
|
+
$focusedCellAPI.setValue(key)
|
|
169
|
+
$focusedCellAPI.focus()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onMount(() => {
|
|
175
|
+
document.addEventListener("keydown", handleKeyDown)
|
|
176
|
+
return () => {
|
|
177
|
+
document.removeEventListener("keydown", handleKeyDown)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
</script>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { clickOutside, Menu, MenuItem, notifications } from "@budibase/bbui"
|
|
3
|
+
import { getContext } from "svelte"
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
focusedRow,
|
|
7
|
+
menu,
|
|
8
|
+
rows,
|
|
9
|
+
columns,
|
|
10
|
+
focusedCellId,
|
|
11
|
+
stickyColumn,
|
|
12
|
+
config,
|
|
13
|
+
copiedCell,
|
|
14
|
+
clipboard,
|
|
15
|
+
dispatch,
|
|
16
|
+
} = getContext("grid")
|
|
17
|
+
|
|
18
|
+
$: style = makeStyle($menu)
|
|
19
|
+
|
|
20
|
+
const makeStyle = menu => {
|
|
21
|
+
return `left:${menu.left}px; top:${menu.top}px;`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const deleteRow = () => {
|
|
25
|
+
rows.actions.deleteRows([$focusedRow])
|
|
26
|
+
menu.actions.close()
|
|
27
|
+
notifications.success("Deleted 1 row")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const duplicate = async () => {
|
|
31
|
+
menu.actions.close()
|
|
32
|
+
const newRow = await rows.actions.duplicateRow($focusedRow)
|
|
33
|
+
if (newRow) {
|
|
34
|
+
const column = $stickyColumn?.name || $columns[0].name
|
|
35
|
+
$focusedCellId = `${newRow._id}-${column}`
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if $menu.visible}
|
|
41
|
+
<div class="menu" {style} use:clickOutside={() => menu.actions.close()}>
|
|
42
|
+
<Menu>
|
|
43
|
+
<MenuItem
|
|
44
|
+
icon="Copy"
|
|
45
|
+
on:click={clipboard.actions.copy}
|
|
46
|
+
on:click={menu.actions.close}
|
|
47
|
+
>
|
|
48
|
+
Copy
|
|
49
|
+
</MenuItem>
|
|
50
|
+
<MenuItem
|
|
51
|
+
icon="Paste"
|
|
52
|
+
disabled={$copiedCell == null}
|
|
53
|
+
on:click={clipboard.actions.paste}
|
|
54
|
+
on:click={menu.actions.close}
|
|
55
|
+
>
|
|
56
|
+
Paste
|
|
57
|
+
</MenuItem>
|
|
58
|
+
<MenuItem
|
|
59
|
+
icon="Maximize"
|
|
60
|
+
disabled={!$config.allowEditRows}
|
|
61
|
+
on:click={() => dispatch("edit-row", $focusedRow)}
|
|
62
|
+
on:click={menu.actions.close}
|
|
63
|
+
>
|
|
64
|
+
Edit row in modal
|
|
65
|
+
</MenuItem>
|
|
66
|
+
<MenuItem
|
|
67
|
+
icon="Duplicate"
|
|
68
|
+
disabled={!$config.allowAddRows}
|
|
69
|
+
on:click={duplicate}
|
|
70
|
+
>
|
|
71
|
+
Duplicate row
|
|
72
|
+
</MenuItem>
|
|
73
|
+
<MenuItem
|
|
74
|
+
icon="Delete"
|
|
75
|
+
disabled={!$config.allowDeleteRows}
|
|
76
|
+
on:click={deleteRow}
|
|
77
|
+
>
|
|
78
|
+
Delete row
|
|
79
|
+
</MenuItem>
|
|
80
|
+
</Menu>
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
<style>
|
|
85
|
+
.menu {
|
|
86
|
+
position: absolute;
|
|
87
|
+
background: var(--cell-background);
|
|
88
|
+
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
89
|
+
width: 180px;
|
|
90
|
+
border-radius: 4px;
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import GridScrollWrapper from "../layout/GridScrollWrapper.svelte"
|
|
4
|
+
import { DefaultRowHeight, GutterWidth } from "../lib/constants"
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
isReordering,
|
|
8
|
+
reorder,
|
|
9
|
+
visibleColumns,
|
|
10
|
+
stickyColumn,
|
|
11
|
+
rowHeight,
|
|
12
|
+
renderedRows,
|
|
13
|
+
scrollLeft,
|
|
14
|
+
} = getContext("grid")
|
|
15
|
+
|
|
16
|
+
$: targetColumn = $reorder.targetColumn
|
|
17
|
+
$: minLeft = GutterWidth + ($stickyColumn?.width || 0)
|
|
18
|
+
$: left = getLeft(targetColumn, $stickyColumn, $visibleColumns, $scrollLeft)
|
|
19
|
+
$: height = $rowHeight * $renderedRows.length + DefaultRowHeight
|
|
20
|
+
$: style = `left:${left}px; height:${height}px;`
|
|
21
|
+
$: visible = $isReordering && left >= minLeft
|
|
22
|
+
|
|
23
|
+
const getLeft = (targetColumn, stickyColumn, visibleColumns, scrollLeft) => {
|
|
24
|
+
let left = GutterWidth + (stickyColumn?.width || 0) - scrollLeft
|
|
25
|
+
|
|
26
|
+
// If this is not the sticky column, add additional left space
|
|
27
|
+
if (targetColumn !== stickyColumn?.name) {
|
|
28
|
+
const column = visibleColumns.find(x => x.name === targetColumn)
|
|
29
|
+
if (!column) {
|
|
30
|
+
return left
|
|
31
|
+
}
|
|
32
|
+
left += column.left + column.width
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return left
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
{#if visible}
|
|
40
|
+
<div class="reorder-wrapper">
|
|
41
|
+
<GridScrollWrapper scrollVertically>
|
|
42
|
+
<div class="reorder-overlay" {style} />
|
|
43
|
+
</GridScrollWrapper>
|
|
44
|
+
</div>
|
|
45
|
+
{/if}
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.reorder-wrapper {
|
|
49
|
+
position: absolute;
|
|
50
|
+
top: 0;
|
|
51
|
+
left: 0;
|
|
52
|
+
height: 100%;
|
|
53
|
+
width: 100%;
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
}
|
|
56
|
+
.reorder-overlay {
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 0;
|
|
59
|
+
width: 2px;
|
|
60
|
+
background: var(--spectrum-global-color-blue-400);
|
|
61
|
+
margin-left: -2px;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { GutterWidth } from "../lib/constants"
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
columns,
|
|
7
|
+
resize,
|
|
8
|
+
renderedColumns,
|
|
9
|
+
stickyColumn,
|
|
10
|
+
isReordering,
|
|
11
|
+
scrollLeft,
|
|
12
|
+
} = getContext("grid")
|
|
13
|
+
|
|
14
|
+
$: cutoff = $scrollLeft + GutterWidth + ($columns[0]?.width || 0)
|
|
15
|
+
$: offset = GutterWidth + ($stickyColumn?.width || 0)
|
|
16
|
+
$: activeColumn = $resize.column
|
|
17
|
+
|
|
18
|
+
const getStyle = (column, offset, scrollLeft) => {
|
|
19
|
+
const left = offset + column.left + column.width - scrollLeft
|
|
20
|
+
return `left:${left}px;`
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{#if !$isReordering}
|
|
25
|
+
{#if $stickyColumn}
|
|
26
|
+
<div
|
|
27
|
+
class="resize-slider"
|
|
28
|
+
class:visible={activeColumn === $stickyColumn.name}
|
|
29
|
+
on:mousedown={e => resize.actions.startResizing($stickyColumn, e)}
|
|
30
|
+
on:dblclick={() => resize.actions.resetSize($stickyColumn)}
|
|
31
|
+
style="left:{GutterWidth + $stickyColumn.width}px;"
|
|
32
|
+
>
|
|
33
|
+
<div class="resize-indicator" />
|
|
34
|
+
</div>
|
|
35
|
+
{/if}
|
|
36
|
+
{#each $renderedColumns as column}
|
|
37
|
+
<div
|
|
38
|
+
class="resize-slider"
|
|
39
|
+
class:visible={activeColumn === column.name}
|
|
40
|
+
on:mousedown={e => resize.actions.startResizing(column, e)}
|
|
41
|
+
on:dblclick={() => resize.actions.resetSize(column)}
|
|
42
|
+
style={getStyle(column, offset, $scrollLeft)}
|
|
43
|
+
>
|
|
44
|
+
<div class="resize-indicator" />
|
|
45
|
+
</div>
|
|
46
|
+
{/each}
|
|
47
|
+
{/if}
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.resize-slider {
|
|
51
|
+
position: absolute;
|
|
52
|
+
top: 0;
|
|
53
|
+
height: var(--default-row-height);
|
|
54
|
+
opacity: 0;
|
|
55
|
+
padding: 0 8px;
|
|
56
|
+
transform: translateX(-50%);
|
|
57
|
+
user-select: none;
|
|
58
|
+
}
|
|
59
|
+
.resize-slider:hover,
|
|
60
|
+
.resize-slider.visible {
|
|
61
|
+
cursor: col-resize;
|
|
62
|
+
opacity: 1;
|
|
63
|
+
}
|
|
64
|
+
.resize-indicator {
|
|
65
|
+
margin-left: -1px;
|
|
66
|
+
width: 2px;
|
|
67
|
+
height: 100%;
|
|
68
|
+
background: var(--spectrum-global-color-blue-400);
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from "svelte"
|
|
3
|
+
import { domDebounce } from "../../../utils/utils"
|
|
4
|
+
import { DefaultRowHeight, ScrollBarSize } from "../lib/constants"
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
scroll,
|
|
8
|
+
contentHeight,
|
|
9
|
+
maxScrollTop,
|
|
10
|
+
contentWidth,
|
|
11
|
+
maxScrollLeft,
|
|
12
|
+
screenWidth,
|
|
13
|
+
showHScrollbar,
|
|
14
|
+
showVScrollbar,
|
|
15
|
+
scrollLeft,
|
|
16
|
+
scrollTop,
|
|
17
|
+
height,
|
|
18
|
+
} = getContext("grid")
|
|
19
|
+
|
|
20
|
+
// State for dragging bars
|
|
21
|
+
let initialMouse
|
|
22
|
+
let initialScroll
|
|
23
|
+
|
|
24
|
+
// Calculate V scrollbar size and offset
|
|
25
|
+
// Terminology is the same for both axes:
|
|
26
|
+
// renderX - the space available to render the bar in, edge to edge
|
|
27
|
+
// availX - the space available to render the bar in, until the edge
|
|
28
|
+
$: renderHeight = $height - 2 * ScrollBarSize
|
|
29
|
+
$: barHeight = Math.max(50, ($height / $contentHeight) * renderHeight)
|
|
30
|
+
$: availHeight = renderHeight - barHeight
|
|
31
|
+
$: barTop =
|
|
32
|
+
ScrollBarSize +
|
|
33
|
+
DefaultRowHeight +
|
|
34
|
+
availHeight * ($scrollTop / $maxScrollTop)
|
|
35
|
+
|
|
36
|
+
// Calculate H scrollbar size and offset
|
|
37
|
+
$: renderWidth = $screenWidth - 2 * ScrollBarSize
|
|
38
|
+
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
|
|
39
|
+
$: availWidth = renderWidth - barWidth
|
|
40
|
+
$: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft)
|
|
41
|
+
|
|
42
|
+
// V scrollbar drag handlers
|
|
43
|
+
const startVDragging = e => {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
initialMouse = e.clientY
|
|
46
|
+
initialScroll = $scrollTop
|
|
47
|
+
document.addEventListener("mousemove", moveVDragging)
|
|
48
|
+
document.addEventListener("mouseup", stopVDragging)
|
|
49
|
+
}
|
|
50
|
+
const moveVDragging = domDebounce(e => {
|
|
51
|
+
const delta = e.clientY - initialMouse
|
|
52
|
+
const weight = delta / availHeight
|
|
53
|
+
const newScrollTop = initialScroll + weight * $maxScrollTop
|
|
54
|
+
scroll.update(state => ({
|
|
55
|
+
...state,
|
|
56
|
+
top: Math.max(0, Math.min(newScrollTop, $maxScrollTop)),
|
|
57
|
+
}))
|
|
58
|
+
})
|
|
59
|
+
const stopVDragging = () => {
|
|
60
|
+
document.removeEventListener("mousemove", moveVDragging)
|
|
61
|
+
document.removeEventListener("mouseup", stopVDragging)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// H scrollbar drag handlers
|
|
65
|
+
const startHDragging = e => {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
initialMouse = e.clientX
|
|
68
|
+
initialScroll = $scrollLeft
|
|
69
|
+
document.addEventListener("mousemove", moveHDragging)
|
|
70
|
+
document.addEventListener("mouseup", stopHDragging)
|
|
71
|
+
}
|
|
72
|
+
const moveHDragging = domDebounce(e => {
|
|
73
|
+
const delta = e.clientX - initialMouse
|
|
74
|
+
const weight = delta / availWidth
|
|
75
|
+
const newScrollLeft = initialScroll + weight * $maxScrollLeft
|
|
76
|
+
scroll.update(state => ({
|
|
77
|
+
...state,
|
|
78
|
+
left: Math.max(0, Math.min(newScrollLeft, $maxScrollLeft)),
|
|
79
|
+
}))
|
|
80
|
+
})
|
|
81
|
+
const stopHDragging = () => {
|
|
82
|
+
document.removeEventListener("mousemove", moveHDragging)
|
|
83
|
+
document.removeEventListener("mouseup", stopHDragging)
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
{#if $showVScrollbar}
|
|
88
|
+
<div
|
|
89
|
+
class="v-scrollbar"
|
|
90
|
+
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
|
91
|
+
on:mousedown={startVDragging}
|
|
92
|
+
/>
|
|
93
|
+
{/if}
|
|
94
|
+
{#if $showHScrollbar}
|
|
95
|
+
<div
|
|
96
|
+
class="h-scrollbar"
|
|
97
|
+
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
|
98
|
+
on:mousedown={startHDragging}
|
|
99
|
+
/>
|
|
100
|
+
{/if}
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
div {
|
|
104
|
+
position: absolute;
|
|
105
|
+
background: var(--spectrum-global-color-gray-500);
|
|
106
|
+
opacity: 0.7;
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
transition: opacity 130ms ease-out;
|
|
109
|
+
}
|
|
110
|
+
div:hover {
|
|
111
|
+
opacity: 1;
|
|
112
|
+
}
|
|
113
|
+
.v-scrollbar {
|
|
114
|
+
width: var(--size);
|
|
115
|
+
right: var(--size);
|
|
116
|
+
}
|
|
117
|
+
.h-scrollbar {
|
|
118
|
+
height: var(--size);
|
|
119
|
+
bottom: var(--size);
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { derived, writable } from "svelte/store"
|
|
2
|
+
|
|
3
|
+
export const createStores = () => {
|
|
4
|
+
const bounds = writable({
|
|
5
|
+
left: 0,
|
|
6
|
+
top: 0,
|
|
7
|
+
width: 0,
|
|
8
|
+
height: 0,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
// Derive height and width as primitives to avoid wasted computation
|
|
12
|
+
const width = derived(bounds, $bounds => $bounds.width, 0)
|
|
13
|
+
const height = derived(bounds, $bounds => $bounds.height, 0)
|
|
14
|
+
|
|
15
|
+
return { bounds, height, width }
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { writable, get } from "svelte/store"
|
|
2
|
+
|
|
3
|
+
export const createStores = () => {
|
|
4
|
+
const copiedCell = writable(null)
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
copiedCell,
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const deriveStores = context => {
|
|
12
|
+
const { copiedCell, focusedCellAPI } = context
|
|
13
|
+
|
|
14
|
+
const copy = () => {
|
|
15
|
+
copiedCell.set(get(focusedCellAPI)?.getValue())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const paste = () => {
|
|
19
|
+
const $copiedCell = get(copiedCell)
|
|
20
|
+
const $focusedCellAPI = get(focusedCellAPI)
|
|
21
|
+
if ($copiedCell != null && $focusedCellAPI) {
|
|
22
|
+
$focusedCellAPI.setValue($copiedCell)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
clipboard: {
|
|
28
|
+
actions: {
|
|
29
|
+
copy,
|
|
30
|
+
paste,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|