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,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file carousel.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
|
+
--carousel-gap: 16px;
|
|
19
|
+
--carousel-duration: 0.4s;
|
|
20
|
+
--carousel-arrow-background: color-mix( in srgb, var( --background-primary ) 80%, transparent );
|
|
21
|
+
--carousel-arrow-color: var( --text-primary );
|
|
22
|
+
--carousel-dot: var( --border-hover );
|
|
23
|
+
--carousel-dot-active: var( --accent-background );
|
|
24
|
+
--carousel-radius: 8px;
|
|
25
|
+
--carousel-caption-color: #ffffff;
|
|
26
|
+
--carousel-caption-background: rgba( 0, 0, 0, 0.6 );
|
|
27
|
+
--carousel-disabled-filter: grayscale( 1 );
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// the code sets --carousel-index (the first slide seen), --carousel-per-view, --carousel-drag (the swipe, px)
|
|
31
|
+
.x4carousel {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 8px;
|
|
35
|
+
outline: none;
|
|
36
|
+
|
|
37
|
+
// the width of a slide: the view less the gaps between the slides seen, shared
|
|
38
|
+
--carousel-slide: calc( ( 100% - ( var( --carousel-per-view, 1 ) - 1 ) * var( --carousel-gap ) ) / var( --carousel-per-view, 1 ) );
|
|
39
|
+
|
|
40
|
+
// disabled: inert (enable), faded as a Box, its images grey
|
|
41
|
+
&[disabled] {
|
|
42
|
+
opacity: var( --box-disabled-opacity );
|
|
43
|
+
filter: var( --carousel-disabled-filter );
|
|
44
|
+
|
|
45
|
+
// a disabled Box in it: faded once, not twice
|
|
46
|
+
.x4box[disabled] {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&:focus-visible .viewport {
|
|
52
|
+
outline: 1px solid var( --border-focus );
|
|
53
|
+
outline-offset: 2px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.viewport {
|
|
57
|
+
position: relative;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
flex: 1;
|
|
60
|
+
min-height: 0;
|
|
61
|
+
touch-action: pan-y; // across: the page scrolls; along: the swipe
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// percents of a translate are the width of the track: the one of the view
|
|
65
|
+
.track {
|
|
66
|
+
display: flex;
|
|
67
|
+
gap: var( --carousel-gap );
|
|
68
|
+
height: 100%;
|
|
69
|
+
transform: translateX( calc( var( --carousel-index, 0 ) * -1 * ( var( --carousel-slide ) + var( --carousel-gap ) ) + var( --carousel-drag, 0px ) ) );
|
|
70
|
+
transition: transform var( --carousel-duration ) ease;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// the slide follows the finger
|
|
74
|
+
&.dragging {
|
|
75
|
+
cursor: grabbing;
|
|
76
|
+
|
|
77
|
+
.track {
|
|
78
|
+
transition: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.slide {
|
|
83
|
+
flex: 0 0 var( --carousel-slide );
|
|
84
|
+
min-width: 0;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// an image: all the slide, its caption over its bottom
|
|
89
|
+
.slide.image {
|
|
90
|
+
position: relative;
|
|
91
|
+
border-radius: var( --carousel-radius );
|
|
92
|
+
|
|
93
|
+
.x4image {
|
|
94
|
+
width: 100%;
|
|
95
|
+
height: 100%;
|
|
96
|
+
background-color: var( --background-ternary ); // while it loads
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.caption {
|
|
100
|
+
position: absolute;
|
|
101
|
+
left: 0;
|
|
102
|
+
right: 0;
|
|
103
|
+
bottom: 0;
|
|
104
|
+
padding: 24px 16px 12px;
|
|
105
|
+
color: var( --carousel-caption-color );
|
|
106
|
+
background: linear-gradient( transparent, var( --carousel-caption-background ) );
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.arrow {
|
|
111
|
+
position: absolute;
|
|
112
|
+
top: 50%;
|
|
113
|
+
transform: translateY( -50% );
|
|
114
|
+
z-index: 1;
|
|
115
|
+
width: 36px;
|
|
116
|
+
min-width: 36px;
|
|
117
|
+
height: 36px;
|
|
118
|
+
padding: 0;
|
|
119
|
+
border-radius: 50%;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
|
|
122
|
+
--button-background: var( --carousel-arrow-background );
|
|
123
|
+
--button-background-hover: var( --carousel-arrow-background );
|
|
124
|
+
--button-background-active: var( --carousel-arrow-background );
|
|
125
|
+
--button-background-focus: var( --carousel-arrow-background );
|
|
126
|
+
--button-icon-color: var( --carousel-arrow-color );
|
|
127
|
+
--button-background-disabled: transparent;
|
|
128
|
+
|
|
129
|
+
box-shadow: 0 1px 4px rgba( 0, 0, 0, 0.25 );
|
|
130
|
+
|
|
131
|
+
&.prev {
|
|
132
|
+
left: 8px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&.next {
|
|
136
|
+
right: 8px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// at an end: not shown, its place kept
|
|
140
|
+
&[disabled] {
|
|
141
|
+
opacity: 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.dots {
|
|
146
|
+
justify-content: center;
|
|
147
|
+
gap: 8px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.dot {
|
|
151
|
+
width: 8px;
|
|
152
|
+
height: 8px;
|
|
153
|
+
padding: 0;
|
|
154
|
+
border: none;
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
background-color: var( --carousel-dot );
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
transition: width var( --carousel-duration ), background-color var( --carousel-duration );
|
|
159
|
+
|
|
160
|
+
&.selected {
|
|
161
|
+
width: 20px;
|
|
162
|
+
background-color: var( --carousel-dot-active );
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file carousel.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 { _tr } from '../../core/core_i18n';
|
|
20
|
+
import { clamp, sprintf, UnsafeHtml } from '../../core/core_tools';
|
|
21
|
+
|
|
22
|
+
import { HBox } from '../boxes/boxes';
|
|
23
|
+
import { Button } from '../button/button';
|
|
24
|
+
import { Image, ImageProps } from '../image/image';
|
|
25
|
+
|
|
26
|
+
import './carousel.module.scss';
|
|
27
|
+
|
|
28
|
+
export interface CarouselEvents extends ComponentEvents {
|
|
29
|
+
change: EvChange;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** a slide that is an image: loaded when it comes near the view */
|
|
33
|
+
export interface CarouselImage {
|
|
34
|
+
src: string;
|
|
35
|
+
alt?: string;
|
|
36
|
+
/** over the bottom of the image */
|
|
37
|
+
caption?: string | UnsafeHtml;
|
|
38
|
+
/** cover by default: the image fills the slide, cut; contain: all of it, with margins */
|
|
39
|
+
fit?: "cover" | "contain";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CarouselItem = Component | CarouselImage;
|
|
43
|
+
|
|
44
|
+
export interface CarouselProps extends Omit<ComponentProps,"content"> {
|
|
45
|
+
/** the slides: components, or images */
|
|
46
|
+
items?: CarouselItem[];
|
|
47
|
+
/** the slides seen side by side, 1 by default; a move goes one slide further */
|
|
48
|
+
perView?: number;
|
|
49
|
+
/** the slide at the start (its index), 0 by default */
|
|
50
|
+
index?: number;
|
|
51
|
+
/** after the last slide comes the first one, and before the first the last */
|
|
52
|
+
loop?: boolean;
|
|
53
|
+
/** ms between two slides: it goes by itself (and loops), paused while the pointer is over it or the focus in it */
|
|
54
|
+
autoplay?: number;
|
|
55
|
+
/** the buttons previous and next on its sides; true by default */
|
|
56
|
+
arrows?: boolean;
|
|
57
|
+
/** a dot for each place, under the slides; true by default */
|
|
58
|
+
dots?: boolean;
|
|
59
|
+
/** the user (arrows, dots, swipe, keys) or the autoplay went to another slide: value is its index; not by the code */
|
|
60
|
+
change?: EventCallback<EvChange>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const icon_prev = `<svg viewBox="0 0 24 24"><path d="M15.4 5.4L14 4l-8 8 8 8 1.4-1.4L8.8 12z"/></svg>`;
|
|
64
|
+
const icon_next = `<svg viewBox="0 0 24 24"><path d="M8.6 5.4L10 4l8 8-8 8-1.4-1.4 6.6-6.6z"/></svg>`;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* slides side by side, one place at a time (or perView): arrows, dots, a swipe with the finger or
|
|
68
|
+
* the mouse, the arrow keys, and an autoplay. A slide is a component, or an image.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* new Carousel( {
|
|
72
|
+
* height: 300, autoplay: 5000,
|
|
73
|
+
* items: photos.map( p => ( { src: p.url, alt: p.title, caption: p.title } ) ),
|
|
74
|
+
* } )
|
|
75
|
+
*
|
|
76
|
+
* @cssvar
|
|
77
|
+
* ```
|
|
78
|
+
* --carousel-gap
|
|
79
|
+
* --carousel-duration
|
|
80
|
+
* --carousel-arrow-background
|
|
81
|
+
* --carousel-arrow-color
|
|
82
|
+
* --carousel-dot
|
|
83
|
+
* --carousel-dot-active
|
|
84
|
+
* --carousel-radius
|
|
85
|
+
* --carousel-caption-color
|
|
86
|
+
* --carousel-caption-background
|
|
87
|
+
* --carousel-disabled-filter
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
export class Carousel extends Component<CarouselProps,CarouselEvents> {
|
|
92
|
+
static classPrefix = "x4";
|
|
93
|
+
|
|
94
|
+
private _index = 0;
|
|
95
|
+
private _slides: Component[] = [];
|
|
96
|
+
private _images = new Map<number, { image: Image, src: string }>( ); // slides that are images, not loaded yet
|
|
97
|
+
|
|
98
|
+
private _viewport: Component;
|
|
99
|
+
private _track: Component;
|
|
100
|
+
private _prev: Button;
|
|
101
|
+
private _next: Button;
|
|
102
|
+
private _dots: HBox;
|
|
103
|
+
|
|
104
|
+
// swipe: where it started; dragging once it moved more than a click
|
|
105
|
+
private _swipe: { id: number, x: number, y: number, dragging: boolean } = null;
|
|
106
|
+
|
|
107
|
+
// autoplay: paused while the pointer is over it, the focus in it, or a swipe
|
|
108
|
+
private _hover = false;
|
|
109
|
+
private _focus = false;
|
|
110
|
+
|
|
111
|
+
constructor( props: CarouselProps ) {
|
|
112
|
+
super( props );
|
|
113
|
+
|
|
114
|
+
this.mapPropEvents( props, "change" );
|
|
115
|
+
|
|
116
|
+
this.setContent( [
|
|
117
|
+
this._viewport = new Component( { cls: "viewport", content: [
|
|
118
|
+
this._track = new Component( { cls: "track" } ),
|
|
119
|
+
this._prev = new Button( { cls: "arrow prev", icon: icon_prev, tabindex: false, attrs: { "aria-label": _tr.global.carousel.prev }, click: ( ) => this._go( this._index-1 ) } ),
|
|
120
|
+
this._next = new Button( { cls: "arrow next", icon: icon_next, tabindex: false, attrs: { "aria-label": _tr.global.carousel.next }, click: ( ) => this._go( this._index+1 ) } ),
|
|
121
|
+
] } ),
|
|
122
|
+
this._dots = new HBox( { cls: "dots" } ),
|
|
123
|
+
] );
|
|
124
|
+
|
|
125
|
+
this.setAttribute( "tabindex", 0 );
|
|
126
|
+
this.setAttribute( "role", "region" );
|
|
127
|
+
this.setAria( "aria-roledescription", _tr.global.carousel.carousel );
|
|
128
|
+
|
|
129
|
+
this._viewport.setDOMEvents( {
|
|
130
|
+
pointerdown: ( ev ) => this._on_pointerdown( ev ),
|
|
131
|
+
pointermove: ( ev ) => this._on_pointermove( ev ),
|
|
132
|
+
pointerup: ( ev ) => this._on_pointerup( ev ),
|
|
133
|
+
pointercancel: ( ev ) => this._on_pointerup( ev ),
|
|
134
|
+
// an image dragged by the browser would take the swipe
|
|
135
|
+
dragstart: ( ev ) => ev.preventDefault( ),
|
|
136
|
+
} );
|
|
137
|
+
|
|
138
|
+
this.setDOMEvents( {
|
|
139
|
+
keydown: ( ev ) => this._on_key( ev ),
|
|
140
|
+
pointerenter: ( ) => { this._hover = true; },
|
|
141
|
+
pointerleave: ( ) => { this._hover = false; },
|
|
142
|
+
focusin: ( ) => { this._focus = true; },
|
|
143
|
+
focusout: ( ) => { this._focus = false; },
|
|
144
|
+
// the timers of a component are not stopped when it leaves the page
|
|
145
|
+
created: ( ) => this._autoplay( ),
|
|
146
|
+
removed: ( ) => this.clearTimeouts( ),
|
|
147
|
+
} );
|
|
148
|
+
|
|
149
|
+
this.setItems( props.items ?? [], props.index ?? 0 );
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private _perView( ) { return Math.max( 1, Math.round( this.props.perView ?? 1 ) ); }
|
|
153
|
+
|
|
154
|
+
// the last place: the last slides fill the view
|
|
155
|
+
private _maxIndex( ) { return Math.max( 0, this._slides.length - this._perView( ) ); }
|
|
156
|
+
|
|
157
|
+
private _loops( ) { return !!this.props.loop || !!this.props.autoplay; }
|
|
158
|
+
|
|
159
|
+
// :: places ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
160
|
+
|
|
161
|
+
// beyond the ends, the other end when it loops; notify: the user (arrows, dots, swipe, keys), not the code
|
|
162
|
+
private _go( index: number, notify = true ) {
|
|
163
|
+
const max = this._maxIndex( );
|
|
164
|
+
|
|
165
|
+
if( this._loops( ) ) {
|
|
166
|
+
index = index>max ? 0 : ( index<0 ? max : index );
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// a move of the user starts the time of the autoplay again
|
|
170
|
+
this._autoplay( );
|
|
171
|
+
|
|
172
|
+
if( this._show( index ) && notify ) {
|
|
173
|
+
this.fire( "change", { value: this._index } );
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// true when the place changed
|
|
178
|
+
private _show( index: number ) {
|
|
179
|
+
index = clamp( index, 0, this._maxIndex( ) );
|
|
180
|
+
const changed = index!=this._index;
|
|
181
|
+
|
|
182
|
+
this._index = index;
|
|
183
|
+
this.setStyleVariable( "--carousel-index", index+"" );
|
|
184
|
+
|
|
185
|
+
// the images of the slides seen, and of their neighbours: ready for the next move
|
|
186
|
+
const pv = this._perView( );
|
|
187
|
+
this._load( index-1, index+pv );
|
|
188
|
+
|
|
189
|
+
// the slides out of the view: no focus, no reader
|
|
190
|
+
this._slides.forEach( ( s, i ) => {
|
|
191
|
+
const seen = i>=index && i<index+pv;
|
|
192
|
+
s.setAttribute( "inert", !seen );
|
|
193
|
+
s.setAria( "aria-hidden", !seen );
|
|
194
|
+
} );
|
|
195
|
+
|
|
196
|
+
this._dots.enumChildComponents( false ).forEach( ( d, i ) => {
|
|
197
|
+
d.setClass( "selected", i==index );
|
|
198
|
+
d.setAria( "aria-current", i==index );
|
|
199
|
+
} );
|
|
200
|
+
|
|
201
|
+
const loops = this._loops( );
|
|
202
|
+
this._prev.enable( loops || index>0 );
|
|
203
|
+
this._next.enable( loops || index<this._maxIndex( ) );
|
|
204
|
+
|
|
205
|
+
return changed;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private _load( from: number, to: number ) {
|
|
209
|
+
const n = this._slides.length;
|
|
210
|
+
if( !n ) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for( let i=from; i<=to; i++ ) {
|
|
215
|
+
// the neighbours of the ends, when it loops
|
|
216
|
+
const k = ( ( i % n ) + n ) % n;
|
|
217
|
+
const img = this._images.get( k );
|
|
218
|
+
if( img ) {
|
|
219
|
+
img.image.setImage( img.src );
|
|
220
|
+
this._images.delete( k );
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private _buildSlide( item: CarouselItem ) {
|
|
226
|
+
if( item instanceof Component ) {
|
|
227
|
+
return item;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src given when it comes near the view (_load)
|
|
231
|
+
const image = new Image<ImageProps>( { src: "", alt: item.alt, fit: item.fit ?? "cover" } );
|
|
232
|
+
const content: Component[] = [ image ];
|
|
233
|
+
|
|
234
|
+
if( item.caption ) {
|
|
235
|
+
content.push( new Component( { cls: "caption", content: item.caption } ) );
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { image, src: item.src, content };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// :: autoplay ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
242
|
+
|
|
243
|
+
private _autoplay( ) {
|
|
244
|
+
const ms = this.props.autoplay;
|
|
245
|
+
if( !ms ) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// a named timer: starting it again replaces it
|
|
250
|
+
this.setInterval( "autoplay", ms, ( ) => {
|
|
251
|
+
if( this._hover || this._focus || this._swipe || document.hidden ) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const index = this._index>=this._maxIndex( ) ? 0 : this._index+1;
|
|
256
|
+
if( this._show( index ) ) {
|
|
257
|
+
this.fire( "change", { value: this._index } );
|
|
258
|
+
}
|
|
259
|
+
} );
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// :: swipe :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
263
|
+
|
|
264
|
+
private _on_pointerdown( ev: PointerEvent ) {
|
|
265
|
+
if( ev.button!=0 || Component.closest( ev.target as Element, ".arrow" ) ) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this._swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY, dragging: false };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private _on_pointermove( ev: PointerEvent ) {
|
|
273
|
+
const sw = this._swipe;
|
|
274
|
+
if( !sw || sw.id!=ev.pointerId ) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let dx = ev.clientX - sw.x;
|
|
279
|
+
|
|
280
|
+
if( !sw.dragging ) {
|
|
281
|
+
// more than a click; along the slides, not across (the page scrolls)
|
|
282
|
+
if( Math.abs( dx )<6 || Math.abs( dx )<Math.abs( ev.clientY - sw.y ) ) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// captured: the click at the end goes to the viewport, not to what is in the slide
|
|
287
|
+
sw.dragging = true;
|
|
288
|
+
this._viewport.setCapture( ev.pointerId );
|
|
289
|
+
this.addClass( "dragging" );
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// at an end, it resists
|
|
293
|
+
const max = this._maxIndex( );
|
|
294
|
+
if( !this._loops( ) && ( ( this._index==0 && dx>0 ) || ( this._index==max && dx<0 ) ) ) {
|
|
295
|
+
dx /= 3;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.setStyleVariable( "--carousel-drag", dx+"px" );
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// far enough (a fifth of a slide): to the next places, as many as the slides it went over
|
|
302
|
+
private _on_pointerup( ev: PointerEvent ) {
|
|
303
|
+
const sw = this._swipe;
|
|
304
|
+
if( !sw || sw.id!=ev.pointerId ) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this._swipe = null;
|
|
309
|
+
if( !sw.dragging ) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this._viewport.releaseCapture( ev.pointerId );
|
|
314
|
+
this.removeClass( "dragging" );
|
|
315
|
+
this.setStyleVariable( "--carousel-drag", "0px" );
|
|
316
|
+
|
|
317
|
+
// taken by the browser (the page scrolls): back to its place
|
|
318
|
+
if( ev.type=="pointercancel" ) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const dx = ev.clientX - sw.x;
|
|
323
|
+
const gap = parseFloat( this._track.getComputedStyle( ).columnGap ) || 0;
|
|
324
|
+
const step = ( this._viewport.getBoundingRect( ).width + gap ) / this._perView( );
|
|
325
|
+
|
|
326
|
+
let n = Math.round( -dx / step );
|
|
327
|
+
if( n==0 && Math.abs( dx )>step/5 ) {
|
|
328
|
+
n = dx<0 ? 1 : -1;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if( n ) {
|
|
332
|
+
this._go( this._index + n );
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private _on_key( ev: KeyboardEvent ) {
|
|
337
|
+
switch( ev.key ) {
|
|
338
|
+
case "ArrowLeft": this._go( this._index-1 ); break;
|
|
339
|
+
case "ArrowRight": this._go( this._index+1 ); break;
|
|
340
|
+
case "Home": this._go( 0 ); break;
|
|
341
|
+
case "End": this._go( this._maxIndex( ) ); break;
|
|
342
|
+
default: return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
ev.preventDefault( );
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// :: api :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* the slides; index: the place to show (the current one by default)
|
|
352
|
+
*/
|
|
353
|
+
|
|
354
|
+
setItems( items: CarouselItem[], index = this._index ) {
|
|
355
|
+
const n = items.length;
|
|
356
|
+
this._images.clear( );
|
|
357
|
+
|
|
358
|
+
this._slides = items.map( ( item, i ) => {
|
|
359
|
+
const built = this._buildSlide( item );
|
|
360
|
+
let s: Component;
|
|
361
|
+
|
|
362
|
+
if( built instanceof Component ) {
|
|
363
|
+
s = new Component( { cls: "slide", content: built } );
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
s = new Component( { cls: "slide image", content: built.content } );
|
|
367
|
+
this._images.set( i, { image: built.image, src: built.src } );
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
s.setAttribute( "role", "group" );
|
|
371
|
+
s.setAria( "aria-roledescription", _tr.global.carousel.slide );
|
|
372
|
+
s.setAria( "aria-label", sprintf( _tr.global.carousel.position, i+1, n ) );
|
|
373
|
+
return s;
|
|
374
|
+
} );
|
|
375
|
+
|
|
376
|
+
this._track.setContent( this._slides );
|
|
377
|
+
this.setStyleVariable( "--carousel-per-view", this._perView( )+"" );
|
|
378
|
+
|
|
379
|
+
// a dot for each place
|
|
380
|
+
const places = this._maxIndex( ) + 1;
|
|
381
|
+
this._dots.setContent( Array.from( { length: places }, ( _, i ) => new Component( {
|
|
382
|
+
tag: "button", cls: "dot",
|
|
383
|
+
attrs: { type: "button", tabindex: -1, "aria-label": sprintf( _tr.global.carousel.position, i+1, places ) },
|
|
384
|
+
dom_events: { click: ( ) => this._go( i ) },
|
|
385
|
+
} ) ) );
|
|
386
|
+
|
|
387
|
+
// nothing to move: no commands
|
|
388
|
+
const moves = places>1;
|
|
389
|
+
this._prev.show( moves && this.props.arrows!==false );
|
|
390
|
+
this._next.show( moves && this.props.arrows!==false );
|
|
391
|
+
this._dots.show( moves && this.props.dots!==false );
|
|
392
|
+
|
|
393
|
+
this._index = -1;
|
|
394
|
+
this._show( index );
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* goes to a slide, without a change event; beyond the ends: the end
|
|
399
|
+
*/
|
|
400
|
+
|
|
401
|
+
select( index: number ) {
|
|
402
|
+
this._show( index );
|
|
403
|
+
this._autoplay( );
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** the next place, the first after the last when it loops; without a change event */
|
|
407
|
+
next( ) {
|
|
408
|
+
this._go( this._index+1, false );
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** the place before, the last before the first when it loops; without a change event */
|
|
412
|
+
prev( ) {
|
|
413
|
+
this._go( this._index-1, false );
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
getIndex( ) {
|
|
417
|
+
return this._index;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
getCount( ) {
|
|
421
|
+
return this._slides.length;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -60,6 +60,14 @@
|
|
|
60
60
|
stroke: var( --chart-axis );
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// radar: the rings and the axes are oblique, crispEdges would make them jagged
|
|
64
|
+
.radar {
|
|
65
|
+
.grid,
|
|
66
|
+
.base {
|
|
67
|
+
shape-rendering: auto;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
// --chart-c is the color of the element (palette slot, or serie.color)
|
|
64
72
|
@for $i from 1 through 8 {
|
|
65
73
|
.c#{$i} {
|