x4js 2.3.26 → 2.3.27
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/aicontext.md +41 -1
- package/package.json +1 -1
- package/src/components/autocomplete/autocomplete.ts +1 -1
- package/src/components/calendar/calendar.ts +1 -1
- package/src/components/chart/chart.ts +1 -1
- package/src/components/components.ts +1 -0
- package/src/components/dockingview/dockingview.ts +1 -1
- package/src/components/listbox/listbox.ts +31 -45
- package/src/components/monaco/monaco.ts +1 -2
- package/src/components/popup/popup.ts +2 -2
- package/src/components/rating/rating.ts +1 -1
- package/src/components/timeline/timeline.module.scss +147 -0
- package/src/components/timeline/timeline.ts +146 -0
- package/src/components/treeview/treeview.ts +16 -22
- package/src/core/component.ts +31 -2
- package/src/core/core_shortcuts.ts +2 -1
package/aicontext.md
CHANGED
|
@@ -36,7 +36,7 @@ CoreElement<E> named timers + event surface (no DOM)
|
|
|
36
36
|
│ │ ├── BtnGroup, Tabs, Video, Saturation
|
|
37
37
|
│ ├── Button, Input, Icon, Label, SimpleText, Checkbox, Radio, Link, SimpleTextArea
|
|
38
38
|
│ ├── Listbox, Gridview, Treeview, Select, Combobox, Slider, Gauge
|
|
39
|
-
│ ├── Progress, TickLine, Canvas, Image, Spreadsheet, MonacoEditor
|
|
39
|
+
│ ├── Progress, TickLine, Timeline, Canvas, Image, Spreadsheet, MonacoEditor
|
|
40
40
|
│ ├── SvgComponent → Chart
|
|
41
41
|
│ ├── CSizer → HSizer / VSizer
|
|
42
42
|
│ ├── FileDialog, ScrollView, Viewport, DockingView, Flex, Space
|
|
@@ -227,12 +227,16 @@ queryAll(selector: string): Component[]
|
|
|
227
227
|
firstChild<T>(): T; lastChild<T>(): T
|
|
228
228
|
nextElement<T>(): T; prevElement<T>(): T
|
|
229
229
|
parentElement<T>(cls?: Constructor<T>): T
|
|
230
|
+
closest<T>(selector: string): T // this one or an ancestor whose element matches the css selector; null if none
|
|
231
|
+
contains(other: Component | Node): boolean // other (a node, or the element of a component) is the element of this one or inside it
|
|
230
232
|
childCount(): number
|
|
231
233
|
enumChildComponents(recursive: boolean): Component[]
|
|
232
234
|
enumChildNodes(recursive: boolean): Node[]
|
|
233
235
|
visitChildren(cb: (el: Component) => boolean): void
|
|
234
236
|
static parentElement<T>(dom: Node, cls?: Constructor<T>): T // starts at the parent of dom, not at dom:
|
|
235
237
|
// test componentFromDOM(dom) first for the component of dom itself
|
|
238
|
+
static closest<T>(dom: Element, selector: string): T // starts at dom itself: in a click handler,
|
|
239
|
+
// Component.closest(ev.target as Element, '.item')
|
|
236
240
|
```
|
|
237
241
|
|
|
238
242
|
### Visibility / state / focus / geometry
|
|
@@ -614,6 +618,41 @@ class Rating extends HBox {
|
|
|
614
618
|
}
|
|
615
619
|
```
|
|
616
620
|
|
|
621
|
+
### `Timeline` — events on a line, each with its icon in a round mark
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
interface TimelineItem {
|
|
625
|
+
icon?: string // in the round mark
|
|
626
|
+
title: string | UnsafeHtml // in bold
|
|
627
|
+
text?: string | UnsafeHtml // the sentence under it, in grey
|
|
628
|
+
date?: Date // written relative to now, in the locale: "il y a 2 heures", "yesterday"; none: no time shown
|
|
629
|
+
done?: boolean // the mark in the accent; else a grey mark
|
|
630
|
+
}
|
|
631
|
+
interface TimelineProps extends ComponentProps {
|
|
632
|
+
items?: TimelineItem[]
|
|
633
|
+
vertical?: boolean // true by default; false: the events side by side
|
|
634
|
+
renderer?: (item: TimelineItem) => Component // what is beside the mark (the mark and the line stay)
|
|
635
|
+
click?: EventCallback<EvClick> // a click anywhere in an event: ev.context is its TimelineItem
|
|
636
|
+
}
|
|
637
|
+
interface TimelineEvents extends ComponentEvents { click: EvClick }
|
|
638
|
+
class Timeline extends Component<TimelineProps, TimelineEvents> {
|
|
639
|
+
setItems(items: TimelineItem[]): void // the relative times are written here, not updated after
|
|
640
|
+
}
|
|
641
|
+
// the line to the next mark: in the accent when the next event is done, dashed after an event not done
|
|
642
|
+
// CSS vars: --timeline-done-color --timeline-done-icon-color --timeline-todo-color --timeline-todo-icon-color
|
|
643
|
+
// --timeline-title-color --timeline-text-color --timeline-time-color
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
new Timeline({
|
|
648
|
+
items: [
|
|
649
|
+
{ icon: branch, title: 'New branch', text: 'fix-notifications, from master', date: created, done: true },
|
|
650
|
+
{ icon: review, title: 'Code review', text: 'Waiting for a reviewer' },
|
|
651
|
+
],
|
|
652
|
+
click: ev => this.openStep(ev.context as TimelineItem),
|
|
653
|
+
})
|
|
654
|
+
```
|
|
655
|
+
|
|
617
656
|
---
|
|
618
657
|
|
|
619
658
|
## Input components
|
|
@@ -1453,6 +1492,7 @@ component.addShortcut(keys, callback, options?) // a view: while the foc
|
|
|
1453
1492
|
Application.instance().addShortcut(keys, callback, options?) // the whole application, wherever the focus is
|
|
1454
1493
|
|
|
1455
1494
|
shortcutText(keys: string): string // for a tooltip or a menu: "Ctrl+Z", "⌘Z" on a Mac
|
|
1495
|
+
isMac: boolean // true on a Mac, an iPhone or an iPad: Mod is Cmd there
|
|
1456
1496
|
|
|
1457
1497
|
class Shortcuts { // the list itself; rarely used directly
|
|
1458
1498
|
add(keys: string | string[], callback: (ev: KeyboardEvent) => void, options?: ShortcutOptions): void
|
package/package.json
CHANGED
|
@@ -268,7 +268,7 @@ export class AutoComplete extends TextEdit<AutoCompleteEvents> {
|
|
|
268
268
|
private _on_focusout( ev: FocusEvent ) {
|
|
269
269
|
// focus is moving inside the control
|
|
270
270
|
const to = ev.relatedTarget as Node;
|
|
271
|
-
if( to && this.
|
|
271
|
+
if( to && this.contains( to ) ) {
|
|
272
272
|
return;
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -264,7 +264,7 @@ export class Chart extends SvgComponent<ChartProps,ChartEvents> {
|
|
|
264
264
|
|
|
265
265
|
this.addDOMEvent( "click", ( e ) => {
|
|
266
266
|
const el = ( e.target as Element ).closest( "[data-index]" );
|
|
267
|
-
if( el && this.
|
|
267
|
+
if( el && this.contains( el ) ) {
|
|
268
268
|
const serie = parseInt( el.getAttribute( "data-serie" ) );
|
|
269
269
|
const index = parseInt( el.getAttribute( "data-index" ) );
|
|
270
270
|
this.fire( "click", { serie, index, value: this._series[serie]?.values[index] } );
|
|
@@ -45,6 +45,7 @@ export * from "./tag/tag"
|
|
|
45
45
|
export * from "./textarea/textarea"
|
|
46
46
|
export * from "./textedit/textedit"
|
|
47
47
|
export * from "./tickline/tickline"
|
|
48
|
+
export * from "./timeline/timeline"
|
|
48
49
|
export * from "./tooltips/tooltips"
|
|
49
50
|
export * from "./treeview/treeview"
|
|
50
51
|
export * from "./video/video"
|
|
@@ -404,7 +404,7 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
404
404
|
this.fills.forEach( fill => fill( ) );
|
|
405
405
|
this.placeFloats( );
|
|
406
406
|
|
|
407
|
-
if( focused instanceof HTMLElement && this.
|
|
407
|
+
if( focused instanceof HTMLElement && this.contains( focused ) ) {
|
|
408
408
|
focused.focus( );
|
|
409
409
|
}
|
|
410
410
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
15
|
**/
|
|
16
16
|
|
|
17
|
-
import { Component, ComponentEvent, ComponentEvents,
|
|
17
|
+
import { Component, ComponentEvent, ComponentEvents, ComponentProps, EvChange, EvClick, EvContextMenu, EvDblClick, EvSelectionChange } from '../../core/component';
|
|
18
18
|
import { EventCallback } from '../../core/core_events';
|
|
19
19
|
import { kbNav, isArray, UnsafeHtml, asap } from '../../core/core_tools';
|
|
20
20
|
|
|
@@ -283,47 +283,38 @@ private _view: Viewport;
|
|
|
283
283
|
*/
|
|
284
284
|
|
|
285
285
|
private _on_click( ev: MouseEvent ) {
|
|
286
|
-
|
|
287
|
-
let on_check = false;
|
|
286
|
+
const target = ev.target as Element;
|
|
288
287
|
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
// avoid trapping child click
|
|
289
|
+
if( Component.closest( target, "input" ) ) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
291
292
|
|
|
292
|
-
|
|
293
|
-
|
|
293
|
+
// an item of this list, not of a list around it
|
|
294
|
+
const c = Component.closest( target, ".x4item" );
|
|
295
|
+
if( c && this.contains( c ) ) {
|
|
296
|
+
// the case changes alone; a double click on it is ignored
|
|
297
|
+
if( Component.closest( target, ".x4check" ) ) {
|
|
298
|
+
if( ev.type == 'click' ) {
|
|
299
|
+
this._toggleCheck( c );
|
|
300
|
+
}
|
|
294
301
|
return;
|
|
295
302
|
}
|
|
296
303
|
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
const id = c.getInternalData( "id" );
|
|
305
|
+
const fev: EvClick = { context:id };
|
|
306
|
+
if (ev.type == 'click') {
|
|
307
|
+
this.fire('click', fev );
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
this.fire('dblClick', fev );
|
|
299
311
|
}
|
|
300
312
|
|
|
301
|
-
if(
|
|
302
|
-
|
|
303
|
-
if( on_check ) {
|
|
304
|
-
if( ev.type == 'click' ) {
|
|
305
|
-
this._toggleCheck( c );
|
|
306
|
-
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const id = c.getInternalData( "id" );
|
|
311
|
-
const fev: EvClick = { context:id };
|
|
312
|
-
if (ev.type == 'click') {
|
|
313
|
-
this.fire('click', fev );
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
this.fire('dblClick', fev );
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (!fev.defaultPrevented) {
|
|
320
|
-
this._selectItem( id, c, ev.ctrlKey ? 'toggle' : 'single', ev.type == 'click' && this.props.reselect );
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return;
|
|
313
|
+
if (!fev.defaultPrevented) {
|
|
314
|
+
this._selectItem( id, c, ev.ctrlKey ? 'toggle' : 'single', ev.type == 'click' && this.props.reselect );
|
|
324
315
|
}
|
|
325
316
|
|
|
326
|
-
|
|
317
|
+
return;
|
|
327
318
|
}
|
|
328
319
|
|
|
329
320
|
if (ev.type == 'click') {
|
|
@@ -344,19 +335,14 @@ private _view: Viewport;
|
|
|
344
335
|
|
|
345
336
|
ev.preventDefault();
|
|
346
337
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
if( c && c.hasClass("x4item") ) {
|
|
351
|
-
const id = c.getInternalData( "id" );
|
|
352
|
-
|
|
353
|
-
this._selectItem(id, c, 'single' );
|
|
354
|
-
this.fire('contextMenu', {uievent: ev, context: id } );
|
|
355
|
-
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
338
|
+
const c = Component.closest( ev.target as Element, ".x4item" );
|
|
339
|
+
if( c && this.contains( c ) ) {
|
|
340
|
+
const id = c.getInternalData( "id" );
|
|
358
341
|
|
|
359
|
-
|
|
342
|
+
this._selectItem(id, c, 'single' );
|
|
343
|
+
this.fire('contextMenu', {uievent: ev, context: id } );
|
|
344
|
+
|
|
345
|
+
return;
|
|
360
346
|
}
|
|
361
347
|
|
|
362
348
|
this.fire('contextMenu', { uievent:ev, context: null } );
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { class_ns } from '../../core/core_tools';
|
|
2
1
|
import { Component, ComponentProps } from '../../core/component';
|
|
3
2
|
|
|
4
3
|
import Monaco from './bin/monaco';
|
|
@@ -17,8 +16,8 @@ interface MonacoEditorProps extends ComponentProps {
|
|
|
17
16
|
options?: Monaco.editor.IEditorOptions & Monaco.editor.IGlobalEditorOptions;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
@class_ns( "x4" )
|
|
21
19
|
export class MonacoEditor extends Component<MonacoEditorProps> {
|
|
20
|
+
static classPrefix = "x4";
|
|
22
21
|
|
|
23
22
|
private static _initCount = 0;
|
|
24
23
|
private static _basePath: string = "monaco/";
|
|
@@ -54,7 +54,7 @@ function getRoot( ) {
|
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
56
|
function dismissLast( e: UIEvent ) {
|
|
57
|
-
if( autoclose_list.some( x => x.
|
|
57
|
+
if( autoclose_list.some( x => x.contains( e.target as Node ) ) ) {
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -224,7 +224,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
224
224
|
// modal: the focus goes back to the element that had it when it opened, unless the
|
|
225
225
|
// user put it elsewhere (read before the popup leaves the page and its focus with it)
|
|
226
226
|
const active = document.activeElement;
|
|
227
|
-
const giveBack = this._ismodal && this._prevFocus && ( !active || active===document.body || this.
|
|
227
|
+
const giveBack = this._ismodal && this._prevFocus && ( !active || active===document.body || this.contains( active ) );
|
|
228
228
|
|
|
229
229
|
this.__hide( );
|
|
230
230
|
this.__remove( );
|
|
@@ -104,7 +104,7 @@ export class Rating extends HBox<RatingProps,RatingEventMap> {
|
|
|
104
104
|
|
|
105
105
|
// the star clicked: its svg, or the Icon itself
|
|
106
106
|
private _on_click( ev: MouseEvent ) {
|
|
107
|
-
const n = this.m_els.findIndex( c => c.
|
|
107
|
+
const n = this.m_els.findIndex( c => c.contains( ev.target as Node ) );
|
|
108
108
|
if( n>=0 ) {
|
|
109
109
|
// the star of the value, clicked again: none is lit
|
|
110
110
|
this.setValue( n+1===this.props.value ? 0 : n+1 );
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file timeline.module.scss
|
|
9
|
+
* @author Etienne Cochard
|
|
10
|
+
*
|
|
11
|
+
* @copyright (c) 2026 R-libre ingenierie
|
|
12
|
+
*
|
|
13
|
+
* Use of this source code is governed by an MIT-style license
|
|
14
|
+
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
--timeline-done-color: var( --accent-background );
|
|
19
|
+
--timeline-done-icon-color: var( --accent-color );
|
|
20
|
+
--timeline-todo-color: var( --border );
|
|
21
|
+
--timeline-todo-icon-color: var( --text-ternary );
|
|
22
|
+
--timeline-title-color: var( --text-primary );
|
|
23
|
+
--timeline-text-color: var( --text-ternary );
|
|
24
|
+
--timeline-time-color: var( --text-primary );
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// side by side: the marks on a line at the top, the content under each one
|
|
28
|
+
|
|
29
|
+
.x4timeline {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: row;
|
|
32
|
+
|
|
33
|
+
.item {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
flex: 1 1 0;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.rail {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: row;
|
|
43
|
+
align-items: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.mark {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
width: 24px;
|
|
52
|
+
height: 24px;
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
background-color: var( --timeline-todo-color );
|
|
55
|
+
|
|
56
|
+
.x4icon {
|
|
57
|
+
height: 12px;
|
|
58
|
+
color: var( --timeline-todo-icon-color );
|
|
59
|
+
fill: var( --timeline-todo-icon-color );
|
|
60
|
+
|
|
61
|
+
&.empty {
|
|
62
|
+
display: none;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// the line to the next mark: only one side has a width
|
|
68
|
+
.line {
|
|
69
|
+
flex: 1;
|
|
70
|
+
border: 0 dashed var( --timeline-todo-color );
|
|
71
|
+
border-top-width: 2px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.item.done > .rail {
|
|
75
|
+
.mark {
|
|
76
|
+
background-color: var( --timeline-done-color );
|
|
77
|
+
|
|
78
|
+
.x4icon {
|
|
79
|
+
color: var( --timeline-done-icon-color );
|
|
80
|
+
fill: var( --timeline-done-icon-color );
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.line {
|
|
85
|
+
border-style: solid;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.item.next-done > .rail > .line {
|
|
90
|
+
border-color: var( --timeline-done-color );
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.item:last-child > .rail > .line {
|
|
94
|
+
display: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.content {
|
|
98
|
+
padding: 8px 12px 0 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.title {
|
|
102
|
+
font-weight: bold;
|
|
103
|
+
color: var( --timeline-title-color );
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.text {
|
|
107
|
+
color: var( --timeline-text-color );
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.time {
|
|
111
|
+
margin-top: 4px;
|
|
112
|
+
font-size: 0.85em;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
color: var( --timeline-time-color );
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// one under the other: the marks on a line on the left, the content beside each one
|
|
118
|
+
|
|
119
|
+
&.vertical {
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
|
|
122
|
+
.item {
|
|
123
|
+
flex-direction: row;
|
|
124
|
+
flex: 0 0 auto;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.rail {
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.line {
|
|
132
|
+
border-top-width: 0;
|
|
133
|
+
border-left-width: 2px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.content {
|
|
137
|
+
flex: 1;
|
|
138
|
+
min-width: 0;
|
|
139
|
+
padding: 0 0 1.5em 12px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// the title on the middle of the mark
|
|
143
|
+
.title {
|
|
144
|
+
line-height: 24px;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file timeline.ts
|
|
9
|
+
* @author Etienne Cochard
|
|
10
|
+
*
|
|
11
|
+
* @copyright (c) 2026 R-libre ingenierie
|
|
12
|
+
*
|
|
13
|
+
* Use of this source code is governed by an MIT-style license
|
|
14
|
+
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
import { Component, ComponentEvents, ComponentProps, EvClick } from '../../core/component';
|
|
18
|
+
import { EventCallback } from '../../core/core_events';
|
|
19
|
+
import { UnsafeHtml } from '../../core/core_tools';
|
|
20
|
+
import { getCurrentLocale } from '../../core/core_i18n';
|
|
21
|
+
import { Icon } from '../icon/icon';
|
|
22
|
+
|
|
23
|
+
import "./timeline.module.scss";
|
|
24
|
+
|
|
25
|
+
export interface TimelineItem {
|
|
26
|
+
icon?: string; // in the round mark
|
|
27
|
+
title: string | UnsafeHtml; // in bold
|
|
28
|
+
text?: string | UnsafeHtml; // the sentence under it
|
|
29
|
+
date?: Date; // written relative to now, in the locale: "2 hours ago"; none: no time shown
|
|
30
|
+
done?: boolean; // the mark filled in the accent; else a grey mark
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TimelineEvents extends ComponentEvents {
|
|
34
|
+
click: EvClick; // context: the TimelineItem clicked
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TimelineProps extends ComponentProps {
|
|
38
|
+
items?: TimelineItem[];
|
|
39
|
+
vertical?: boolean; // true by default; false: the events side by side
|
|
40
|
+
renderer?: ( item: TimelineItem ) => Component; // what is beside the mark
|
|
41
|
+
click?: EventCallback<EvClick>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* "2 hours ago", "yesterday", "in 3 days": the largest unit that gives at least 1
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
function relativeTime( date: Date ): string {
|
|
49
|
+
const fmt = new Intl.RelativeTimeFormat( getCurrentLocale( ), { numeric: "auto" } );
|
|
50
|
+
const sec = ( date.getTime( ) - Date.now( ) ) / 1000; // < 0: in the past
|
|
51
|
+
const abs = Math.abs( sec );
|
|
52
|
+
|
|
53
|
+
if( abs < 60 ) {
|
|
54
|
+
return fmt.format( Math.trunc( sec ), "second" );
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if( abs < 3600 ) {
|
|
58
|
+
return fmt.format( Math.trunc( sec / 60 ), "minute" );
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if( abs < 86400 ) {
|
|
62
|
+
return fmt.format( Math.trunc( sec / 3600 ), "hour" );
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if( abs < 86400 * 30 ) {
|
|
66
|
+
return fmt.format( Math.trunc( sec / 86400 ), "day" );
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if( abs < 86400 * 365 ) {
|
|
70
|
+
return fmt.format( Math.trunc( sec / ( 86400 * 30 ) ), "month" );
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return fmt.format( Math.trunc( sec / ( 86400 * 365 ) ), "year" );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Events one after the other on a line, each with its icon in a round mark.
|
|
78
|
+
* The line from a mark to the next one is in the accent when the next event is done,
|
|
79
|
+
* dashed when the event it leaves is not done.
|
|
80
|
+
*
|
|
81
|
+
* @cssvar
|
|
82
|
+
* ```
|
|
83
|
+
* --timeline-done-color
|
|
84
|
+
* --timeline-done-icon-color
|
|
85
|
+
* --timeline-todo-color
|
|
86
|
+
* --timeline-todo-icon-color
|
|
87
|
+
* --timeline-title-color
|
|
88
|
+
* --timeline-text-color
|
|
89
|
+
* --timeline-time-color
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
export class Timeline extends Component<TimelineProps, TimelineEvents> {
|
|
94
|
+
static classPrefix = "x4";
|
|
95
|
+
|
|
96
|
+
constructor( props: TimelineProps ) {
|
|
97
|
+
super( props );
|
|
98
|
+
|
|
99
|
+
this.setClass( "vertical", props.vertical ?? true );
|
|
100
|
+
|
|
101
|
+
this.mapPropEvents( props, "click" );
|
|
102
|
+
this.addDOMEvent( "click", ( ev ) => this._onClick( ev ) );
|
|
103
|
+
|
|
104
|
+
if( props.items ) {
|
|
105
|
+
this.setItems( props.items );
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setItems( items: TimelineItem[] ) {
|
|
110
|
+
const renderer = this.props.renderer;
|
|
111
|
+
|
|
112
|
+
const content = items.map( ( item, i ) => {
|
|
113
|
+
const body = renderer ? renderer( item ) : [
|
|
114
|
+
new Component( { cls: "title", content: item.title } ),
|
|
115
|
+
item.text ? new Component( { cls: "text", content: item.text } ) : null,
|
|
116
|
+
item.date ? new Component( { cls: "time", content: relativeTime( item.date ) } ) : null,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const el = new Component( { cls: "item", content: [
|
|
120
|
+
new Component( { cls: "rail", content: [
|
|
121
|
+
new Component( { cls: "mark", content: new Icon( { iconId: item.icon } ) } ),
|
|
122
|
+
new Component( { cls: "line" } ),
|
|
123
|
+
] } ),
|
|
124
|
+
new Component( { cls: "content", content: body } ),
|
|
125
|
+
] } );
|
|
126
|
+
|
|
127
|
+
el.setClass( "done", !!item.done );
|
|
128
|
+
el.setClass( "next-done", i + 1 < items.length && !!items[i + 1].done );
|
|
129
|
+
el.setInternalData( "item", item );
|
|
130
|
+
return el;
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
this.setContent( content );
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* a click anywhere in an event: its item
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
private _onClick( ev: MouseEvent ) {
|
|
141
|
+
const el = Component.closest( ev.target as Element, ".x4timeline > .item" );
|
|
142
|
+
if( el ) {
|
|
143
|
+
this.fire( "click", { context: el.getInternalData( "item" ) } );
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { asap } from '../../core/core_tools';
|
|
18
18
|
import { EventCallback } from '../../core/core_events';
|
|
19
|
-
import { Component, ComponentEvent, ComponentEvents, ComponentProps, EvClick, EvDblClick, EvSelectionChange
|
|
19
|
+
import { Component, ComponentEvent, ComponentEvents, ComponentProps, EvClick, EvDblClick, EvSelectionChange } from '../../core/component';
|
|
20
20
|
|
|
21
21
|
import { ScrollView, Viewport } from '../viewport/viewport';
|
|
22
22
|
import { Label, SimpleText } from '../label/label';
|
|
@@ -244,30 +244,24 @@ export class Treeview extends Component<TreeviewProps,TreeviewEvents> {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
private _on_click( ev: UIEvent ) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
else {
|
|
260
|
-
this.fire('dblClick', fev );
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (!fev.defaultPrevented) {
|
|
264
|
-
this._selectItem( id, c );
|
|
265
|
-
}
|
|
247
|
+
// an item of this tree, not of a component around it
|
|
248
|
+
const c = Component.closest( ev.target as Element, ".item" );
|
|
249
|
+
if( c && this.contains( c ) ) {
|
|
250
|
+
const id = c.getInternalData( "id" );
|
|
251
|
+
|
|
252
|
+
const fev: ComponentEvent = { context:id };
|
|
253
|
+
if (ev.type == 'click') {
|
|
254
|
+
this.fire('click', fev );
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.fire('dblClick', fev );
|
|
258
|
+
}
|
|
266
259
|
|
|
267
|
-
|
|
260
|
+
if (!fev.defaultPrevented) {
|
|
261
|
+
this._selectItem( id, c );
|
|
268
262
|
}
|
|
269
263
|
|
|
270
|
-
|
|
264
|
+
return;
|
|
271
265
|
}
|
|
272
266
|
|
|
273
267
|
this.clearSelection( );
|
package/src/core/component.ts
CHANGED
|
@@ -959,7 +959,24 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
959
959
|
*/
|
|
960
960
|
|
|
961
961
|
parentElement<T extends Component>( cls?: Constructor<T> ): T {
|
|
962
|
-
return Component.parentElement<T>( this.dom, cls );
|
|
962
|
+
return Component.parentElement<T>( this.dom, cls );
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* The nearest component whose element matches a css selector: this one, or one of its ancestors.
|
|
967
|
+
* @returns null when there is none
|
|
968
|
+
*/
|
|
969
|
+
|
|
970
|
+
closest<T extends Component = Component>( selector: string ): T {
|
|
971
|
+
return Component.closest<T>( this.dom, selector );
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* true when a node, or the element of a component, is the element of this one or inside it
|
|
976
|
+
*/
|
|
977
|
+
|
|
978
|
+
contains( other: Component | Node ): boolean {
|
|
979
|
+
return this.dom.contains( other instanceof Component ? other.dom : other );
|
|
963
980
|
}
|
|
964
981
|
|
|
965
982
|
/**
|
|
@@ -992,10 +1009,22 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
992
1009
|
|
|
993
1010
|
dom = dom.parentElement;
|
|
994
1011
|
}
|
|
995
|
-
|
|
1012
|
+
|
|
996
1013
|
return null;
|
|
997
1014
|
}
|
|
998
1015
|
|
|
1016
|
+
/**
|
|
1017
|
+
* The nearest component whose element matches a css selector: the one of dom, or one of its ancestors.
|
|
1018
|
+
* @returns null when there is none
|
|
1019
|
+
* @example
|
|
1020
|
+
* // in a click handler: the item hit, wherever the click is in it
|
|
1021
|
+
* const item = Component.closest( ev.target as Element, ".x4timeline > .item" );
|
|
1022
|
+
*/
|
|
1023
|
+
|
|
1024
|
+
static closest<T extends Component = Component>( dom: Element, selector: string ): T {
|
|
1025
|
+
return componentFromDOM<T>( dom.closest( selector ) );
|
|
1026
|
+
}
|
|
1027
|
+
|
|
999
1028
|
/**
|
|
1000
1029
|
* Returns the first child element as a Component instance.
|
|
1001
1030
|
* @returns The first child component, or `null` if none exists.
|
|
@@ -51,7 +51,8 @@ interface Keys {
|
|
|
51
51
|
key: string;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/** true on a Mac, an iPhone or an iPad: Mod is Cmd there */
|
|
55
|
+
export const isMac = /mac|iphone|ipad/i.test( navigator.platform ?? "" );
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* "Shift+Mod+K" -> shift, ctrl (cmd on a Mac), k
|