x4js 2.3.28 → 2.3.29
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 +380 -19
- package/package.json +7 -3
- 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/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/planner/planner.module.scss +688 -0
- package/src/components/planner/planner.ts +1792 -0
- package/src/components/radio/radio.module.scss +4 -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 +19 -6
- package/src/core/core_i18n.ts +65 -0
- package/src/core/core_tools.ts +44 -0
- package/src/x4doc.ts +3 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file bargraph.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 { Application } from '../../core/core_application';
|
|
18
|
+
import { Color } from '../../core/core_colors';
|
|
19
|
+
import { clamp } from '../../core/core_tools';
|
|
20
|
+
|
|
21
|
+
import { Canvas, CanvasProps } from '../canvas/canvas';
|
|
22
|
+
import { CanvasEx } from '../canvas/canvas_ex';
|
|
23
|
+
|
|
24
|
+
import './bargraph.module.scss';
|
|
25
|
+
|
|
26
|
+
// no colors given: the accent, pale to full (variables: the theme gives them)
|
|
27
|
+
const accentStops = [
|
|
28
|
+
'var(--bargraph-color-start)',
|
|
29
|
+
'var(--bargraph-color-end)',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const PEAK_HOLD = 1000; // ms the peak stays
|
|
33
|
+
const PEAK_FALL = 1500; // ms to fall the whole range
|
|
34
|
+
|
|
35
|
+
export interface BargraphProps extends Omit<CanvasProps, "paint" | "paint_cb" | "clear"> {
|
|
36
|
+
value?: number;
|
|
37
|
+
/** 0 to 100 by default */
|
|
38
|
+
min?: number;
|
|
39
|
+
max?: number;
|
|
40
|
+
/** the segments go up from the bottom; else from the left */
|
|
41
|
+
vertical?: boolean;
|
|
42
|
+
/** the count of segments, 20 by default */
|
|
43
|
+
segments?: number;
|
|
44
|
+
/** the colors from min to max, as Gauge (linearColorStops: green to red); one color: a plain bar.
|
|
45
|
+
* none: the accent, pale to full */
|
|
46
|
+
colors?: string[];
|
|
47
|
+
/** the segments that start at this value or above have the overshoot color (alert); the colors go under it */
|
|
48
|
+
overshoot?: number;
|
|
49
|
+
/** the bar goes up quickly and down slowly, in a few frames; false: at once (true by default) */
|
|
50
|
+
animated?: boolean;
|
|
51
|
+
/** the highest segment reached stays lit a second, then falls */
|
|
52
|
+
peak?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* a bar of segments, lit from min up to the value (a level meter), drawn in a canvas.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* new Bargraph( { vertical: true, overshoot: 85, peak: true } )
|
|
60
|
+
* meter.setValue( level ) // as often as needed
|
|
61
|
+
* each segment has the color of its place between the colors; an unlit one, a trace of it.
|
|
62
|
+
*
|
|
63
|
+
* setValue can be called many times a second: the bars are drawn once a frame, all together,
|
|
64
|
+
* and only when a segment changes.
|
|
65
|
+
*
|
|
66
|
+
* @cssvar
|
|
67
|
+
* ```
|
|
68
|
+
* --bargraph-color-start
|
|
69
|
+
* --bargraph-color-end
|
|
70
|
+
* --bargraph-overshoot
|
|
71
|
+
* --bargraph-off
|
|
72
|
+
* --bargraph-off-mix
|
|
73
|
+
* --bargraph-gap
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
export class Bargraph extends Canvas<BargraphProps> {
|
|
78
|
+
static classPrefix = "x4";
|
|
79
|
+
|
|
80
|
+
private _value = 0; // the value given
|
|
81
|
+
private _shown = 0; // the value drawn: it goes to _value when animated
|
|
82
|
+
private _peak = 0; // the peak drawn
|
|
83
|
+
private _peakTime = 0;
|
|
84
|
+
|
|
85
|
+
// what is drawn: the counts of segments
|
|
86
|
+
private _lit = 0;
|
|
87
|
+
private _peakLit = 0;
|
|
88
|
+
|
|
89
|
+
// the colors of the segments, read from the css; null: to read again
|
|
90
|
+
private _on: string[] = null;
|
|
91
|
+
private _off: string[] = null;
|
|
92
|
+
private _theme = ""; // the theme of the colors read
|
|
93
|
+
private _gap = 2;
|
|
94
|
+
|
|
95
|
+
constructor( props: BargraphProps ) {
|
|
96
|
+
super( props );
|
|
97
|
+
|
|
98
|
+
if( props.vertical ) {
|
|
99
|
+
this.addClass( 'vertical' );
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.setAttribute( "role", "meter" );
|
|
103
|
+
|
|
104
|
+
this._value = this._shown = this._peak = props.value ?? this._min( );
|
|
105
|
+
this._aria( );
|
|
106
|
+
this._count( );
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _min( ) { return this.props.min ?? 0; }
|
|
110
|
+
private _max( ) { return this.props.max ?? 100; }
|
|
111
|
+
private _segments( ) { return Math.max( 1, this.props.segments ?? 20 ); }
|
|
112
|
+
|
|
113
|
+
private _aria( ) {
|
|
114
|
+
this.setAria( "aria-valuemin", this._min( ) );
|
|
115
|
+
this.setAria( "aria-valuemax", this._max( ) );
|
|
116
|
+
this.setAria( "aria-valuenow", this._value );
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// :: animation: one loop for all the bargraphs, while one of them moves :::::::
|
|
120
|
+
|
|
121
|
+
private static _moving = new Set<Bargraph>( );
|
|
122
|
+
private static _raf = 0;
|
|
123
|
+
private static _last = 0;
|
|
124
|
+
|
|
125
|
+
private static _tick( now: number ) {
|
|
126
|
+
const dt = Bargraph._last ? Math.min( now - Bargraph._last, 100 ) : 16.7;
|
|
127
|
+
Bargraph._last = now;
|
|
128
|
+
Bargraph._raf = 0;
|
|
129
|
+
|
|
130
|
+
for( const b of Bargraph._moving ) {
|
|
131
|
+
if( !b._step( now, dt ) ) {
|
|
132
|
+
Bargraph._moving.delete( b );
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if( Bargraph._moving.size ) {
|
|
137
|
+
Bargraph._raf = requestAnimationFrame( Bargraph._tick );
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
Bargraph._last = 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private _animate( ) {
|
|
145
|
+
Bargraph._moving.add( this );
|
|
146
|
+
if( !Bargraph._raf ) {
|
|
147
|
+
Bargraph._raf = requestAnimationFrame( Bargraph._tick );
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// a frame: false when it does not move anymore
|
|
152
|
+
private _step( now: number, dt: number ) {
|
|
153
|
+
const range = Math.abs( this._max( ) - this._min( ) ) || 1;
|
|
154
|
+
const delta = this._value - this._shown;
|
|
155
|
+
|
|
156
|
+
if( this.props.animated===false ) {
|
|
157
|
+
this._shown = this._value;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// up quickly, down slowly: as a level meter
|
|
161
|
+
this._shown += delta * ( 1 - Math.pow( delta>0 ? 0.5 : 0.8, dt/16.7 ) );
|
|
162
|
+
if( Math.abs( this._value - this._shown ) < range/1000 ) {
|
|
163
|
+
this._shown = this._value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let moving = this._shown!==this._value;
|
|
168
|
+
|
|
169
|
+
if( this.props.peak ) {
|
|
170
|
+
if( this._shown>=this._peak ) {
|
|
171
|
+
this._peak = this._shown;
|
|
172
|
+
this._peakTime = now;
|
|
173
|
+
}
|
|
174
|
+
else if( now - this._peakTime > PEAK_HOLD ) {
|
|
175
|
+
this._peak = Math.max( this._shown, this._peak - range * dt / PEAK_FALL );
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
moving ||= this._peak>this._shown;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if( this._count( ) ) {
|
|
182
|
+
this.redraw( );
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return moving;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// the counts of segments lit; true when they changed
|
|
189
|
+
private _count( ) {
|
|
190
|
+
const min = this._min( );
|
|
191
|
+
const max = this._max( );
|
|
192
|
+
const n = this._segments( );
|
|
193
|
+
const at = ( v: number ) => max>min ? Math.round( clamp( (v-min) / (max-min), 0, 1 ) * n ) : 0;
|
|
194
|
+
|
|
195
|
+
const lit = at( this._shown );
|
|
196
|
+
const peak = this.props.peak ? at( this._peak ) : 0;
|
|
197
|
+
|
|
198
|
+
if( lit==this._lit && peak==this._peakLit ) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this._lit = lit;
|
|
203
|
+
this._peakLit = peak;
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// :: colors ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
208
|
+
|
|
209
|
+
private _recolor( ) {
|
|
210
|
+
this._on = null;
|
|
211
|
+
this.redraw( );
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// the variables as the bargraph sees them: a box may give its own
|
|
215
|
+
private _readColors( ) {
|
|
216
|
+
const style = getComputedStyle( this.dom );
|
|
217
|
+
this._theme = Application.getTheme( );
|
|
218
|
+
const read = ( value: string ) => new Color( value.replace( /var\(\s*(--[\w-]+)\s*\)/g, ( _, name ) => style.getPropertyValue( name ).trim( ) ) );
|
|
219
|
+
const copy = ( c: Color ) => { const x = c.toRgb( ); return new Color( x.red, x.green, x.blue, x.alpha ); };
|
|
220
|
+
|
|
221
|
+
const n = this._segments( );
|
|
222
|
+
const min = this._min( );
|
|
223
|
+
const size = ( this._max( ) - min ) / n;
|
|
224
|
+
const overshoot = this.props.overshoot;
|
|
225
|
+
|
|
226
|
+
// the segments under the overshoot: the colors are spread on them only
|
|
227
|
+
let low = n;
|
|
228
|
+
if( overshoot!==undefined ) {
|
|
229
|
+
low = 0;
|
|
230
|
+
while( low<n && min + low*size < overshoot ) {
|
|
231
|
+
low++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const stops = ( this.props.colors?.length ? this.props.colors : accentStops ).map( read );
|
|
236
|
+
const over = read( "var(--bargraph-overshoot)" );
|
|
237
|
+
const off = read( "var(--bargraph-off)" );
|
|
238
|
+
const offMix = ( parseFloat( style.getPropertyValue( "--bargraph-off-mix" ) ) || 15 ) / 100;
|
|
239
|
+
const disabled = this.isDisabled( ) ? read( "var(--disabled-color-light)" ) : null;
|
|
240
|
+
|
|
241
|
+
// the color at pos (0 to 1) between the stops
|
|
242
|
+
const colorAt = ( pos: number ) => {
|
|
243
|
+
if( stops.length==1 ) {
|
|
244
|
+
return copy( stops[0] );
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const at = pos * (stops.length-1);
|
|
248
|
+
const k = Math.min( Math.floor( at ), stops.length-2 );
|
|
249
|
+
return copy( stops[k] ).mix( stops[k+1], at-k );
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
this._on = [];
|
|
253
|
+
this._off = [];
|
|
254
|
+
|
|
255
|
+
for( let i=0; i<n; i++ ) {
|
|
256
|
+
const on = i<low ? colorAt( low>1 ? i/(low-1) : 0 ) : copy( over );
|
|
257
|
+
|
|
258
|
+
// a trace of its color
|
|
259
|
+
this._off.push( copy( off ).mix( on, offMix ).toRgbString( true ) );
|
|
260
|
+
this._on.push( ( disabled ?? on ).toRgbString( true ) );
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this._gap = parseFloat( style.getPropertyValue( "--bargraph-gap" ) ) || 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// :: drawing :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
267
|
+
|
|
268
|
+
// in pixels of the screen, not of the page: the segments stay sharp at any zoom.
|
|
269
|
+
// the theme has no event: it is compared at each drawing (a bargraph that does not move
|
|
270
|
+
// keeps the colors it has until it is drawn again: refresh)
|
|
271
|
+
protected override paint( ctx: CanvasEx ) {
|
|
272
|
+
if( !this._on || this._theme!=Application.getTheme( ) ) {
|
|
273
|
+
this._readColors( );
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const canvas = this.canvas.dom as HTMLCanvasElement;
|
|
277
|
+
const W = canvas.width;
|
|
278
|
+
const H = canvas.height;
|
|
279
|
+
|
|
280
|
+
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
|
|
281
|
+
ctx.clearRect( 0, 0, W, H );
|
|
282
|
+
|
|
283
|
+
const vertical = !!this.props.vertical;
|
|
284
|
+
const n = this._on.length;
|
|
285
|
+
const gap = Math.round( this._gap * ( window.devicePixelRatio || 1 ) );
|
|
286
|
+
const len = vertical ? H : W;
|
|
287
|
+
|
|
288
|
+
for( let i=0; i<n; i++ ) {
|
|
289
|
+
const a = Math.round( i * (len+gap) / n );
|
|
290
|
+
const b = Math.round( (i+1) * (len+gap) / n ) - gap;
|
|
291
|
+
const lit = i<this._lit || i==this._peakLit-1;
|
|
292
|
+
|
|
293
|
+
ctx.fillStyle = lit ? this._on[i] : this._off[i];
|
|
294
|
+
|
|
295
|
+
if( vertical ) {
|
|
296
|
+
ctx.fillRect( 0, H-b, W, b-a );
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
ctx.fillRect( a, 0, b-a, H );
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// :: api :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* drawn at the next frame: call it as often as needed
|
|
308
|
+
*/
|
|
309
|
+
|
|
310
|
+
setValue( v: number ) {
|
|
311
|
+
this._value = v;
|
|
312
|
+
this.setAria( "aria-valuenow", v );
|
|
313
|
+
this._animate( );
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
getValue( ) {
|
|
317
|
+
return this._value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
setMin( min: number ) {
|
|
321
|
+
this.props.min = min;
|
|
322
|
+
this._changed( );
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
setMax( max: number ) {
|
|
326
|
+
this.props.max = max;
|
|
327
|
+
this._changed( );
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
setColorStops( colors: string[] ) {
|
|
331
|
+
this.props.colors = colors;
|
|
332
|
+
this._changed( );
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** undefined: no overshoot */
|
|
336
|
+
setOvershoot( overshoot: number ) {
|
|
337
|
+
this.props.overshoot = overshoot;
|
|
338
|
+
this._changed( );
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* read the colors again and draw: after a change of the css variables, of the theme
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
refresh( ) {
|
|
346
|
+
this._recolor( );
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// disabled: the segments lit take the disabled color
|
|
350
|
+
override enable( ena = true ): this {
|
|
351
|
+
super.enable( ena );
|
|
352
|
+
this._recolor( );
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private _changed( ) {
|
|
357
|
+
this._aria( );
|
|
358
|
+
this._on = null;
|
|
359
|
+
this._count( );
|
|
360
|
+
this.redraw( );
|
|
361
|
+
}
|
|
362
|
+
}
|
package/src/components/base.scss
CHANGED
|
@@ -108,4 +108,74 @@
|
|
|
108
108
|
option:checked {
|
|
109
109
|
background-color: var( --accent-background );
|
|
110
110
|
color: var( --accent-color );
|
|
111
|
-
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
// :: label-top ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
115
|
+
// The label above its field, smaller and discreet, darker while the field has the focus: the
|
|
116
|
+
// class label-top on a TextEdit (an AutoComplete), a Combobox, a TextArea, or on any box that
|
|
117
|
+
// holds them (a Form, the main view: all the application). labelWidth is not used.
|
|
118
|
+
|
|
119
|
+
:root {
|
|
120
|
+
--label-top-color: var( --text-secondary );
|
|
121
|
+
--label-top-color-focus: var( --text-primary ); // the field has the focus: darker, lighter in a dark theme
|
|
122
|
+
--label-top-size: 0.85em;
|
|
123
|
+
--label-top-gap: 2px; // between the label and the field
|
|
124
|
+
--label-top-gap-textarea: 4px; // the same, above a TextArea
|
|
125
|
+
--label-top-suffix: ":"; // after the label, as when it is on the left
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// TextEdit (and AutoComplete), Combobox: the line becomes a column
|
|
129
|
+
:is( .label-top .x4textedit, .x4textedit.label-top, .label-top .x4combobox, .x4combobox.label-top ) {
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
align-items: stretch;
|
|
132
|
+
gap: var( --label-top-gap );
|
|
133
|
+
|
|
134
|
+
// labelWidth is not used: the label has all the width
|
|
135
|
+
& > #label {
|
|
136
|
+
width: auto !important;
|
|
137
|
+
justify-content: start;
|
|
138
|
+
font-size: var( --label-top-size );
|
|
139
|
+
|
|
140
|
+
& > .x4label {
|
|
141
|
+
width: auto !important;
|
|
142
|
+
height: auto;
|
|
143
|
+
justify-content: start;
|
|
144
|
+
font-weight: normal;
|
|
145
|
+
--label-color: var( --label-top-color );
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
& > #edit {
|
|
150
|
+
flex: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
&:focus-within > #label > .x4label {
|
|
154
|
+
--label-color: var( --label-top-color-focus );
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// the colon of a TextEdit is after its #label, the one of a Combobox after its text
|
|
159
|
+
:is( .label-top .x4textedit, .x4textedit.label-top ) > #label::after {
|
|
160
|
+
content: var( --label-top-suffix );
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
:is( .label-top .x4combobox, .x4combobox.label-top ) > #label > .x4label #text:not(.empty)::after {
|
|
164
|
+
content: var( --label-top-suffix );
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// TextArea: its label is above already; the same look
|
|
168
|
+
:is( .label-top .x4textarea, .x4textarea.label-top ) > .x4label {
|
|
169
|
+
font-size: var( --label-top-size );
|
|
170
|
+
font-weight: normal;
|
|
171
|
+
padding: 0 0 var( --label-top-gap-textarea );
|
|
172
|
+
--label-color: var( --label-top-color );
|
|
173
|
+
|
|
174
|
+
&::after {
|
|
175
|
+
content: var( --label-top-suffix );
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
:is( .label-top .x4textarea, .x4textarea.label-top ):focus-within > .x4label {
|
|
180
|
+
--label-color: var( --label-top-color-focus );
|
|
181
|
+
}
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
--calendar-header-color: var( --text-primary );
|
|
38
38
|
--calendar-out-color: var( --disabled-color-dark );
|
|
39
|
+
--calendar-disabled-background: var( --background-ternary );
|
|
39
40
|
--calendar-day-color: var( --text-primary );
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -142,7 +143,6 @@
|
|
|
142
143
|
|
|
143
144
|
.out {
|
|
144
145
|
color: var( --calendar-out-color );
|
|
145
|
-
font-size: 80%;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
.day:hover {
|
|
@@ -152,16 +152,17 @@
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
// out of minDate..maxDate: cannot be chosen
|
|
155
|
+
// out of minDate..maxDate: cannot be chosen; a background on all their cell (a band over the
|
|
156
|
+
// days closed), their text as the other days
|
|
156
157
|
.disabled {
|
|
157
|
-
color: var( --calendar-
|
|
158
|
-
text-decoration: line-through;
|
|
158
|
+
background-color: var( --calendar-disabled-background );
|
|
159
159
|
cursor: default;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// the hover of a day: none on them
|
|
162
163
|
.disabled:hover .text {
|
|
163
164
|
background-color: transparent;
|
|
164
|
-
color:
|
|
165
|
+
color: inherit;
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
**/
|
|
16
16
|
|
|
17
17
|
import { Component, ComponentEvents, ComponentProps, EvChange, Flex } from '../../core/component'
|
|
18
|
-
import { date_clone, date_hash, formatIntlDate, Point, unsafeHtml } from "../../core/core_tools"
|
|
18
|
+
import { date_clone, date_hash, date_week_start, formatIntlDate, Point, unsafeHtml } from "../../core/core_tools"
|
|
19
19
|
import { _tr } from '../../core/core_i18n';
|
|
20
20
|
import { EventCallback } from '../../core/core_events';
|
|
21
21
|
|
|
@@ -66,6 +66,7 @@ interface CalendarProps extends ComponentProps {
|
|
|
66
66
|
* --calendar-today-border
|
|
67
67
|
* --calendar-header-color
|
|
68
68
|
* --calendar-out-color
|
|
69
|
+
* --calendar-disabled-background
|
|
69
70
|
* --calendar-day-color
|
|
70
71
|
* ```
|
|
71
72
|
*/
|
|
@@ -89,16 +90,11 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
89
90
|
|
|
90
91
|
private _update( ) {
|
|
91
92
|
|
|
93
|
+
// the first line: the week of the 1st, from its monday
|
|
92
94
|
let month_start = date_clone(this.m_date);
|
|
93
95
|
month_start.setDate(1);
|
|
94
96
|
|
|
95
|
-
let
|
|
96
|
-
if (day == 0) {
|
|
97
|
-
day = 7;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
month_start.setDate(-day + 1 + 1);
|
|
101
|
-
let dte = date_clone(month_start);
|
|
97
|
+
let dte = date_week_start(month_start);
|
|
102
98
|
|
|
103
99
|
let selection = date_hash( this.m_date );
|
|
104
100
|
let today = date_hash( new Date() );
|
|
@@ -287,7 +283,17 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
287
283
|
let items: MenuItem[] = [];
|
|
288
284
|
|
|
289
285
|
if (type == 'month') {
|
|
286
|
+
// the months of the year shown that have days between minDate and maxDate (as year * 12 + month)
|
|
287
|
+
const year = this.m_date.getFullYear( );
|
|
288
|
+
const { minDate, maxDate } = this.props;
|
|
289
|
+
const first = minDate ? minDate.getFullYear( ) * 12 + minDate.getMonth( ) : -Infinity;
|
|
290
|
+
const last = maxDate ? maxDate.getFullYear( ) * 12 + maxDate.getMonth( ) : Infinity;
|
|
291
|
+
|
|
290
292
|
for (let m = 0; m < 12; m++) {
|
|
293
|
+
if( year * 12 + m < first || year * 12 + m > last ) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
291
297
|
items.push(({
|
|
292
298
|
text: _tr.global.month_long[m],
|
|
293
299
|
click: () => { this._goto( this.m_date.getFullYear( ), m ); }
|
|
@@ -24,7 +24,7 @@ export interface EvPaint extends ComponentEvent {
|
|
|
24
24
|
ctx: CanvasEx;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
interface CanvasEventMap extends ComponentEvents {
|
|
27
|
+
export interface CanvasEventMap extends ComponentEvents {
|
|
28
28
|
paint: EvPaint;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -41,10 +41,11 @@ export interface CanvasProps extends ComponentProps {
|
|
|
41
41
|
// ============================================================================
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Standard Canvas
|
|
44
|
+
* Standard Canvas; a class derived from it gives its props and its events:
|
|
45
|
+
* class Scope extends Canvas<ScopeProps, ScopeEvents>
|
|
45
46
|
*/
|
|
46
47
|
|
|
47
|
-
export class Canvas extends
|
|
48
|
+
export class Canvas<P extends CanvasProps = CanvasProps, E extends CanvasEventMap = CanvasEventMap> extends Component<P, E> {
|
|
48
49
|
static classPrefix = "x4";
|
|
49
50
|
|
|
50
51
|
private m_iwidth: number = -1;
|
|
@@ -52,7 +53,7 @@ export class Canvas extends Component<CanvasProps, CanvasEventMap> {
|
|
|
52
53
|
private m_scale = 1.0;
|
|
53
54
|
private m_canvas: Component;
|
|
54
55
|
|
|
55
|
-
constructor(props:
|
|
56
|
+
constructor(props: P) {
|
|
56
57
|
super(props);
|
|
57
58
|
|
|
58
59
|
this.mapPropEvents( props, 'paint' );
|