terrier-engine 4.8.11 → 4.9.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/package.json +1 -1
- package/terrier/list-viewer.ts +146 -0
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import TerrierPart from "./parts/terrier-part"
|
|
2
|
+
import {PartTag} from "tuff-core/parts"
|
|
3
|
+
import Messages from "tuff-core/messages"
|
|
4
|
+
import {Logger} from "tuff-core/logging"
|
|
5
|
+
import Html from "tuff-core/html"
|
|
6
|
+
|
|
7
|
+
const log = new Logger('List Viewer')
|
|
8
|
+
|
|
9
|
+
const detailsSelector = '.tt-list-viewer-details'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Part for viewing a list of items and the details associated with them.
|
|
13
|
+
* Each item must have an `id` so that they can be distinguished.
|
|
14
|
+
*/
|
|
15
|
+
export abstract class ListViewerPart<T extends {id: string}> extends TerrierPart<any> {
|
|
16
|
+
|
|
17
|
+
items: T[] = []
|
|
18
|
+
itemMap: Record<string, T> = {}
|
|
19
|
+
|
|
20
|
+
itemClickedKey = Messages.typedKey<{id: string}>()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A message with this key gets emitted whenever the details are shown.
|
|
24
|
+
*/
|
|
25
|
+
detailsShownKey = Messages.typedKey<{ id: string }>()
|
|
26
|
+
|
|
27
|
+
async init() {
|
|
28
|
+
await super.init()
|
|
29
|
+
|
|
30
|
+
await this.reload()
|
|
31
|
+
|
|
32
|
+
this.onClick(this.itemClickedKey, m => {
|
|
33
|
+
log.info(`Clicked on list item ${m.data.id}`, m)
|
|
34
|
+
this.showDetails(m.data.id)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/// Fetching
|
|
40
|
+
|
|
41
|
+
abstract fetchItems(): Promise<T[]>
|
|
42
|
+
|
|
43
|
+
async reload() {
|
|
44
|
+
this.items = await this.fetchItems()
|
|
45
|
+
this.items.forEach((item) => {
|
|
46
|
+
this.itemMap[item.id] = item
|
|
47
|
+
})
|
|
48
|
+
this.dirty()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/// Rendering
|
|
53
|
+
|
|
54
|
+
get parentClasses(): Array<string> {
|
|
55
|
+
return ['tt-list-viewer']
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render(parent: PartTag): any {
|
|
59
|
+
parent.div('.tt-list-viewer-list', list => {
|
|
60
|
+
for (const item of this.items) {
|
|
61
|
+
list.a('.tt-list-viewer-item', {id: `item-${item.id}`}, itemView => {
|
|
62
|
+
this.renderListItem(itemView, item)
|
|
63
|
+
}).emitClick(this.itemClickedKey, {id: item.id})
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
parent.div('.tt-list-viewer-details-container', detailsView => {
|
|
67
|
+
detailsView.div(detailsSelector)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
abstract renderListItem(parent: PartTag, item: T): any
|
|
71
|
+
|
|
72
|
+
abstract renderItemDetail(parent: PartTag, item: T): any
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Details
|
|
76
|
+
|
|
77
|
+
private setCurrent(id: string) {
|
|
78
|
+
// clear any existing current item
|
|
79
|
+
const existingCurrents = this.element!.querySelectorAll('.tt-list-viewer-list a.current')
|
|
80
|
+
existingCurrents.forEach((elem) => {
|
|
81
|
+
elem.classList.remove('current')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// add .current to the new item
|
|
85
|
+
const itemView = this.element!.querySelector(`#item-${id}`)
|
|
86
|
+
if (itemView) {
|
|
87
|
+
itemView.classList.add('current')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Show the details view for the item with the given id
|
|
93
|
+
* @param id
|
|
94
|
+
*/
|
|
95
|
+
showDetails(id: string) {
|
|
96
|
+
this.setCurrent(id)
|
|
97
|
+
const item = this.itemMap[id]
|
|
98
|
+
if (!item) {
|
|
99
|
+
throw `No item ${id}`
|
|
100
|
+
}
|
|
101
|
+
const container = this.element!.querySelector(detailsSelector)
|
|
102
|
+
if (container) {
|
|
103
|
+
// render the details
|
|
104
|
+
const detailsView = Html.createElement('div', div => {
|
|
105
|
+
this.renderItemDetail(div, item)
|
|
106
|
+
})
|
|
107
|
+
container.innerHTML = detailsView.innerHTML
|
|
108
|
+
this.arrangeDetails(id, container as HTMLElement)
|
|
109
|
+
|
|
110
|
+
// let the world know
|
|
111
|
+
this.emitMessage(this.detailsShownKey, {id})
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
log.warn(`Tried to show item ${id} but there was no ${detailsSelector}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* If necessary, move the details next to the item
|
|
120
|
+
* @param id the item id
|
|
121
|
+
* @param detailsView
|
|
122
|
+
*/
|
|
123
|
+
arrangeDetails(id: string, detailsView: HTMLElement) {
|
|
124
|
+
const itemView = this.element!.querySelector(`#item-${id}`)
|
|
125
|
+
if (itemView) {
|
|
126
|
+
// const listView = itemIVew.parentElement
|
|
127
|
+
log.info(`Item is ${itemView.clientWidth} wide and the window is ${window.innerWidth} wide`)
|
|
128
|
+
// crude but effective way to determine if the list is collapsed due to the media breakpoint
|
|
129
|
+
if (itemView.clientWidth > window.innerWidth * 0.8) {
|
|
130
|
+
// move the details to right after the list item
|
|
131
|
+
itemView.after(detailsView)
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// move the details back to the container
|
|
135
|
+
const detailsContainer = this.element!.querySelector(`.tt-list-viewer-details-container`)
|
|
136
|
+
if (detailsContainer) {
|
|
137
|
+
detailsContainer.append(detailsView)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
log.warn(`No item view for ${id}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
}
|