terrier-engine 4.0.3 → 4.0.5
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/app.ts +81 -0
- package/db-client.ts +3 -3
- package/dropdowns.ts +96 -0
- package/fragments.ts +391 -0
- package/lightbox.ts +95 -0
- package/loading.ts +70 -0
- package/modals.ts +154 -0
- package/overlays.ts +234 -0
- package/package.json +3 -2
- package/parts.ts +439 -0
- package/theme.ts +89 -0
- package/toasts.ts +77 -0
- package/tooltips.ts +65 -0
package/app.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Logger } from "tuff-core/logging"
|
|
2
|
+
import {Part, PartParent} from "tuff-core/parts"
|
|
3
|
+
import {TerrierPart} from "./parts"
|
|
4
|
+
import Tooltips from "./tooltips"
|
|
5
|
+
import Lightbox from "./lightbox"
|
|
6
|
+
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import logoUrl from './images/optimized/terrier-hub-logo-light.svg'
|
|
9
|
+
import Theme, {ThemeType} from "./theme"
|
|
10
|
+
import {ModalPart, ModalStackPart} from "./modals"
|
|
11
|
+
import {OverlayLayer, OverlayPart} from "./overlays"
|
|
12
|
+
|
|
13
|
+
const log = new Logger('App')
|
|
14
|
+
Logger.level = 'info'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Main application part that renders the entire page.
|
|
18
|
+
*/
|
|
19
|
+
export abstract class TerrierApp<TT extends ThemeType> extends TerrierPart<{theme: Theme<TT>}, TT> {
|
|
20
|
+
|
|
21
|
+
_theme!: Theme<TT>
|
|
22
|
+
|
|
23
|
+
get theme(): Theme<TT> {
|
|
24
|
+
return this._theme
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
overlayPart!: OverlayPart
|
|
28
|
+
|
|
29
|
+
async init() {
|
|
30
|
+
this._theme = this.state.theme
|
|
31
|
+
this.overlayPart = this.makePart(OverlayPart, {})
|
|
32
|
+
log.info("Initialized")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
load() {
|
|
36
|
+
// clear all overlays (i.e. dropdowns) whenever the page changes
|
|
37
|
+
this.clearOverlays()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
update(root: HTMLElement) {
|
|
41
|
+
log.info(`Update`, root)
|
|
42
|
+
Tooltips.init(root)
|
|
43
|
+
Lightbox.init(root, this, 'body-content')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/// Overlays
|
|
48
|
+
|
|
49
|
+
makeOverlay<OverlayType extends Part<StateType>, StateType>(
|
|
50
|
+
constructor: { new(p: PartParent, id: string, state: StateType): OverlayType; },
|
|
51
|
+
state: StateType,
|
|
52
|
+
layer: OverlayLayer): OverlayType {
|
|
53
|
+
return this.overlayPart.makeLayer(constructor, state, layer)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
clearOverlay(layer: OverlayLayer) {
|
|
57
|
+
this.overlayPart.clearLayer(layer)
|
|
58
|
+
this.lastDropdownTarget = undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clearOverlays() {
|
|
62
|
+
this.overlayPart.clearAll()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
lastDropdownTarget?: HTMLElement
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/// Modals
|
|
70
|
+
|
|
71
|
+
showModal<ModalType extends ModalPart<StateType, TT>, StateType>(constructor: { new(p: PartParent, id: string, state: StateType): ModalType; }, state: StateType): ModalType {
|
|
72
|
+
const modalStack =
|
|
73
|
+
(this.overlayPart.parts.modal as ModalStackPart<TT>)
|
|
74
|
+
?? this.makeOverlay(ModalStackPart, {}, 'modal')
|
|
75
|
+
const modal = modalStack.pushModal(constructor, state)
|
|
76
|
+
modalStack.dirty()
|
|
77
|
+
return modal
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
}
|
package/db-client.ts
CHANGED
|
@@ -197,7 +197,7 @@ export default class DbClient<PM extends ModelTypeMap, UM extends ModelTypeMap,
|
|
|
197
197
|
* @param id the id of the record
|
|
198
198
|
* @param includes relations to include in the returned object
|
|
199
199
|
*/
|
|
200
|
-
async find<T extends keyof PM & string>(modelType: T, id: string, includes?: I
|
|
200
|
+
async find<T extends keyof PM & string>(modelType: T, id: string, includes?: Includes<PM,T,I>): Promise<PM[T]> {
|
|
201
201
|
const query = new ModelQuery<PM,T,I>(modelType).where("id = ?", id)
|
|
202
202
|
if (includes) {
|
|
203
203
|
query.includes(includes)
|
|
@@ -218,9 +218,9 @@ export default class DbClient<PM extends ModelTypeMap, UM extends ModelTypeMap,
|
|
|
218
218
|
* @param idOrSlug the id or slug of the record
|
|
219
219
|
* @param includes relations to include in the returned object
|
|
220
220
|
*/
|
|
221
|
-
async findByIdOrSlug<T extends keyof PM & string>(modelType: T, idOrSlug: string, includes?: I
|
|
221
|
+
async findByIdOrSlug<T extends keyof PM & string>(modelType: T, idOrSlug: string, includes?: Includes<PM, T, I>): Promise<PM[T]> {
|
|
222
222
|
const column = isUuid(idOrSlug) ? "id" : "slug"
|
|
223
|
-
const query = new ModelQuery(modelType).where(`${column} = ?`, idOrSlug)
|
|
223
|
+
const query = new ModelQuery<PM, T, I>(modelType).where(`${column} = ?`, idOrSlug)
|
|
224
224
|
if (includes) {
|
|
225
225
|
query.includes(includes)
|
|
226
226
|
}
|
package/dropdowns.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Logger } from "tuff-core/logging"
|
|
2
|
+
import { untypedKey } from "tuff-core/messages"
|
|
3
|
+
import { unique } from "tuff-core/arrays"
|
|
4
|
+
import {PartTag, StatelessPart} from "tuff-core/parts"
|
|
5
|
+
import Overlays from "./overlays"
|
|
6
|
+
import {TerrierPart} from "./parts"
|
|
7
|
+
import Objects from "tuff-core/objects"
|
|
8
|
+
import {Action, ThemeType} from "./theme"
|
|
9
|
+
|
|
10
|
+
const log = new Logger('Dropdowns')
|
|
11
|
+
|
|
12
|
+
const clearDropdownKey = untypedKey()
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Abstract base class for dropdown parts.
|
|
16
|
+
* Subclasses must implement the `renderContent()` method to render the dropdown content.
|
|
17
|
+
*/
|
|
18
|
+
export abstract class Dropdown<T, TT extends ThemeType> extends TerrierPart<T, TT> {
|
|
19
|
+
|
|
20
|
+
parentPart?: StatelessPart
|
|
21
|
+
|
|
22
|
+
// the computed absolute position of the
|
|
23
|
+
left = 0
|
|
24
|
+
top = 0
|
|
25
|
+
|
|
26
|
+
async init() {
|
|
27
|
+
this.onClick(clearDropdownKey, _ => {
|
|
28
|
+
this.clear()
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Removes itself from the DOM.
|
|
34
|
+
*/
|
|
35
|
+
clear() {
|
|
36
|
+
log.info("Clearing dropdown")
|
|
37
|
+
this.app.clearOverlay('dropdown')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
render(parent: PartTag) {
|
|
41
|
+
parent.div('.dropdown-content', content => {
|
|
42
|
+
this.renderContent(content)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Subclasses must implement this to render the actual dropdown content.
|
|
48
|
+
* @param parent
|
|
49
|
+
*/
|
|
50
|
+
abstract renderContent(parent: PartTag): void
|
|
51
|
+
|
|
52
|
+
anchorTarget?: HTMLElement
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Store the anchor target in order to compute the dropdown position when it's rendered.
|
|
56
|
+
* @param target
|
|
57
|
+
*/
|
|
58
|
+
anchor(target: HTMLElement) {
|
|
59
|
+
log.info(`Anchoring dropdown to target`, target)
|
|
60
|
+
this.anchorTarget = target
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
update(_elem: HTMLElement) {
|
|
64
|
+
const content = _elem.querySelector('.dropdown-content')
|
|
65
|
+
if (this.anchorTarget && content) {
|
|
66
|
+
log.info(`Anchoring dropdown`, content, this.anchorTarget)
|
|
67
|
+
Overlays.anchorElement(content as HTMLElement, this.anchorTarget)
|
|
68
|
+
content.classList.add('show')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A concrete dropdown part that shows a list of actions.
|
|
76
|
+
*/
|
|
77
|
+
export class ActionsDropdown<TT extends ThemeType> extends Dropdown<Array<Action<TT>>, TT> {
|
|
78
|
+
renderContent(parent: PartTag) {
|
|
79
|
+
// handle each key declared on the actions directly,
|
|
80
|
+
// then clear the dropdown and re-emit them on the parent part
|
|
81
|
+
const keys = unique(this.state.map(action => action.click?.key).filter(Objects.notNull))
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
this.onClick(key, m => {
|
|
84
|
+
this.app.clearOverlay('dropdown')
|
|
85
|
+
log.info(`Re-emitting ${key.id} message`, m, this.parentPart)
|
|
86
|
+
if (this.parentPart) {
|
|
87
|
+
this.parentPart.emit('click', key, m.event, m.data)
|
|
88
|
+
} else {
|
|
89
|
+
this.emit('click', key, m.event, m.data)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
this.theme.renderActions(parent, this.state, {iconColor: 'white'})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
}
|
package/fragments.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import {PartTag} from "tuff-core/parts"
|
|
2
|
+
import {AnchorTagAttrs, HtmlParentTag} from "tuff-core/html"
|
|
3
|
+
import Theme, {Action, Packet, ThemeType} from "./theme"
|
|
4
|
+
import {ActionLevel, PanelActions} from "./parts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base class for Panel and Card fragment builders.
|
|
8
|
+
*/
|
|
9
|
+
abstract class ContentFragment<TT extends ThemeType> {
|
|
10
|
+
protected constructor(readonly prefix: string, readonly theme: Theme<TT>) {
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
protected _title?: string
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param t the title
|
|
18
|
+
* @param icon the optional icon
|
|
19
|
+
*/
|
|
20
|
+
title(t: string, icon?: TT['icons']) {
|
|
21
|
+
this._title = t
|
|
22
|
+
this._icon = icon
|
|
23
|
+
return this
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected _icon?: TT['icons']
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param i the panel icon
|
|
30
|
+
*/
|
|
31
|
+
icon(i: TT['icons']) {
|
|
32
|
+
this._icon = i
|
|
33
|
+
return this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protected _content?: (parent: HtmlParentTag) => void
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param fun a function that renders the panel content
|
|
40
|
+
*/
|
|
41
|
+
content(fun: (parent: HtmlParentTag) => void) {
|
|
42
|
+
this._content = fun
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export class PanelFragment<TT extends ThemeType> extends ContentFragment<TT> {
|
|
50
|
+
constructor(theme: Theme<TT>) {
|
|
51
|
+
super('tt-panel', theme)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Actions
|
|
55
|
+
|
|
56
|
+
actions = {
|
|
57
|
+
primary: Array<Action<TT>>(),
|
|
58
|
+
secondary: Array<Action<TT>>(),
|
|
59
|
+
tertiary: Array<Action<TT>>()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add an action to the panel.
|
|
64
|
+
* @param action the action to add
|
|
65
|
+
* @param level whether it's a primary, secondary, or tertiary action
|
|
66
|
+
*/
|
|
67
|
+
addAction(action: Action<TT>, level: ActionLevel = 'primary') {
|
|
68
|
+
this.actions[level].push(action)
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Renders the panel into the given parent tag
|
|
74
|
+
* @param parent
|
|
75
|
+
*/
|
|
76
|
+
render(parent: PartTag) {
|
|
77
|
+
return parent.div(this.prefix, panel => {
|
|
78
|
+
if (this._title?.length) {
|
|
79
|
+
panel.div(`.${this.prefix}-header`, header => {
|
|
80
|
+
header.h2(h2 => {
|
|
81
|
+
if (this._icon) {
|
|
82
|
+
this.theme.renderIcon(h2, this._icon, 'link')
|
|
83
|
+
}
|
|
84
|
+
h2.div('.title', {text: this._title})
|
|
85
|
+
})
|
|
86
|
+
this.theme.renderActions(header, this.actions.tertiary)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
panel.div(`.${this.prefix}-content`, content => {
|
|
90
|
+
if (this._content) {
|
|
91
|
+
this._content(content)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
panelActions(panel, this.actions, this.theme)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Render the primary and secondary actions to the bottom of a panel
|
|
102
|
+
* @param panel the .panel container
|
|
103
|
+
* @param actions the actions
|
|
104
|
+
*/
|
|
105
|
+
function panelActions<TT extends ThemeType>(panel: PartTag, actions: PanelActions<TT>, theme: Theme<TT>) {
|
|
106
|
+
if (actions.primary.length || actions.secondary.length) {
|
|
107
|
+
panel.div('.panel-actions', actionsContainer => {
|
|
108
|
+
actionsContainer.div('.secondary-actions', secondaryContainer => {
|
|
109
|
+
theme.renderActions(secondaryContainer, actions.secondary, {iconColor: 'white', defaultClass: 'link'})
|
|
110
|
+
})
|
|
111
|
+
actionsContainer.div('.primary-actions', primaryContainer => {
|
|
112
|
+
theme.renderActions(primaryContainer, actions.primary, {iconColor: 'white'})
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Cards are like panels except they don't have any actions and are themselves an anchor.
|
|
121
|
+
*/
|
|
122
|
+
class CardFragment<TT extends ThemeType> extends ContentFragment<TT> {
|
|
123
|
+
constructor(theme: Theme<TT>) {
|
|
124
|
+
super('tt-card', theme)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private _href?: string
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sets the href of the anchor tag
|
|
131
|
+
* @param h
|
|
132
|
+
*/
|
|
133
|
+
href(h: string) {
|
|
134
|
+
this._href = h
|
|
135
|
+
return this
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Renders the card into the given parent tag
|
|
140
|
+
* @param parent
|
|
141
|
+
*/
|
|
142
|
+
render(parent: PartTag) {
|
|
143
|
+
return parent.a({href: this._href}, this.prefix, panel => {
|
|
144
|
+
if (this._title?.length) {
|
|
145
|
+
panel.div(`.${this.prefix}-header`, header => {
|
|
146
|
+
if (this._icon) {
|
|
147
|
+
this.theme.renderIcon(header, this._icon, 'link')
|
|
148
|
+
}
|
|
149
|
+
header.h3({text: this._title})
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
panel.div(`.${this.prefix}-content`, content => {
|
|
153
|
+
if (this._content) {
|
|
154
|
+
this._content(content)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class LabeledValueFragment<TT extends ThemeType> extends ContentFragment<TT> {
|
|
164
|
+
|
|
165
|
+
constructor(theme: Theme<TT>) {
|
|
166
|
+
super('tt-labeled-value', theme)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private _value?: string
|
|
170
|
+
private _valueIcon?: TT['icons']
|
|
171
|
+
private _valueIconColor?: TT['colors']
|
|
172
|
+
private _valueClass?: string[]
|
|
173
|
+
|
|
174
|
+
private _href?: string
|
|
175
|
+
private _hrefTarget?: string
|
|
176
|
+
|
|
177
|
+
private _click?: Packet
|
|
178
|
+
|
|
179
|
+
private _tooltip?: string
|
|
180
|
+
|
|
181
|
+
value(value: string, icon?: TT['icons'], iconColor?: TT['colors']) {
|
|
182
|
+
this._value = value
|
|
183
|
+
this._valueIcon = icon
|
|
184
|
+
this._valueIconColor = iconColor
|
|
185
|
+
return this
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
valueClass(...classes: string[]) {
|
|
189
|
+
this._valueClass = classes
|
|
190
|
+
return this
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
href(value: string, target?: string) {
|
|
194
|
+
this._href = value
|
|
195
|
+
if (target) this._hrefTarget = target
|
|
196
|
+
return this
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
emitClick(packet?: Packet) {
|
|
200
|
+
this._click = packet
|
|
201
|
+
return this
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Set the tooltip that will show for the value
|
|
206
|
+
* @param tooltip
|
|
207
|
+
*/
|
|
208
|
+
tooltip(tooltip: string) {
|
|
209
|
+
this._tooltip = tooltip
|
|
210
|
+
return this
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
render(parent: PartTag) {
|
|
214
|
+
parent.div('.tt-labeled-value', container => {
|
|
215
|
+
if (this._title) {
|
|
216
|
+
container.div('.title', title => {
|
|
217
|
+
if (this._icon) {
|
|
218
|
+
this.theme.renderIcon(title, this._icon, 'link')
|
|
219
|
+
}
|
|
220
|
+
title.div({text: this._title})
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
container.div('.value', div => {
|
|
225
|
+
let valueBox: HtmlParentTag = div
|
|
226
|
+
if (this._href?.length) {
|
|
227
|
+
valueBox = div.a({ href: this._href, target: this._hrefTarget })
|
|
228
|
+
} else if (this._click) {
|
|
229
|
+
valueBox = div.a()
|
|
230
|
+
valueBox.emitClick(this._click.key, this._click.data ?? {})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (this._valueClass?.length) div.class(...this._valueClass)
|
|
234
|
+
if (this._tooltip) valueBox.dataAttr("tooltip", this._tooltip)
|
|
235
|
+
if (this._value) {
|
|
236
|
+
if (this._valueIcon) {
|
|
237
|
+
this.theme.renderIcon(valueBox, this._valueIcon, this._valueIconColor ?? 'link')
|
|
238
|
+
}
|
|
239
|
+
valueBox.div('.value-text', {text: this._value})
|
|
240
|
+
}
|
|
241
|
+
else if (this._content) {
|
|
242
|
+
this._content(valueBox)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type ListValueDefinition = {
|
|
250
|
+
value: string
|
|
251
|
+
href?: string
|
|
252
|
+
hrefTarget?: string
|
|
253
|
+
tooltip?: string
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
class LabeledListFragment<TT extends ThemeType> extends ContentFragment<TT> {
|
|
257
|
+
private _values?: ListValueDefinition[]
|
|
258
|
+
|
|
259
|
+
constructor(theme: Theme<TT>) {
|
|
260
|
+
super('tt-labeled-list', theme)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
render(parent: PartTag) {
|
|
264
|
+
parent.div('.tt-labeled-list', container => {
|
|
265
|
+
if (this._title) {
|
|
266
|
+
container.div('.title', title => {
|
|
267
|
+
if (this._icon) {
|
|
268
|
+
this.theme.renderIcon(title, this._icon, 'link')
|
|
269
|
+
}
|
|
270
|
+
title.div({ text: this._title })
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
container.div('.value.tt-flex.wrap', valueBox => {
|
|
274
|
+
for (const listItem of this._values ?? []) {
|
|
275
|
+
valueBox.div('.tt-flex.shrink.align-center.secondary', badge => {
|
|
276
|
+
if (listItem.tooltip) badge.dataAttr("tooltip", listItem.tooltip)
|
|
277
|
+
|
|
278
|
+
const anchorAttrs: AnchorTagAttrs = { classes: ['button'], text: listItem.value }
|
|
279
|
+
if (listItem.href?.length) {
|
|
280
|
+
anchorAttrs.classes?.push('secondary')
|
|
281
|
+
anchorAttrs.href = listItem.href
|
|
282
|
+
anchorAttrs.target = listItem.hrefTarget ?? '_blank'
|
|
283
|
+
} else {
|
|
284
|
+
anchorAttrs.classes?.push('readonly disabled')
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
badge.a(anchorAttrs)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
values(values: ListValueDefinition[]) {
|
|
295
|
+
this._values = values
|
|
296
|
+
return this
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Creates a new card fragment builder.
|
|
303
|
+
* Make sure to call `render()` in order to render it to a parent tag.
|
|
304
|
+
*/
|
|
305
|
+
function card<TT extends ThemeType>(theme: Theme<TT>) {
|
|
306
|
+
return new CardFragment(theme)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Creates a new panel fragment builder.
|
|
312
|
+
* Make sure to call `render()` in order to render it to a parent tag.
|
|
313
|
+
*/
|
|
314
|
+
function panel<TT extends ThemeType>(theme: Theme<TT>) {
|
|
315
|
+
return new PanelFragment(theme)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Creates a new labeled value fragment builder.
|
|
320
|
+
* Make sure to call `render()` in order to render it to a parent tag.
|
|
321
|
+
*/
|
|
322
|
+
function labeledValue<TT extends ThemeType>(theme: Theme<TT>) {
|
|
323
|
+
return new LabeledValueFragment(theme)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function labeledList<TT extends ThemeType>(theme: Theme<TT>) {
|
|
327
|
+
return new LabeledListFragment(theme)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Create a new button in the parent.
|
|
332
|
+
* @param parent
|
|
333
|
+
* @param title
|
|
334
|
+
* @param icon
|
|
335
|
+
*/
|
|
336
|
+
function button<TT extends ThemeType>(parent: PartTag, theme: Theme<TT>, title: string, icon?: TT['icons']) {
|
|
337
|
+
return parent.a('.tt-button', button => {
|
|
338
|
+
if (icon) {
|
|
339
|
+
theme.renderIcon(button, icon, 'white')
|
|
340
|
+
}
|
|
341
|
+
button.div('.title', {text: title})
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Create a new simple value display in the parent.
|
|
347
|
+
* This is just some text with an optional icon that doesn't have a separate label.
|
|
348
|
+
* @param parent
|
|
349
|
+
* @param title
|
|
350
|
+
* @param icon
|
|
351
|
+
* @param iconColor
|
|
352
|
+
*/
|
|
353
|
+
function simpleValue<TT extends ThemeType>(parent: PartTag, theme: Theme<TT>, title: string, icon?: TT['icons'], iconColor?: TT['colors']) {
|
|
354
|
+
return parent.div('.tt-simple-value.shrink', button => {
|
|
355
|
+
if (icon) {
|
|
356
|
+
theme.renderIcon(button, icon, iconColor || 'link')
|
|
357
|
+
}
|
|
358
|
+
button.div('.title', {text: title})
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Helper to create a heading with an optional icon.
|
|
364
|
+
* @param parent
|
|
365
|
+
* @param title
|
|
366
|
+
* @param icon
|
|
367
|
+
*/
|
|
368
|
+
function simpleHeading<TT extends ThemeType>(parent: PartTag, theme: Theme<TT>, title: string, icon?: TT['icons']) {
|
|
369
|
+
return parent.h3('.shrink', heading => {
|
|
370
|
+
if (icon) {
|
|
371
|
+
theme.renderIcon(heading, icon, 'link')
|
|
372
|
+
}
|
|
373
|
+
heading.div('.title', {text: title})
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
const Fragments = {
|
|
381
|
+
panel,
|
|
382
|
+
card,
|
|
383
|
+
labeledValue,
|
|
384
|
+
labeledList,
|
|
385
|
+
button,
|
|
386
|
+
simpleValue,
|
|
387
|
+
simpleHeading,
|
|
388
|
+
panelActions
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export default Fragments
|
package/lightbox.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Logger } from "tuff-core/logging"
|
|
2
|
+
import { untypedKey } from "tuff-core/messages"
|
|
3
|
+
import {Part, PartTag} from "tuff-core/parts"
|
|
4
|
+
import {TerrierApp} from "./app"
|
|
5
|
+
|
|
6
|
+
const log = new Logger('Lightbox')
|
|
7
|
+
|
|
8
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
9
|
+
// Global Event Listener
|
|
10
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initializes a global event listener for image elements contained in the `containerClass`
|
|
14
|
+
* @param root
|
|
15
|
+
* @param containerClass
|
|
16
|
+
*/
|
|
17
|
+
function init(root: HTMLElement, app: TerrierApp<any>, containerClass: string) {
|
|
18
|
+
log.info("Init", root)
|
|
19
|
+
root.addEventListener("click", evt => {
|
|
20
|
+
if (!(evt.target instanceof HTMLElement) || evt.target.tagName != 'IMG') {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const elem = evt.target as HTMLImageElement
|
|
24
|
+
|
|
25
|
+
// filter the target by the container class
|
|
26
|
+
let show = false
|
|
27
|
+
for (const ancestor of evt.composedPath()) {
|
|
28
|
+
if (ancestor instanceof HTMLElement && ancestor.classList.contains(containerClass)) {
|
|
29
|
+
show = true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!show) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const src = elem.src
|
|
37
|
+
log.info(`Clicked on lightbox image ${src}`, evt, elem)
|
|
38
|
+
showPart(app, {src})
|
|
39
|
+
|
|
40
|
+
}, {capture: true})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
45
|
+
// Part
|
|
46
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
47
|
+
|
|
48
|
+
type LightboxState = { src: string }
|
|
49
|
+
|
|
50
|
+
function showPart(app: TerrierApp<any>, state: LightboxState) {
|
|
51
|
+
app.makeOverlay(LightboxPart, {app,...state}, 'lightbox')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const closeKey = untypedKey()
|
|
55
|
+
|
|
56
|
+
class LightboxPart extends Part<LightboxState & {app: TerrierApp<any>}> {
|
|
57
|
+
|
|
58
|
+
async init() {
|
|
59
|
+
this.onClick(closeKey, _ => {
|
|
60
|
+
this.close()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
render(parent: PartTag) {
|
|
65
|
+
parent.div('.scroller', scroller => {
|
|
66
|
+
scroller.img({src: this.state.src})
|
|
67
|
+
})
|
|
68
|
+
.emitClick(closeKey)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
update(_elem: HTMLElement) {
|
|
72
|
+
setTimeout(_ => {
|
|
73
|
+
_elem.classList.add('active') // start css fade in
|
|
74
|
+
}, 10)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
close() {
|
|
78
|
+
this.element?.classList.remove('active')
|
|
79
|
+
setTimeout(_ => {
|
|
80
|
+
this.state.app.clearOverlay('lightbox')
|
|
81
|
+
}, 500)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
88
|
+
// Export
|
|
89
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
90
|
+
|
|
91
|
+
const Lightbox = {
|
|
92
|
+
init
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default Lightbox
|