x4js 2.3.28 → 2.3.30
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/README.md +1 -1
- package/aicontext.md +385 -21
- package/package.json +8 -3
- package/src/components/assets/icons.ts +2 -0
- package/src/components/bargraph/bargraph.module.scss +44 -0
- package/src/components/bargraph/bargraph.ts +362 -0
- package/src/components/base.scss +71 -1
- package/src/components/calendar/calendar.module.scss +6 -5
- package/src/components/calendar/calendar.ts +14 -8
- package/src/components/canvas/canvas.ts +5 -4
- package/src/components/carousel/carousel.module.scss +165 -0
- package/src/components/carousel/carousel.ts +423 -0
- package/src/components/chart/chart.module.scss +8 -0
- package/src/components/chart/chart.ts +170 -12
- package/src/components/combobox/combobox.module.scss +2 -1
- package/src/components/components.ts +3 -0
- package/src/components/dialog/dialog.module.scss +2 -0
- package/src/components/dialog/dialog.ts +1 -0
- package/src/components/gantt/gantt.module.scss +462 -0
- package/src/components/gantt/gantt.ts +1254 -0
- package/src/components/knob/knob.module.scss +113 -0
- package/src/components/knob/knob.ts +355 -0
- package/src/components/mapview/mapview.module.scss +177 -0
- package/src/components/mapview/mapview.ts +764 -0
- package/src/components/menu/menu.module.scss +4 -6
- package/src/components/menu/menu.ts +4 -2
- package/src/components/planner/planner.module.scss +688 -0
- package/src/components/planner/planner.ts +1792 -0
- package/src/components/popup/popup.ts +29 -3
- package/src/components/radio/radio.module.scss +4 -1
- package/src/components/shared.scss +2 -1
- package/src/components/slider/slider.module.scss +22 -20
- package/src/components/switch/switch.module.scss +68 -44
- package/src/components/switch/switch.ts +6 -3
- package/src/components/tag/tag.module.scss +4 -4
- package/src/components/textedit/textedit.module.scss +5 -2
- package/src/components/themes.scss +27 -6
- package/src/components/tooltips/tooltips.scss +8 -0
- package/src/components/tooltips/tooltips.ts +31 -0
- package/src/core/core_i18n.ts +65 -0
- package/src/core/core_tools.ts +44 -0
- package/src/x4doc.ts +3 -0
- package/src/components/menu/caret-right-solid.svg +0 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file knob.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
|
+
--knob-track: var( --border );
|
|
19
|
+
--knob-value: var( --accent-background );
|
|
20
|
+
--knob-cap-background: var( --background-secondary );
|
|
21
|
+
--knob-cap-border: var( --border );
|
|
22
|
+
--knob-pointer: var( --accent-background );
|
|
23
|
+
--knob-border-focus: var( --border-focus );
|
|
24
|
+
--knob-track-disabled: color-mix( in srgb, var( --disabled-color-light ) 50%, var( --background-primary ) );
|
|
25
|
+
--knob-value-disabled: var( --disabled-color-light );
|
|
26
|
+
--knob-cap-background-disabled: var( --background-primary );
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.x4knob {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 4px;
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
outline: none;
|
|
36
|
+
|
|
37
|
+
// the round: its diameter in --knob-size (the prop size)
|
|
38
|
+
.dial {
|
|
39
|
+
position: relative;
|
|
40
|
+
flex-shrink: 0;
|
|
41
|
+
width: var( --knob-size, 48px );
|
|
42
|
+
height: var( --knob-size, 48px );
|
|
43
|
+
border-radius: 50%;
|
|
44
|
+
touch-action: none;
|
|
45
|
+
cursor: grab;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&.dragging .dial {
|
|
49
|
+
cursor: grabbing;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// range: the arc of 270° from -135°, the value in --knob-perc (set by the code)
|
|
53
|
+
.ring {
|
|
54
|
+
position: absolute;
|
|
55
|
+
inset: 0;
|
|
56
|
+
border-radius: 50%;
|
|
57
|
+
|
|
58
|
+
background: conic-gradient( from -135deg,
|
|
59
|
+
var( --knob-value ) 0 var( --knob-perc, 0deg ),
|
|
60
|
+
var( --knob-track ) var( --knob-perc, 0deg ) 270deg,
|
|
61
|
+
transparent 270deg );
|
|
62
|
+
|
|
63
|
+
// a ring of 4px
|
|
64
|
+
mask: radial-gradient( farthest-side, transparent calc( 100% - 4px ), #000 calc( 100% - 3.5px ) );
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.cap {
|
|
68
|
+
position: absolute;
|
|
69
|
+
inset: 8px;
|
|
70
|
+
border-radius: 50%;
|
|
71
|
+
background-color: var( --knob-cap-background );
|
|
72
|
+
border: 1px solid var( --knob-cap-border );
|
|
73
|
+
transform: rotate( var( --knob-angle, 0deg ) );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.pointer {
|
|
77
|
+
position: absolute;
|
|
78
|
+
left: 50%;
|
|
79
|
+
top: 3px;
|
|
80
|
+
width: 3px;
|
|
81
|
+
height: 30%;
|
|
82
|
+
border-radius: 2px;
|
|
83
|
+
background-color: var( --knob-pointer );
|
|
84
|
+
transform: translateX( -50% );
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// infinite: no start nor end, ticks all around
|
|
88
|
+
&.infinite .ring {
|
|
89
|
+
background: repeating-conic-gradient( from -1.5deg,
|
|
90
|
+
var( --knob-track ) 0 3deg,
|
|
91
|
+
transparent 3deg 30deg );
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// the colors of the knob take the disabled ones
|
|
95
|
+
&[disabled] {
|
|
96
|
+
--knob-track: var( --knob-track-disabled );
|
|
97
|
+
--knob-value: var( --knob-value-disabled );
|
|
98
|
+
--knob-pointer: var( --knob-value-disabled );
|
|
99
|
+
--knob-cap-background: var( --knob-cap-background-disabled );
|
|
100
|
+
--knob-cap-border: var( --knob-track-disabled );
|
|
101
|
+
|
|
102
|
+
&, .dial {
|
|
103
|
+
cursor: not-allowed;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&:focus .cap {
|
|
108
|
+
outline-offset: 2px;
|
|
109
|
+
outline-color: var( --knob-border-focus );
|
|
110
|
+
outline-style: solid;
|
|
111
|
+
outline-width: 1px;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file knob.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 { EventCallback } from '../../core/core_events';
|
|
18
|
+
import { Component, ComponentEvents, ComponentProps, EvChange } from '../../core/component';
|
|
19
|
+
import { clamp, IComponentInterface, IFormElement, UnsafeHtml } from '../../core/core_tools';
|
|
20
|
+
|
|
21
|
+
import { Label } from '../label/label';
|
|
22
|
+
|
|
23
|
+
import './knob.module.scss';
|
|
24
|
+
|
|
25
|
+
export interface KnobEvents extends ComponentEvents {
|
|
26
|
+
change: EvChange;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface KnobProps extends ComponentProps {
|
|
30
|
+
value?: number;
|
|
31
|
+
/** range: the value between min and max (0 to 100 by default), on an arc of 270° */
|
|
32
|
+
min?: number;
|
|
33
|
+
max?: number;
|
|
34
|
+
step?: number;
|
|
35
|
+
/** the knob turns without end: the value has no min nor max */
|
|
36
|
+
infinite?: boolean;
|
|
37
|
+
/** infinite: the value of a whole turn (360 by default: the value is an angle) */
|
|
38
|
+
turn?: number;
|
|
39
|
+
/** the diameter of the dial, in px (48 by default): width and height size the whole, label included */
|
|
40
|
+
size?: number;
|
|
41
|
+
/** under the dial, with its icon */
|
|
42
|
+
label?: string | UnsafeHtml;
|
|
43
|
+
icon?: string;
|
|
44
|
+
/** for a Form: it reads the value as a number */
|
|
45
|
+
name?: string;
|
|
46
|
+
change?: EventCallback<EvChange>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// range: the arc goes from -135° to +135°, 0 at the top
|
|
50
|
+
const SWEEP = 270;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* a rotary button: on a range (min to max), or infinite.
|
|
54
|
+
* the pointer turns it around its center, the wheel (when it has the focus) and the arrows by step.
|
|
55
|
+
* infinite: a double click sets it back to its first value.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* new Knob( { label: "Volume", value: 25, change: ( ev ) => player.setVolume( ev.value ) } )
|
|
59
|
+
* new Knob( { infinite: true, turn: 20, change: ( ev ) => list.scrollTo( ev.value ) } )
|
|
60
|
+
*
|
|
61
|
+
* @cssvar
|
|
62
|
+
* ```
|
|
63
|
+
* --knob-track
|
|
64
|
+
* --knob-value
|
|
65
|
+
* --knob-cap-background
|
|
66
|
+
* --knob-cap-border
|
|
67
|
+
* --knob-pointer
|
|
68
|
+
* --knob-border-focus
|
|
69
|
+
* --knob-track-disabled
|
|
70
|
+
* --knob-value-disabled
|
|
71
|
+
* --knob-cap-background-disabled
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
export class Knob extends Component<KnobProps,KnobEvents> {
|
|
76
|
+
static classPrefix = "x4";
|
|
77
|
+
|
|
78
|
+
private _value = 0;
|
|
79
|
+
private _raw = 0; // while dragged: the value not yet on the step
|
|
80
|
+
private _angle = 0; // while dragged: the angle of the pointer at the last move
|
|
81
|
+
private _center: { x: number, y: number } = null;
|
|
82
|
+
|
|
83
|
+
private _dial: Component = null;
|
|
84
|
+
private _label: Label = null;
|
|
85
|
+
|
|
86
|
+
constructor( props: KnobProps ) {
|
|
87
|
+
super( props );
|
|
88
|
+
|
|
89
|
+
this.mapPropEvents( props, "change" );
|
|
90
|
+
|
|
91
|
+
if( props.infinite ) {
|
|
92
|
+
this.addClass( 'infinite' );
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if( props.size ) {
|
|
96
|
+
this.setStyleVariable( "--knob-size", props.size+"px" );
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.setContent( [
|
|
100
|
+
this._dial = new Component( { cls: "dial", content: [
|
|
101
|
+
new Component( { cls: "ring" } ),
|
|
102
|
+
new Component( { cls: "cap", content: new Component( { cls: "pointer" } ) } ),
|
|
103
|
+
] } ),
|
|
104
|
+
this._label = new Label( { text: props.label, icon: props.icon, align: "center" } ),
|
|
105
|
+
] );
|
|
106
|
+
|
|
107
|
+
this._showLabel( );
|
|
108
|
+
|
|
109
|
+
this.setAttribute( "tabindex", 0 );
|
|
110
|
+
this.setAttribute( "role", "slider" );
|
|
111
|
+
|
|
112
|
+
if( props.name ) {
|
|
113
|
+
this.setAttribute( "name", props.name );
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// the dial is turned; the label is not
|
|
117
|
+
this._dial.setDOMEvents( {
|
|
118
|
+
pointerdown: ( ev ) => this._on_pointerdown( ev ),
|
|
119
|
+
pointermove: ( ev ) => this._on_pointermove( ev ),
|
|
120
|
+
pointerup: ( ev ) => this._on_pointerup( ev ),
|
|
121
|
+
pointercancel: ( ev ) => this._on_pointerup( ev ),
|
|
122
|
+
dblclick: ( ) => this._on_dblclick( ),
|
|
123
|
+
} );
|
|
124
|
+
|
|
125
|
+
this.setDOMEvents( {
|
|
126
|
+
wheel: ( ev ) => this._on_wheel( ev ),
|
|
127
|
+
keydown: ( ev ) => this._on_key( ev ),
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
this._value = this._fix( props.value ?? this._min( ) );
|
|
131
|
+
this._update( );
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private _min( ) { return this.props.min ?? 0; }
|
|
135
|
+
private _max( ) { return this.props.max ?? 100; }
|
|
136
|
+
private _step( ) { return this.props.step ?? 1; }
|
|
137
|
+
private _turn( ) { return this.props.turn ?? 360; }
|
|
138
|
+
|
|
139
|
+
// on the step (from min on a range), between min and max on a range
|
|
140
|
+
private _fix( v: number ) {
|
|
141
|
+
const step = this._step( );
|
|
142
|
+
|
|
143
|
+
if( this.props.infinite ) {
|
|
144
|
+
return step>0 ? Math.round( v/step )*step : v;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const min = this._min( );
|
|
148
|
+
const max = this._max( );
|
|
149
|
+
|
|
150
|
+
if( step>0 ) {
|
|
151
|
+
v = min + Math.round( (v-min)/step )*step;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return clamp( v, min, Math.max( min, max ) );
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// the angle of the point of the page, around the center: 0 at the top, clockwise
|
|
158
|
+
private _angleOf( ev: PointerEvent ) {
|
|
159
|
+
const dx = ev.clientX - this._center.x;
|
|
160
|
+
const dy = ev.clientY - this._center.y;
|
|
161
|
+
return Math.atan2( dx, -dy ) * 180 / Math.PI;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private _on_pointerdown( ev: PointerEvent ) {
|
|
165
|
+
ev.stopPropagation( );
|
|
166
|
+
ev.preventDefault( );
|
|
167
|
+
|
|
168
|
+
this.focus( );
|
|
169
|
+
|
|
170
|
+
const rc = this._dial.getBoundingRect( );
|
|
171
|
+
this._center = { x: rc.left + rc.width/2, y: rc.top + rc.height/2 };
|
|
172
|
+
this._angle = this._angleOf( ev );
|
|
173
|
+
this._raw = this._value;
|
|
174
|
+
|
|
175
|
+
this.addClass( "dragging" );
|
|
176
|
+
this._dial.setCapture( ev.pointerId );
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// the knob follows the turn of the pointer, not its position: no jump when it is taken,
|
|
180
|
+
// nor from max to min on a range
|
|
181
|
+
private _on_pointermove( ev: PointerEvent ) {
|
|
182
|
+
if( !this._center ) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const angle = this._angleOf( ev );
|
|
187
|
+
let delta = angle - this._angle;
|
|
188
|
+
|
|
189
|
+
// across ±180°: the short way
|
|
190
|
+
if( delta>180 ) delta -= 360;
|
|
191
|
+
else if( delta<-180 ) delta += 360;
|
|
192
|
+
|
|
193
|
+
this._angle = angle;
|
|
194
|
+
|
|
195
|
+
if( this.props.infinite ) {
|
|
196
|
+
this._raw += delta / 360 * this._turn( );
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const min = this._min( );
|
|
200
|
+
const max = this._max( );
|
|
201
|
+
this._raw = clamp( this._raw + delta / SWEEP * (max-min), min, max );
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this._set( this._fix( this._raw ), true );
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private _on_pointerup( ev: PointerEvent ) {
|
|
208
|
+
if( this._center ) {
|
|
209
|
+
this._dial.releaseCapture( ev.pointerId );
|
|
210
|
+
this.removeClass( "dragging" );
|
|
211
|
+
this._center = null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// infinite: back to the value it started with (0 by default)
|
|
216
|
+
private _on_dblclick( ) {
|
|
217
|
+
if( this.props.infinite ) {
|
|
218
|
+
this._set( this._fix( this.props.value ?? 0 ), true );
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// only with the focus: the wheel over the page scrolls it, and does not turn the knobs it meets
|
|
223
|
+
private _on_wheel( ev: WheelEvent ) {
|
|
224
|
+
if( !this.hasFocus( ) || !ev.deltaY ) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
ev.preventDefault( );
|
|
229
|
+
this._inc( ev.deltaY<0 ? 1 : -1, ev.ctrlKey );
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private _on_key( ev: KeyboardEvent ) {
|
|
233
|
+
switch( ev.key ) {
|
|
234
|
+
case "ArrowRight":
|
|
235
|
+
case "ArrowUp": this._inc( 1, ev.ctrlKey ); break;
|
|
236
|
+
|
|
237
|
+
case "ArrowLeft":
|
|
238
|
+
case "ArrowDown": this._inc( -1, ev.ctrlKey ); break;
|
|
239
|
+
|
|
240
|
+
case "PageUp": this._inc( 1, true ); break;
|
|
241
|
+
case "PageDown": this._inc( -1, true ); break;
|
|
242
|
+
|
|
243
|
+
case "Home": {
|
|
244
|
+
if( this.props.infinite ) return;
|
|
245
|
+
this._set( this._min( ), true );
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case "End": {
|
|
250
|
+
if( this.props.infinite ) return;
|
|
251
|
+
this._set( this._fix( this._max( ) ), true );
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
default:
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
ev.preventDefault( );
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// a step, ten with ctrl
|
|
263
|
+
private _inc( sens: number, big: boolean ) {
|
|
264
|
+
const step = this._step( ) || 1;
|
|
265
|
+
this._set( this._fix( this._value + sens * step * (big ? 10 : 1) ), true );
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// notify: a change made by the user (pointer, wheel, keys); not by the code, nor at the construction
|
|
269
|
+
private _set( v: number, notify: boolean ) {
|
|
270
|
+
if( v===this._value ) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this._value = v;
|
|
275
|
+
this._update( );
|
|
276
|
+
|
|
277
|
+
if( notify ) {
|
|
278
|
+
this.fire( "change", { value: v } );
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private _update( ) {
|
|
283
|
+
let angle: number;
|
|
284
|
+
|
|
285
|
+
if( this.props.infinite ) {
|
|
286
|
+
angle = ( this._value / this._turn( ) * 360 ) % 360;
|
|
287
|
+
this.setAria( "aria-valuenow", this._value );
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const min = this._min( );
|
|
291
|
+
const max = this._max( );
|
|
292
|
+
const perc = max>min ? (this._value-min) / (max-min) : 0;
|
|
293
|
+
|
|
294
|
+
angle = -SWEEP/2 + perc * SWEEP;
|
|
295
|
+
this.setStyleVariable( "--knob-perc", perc*SWEEP + "deg" );
|
|
296
|
+
|
|
297
|
+
this.setAria( "aria-valuemin", min );
|
|
298
|
+
this.setAria( "aria-valuemax", max );
|
|
299
|
+
this.setAria( "aria-valuenow", this._value );
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.setStyleVariable( "--knob-angle", angle + "deg" );
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// no label nor icon: no room under the dial
|
|
306
|
+
private _showLabel( ) {
|
|
307
|
+
this._label.show( !!( this.props.label || this.props.icon ) );
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
setLabel( text: string | UnsafeHtml ) {
|
|
311
|
+
this.props.label = text;
|
|
312
|
+
this._label.setText( text );
|
|
313
|
+
this._showLabel( );
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
setIcon( icon: string ) {
|
|
317
|
+
this.props.icon = icon;
|
|
318
|
+
this._label.setIcon( icon );
|
|
319
|
+
this._showLabel( );
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
setMin( min: number ) {
|
|
323
|
+
this.props.min = min;
|
|
324
|
+
this._value = this._fix( this._value );
|
|
325
|
+
this._update( );
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
setMax( max: number ) {
|
|
329
|
+
this.props.max = max;
|
|
330
|
+
this._value = this._fix( this._value );
|
|
331
|
+
this._update( );
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
setValue( v: number ) {
|
|
335
|
+
this._set( this._fix( v ), false );
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
getValue( ) {
|
|
339
|
+
return this._value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
override queryInterface<T extends IComponentInterface>( name: string ): T {
|
|
343
|
+
if( name=="form-element" ) {
|
|
344
|
+
const i: IFormElement = {
|
|
345
|
+
getRawValue: ( ) => this.getValue( ),
|
|
346
|
+
setRawValue: ( v: any ) => { this.setValue( +v ); },
|
|
347
|
+
isValid: ( ) => true,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return i as unknown as T;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return super.queryInterface( name );
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file mapview.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
|
+
--mapview-background: var( --background-ternary );
|
|
19
|
+
--mapview-marker: var( --accent-background );
|
|
20
|
+
--mapview-tile-filter: none; // the dark theme darkens the tiles (themes.scss)
|
|
21
|
+
--mapview-credit-background: color-mix( in srgb, var( --background-primary ) 75%, transparent );
|
|
22
|
+
--mapview-shape-opacity: 0.2;
|
|
23
|
+
--mapview-shape-opacity-hover: 0.35;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.x4mapview {
|
|
27
|
+
position: relative;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
width: 400px;
|
|
30
|
+
height: 300px;
|
|
31
|
+
background-color: var( --mapview-background );
|
|
32
|
+
outline: none;
|
|
33
|
+
touch-action: none;
|
|
34
|
+
cursor: grab;
|
|
35
|
+
|
|
36
|
+
&.dragging {
|
|
37
|
+
cursor: grabbing;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&:focus-visible {
|
|
41
|
+
outline: 1px solid var( --border-focus );
|
|
42
|
+
outline-offset: -1px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.tiles,
|
|
46
|
+
.markers {
|
|
47
|
+
position: absolute;
|
|
48
|
+
inset: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// the front layer over the back one (the zoom before, while it grows or shrinks)
|
|
52
|
+
.tiles.front {
|
|
53
|
+
z-index: 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.tiles.back {
|
|
57
|
+
transition: transform 0.25s ease-out;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// the zones: only their shapes take the pointer, the map is moved through the rest
|
|
62
|
+
.shapes {
|
|
63
|
+
position: absolute;
|
|
64
|
+
inset: 0;
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 100%;
|
|
67
|
+
z-index: 2;
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
|
|
71
|
+
// the palette of Chart, or the color of the zone
|
|
72
|
+
@for $i from 1 through 8 {
|
|
73
|
+
.c#{$i} {
|
|
74
|
+
--shape-c: var( --chart-color-#{$i} );
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.shape {
|
|
79
|
+
fill: var( --shape-c );
|
|
80
|
+
fill-opacity: var( --mapview-shape-opacity );
|
|
81
|
+
stroke: var( --shape-c );
|
|
82
|
+
stroke-width: 2px;
|
|
83
|
+
stroke-linejoin: round;
|
|
84
|
+
pointer-events: visiblePainted;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
transition: fill-opacity 0.15s;
|
|
87
|
+
|
|
88
|
+
&:hover {
|
|
89
|
+
fill-opacity: var( --mapview-shape-opacity-hover );
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.line {
|
|
94
|
+
fill: none;
|
|
95
|
+
stroke-width: 4px;
|
|
96
|
+
stroke-linecap: round;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.markers {
|
|
101
|
+
z-index: 3;
|
|
102
|
+
pointer-events: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// two fingers: the markers and the zones would not follow the tiles
|
|
106
|
+
&.pinching {
|
|
107
|
+
.markers,
|
|
108
|
+
.shapes {
|
|
109
|
+
visibility: hidden;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.tile {
|
|
114
|
+
position: absolute;
|
|
115
|
+
width: 256px;
|
|
116
|
+
height: 256px;
|
|
117
|
+
max-width: none;
|
|
118
|
+
opacity: 0;
|
|
119
|
+
transition: opacity 0.2s;
|
|
120
|
+
filter: var( --mapview-tile-filter );
|
|
121
|
+
|
|
122
|
+
&.loaded {
|
|
123
|
+
opacity: 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// the point of the pin on the place
|
|
128
|
+
.marker {
|
|
129
|
+
position: absolute;
|
|
130
|
+
width: 24px;
|
|
131
|
+
height: 32px;
|
|
132
|
+
transform: translate( -50%, -100% );
|
|
133
|
+
color: var( --mapview-marker );
|
|
134
|
+
filter: drop-shadow( 0 1px 2px rgba( 0, 0, 0, 0.4 ) );
|
|
135
|
+
pointer-events: auto;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
|
|
138
|
+
svg {
|
|
139
|
+
width: 100%;
|
|
140
|
+
height: 100%;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.zoom {
|
|
145
|
+
position: absolute;
|
|
146
|
+
z-index: 4;
|
|
147
|
+
top: 8px;
|
|
148
|
+
left: 8px;
|
|
149
|
+
gap: 2px;
|
|
150
|
+
cursor: default;
|
|
151
|
+
|
|
152
|
+
.x4button {
|
|
153
|
+
min-width: 28px;
|
|
154
|
+
width: 28px;
|
|
155
|
+
height: 28px;
|
|
156
|
+
padding: 0;
|
|
157
|
+
justify-content: center;
|
|
158
|
+
font-size: 16px;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.credit {
|
|
163
|
+
position: absolute;
|
|
164
|
+
z-index: 4;
|
|
165
|
+
right: 0;
|
|
166
|
+
bottom: 0;
|
|
167
|
+
padding: 1px 6px;
|
|
168
|
+
font-size: 11px;
|
|
169
|
+
background-color: var( --mapview-credit-background );
|
|
170
|
+
cursor: default;
|
|
171
|
+
user-select: text;
|
|
172
|
+
|
|
173
|
+
a {
|
|
174
|
+
color: inherit;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|