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.
- package/data-dive/dives/dive-list.ts +1 -2
- package/package.json +1 -1
- package/terrier/hints.ts +182 -0
- package/terrier/parts/page-part.ts +13 -5
- package/terrier/theme.ts +9 -0
|
@@ -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
package/terrier/hints.ts
ADDED
|
@@ -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
|
}
|