terrier-engine 4.9.1 → 4.10.3

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.9.1",
7
+ "version": "4.10.3",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
@@ -1,23 +1,166 @@
1
1
  import TerrierPart from "./parts/terrier-part"
2
- import {PartTag} from "tuff-core/parts"
2
+ import {Part, PartConstructor, PartTag, StatelessPart} from "tuff-core/parts"
3
3
  import Messages from "tuff-core/messages"
4
4
  import {Logger} from "tuff-core/logging"
5
- import Html from "tuff-core/html"
5
+ import {PageBreakpoints} from "./parts/page-part"
6
6
 
7
7
  const log = new Logger('List Viewer')
8
+ Logger.level = 'debug'
9
+
10
+ /**
11
+ * Optional values to return from rendering a list item that control
12
+ * its rendering behavior.
13
+ */
14
+ export type ListItemRenderOptions = {
15
+ style?: 'panel' | 'header'
16
+ clickable?: boolean
17
+ }
18
+
19
+ /**
20
+ * All list items should have an `id` value so that we can distinguish them.
21
+ */
22
+ export type ListItem = {id: string}
23
+
24
+ /**
25
+ * One of these gets created for each item in the list.
26
+ */
27
+ class ListItemPart<T extends ListItem> extends Part<T> {
28
+
29
+ viewer!: ListViewerPart<T>
30
+
31
+ render(parent: PartTag) {
32
+ const isCurrent = this.viewer.detailsContext?.id == this.state.id
33
+
34
+ parent.div('.tt-list-viewer-item', itemView => {
35
+ const opts = this.viewer.renderListItem(itemView, this.state)
36
+ const style = opts?.style || 'panel'
37
+ itemView.class(style)
38
+ if (opts?.clickable) {
39
+ itemView.class('clickable')
40
+ itemView.emitClick(this.viewer.itemClickedKey, {id: this.state.id})
41
+ }
42
+ if (isCurrent) {
43
+ itemView.class('current')
44
+ }
45
+ }).id(`item-${this.state.id}`)
46
+
47
+ // render the details if this is the current item and it's supposed to be rendered inline
48
+ if (isCurrent && this.viewer.layout == 'inline') {
49
+ log.debug(`ListItemPart: rendering inline item details`)
50
+ if (this.viewer.currentDetailsPart) {
51
+ parent.part(this.viewer.currentDetailsPart)
52
+ }
53
+ }
54
+ }
55
+
56
+ }
57
+
58
+ /**
59
+ * Allows ListViewerPart subclasses to either directly render content for
60
+ * the list details or make a part to do it.
61
+ */
62
+ export class ListViewerDetailsContext<T extends ListItem> {
63
+
64
+ /**
65
+ * The item id for which this context is representing the details.
66
+ */
67
+ get id(): string {
68
+ return this.item.id
69
+ }
70
+
71
+ /**
72
+ * The part created by `makePart()` to render the details.
73
+ */
74
+ part?: StatelessPart
75
+
76
+ /**
77
+ * The part representing this item in the list.
78
+ */
79
+ itemPart?: ListItemPart<T>
80
+
81
+ constructor(readonly viewer: ListViewerPart<T>, readonly item: T) {
82
+ }
83
+
84
+ /**
85
+ * Make a part to render the details for the list item.
86
+ * @param partType
87
+ * @param state
88
+ */
89
+ makePart<PartType extends Part<StateType>, StateType>(partType: PartConstructor<PartType, StateType>, state: StateType) {
90
+ if (this.viewer.layout == 'side') {
91
+ this.part = this.viewer.sideContainerPart.makePart(partType, state)
92
+ }
93
+ else {
94
+ this.part = this.viewer.currentItemPart?.makePart(partType, state)
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Ensure that the rendered part is disposed and the related item part is marked dirty.
100
+ */
101
+ clear() {
102
+ if (this.part) {
103
+ this.viewer.removeChild(this.part)
104
+ this.part = undefined
105
+ }
106
+ if (this.itemPart) {
107
+ this.itemPart.dirty()
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * This part sits permanently on the side and will render the details if
114
+ * the viewer's detailsLocation = 'side'.
115
+ */
116
+ class SideContainerPart extends Part<{viewer: ListViewerPart<any>}> {
117
+
118
+ viewer!: ListViewerPart<any>
119
+
120
+ async init() {
121
+ this.viewer = this.state.viewer
122
+ }
123
+
124
+ get parentClasses(): Array<string> {
125
+ return ['tt-list-viewer-side-details']
126
+ }
127
+
128
+ render(parent: PartTag) {
129
+ if (this.viewer.currentDetailsPart) {
130
+ log.debug(`[SideContainerPart] Rendering details part`, this.viewer.currentDetailsPart)
131
+ parent.part(this.viewer.currentDetailsPart)
132
+ }
133
+ else {
134
+ log.debug(`[SideContainerPart] No details part to render`)
135
+ }
136
+ }
137
+
138
+ }
8
139
 
9
- const detailsSelector = '.tt-list-viewer-details'
10
140
 
11
141
  /**
12
142
  * Part for viewing a list of items and the details associated with them.
13
143
  * Each item must have an `id` so that they can be distinguished.
14
144
  */
15
- export abstract class ListViewerPart<T extends {id: string}> extends TerrierPart<any> {
145
+ export abstract class ListViewerPart<T extends ListItem> extends TerrierPart<any> {
146
+
147
+ sideContainerPart!: SideContainerPart
148
+
149
+ detailsContext?: ListViewerDetailsContext<T>
150
+ layout: 'inline' | 'side' = 'side'
151
+
152
+ get currentDetailsPart(): StatelessPart | undefined {
153
+ return this.detailsContext?.part
154
+ }
155
+
156
+ get currentItemPart(): StatelessPart | undefined {
157
+ return this.detailsContext?.itemPart
158
+ }
16
159
 
17
160
  items: T[] = []
18
- itemMap: Record<string, T> = {}
161
+ itemPartMap: Record<string, ListItemPart<T>> = {}
19
162
 
20
- itemClickedKey = Messages.typedKey<{id: string}>()
163
+ itemClickedKey = Messages.typedKey<ListItem>()
21
164
 
22
165
  /**
23
166
  * A message with this key gets emitted whenever the details are shown.
@@ -27,10 +170,12 @@ export abstract class ListViewerPart<T extends {id: string}> extends TerrierPart
27
170
  async init() {
28
171
  await super.init()
29
172
 
173
+ this.sideContainerPart = this.makePart(SideContainerPart, {viewer: this})
174
+
30
175
  await this.reload()
31
176
 
32
177
  this.onClick(this.itemClickedKey, m => {
33
- log.info(`Clicked on list item ${m.data.id}`, m)
178
+ log.debug(`Clicked on list item ${m.data.id}`, m)
34
179
  this.showDetails(m.data.id)
35
180
  })
36
181
  }
@@ -38,13 +183,36 @@ export abstract class ListViewerPart<T extends {id: string}> extends TerrierPart
38
183
 
39
184
  /// Fetching
40
185
 
186
+ /**
187
+ * Subclasses must implement this to provide a list of items to render.
188
+ */
41
189
  abstract fetchItems(): Promise<T[]>
42
190
 
191
+ /**
192
+ * Fetches the items with `fetchItems()` and re-renders the list.
193
+ */
43
194
  async reload() {
44
195
  this.items = await this.fetchItems()
45
- this.items.forEach((item) => {
46
- this.itemMap[item.id] = item
196
+ this.assignCollection('items', ListItemPart, this.items)
197
+ this.getCollectionParts('items').forEach(itemPart => {
198
+ // HACK
199
+ (itemPart as ListItemPart<T>).viewer = this
200
+ // log.debug(`Adding item part ${itemPart.state.id}`, itemPart)
201
+ this.itemPartMap[itemPart.state.id] = (itemPart as ListItemPart<T>)
47
202
  })
203
+ this.relayout()
204
+ }
205
+
206
+ /**
207
+ * Determine whether the details should be shown inline with the list or
208
+ * off to the side, based on screen size.
209
+ */
210
+ relayout() {
211
+ if (window.innerWidth > PageBreakpoints.phone) {
212
+ this.layout = 'side'
213
+ } else {
214
+ this.layout = 'inline'
215
+ }
48
216
  this.dirty()
49
217
  }
50
218
 
@@ -56,91 +224,57 @@ export abstract class ListViewerPart<T extends {id: string}> extends TerrierPart
56
224
  }
57
225
 
58
226
  render(parent: PartTag): any {
227
+ log.debug(`Rendering the viewer`)
59
228
  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)
229
+ this.renderCollection(list, 'items')
68
230
  })
231
+ if (this.layout == 'side') {
232
+ log.debug(`Rendering sideContainerPart inside the viewer`)
233
+ parent.part(this.sideContainerPart)
234
+ }
69
235
  }
70
- abstract renderListItem(parent: PartTag, item: T): any
71
-
72
- abstract renderItemDetail(parent: PartTag, item: T): any
73
236
 
237
+ /**
238
+ * Subclasses must override this to render the content of an item.
239
+ * @param parent
240
+ * @param item
241
+ */
242
+ abstract renderListItem(parent: PartTag, item: T): ListItemRenderOptions | void
74
243
 
75
- // Details
244
+ /**
245
+ * Subclasses must implement this to render an item details or provide a part to do so.
246
+ * @param context
247
+ */
248
+ abstract renderDetails(context: ListViewerDetailsContext<T>): any
76
249
 
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
250
 
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
- }
251
+ // Details
90
252
 
91
253
  /**
92
254
  * Show the details view for the item with the given id
93
255
  * @param id
94
256
  */
95
257
  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
- }
258
+ this.detailsContext?.clear()
117
259
 
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
- }
260
+ const itemPart = this.itemPartMap[id]
261
+ if (!itemPart) {
262
+ log.debug(`${Object.keys(this.itemPartMap).length} item parts: `, this.itemPartMap)
263
+ throw `No item part ${id}`
140
264
  }
141
- else {
142
- log.warn(`No item view for ${id}`)
265
+
266
+ this.detailsContext = new ListViewerDetailsContext(this, itemPart.state)
267
+ this.detailsContext.itemPart = itemPart
268
+ this.renderDetails(this.detailsContext)
269
+ if (this.layout == 'side') {
270
+ this.sideContainerPart.dirty()
143
271
  }
272
+ itemPart.dirty()
273
+
274
+ // let the world know
275
+ this.emitMessage(this.detailsShownKey, {id})
276
+
144
277
  }
145
278
 
279
+
146
280
  }
@@ -12,6 +12,16 @@ const log = new Logger("Terrier PagePart")
12
12
  */
13
13
  export type ContentWidth = "normal" | "wide" | "fill"
14
14
 
15
+ /**
16
+ * The width breakpoints for various device classes used by the styles.
17
+ */
18
+ export const PageBreakpoints = {
19
+ phone: 550,
20
+ mobile: 850,
21
+ tablet: 1050,
22
+ fixed: 320
23
+ }
24
+
15
25
  /// Toolbar fields
16
26
 
17
27
  type BaseFieldDef = { name: string } & ToolbarFieldDefOptions