terrier-engine 4.51.2 → 4.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data-dive/dives/dive-editor.ts +58 -50
- package/package.json +1 -1
- package/terrier/plugins/load-on-scroll-plugin.ts +24 -46
- package/terrier/tabs.ts +48 -31
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
import Schema, {SchemaDef} from "../../terrier/schema"
|
|
2
|
-
import {PartTag} from "tuff-core/parts"
|
|
1
|
+
import Schema, { SchemaDef } from "../../terrier/schema"
|
|
2
|
+
import { PartTag } from "tuff-core/parts"
|
|
3
3
|
import Dives from "./dives"
|
|
4
|
-
import Queries, {Query, QueryModelPicker} from "../queries/queries"
|
|
4
|
+
import Queries, { Query, QueryModelPicker } from "../queries/queries"
|
|
5
5
|
import QueryEditor from "../queries/query-editor"
|
|
6
|
-
import {Logger} from "tuff-core/logging"
|
|
6
|
+
import { Logger } from "tuff-core/logging"
|
|
7
7
|
import QueryForm from "../queries/query-form"
|
|
8
|
-
import {TabContainerPart} from "../../terrier/tabs"
|
|
8
|
+
import { TabContainerPart } from "../../terrier/tabs"
|
|
9
9
|
import ContentPart from "../../terrier/parts/content-part"
|
|
10
|
-
import {ModalPart} from "../../terrier/modals"
|
|
11
|
-
import {DdDive} from "../gen/models"
|
|
10
|
+
import { ModalPart } from "../../terrier/modals"
|
|
11
|
+
import { DdDive } from "../gen/models"
|
|
12
12
|
import Ids from "../../terrier/ids"
|
|
13
13
|
import Db from "../dd-db"
|
|
14
14
|
import DdSession from "../dd-session"
|
|
15
|
-
import {DiveRunModal} from "./dive-runs"
|
|
15
|
+
import { DiveRunModal } from "./dive-runs"
|
|
16
16
|
import Nav from "tuff-core/nav"
|
|
17
17
|
import Messages from "tuff-core/messages"
|
|
18
|
-
import
|
|
19
|
-
import {FormFields} from "tuff-core/forms"
|
|
18
|
+
import { FormFields } from "tuff-core/forms"
|
|
20
19
|
import Fragments from "../../terrier/fragments"
|
|
21
|
-
import {DiveDeliveryPanel} from "./dive-delivery"
|
|
20
|
+
import { DiveDeliveryPanel } from "./dive-delivery"
|
|
22
21
|
import DivePlotList from "../plots/dive-plot-list"
|
|
23
|
-
import {DivePage} from "./dive-page"
|
|
22
|
+
import { DivePage } from "./dive-page"
|
|
24
23
|
|
|
25
24
|
const log = new Logger("DiveEditor")
|
|
26
25
|
|
|
@@ -49,11 +48,13 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
49
48
|
|
|
50
49
|
static readonly diveChangedKey = Messages.untypedKey()
|
|
51
50
|
|
|
52
|
-
queries = new
|
|
51
|
+
queries = new Map<string, Query>()
|
|
52
|
+
// Array of map keys to give the queries a sort order.
|
|
53
|
+
queryOrder = new Array<string>()
|
|
53
54
|
|
|
54
55
|
async init() {
|
|
55
|
-
this.queryTabs = this.makePart(TabContainerPart, {side: 'top'})
|
|
56
|
-
this.settingsTabs = this.makePart(TabContainerPart, {side: 'top'})
|
|
56
|
+
this.queryTabs = this.makePart(TabContainerPart, { side: 'top', reorderable: true })
|
|
57
|
+
this.settingsTabs = this.makePart(TabContainerPart, { side: 'top' })
|
|
57
58
|
|
|
58
59
|
this.queryTabs.addBeforeAction({
|
|
59
60
|
title: 'Queries:',
|
|
@@ -63,40 +64,47 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
63
64
|
title: "Add Another Query",
|
|
64
65
|
classes: ['dd-hint', 'arrow-right', 'glyp-hint'],
|
|
65
66
|
tooltip: "Each query represents a separate tab in the resulting spreadsheet",
|
|
66
|
-
click: {key: this.newQueryKey}
|
|
67
|
+
click: { key: this.newQueryKey }
|
|
67
68
|
})
|
|
68
69
|
this.queryTabs.addAfterAction({
|
|
69
70
|
icon: 'glyp-copy',
|
|
70
71
|
classes: ['duplicate-query'],
|
|
71
72
|
tooltip: "Duplicate this query",
|
|
72
|
-
click: {key: this.duplicateQueryKey}
|
|
73
|
+
click: { key: this.duplicateQueryKey }
|
|
73
74
|
})
|
|
74
75
|
this.queryTabs.addAfterAction({
|
|
75
76
|
icon: 'glyp-plus_outline',
|
|
76
77
|
classes: ['new-query'],
|
|
77
78
|
tooltip: "Add a new query to this Dive",
|
|
78
|
-
click: {key: this.newQueryKey}
|
|
79
|
+
click: { key: this.newQueryKey }
|
|
79
80
|
})
|
|
80
81
|
|
|
81
|
-
this.queries =
|
|
82
|
-
for (const query of this.queries) {
|
|
82
|
+
this.queries = new Map()
|
|
83
|
+
for (const query of this.state.dive.query_data?.queries || []) {
|
|
84
|
+
this.queries.set(query.id, query)
|
|
83
85
|
this.addQueryTab(query)
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
this.listenMessage(QueryForm.settingsChangedKey, m => {
|
|
87
89
|
const query = m.data
|
|
88
90
|
log.info(`Query settings changed`, query)
|
|
89
|
-
this.queryTabs.updateTab({key: query.id, title: query.name})
|
|
91
|
+
this.queryTabs.updateTab({ key: query.id, title: query.name })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Reorder queries in the list when the tab sort order is updated.
|
|
95
|
+
this.listenMessage(this.queryTabs.tabReorderedKey, m => {
|
|
96
|
+
const { newOrder } = m.data
|
|
97
|
+
this.queryOrder = newOrder
|
|
90
98
|
})
|
|
91
99
|
|
|
92
100
|
this.onClick(this.newQueryKey, _ => {
|
|
93
|
-
this.app.showModal(NewQueryModal, {editor: this as DiveEditor, schema: this.state.schema})
|
|
101
|
+
this.app.showModal(NewQueryModal, { editor: this as DiveEditor, schema: this.state.schema })
|
|
94
102
|
})
|
|
95
103
|
|
|
96
104
|
this.onClick(this.duplicateQueryKey, _ => {
|
|
97
105
|
const id = this.queryTabs.currentTagKey
|
|
98
106
|
if (id?.length) {
|
|
99
|
-
const query = this.queries.
|
|
107
|
+
const query = this.queries.get(id)
|
|
100
108
|
if (query) {
|
|
101
109
|
this.app.showModal(DuplicateQueryModal, {
|
|
102
110
|
editor: this as DiveEditor,
|
|
@@ -114,7 +122,7 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
114
122
|
})
|
|
115
123
|
|
|
116
124
|
this.onClick(DiveEditor.deleteQueryKey, m => {
|
|
117
|
-
this.app.confirm({title: "Delete Query", icon: 'glyp-delete', body: "Are you sure you want to delete this query?"}, () => {
|
|
125
|
+
this.app.confirm({ title: "Delete Query", icon: 'glyp-delete', body: "Are you sure you want to delete this query?" }, () => {
|
|
118
126
|
this.deleteQuery(m.data.id)
|
|
119
127
|
})
|
|
120
128
|
})
|
|
@@ -137,7 +145,7 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
137
145
|
* @param query
|
|
138
146
|
*/
|
|
139
147
|
addQuery(query: Query) {
|
|
140
|
-
this.queries.
|
|
148
|
+
this.queries.set(query.id, query)
|
|
141
149
|
this.addQueryTab(query)
|
|
142
150
|
}
|
|
143
151
|
|
|
@@ -146,16 +154,17 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
146
154
|
* @param query
|
|
147
155
|
*/
|
|
148
156
|
private addQueryTab(query: Query) {
|
|
149
|
-
const state = {...this.state, query}
|
|
150
|
-
this.queryTabs.upsertTab({key: query.id, title: query.name}, QueryEditor, state)
|
|
157
|
+
const state = { ...this.state, query }
|
|
158
|
+
this.queryTabs.upsertTab({ key: query.id, title: query.name }, QueryEditor, state)
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
deleteQuery(id: string) {
|
|
154
162
|
log.info(`Deleting query ${id}`)
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
163
|
+
if (!this.queries.has(id)) return
|
|
164
|
+
|
|
165
|
+
this.queries.delete(id)
|
|
166
|
+
this.queryTabs.removeTab(id)
|
|
167
|
+
this.dirty()
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
get parentClasses(): Array<string> {
|
|
@@ -178,7 +187,7 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
178
187
|
|
|
179
188
|
return {
|
|
180
189
|
...this.state.dive,
|
|
181
|
-
query_data: {queries}
|
|
190
|
+
query_data: { queries: this.queryOrder.map(id => queries.get(id)!) }
|
|
182
191
|
}
|
|
183
192
|
}
|
|
184
193
|
|
|
@@ -189,7 +198,7 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
189
198
|
// Editor Page
|
|
190
199
|
////////////////////////////////////////////////////////////////////////////////
|
|
191
200
|
|
|
192
|
-
export class DiveEditorPage extends DivePage<{id: string}> {
|
|
201
|
+
export class DiveEditorPage extends DivePage<{ id: string }> {
|
|
193
202
|
|
|
194
203
|
editor!: DiveEditor
|
|
195
204
|
session!: DdSession
|
|
@@ -205,7 +214,7 @@ export class DiveEditorPage extends DivePage<{id: string}> {
|
|
|
205
214
|
const schema = await Schema.get()
|
|
206
215
|
this.session = await DdSession.get()
|
|
207
216
|
const dive = await Dives.get(this.state.id)
|
|
208
|
-
this.editor = this.makePart(DiveEditor, {schema, dive, session: this.session})
|
|
217
|
+
this.editor = this.makePart(DiveEditor, { schema, dive, session: this.session })
|
|
209
218
|
|
|
210
219
|
this.mainContentWidth = 'wide'
|
|
211
220
|
|
|
@@ -240,20 +249,20 @@ export class DiveEditorPage extends DivePage<{id: string}> {
|
|
|
240
249
|
title: 'Discard',
|
|
241
250
|
icon: 'glyp-cancelled',
|
|
242
251
|
classes: ['discard-dive-action'],
|
|
243
|
-
click: {key: this.discardKey}
|
|
252
|
+
click: { key: this.discardKey }
|
|
244
253
|
}, 'tertiary')
|
|
245
254
|
|
|
246
255
|
this.addAction({
|
|
247
256
|
title: 'Save',
|
|
248
257
|
icon: 'glyp-complete',
|
|
249
258
|
classes: ['save-dive-action'],
|
|
250
|
-
click: {key: this.saveKey}
|
|
259
|
+
click: { key: this.saveKey }
|
|
251
260
|
}, 'tertiary')
|
|
252
261
|
|
|
253
262
|
this.addAction({
|
|
254
263
|
title: 'Run',
|
|
255
264
|
icon: 'glyp-play',
|
|
256
|
-
click: {key: this.runKey}
|
|
265
|
+
click: { key: this.runKey }
|
|
257
266
|
}, 'tertiary')
|
|
258
267
|
|
|
259
268
|
this.onClick(this.discardKey, _ => {
|
|
@@ -275,7 +284,7 @@ export class DiveEditorPage extends DivePage<{id: string}> {
|
|
|
275
284
|
this.listenMessage(DiveEditor.diveChangedKey, _ => {
|
|
276
285
|
log.info("Dive changed")
|
|
277
286
|
this.element?.classList.add('changed')
|
|
278
|
-
}, {attach: 'passive'})
|
|
287
|
+
}, { attach: 'passive' })
|
|
279
288
|
|
|
280
289
|
this.dirty()
|
|
281
290
|
}
|
|
@@ -303,7 +312,7 @@ export class DiveEditorPage extends DivePage<{id: string}> {
|
|
|
303
312
|
|
|
304
313
|
async run() {
|
|
305
314
|
const dive = await this.editor.serialize()
|
|
306
|
-
this.app.showModal(DiveRunModal, {dive})
|
|
315
|
+
this.app.showModal(DiveRunModal, { dive })
|
|
307
316
|
}
|
|
308
317
|
}
|
|
309
318
|
|
|
@@ -324,8 +333,8 @@ class NewQueryModal extends ModalPart<NewQueryState> {
|
|
|
324
333
|
modelPicker!: QueryModelPicker
|
|
325
334
|
|
|
326
335
|
async init() {
|
|
327
|
-
this.settingsForm = this.makePart(QueryForm, {query: {id: 'new', name: '', notes: ''}})
|
|
328
|
-
this.modelPicker = this.makePart(QueryModelPicker, {schema: this.state.schema})
|
|
336
|
+
this.settingsForm = this.makePart(QueryForm, { query: { id: 'new', name: '', notes: '' } })
|
|
337
|
+
this.modelPicker = this.makePart(QueryModelPicker, { schema: this.state.schema })
|
|
329
338
|
|
|
330
339
|
this.setIcon('glyp-data_dive_query')
|
|
331
340
|
this.setTitle("New Query")
|
|
@@ -333,7 +342,7 @@ class NewQueryModal extends ModalPart<NewQueryState> {
|
|
|
333
342
|
this.addAction({
|
|
334
343
|
title: "Add",
|
|
335
344
|
icon: 'glyp-plus',
|
|
336
|
-
click: {key: this.addKey}
|
|
345
|
+
click: { key: this.addKey }
|
|
337
346
|
})
|
|
338
347
|
|
|
339
348
|
this.onClick(this.addKey, async _ => {
|
|
@@ -361,16 +370,16 @@ class NewQueryModal extends ModalPart<NewQueryState> {
|
|
|
361
370
|
log.info(`Saving new query`)
|
|
362
371
|
const settings = await this.settingsForm.fields.serialize()
|
|
363
372
|
if (!settings.name?.length) {
|
|
364
|
-
this.showToast("Please enter a query name", {color: 'alert'})
|
|
373
|
+
this.showToast("Please enter a query name", { color: 'alert' })
|
|
365
374
|
this.dirty()
|
|
366
375
|
return
|
|
367
376
|
}
|
|
368
377
|
const model = this.modelPicker.model
|
|
369
378
|
if (!model) {
|
|
370
|
-
this.showToast("Please select a model", {color: 'alert'})
|
|
379
|
+
this.showToast("Please select a model", { color: 'alert' })
|
|
371
380
|
return
|
|
372
381
|
}
|
|
373
|
-
const query = {...settings, id: Ids.makeUuid(), from: {model: model.name}}
|
|
382
|
+
const query = { ...settings, id: Ids.makeUuid(), from: { model: model.name } }
|
|
374
383
|
this.state.editor.addQuery(query)
|
|
375
384
|
this.state.editor.queryTabs.showTab(query.id)
|
|
376
385
|
this.pop()
|
|
@@ -398,13 +407,13 @@ class DuplicateQueryModal extends ModalPart<DuplicateQueryState> {
|
|
|
398
407
|
this.setIcon('glyp-data_dive_query')
|
|
399
408
|
this.setTitle("Duplicate Query")
|
|
400
409
|
|
|
401
|
-
const newQuery = {...this.state.query, name: `${this.state.query.name} Copy`}
|
|
410
|
+
const newQuery = { ...this.state.query, name: `${this.state.query.name} Copy` }
|
|
402
411
|
this.fields = new FormFields<Query>(this, newQuery)
|
|
403
412
|
|
|
404
413
|
this.addAction({
|
|
405
414
|
title: "Duplicate",
|
|
406
415
|
icon: 'glyp-checkmark',
|
|
407
|
-
click: {key: this.dupKey}
|
|
416
|
+
click: { key: this.dupKey }
|
|
408
417
|
})
|
|
409
418
|
|
|
410
419
|
this.onClick(this.dupKey, async _ => {
|
|
@@ -414,7 +423,7 @@ class DuplicateQueryModal extends ModalPart<DuplicateQueryState> {
|
|
|
414
423
|
|
|
415
424
|
async save() {
|
|
416
425
|
const newName = this.fields.data.name
|
|
417
|
-
const query = {...Queries.duplicate(this.state.query), name: newName}
|
|
426
|
+
const query = { ...Queries.duplicate(this.state.query), name: newName }
|
|
418
427
|
this.state.editor.addQuery(query)
|
|
419
428
|
this.state.editor.queryTabs.showTab(query.id)
|
|
420
429
|
this.pop()
|
|
@@ -428,5 +437,4 @@ class DuplicateQueryModal extends ModalPart<DuplicateQueryState> {
|
|
|
428
437
|
this.fields.textInput(col, 'name')
|
|
429
438
|
})
|
|
430
439
|
}
|
|
431
|
-
}
|
|
432
|
-
|
|
440
|
+
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,12 @@ const log = new Logger('LoadOnScrollPlugin')
|
|
|
6
6
|
|
|
7
7
|
type MarginUnit = '%' | 'px'
|
|
8
8
|
type MarginLength = `${number}${MarginUnit}`
|
|
9
|
-
type MarginString = `${MarginLength} ${MarginLength} ${MarginLength} ${MarginLength}`
|
|
9
|
+
type MarginString = MarginLength | `${MarginLength} ${MarginLength}` | `${MarginLength} ${MarginLength} ${MarginLength} ${MarginLength}`
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Define the behavior of the load on scroll functionality.
|
|
13
|
+
* For information about the intersect parameters, see https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
|
14
|
+
*/
|
|
10
15
|
export type LoadOnScrollOptions<TState> = {
|
|
11
16
|
// The name of the collection to load on scroll
|
|
12
17
|
collectionName: string
|
|
@@ -14,15 +19,14 @@ export type LoadOnScrollOptions<TState> = {
|
|
|
14
19
|
collectionPartType: PartConstructor<Part<TState>, TState>
|
|
15
20
|
// Called to load the next state. If undefined is returned, no more states will be loaded
|
|
16
21
|
loadNextStates: (existingStates: TState[]) => Promise<TState[] | undefined>
|
|
22
|
+
// The root element whose viewport is scrolling. If not provided, the document viewport is used.
|
|
23
|
+
// Usually not required, but if using intersectThreshold on a scrollable element, might be necessary.
|
|
24
|
+
intersectRootSelector: string
|
|
17
25
|
// Percentage of the last item in the collection that must be visible before the next state is loaded.
|
|
18
|
-
// If not provided, a default value of 25% is used. Must be between 0 and 1
|
|
19
|
-
// See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#threshold
|
|
26
|
+
// If not provided, a default value of 25% is used. Must be between 0 and 1.
|
|
20
27
|
intersectThreshold?: number
|
|
21
|
-
//
|
|
22
|
-
// See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#rootmargin
|
|
28
|
+
// Expands the intersection region by the specified margin.
|
|
23
29
|
intersectRootMargin?: MarginString
|
|
24
|
-
// Configure the intersection observer
|
|
25
|
-
configureIntersect?: (builder: IntersectionObserverBuilder, partElem: HTMLElement) => any
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -50,17 +54,20 @@ export default class LoadOnScrollPlugin<TState> extends PartPlugin<LoadOnScrollO
|
|
|
50
54
|
return
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.state.
|
|
57
|
+
const intersectRoot = (this.state.intersectRootSelector) ? elem.querySelector(this.state.intersectRootSelector) : null
|
|
58
|
+
|
|
59
|
+
// We may have re-rendered since the last update, in which case the intersectionRoot will be a new element.
|
|
60
|
+
// In that case, recreate observer with the new root.
|
|
61
|
+
if (!this.observer || this.observer.root !== intersectRoot) {
|
|
62
|
+
const config: IntersectionObserverInit = {
|
|
63
|
+
root: intersectRoot,
|
|
64
|
+
rootMargin: this.state.intersectRootMargin ?? '0px 0px 0px 0px',
|
|
65
|
+
threshold: this.state.intersectThreshold ?? 0.25,
|
|
62
66
|
}
|
|
63
|
-
this.observer
|
|
67
|
+
this.observer?.disconnect()
|
|
68
|
+
this.observer = new IntersectionObserver(this.onIntersect.bind(this), config)
|
|
69
|
+
} else {
|
|
70
|
+
this.observer.disconnect()
|
|
64
71
|
}
|
|
65
72
|
this.observer.observe(lastElement)
|
|
66
73
|
}
|
|
@@ -88,33 +95,4 @@ export default class LoadOnScrollPlugin<TState> extends PartPlugin<LoadOnScrollO
|
|
|
88
95
|
this.isLoading = false
|
|
89
96
|
this.part.stale()
|
|
90
97
|
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export class IntersectionObserverBuilder {
|
|
94
|
-
private readonly callback: IntersectionObserverCallback
|
|
95
|
-
private readonly config: IntersectionObserverInit
|
|
96
|
-
|
|
97
|
-
constructor(callback: IntersectionObserverCallback) {
|
|
98
|
-
this.callback = callback
|
|
99
|
-
this.config = {}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
root(root: Element | Document | undefined): IntersectionObserverBuilder {
|
|
103
|
-
this.config.root = root
|
|
104
|
-
return this
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
rootMargin(margin: MarginString): IntersectionObserverBuilder {
|
|
108
|
-
this.config.rootMargin = margin
|
|
109
|
-
return this
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
threshold(threshold: number): IntersectionObserverBuilder {
|
|
113
|
-
this.config.threshold = threshold
|
|
114
|
-
return this
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
build() : IntersectionObserver {
|
|
118
|
-
return new IntersectionObserver(this.callback, this.config)
|
|
119
|
-
}
|
|
120
98
|
}
|
package/terrier/tabs.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {Logger} from "tuff-core/logging"
|
|
1
|
+
import { Logger } from "tuff-core/logging"
|
|
2
2
|
import Messages from "tuff-core/messages"
|
|
3
|
-
import {Part, PartParent, PartTag, StatelessPart} from "tuff-core/parts"
|
|
3
|
+
import { Part, PartParent, PartTag, StatelessPart } from "tuff-core/parts"
|
|
4
4
|
import TerrierPart from "./parts/terrier-part"
|
|
5
|
-
import {Action, IconName, Packet} from "./theme"
|
|
5
|
+
import { Action, IconName, Packet } from "./theme"
|
|
6
|
+
import SortablePlugin from "tuff-sortable/sortable-plugin"
|
|
6
7
|
|
|
7
8
|
const log = new Logger("Tabs")
|
|
8
9
|
|
|
@@ -31,7 +32,8 @@ export type TabSide = typeof Sides[number]
|
|
|
31
32
|
|
|
32
33
|
export type TabContainerState = {
|
|
33
34
|
side: TabSide
|
|
34
|
-
|
|
35
|
+
reorderable?: boolean
|
|
36
|
+
currentTab?: string
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export class TabContainerPart extends TerrierPart<TabContainerState> {
|
|
@@ -39,8 +41,11 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
|
|
|
39
41
|
private tabs = {} as Record<string, TabDefinition>
|
|
40
42
|
changeTabKey = Messages.typedKey<{ tabKey: string }>()
|
|
41
43
|
changeSideKey = Messages.typedKey<{ side: TabSide }>()
|
|
44
|
+
tabReorderedKey = Messages.typedKey<{ newOrder: string[] }>()
|
|
42
45
|
|
|
43
46
|
async init() {
|
|
47
|
+
Object.assign(this.state, { reorderable: false }, this.state)
|
|
48
|
+
|
|
44
49
|
this.onClick(this.changeTabKey, m => {
|
|
45
50
|
log.info(`Clicked on tab ${m.data.tabKey}`)
|
|
46
51
|
this.showTab(m.data.tabKey)
|
|
@@ -51,6 +56,23 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
|
|
|
51
56
|
this.state.side = m.data.side
|
|
52
57
|
this.dirty()
|
|
53
58
|
})
|
|
59
|
+
|
|
60
|
+
if (this.state.reorderable) {
|
|
61
|
+
this.makePlugin(SortablePlugin, {
|
|
62
|
+
zoneClass: 'tt-tab-list',
|
|
63
|
+
targetClass: 'tab',
|
|
64
|
+
onSorted: (_, evt) => {
|
|
65
|
+
this.renumberTabs(evt.toChildren)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
renumberTabs(tabElementsMaybe?: HTMLElement[]) {
|
|
72
|
+
const tabElements = tabElementsMaybe ??
|
|
73
|
+
Array.from(this.element?.querySelectorAll('.tt-tab-list') ?? [])
|
|
74
|
+
const newOrder = tabElements.map(tabElement => tabElement.dataset.key)
|
|
75
|
+
this.emitMessage(this.tabReorderedKey, { newOrder })
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
/**
|
|
@@ -65,7 +87,10 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
|
|
|
65
87
|
state: InferredPartStateType
|
|
66
88
|
): PartType {
|
|
67
89
|
const existingTab = this.tabs[tab.key] ?? {}
|
|
68
|
-
this.tabs[tab.key] = Object.assign(existingTab, {
|
|
90
|
+
this.tabs[tab.key] = Object.assign(existingTab, {
|
|
91
|
+
state: 'enabled',
|
|
92
|
+
}, tab)
|
|
93
|
+
this.renumberTabs()
|
|
69
94
|
const part = this.makePart(constructor, state)
|
|
70
95
|
existingTab.part = part
|
|
71
96
|
this.dirty()
|
|
@@ -89,16 +114,14 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
|
|
|
89
114
|
*/
|
|
90
115
|
removeTab(key: string) {
|
|
91
116
|
const tab = this.tabs[key]
|
|
92
|
-
if (tab) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
log.warn(`No tab ${key} to remove!`)
|
|
101
|
-
}
|
|
117
|
+
if (!tab) return log.warn(`No tab ${key} to remove!`)
|
|
118
|
+
|
|
119
|
+
log.info(`Removing tab ${key}`, tab)
|
|
120
|
+
delete this.tabs[key]
|
|
121
|
+
this.renumberTabs()
|
|
122
|
+
this.removeChild(tab.part)
|
|
123
|
+
this.state.currentTab = undefined
|
|
124
|
+
this.dirty()
|
|
102
125
|
}
|
|
103
126
|
|
|
104
127
|
/**
|
|
@@ -149,38 +172,32 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
|
|
|
149
172
|
parent.div('tt-tab-container', this.state.side, container => {
|
|
150
173
|
container.div('.tt-flex.tt-tab-list', tabList => {
|
|
151
174
|
if (this._beforeActions.length) {
|
|
152
|
-
this.theme.renderActions(tabList, this._beforeActions, {defaultClass: 'action'})
|
|
175
|
+
this.theme.renderActions(tabList, this._beforeActions, { defaultClass: 'action' })
|
|
153
176
|
}
|
|
154
177
|
for (const tab of Object.values(this.tabs)) {
|
|
155
178
|
if (tab.state == 'hidden') continue
|
|
156
179
|
|
|
157
180
|
tabList.a('.tab', a => {
|
|
181
|
+
a.attrs({ draggable: this.state.reorderable })
|
|
182
|
+
a.data({ key: tab.key })
|
|
158
183
|
a.class(tab.state || 'enabled')
|
|
159
|
-
if (tab.key === currentTabKey)
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
a.span({text: tab.title})
|
|
166
|
-
a.emitClick(this.changeTabKey, {tabKey: tab.key})
|
|
167
|
-
if (tab.click) {
|
|
168
|
-
a.emitClick(tab.click.key, tab.click.data || {})
|
|
169
|
-
}
|
|
184
|
+
if (tab.key === currentTabKey) a.class('active')
|
|
185
|
+
if (tab.icon) this.theme.renderIcon(a, tab.icon)
|
|
186
|
+
a.span({ text: tab.title })
|
|
187
|
+
a.emitClick(this.changeTabKey, { tabKey: tab.key })
|
|
188
|
+
if (tab.click) a.emitClick(tab.click.key, tab.click.data || {})
|
|
170
189
|
})
|
|
171
190
|
}
|
|
172
191
|
if (this._afterActions.length) {
|
|
173
192
|
tabList.div('.spacer')
|
|
174
|
-
this.theme.renderActions(tabList, this._afterActions, {defaultClass: 'action'})
|
|
193
|
+
this.theme.renderActions(tabList, this._afterActions, { defaultClass: 'action' })
|
|
175
194
|
}
|
|
176
195
|
})
|
|
177
196
|
|
|
178
197
|
if (currentTabKey) {
|
|
179
198
|
const currentTab = this.tabs[currentTabKey]
|
|
180
199
|
container.div('.tt-tab-content', panel => {
|
|
181
|
-
if (currentTab.classes?.length)
|
|
182
|
-
panel.class(...currentTab.classes)
|
|
183
|
-
}
|
|
200
|
+
if (currentTab.classes?.length) panel.class(...currentTab.classes)
|
|
184
201
|
panel.part(currentTab.part as StatelessPart)
|
|
185
202
|
})
|
|
186
203
|
}
|