terrier-engine 4.24.2 → 4.25.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.
@@ -186,7 +186,6 @@ export class DiveListPart extends TerrierPart<DiveListState> {
186
186
  }
187
187
 
188
188
  renderDiveSchedule(row: PartTag, dive: DdDive) {
189
- log.info(`Rendering dive schedule: ${dive.delivery_schedule?.schedule_type}`, dive.delivery_schedule)
190
189
  if (!dive.delivery_schedule || dive.delivery_schedule.schedule_type == 'none') {
191
190
  return
192
191
  }
@@ -194,7 +193,7 @@ export class DiveListPart extends TerrierPart<DiveListState> {
194
193
  if (dive.delivery_recipients?.length) {
195
194
  description += ` to:<br>${dive.delivery_recipients.join('<br>')}`
196
195
  }
197
- row.div(".glyp-setup.with-icon").text(inflection.titleize(dive.delivery_schedule.schedule_type))
196
+ row.div(".glyp-setup.with-icon.tt-flex.align-center").text(inflection.titleize(dive.delivery_schedule.schedule_type))
198
197
  .data({tooltip: description})
199
198
  }
200
199
 
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.24.2",
7
+ "version": "4.25.0",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
@@ -0,0 +1,182 @@
1
+ import Html, {DivTag, HtmlParentTag} from "tuff-core/html"
2
+ import Messages from "tuff-core/messages"
3
+ import {PartPlugin} from "tuff-core/plugins"
4
+ import Theme, {IconName} from "./theme"
5
+ import PagePart from "./parts/page-part"
6
+ import TerrierPart from "./parts/terrier-part"
7
+
8
+ export type Hint = {
9
+ title?: string
10
+ icon?: IconName
11
+ tooltip?: string
12
+ }
13
+
14
+ export type HintRenderOptions = {
15
+ classes?: string[]
16
+ side?: 'inline' | 'top' | 'right' | 'bottom' | 'left' | 'inline-top' | 'inline-right' | 'inline-bottom' | 'inline-left'
17
+ hideIcon?: boolean
18
+ }
19
+
20
+ /**
21
+ * Render a hint into the given tag
22
+ * @param theme the theme to render the hint icon with
23
+ * @param parent the parent tag to render the hint into
24
+ * @param hint the hint to render
25
+ * @param options options to modify how the hint is rendered
26
+ */
27
+ function renderHint(theme: Theme, parent: HtmlParentTag, hint: Hint, options?: HintRenderOptions): DivTag {
28
+ const hintTag = parent.div('.tt-hint')
29
+
30
+ if (hint.tooltip) hintTag.dataAttr('tooltip', hint.tooltip)
31
+ if (options?.classes) hintTag.class(...options.classes)
32
+ if (options?.side && options.side != 'inline') hintTag.dataAttr('tt-hint-side', options.side)
33
+
34
+ const hideIcon = options?.hideIcon ?? false
35
+ if (!hideIcon) {
36
+ const icon = hint.icon ?? 'glyp-hint'
37
+ theme.renderIcon(hintTag, icon)
38
+ }
39
+
40
+ if (hint.title) hintTag.span('.tt-hint-title').text(hint.title)
41
+
42
+ return hintTag
43
+ }
44
+
45
+ /**
46
+ * Add a hint element to the given parent element
47
+ * @param theme the theme to render the hint icon with
48
+ * @param parentElement the element to add the hint element to
49
+ * @param hint the hint to add
50
+ * @param options options to modify how the hint is rendered
51
+ * @param insertPosition specify where to insert the hint relative to the parent element
52
+ */
53
+ function injectHint(theme: Theme, parentElement: Element, hint: Hint, options?: HintRenderOptions, insertPosition: InsertPosition = 'beforeend'): HTMLDivElement {
54
+ const elem = Html.createElement('div', div => renderHint(theme, div, hint, options)).firstElementChild as HTMLDivElement
55
+ parentElement.insertAdjacentElement(insertPosition, elem)
56
+ return elem
57
+ }
58
+
59
+ export type DynamicHint = {
60
+ selector: string
61
+ hint: Hint
62
+ options?: HintRenderOptions
63
+ insertPosition?: InsertPosition
64
+ onlyFirstMatch?: boolean // only add a hint to the first element that matches the selector
65
+ }
66
+
67
+ /**
68
+ * Adds dynamic hints to a given part. Dynamic hints will detect changes to the DOM to update where they are rendered
69
+ * @param part the part to add dynamic hints to. Dynamic hints will only be added to matching elements under the part's root element
70
+ * @param hints the hints to add.
71
+ */
72
+ function addDynamicHints(part: TerrierPart<any>, hints: DynamicHint[]) {
73
+ part.makePlugin(DynamicHintsPlugin, hints)
74
+ }
75
+
76
+ class DynamicHintsPlugin extends PartPlugin<DynamicHint[]> {
77
+ observer: MutationObserver = new MutationObserver(this.handleMutations.bind(this))
78
+
79
+ async init() {
80
+ if (!('theme' in this.part)) throw new Error("DynamicHintsPlugin requires a TerrierPart")
81
+ }
82
+
83
+ update(elem: HTMLElement) {
84
+ super.update(elem)
85
+
86
+ this.addDynamicHints(elem)
87
+ this.observer.disconnect()
88
+ this.observer.observe(elem, { childList: true, subtree: true })
89
+ }
90
+
91
+ private handleMutations(mutations: MutationRecord[], _observer: MutationObserver): void {
92
+ // Ignore mutations that added hint elements (prevents infinite recursion)
93
+ const addedHints = mutations.some(m =>
94
+ Array.from(m.addedNodes).some(n => n instanceof HTMLElement && n.dataset.hintSource === 'DynamicHintsPlugin')
95
+ )
96
+ if (addedHints) return
97
+ if (!this.part.element) return
98
+ this.addDynamicHints(this.part.element)
99
+ }
100
+
101
+ private addDynamicHints(elem: HTMLElement) {
102
+ const theme = (this.part as TerrierPart<any>).theme
103
+
104
+ elem.querySelectorAll('.tt-hint[data-hint-source=DynamicHintsPlugin]').forEach(e => e.remove())
105
+
106
+ for (const dynamicHint of this.state) {
107
+ const matches = elem.querySelectorAll(dynamicHint.selector)
108
+ if (!matches.length) continue
109
+ if (dynamicHint.onlyFirstMatch) {
110
+ this.addDynamicHint(theme, matches[0], dynamicHint)
111
+ } else {
112
+ matches.forEach(match => this.addDynamicHint(theme, match, dynamicHint))
113
+ }
114
+ }
115
+ }
116
+
117
+ private addDynamicHint(theme: Theme, elem: Element, hint: DynamicHint) {
118
+ const hintElement = injectHint(theme, elem, hint.hint, hint.options, hint.insertPosition ?? 'beforeend')
119
+ hintElement.dataset.hintSource = 'DynamicHintsPlugin'
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Adds a hints toggle checkbox to a page's toolbar and handles persisting the state of the checkbox between visits.
125
+ * @param part the page to add a checkbox to
126
+ * @param hintKey a key for this page so that the checkbox state can be persisted
127
+ */
128
+ function addHintToggle(part: PagePart<any>, hintKey: string) {
129
+ part.makePlugin(HintTogglePlugin, { hintKey })
130
+ }
131
+
132
+ class HintTogglePlugin extends PartPlugin<{ hintKey: string }> {
133
+
134
+ checkboxChangedKey = Messages.untypedKey()
135
+ storageKey = `show-hints-${this.state.hintKey}`
136
+
137
+ get storedValue() {
138
+ const stored = localStorage.getItem(this.storageKey)
139
+ return stored == null ? true : stored == 'true'
140
+ }
141
+
142
+ set storedValue(value: boolean) {
143
+ localStorage.setItem(this.storageKey, value.toString())
144
+ }
145
+
146
+ async init() {
147
+ document.body.classList.toggle('tt-hints-hidden', !this.storedValue)
148
+
149
+ const part = this.part as PagePart<any>
150
+ part.addToolbarInput('show-hints', 'checkbox', {
151
+ title: "Hints",
152
+ icon: 'glyp-hint',
153
+ onChangeKey: this.checkboxChangedKey,
154
+ onInputKey: this.checkboxChangedKey,
155
+ })
156
+
157
+ part.onInput(this.checkboxChangedKey, m => {
158
+ const checked = (m.event.target as HTMLInputElement).checked
159
+ this.storedValue = checked
160
+ document.body.classList.toggle('tt-hints-hidden', !checked)
161
+ })
162
+ }
163
+
164
+ update(elem: HTMLElement) {
165
+ super.update(elem)
166
+
167
+ const checkbox = elem.querySelector(`[data-toolbar-field-name=show-hints]`)
168
+
169
+ if (checkbox instanceof HTMLInputElement) {
170
+ checkbox.checked = this.storedValue
171
+ }
172
+ }
173
+ }
174
+
175
+ const Hints = {
176
+ renderHint,
177
+ injectHint,
178
+ addDynamicHints,
179
+ addHintToggle,
180
+ }
181
+
182
+ export default Hints
@@ -4,6 +4,7 @@ import {PartTag} from "tuff-core/parts"
4
4
  import {optionsForSelect, SelectOptions} from "tuff-core/forms"
5
5
  import {UntypedKey} from "tuff-core/messages"
6
6
  import {Logger} from "tuff-core/logging"
7
+ import {InputTagAttrs, SelectTagAttrs} from "tuff-core/html"
7
8
 
8
9
  const log = new Logger("Terrier PagePart")
9
10
 
@@ -35,10 +36,10 @@ type ToolbarFieldDefOptions = {
35
36
  icon?: IconName
36
37
  }
37
38
 
38
- type ToolbarSelectDef = { type: 'select', options: SelectOptions } & BaseFieldDef
39
+ type ToolbarSelectDef = { type: 'select', options: SelectOptions, attrs?: SelectTagAttrs } & BaseFieldDef
39
40
 
40
41
  type ValuedInputType = 'text' | 'color' | 'date' | 'datetime-local' | 'email' | 'hidden' | 'month' | 'number' | 'password' | 'search' | 'tel' | 'time' | 'url' | 'week' | 'checkbox'
41
- type ToolbarValuedInputDef = { type: ValuedInputType } & BaseFieldDef
42
+ type ToolbarValuedInputDef = { type: ValuedInputType, attrs?: InputTagAttrs } & BaseFieldDef
42
43
 
43
44
  /**
44
45
  * Defines a field to be rendered in the page's toolbar
@@ -90,7 +91,7 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
90
91
  * @param selectOptions an array of select options
91
92
  * @param opts
92
93
  */
93
- addToolbarSelect(name: string, selectOptions: SelectOptions, opts?: ToolbarFieldDefOptions) {
94
+ addToolbarSelect(name: string, selectOptions: SelectOptions, opts?: ToolbarFieldDefOptions & { attrs?: SelectTagAttrs }) {
94
95
  this.addToolbarFieldDef({ type: 'select', name, options: selectOptions, ...opts })
95
96
  }
96
97
 
@@ -100,7 +101,7 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
100
101
  * @param type the type attribute of the input field
101
102
  * @param opts
102
103
  */
103
- addToolbarInput(name: string, type: ValuedInputType, opts?: ToolbarFieldDefOptions) {
104
+ addToolbarInput(name: string, type: ValuedInputType, opts?: ToolbarFieldDefOptions & { attrs?: InputTagAttrs }) {
104
105
  this.addToolbarFieldDef({ type, name, ...opts })
105
106
  }
106
107
 
@@ -203,6 +204,10 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
203
204
  })
204
205
  if (def.onChangeKey) select.emitChange(def.onChangeKey)
205
206
  if (def.onInputKey) select.emitInput(def.onInputKey)
207
+
208
+ if (def.attrs) select.attrs(def.attrs)
209
+
210
+ select.dataAttr('toolbar-field-name', name)
206
211
  } else {
207
212
  const input = label.input({name: def.name, type: def.type, value: def.defaultValue})
208
213
  if (def.type == 'checkbox' && def.defaultValue?.length) {
@@ -210,8 +215,11 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
210
215
  }
211
216
  if (def.onChangeKey) input.emitChange(def.onChangeKey)
212
217
  if (def.onInputKey) input.emitInput(def.onInputKey)
213
- }
214
218
 
219
+ if (def.attrs) input.attrs(def.attrs)
220
+
221
+ input.dataAttr('toolbar-field-name', name)
222
+ }
215
223
 
216
224
  if (def.tooltip?.length) label.dataAttr('tooltip', def.tooltip)
217
225
  })
package/terrier/theme.ts CHANGED
@@ -2,6 +2,7 @@ import {PartTag} from "tuff-core/parts"
2
2
  import {GlypName} from "./glyps"
3
3
  import HubIcons, {HubIconName} from "./gen/hub-icons"
4
4
  import {Key} from "tuff-core/messages"
5
+ import Hints, {Hint, HintRenderOptions} from "./hints";
5
6
 
6
7
  export interface ThemeType {
7
8
  readonly icons: string
@@ -38,6 +39,10 @@ export type Action = {
38
39
  classes?: string[]
39
40
  click?: Packet
40
41
  badge?: string
42
+ hint?: {
43
+ hint: Hint
44
+ options?: HintRenderOptions
45
+ }
41
46
  }
42
47
 
43
48
  /**
@@ -115,6 +120,10 @@ export default class Theme {
115
120
  const badgeColor = options?.badgeColor || 'alert'
116
121
  a.div(`.badge.${badgeColor}`, {text: action.badge})
117
122
  }
123
+
124
+ if (action.hint) {
125
+ Hints.renderHint(this, a, action.hint.hint, action.hint.options)
126
+ }
118
127
  })
119
128
  }
120
129
  }