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.
@@ -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 Arrays from "tuff-core/arrays"
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 Array<Query>()
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 = this.state.dive.query_data?.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.find(q => q.id == id)
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.push(query)
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 (Arrays.deleteIf(this.queries, q => q.id == id) > 0) {
156
- this.queryTabs.removeTab(id)
157
- this.dirty()
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
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.51.2",
7
+ "version": "4.52.0",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
@@ -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
- // Defines a margin on the document that shifts when the last element is intersecting.
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.observer?.disconnect()
54
- if (this.observer) {
55
- this.observer.disconnect()
56
- } else {
57
- const builder = new IntersectionObserverBuilder(this.onIntersect.bind(this))
58
- builder.rootMargin(this.state.intersectRootMargin ?? '0px 0px 0px 0px')
59
- builder.threshold(this.state.intersectThreshold ?? 0.25)
60
- if (this.state.configureIntersect) {
61
- this.state.configureIntersect(builder, elem)
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 = builder.build()
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
- currentTab? : string
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, {state: 'enabled'}, tab)
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
- log.info(`Removing tab ${key}`, tab)
94
- delete this.tabs[key]
95
- this.removeChild(tab.part)
96
- this.state.currentTab = undefined
97
- this.dirty()
98
- }
99
- else {
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
- a.class('active')
161
- }
162
- if (tab.icon) {
163
- this.theme.renderIcon(a, tab.icon)
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
  }