x4js 2.3.21 → 2.3.22
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 +81 -4
- package/package.json +1 -1
- package/src/components/components.ts +1 -0
- package/src/components/keyboard/keyboard.ts +25 -3
- package/src/components/settingsview/settingsview.module.scss +178 -0
- package/src/components/settingsview/settingsview.ts +289 -0
- package/src/core/core_i18n.ts +157 -0
package/aicontext.md
CHANGED
|
@@ -41,7 +41,7 @@ CoreElement<E> named timers + event surface (no DOM)
|
|
|
41
41
|
│ ├── CSizer → HSizer / VSizer
|
|
42
42
|
│ ├── FileDialog, ScrollView, Viewport, DockingView, Flex, Space
|
|
43
43
|
│ ├── HBox-based: ColorInput, Rating, Switch, Tag, Keyboard, TextEdit → AutoComplete, Breadcrumbs
|
|
44
|
-
│ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea
|
|
44
|
+
│ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea, SettingsView, SettingItem
|
|
45
45
|
└── Application<E> singleton, no DOM of its own
|
|
46
46
|
|
|
47
47
|
EventSource<E> standalone event registry
|
|
@@ -1153,6 +1153,78 @@ class Form extends Box {
|
|
|
1153
1153
|
|
|
1154
1154
|
---
|
|
1155
1155
|
|
|
1156
|
+
## `SettingsView` — a page of settings, as the settings of VS Code
|
|
1157
|
+
|
|
1158
|
+
The summary of the sections on the left, the sections on the right, one after the other, each a panel with its title on a band; in a section, each `SettingItem` with its name, what it does, then its controls. A click in the summary brings its section to the top; the summary follows the scroll and marks the section at the top of the view.
|
|
1159
|
+
|
|
1160
|
+
```ts
|
|
1161
|
+
interface SettingItemProps extends BoxProps {
|
|
1162
|
+
title: string
|
|
1163
|
+
description?: string // under the name
|
|
1164
|
+
content: Component | Component[] // its controls, on a line that wraps
|
|
1165
|
+
}
|
|
1166
|
+
class SettingItem extends VBox {
|
|
1167
|
+
getSearchText(): string // the title and the description: what a search looks in
|
|
1168
|
+
}
|
|
1169
|
+
// A SettingItem has no value of its own: its controls keep theirs. To read and write them together,
|
|
1170
|
+
// put the items in a Form (a section can hold a Form of SettingItem), give the controls a name,
|
|
1171
|
+
// and use getValues / setValues. The item is marked on its left while one of its controls has the focus.
|
|
1172
|
+
|
|
1173
|
+
interface SettingsSection {
|
|
1174
|
+
id: string
|
|
1175
|
+
title: string
|
|
1176
|
+
description?: string // under the title
|
|
1177
|
+
content: Component[] // SettingItem, Form of SettingItem, or any component
|
|
1178
|
+
}
|
|
1179
|
+
interface SettingsViewProps extends BoxProps {
|
|
1180
|
+
sections?: SettingsSection[]
|
|
1181
|
+
searchable?: boolean // a search field above the sections
|
|
1182
|
+
searchPlaceholder?: string // _tr.global.settings.search when missing
|
|
1183
|
+
noMatch?: string // shown when the search finds nothing; _tr.global.settings.nomatch when missing
|
|
1184
|
+
sectionChange?: EventCallback<EvSectionChange>
|
|
1185
|
+
}
|
|
1186
|
+
interface EvSectionChange extends ComponentEvent { readonly section: string } // the id of the section at the top
|
|
1187
|
+
class SettingsView<P extends SettingsViewProps = SettingsViewProps> extends VBox<P, SettingsViewEvents> {
|
|
1188
|
+
setSections(sections: SettingsSection[]): void // replaces the sections
|
|
1189
|
+
showSection(id: string, smooth = true): void // brings a section to the top
|
|
1190
|
+
getSection(): string // the id of the section at the top
|
|
1191
|
+
}
|
|
1192
|
+
// Search: the words typed are looked for without case nor accents; an item is shown when its title or
|
|
1193
|
+
// its description holds all of them. A section whose title or description holds them is shown whole;
|
|
1194
|
+
// otherwise only its items that hold them, and what holds those items (a Form, a box). What holds no
|
|
1195
|
+
// item (a table, a text) is shown with its whole section only. Escape empties the field.
|
|
1196
|
+
// The section marked in the summary is the last one whose top is above the top of the view (40px of
|
|
1197
|
+
// margin); the first one while the view is hidden (in a tab not shown). The last section shown is at
|
|
1198
|
+
// least as high as the view (class "last"): a click in the summary can bring any section to the top.
|
|
1199
|
+
// Classes: .x4settingsview > .search > .x4input, > .body > .toc (a Listbox), > .body > .sections;
|
|
1200
|
+
// .section(.last) > .title, .description; .sections > .nomatch; .x4settingitem > .name, .description, .controls
|
|
1201
|
+
// CSS variables: --settingsview-toc-width, --settingsview-section-background,
|
|
1202
|
+
// --settingsview-section-title-background, --settingsview-hover-background, --settingsview-marker
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
A page that builds its sections with references to its own components (`this.refs.x = ...`) cannot give them to `super()`: `this` is not there yet. It calls `setSections` after `super()`. `SettingsView` keeps its own parts in private fields, not in `refs`: `refs` is left to the classes that derive from it.
|
|
1206
|
+
|
|
1207
|
+
```ts
|
|
1208
|
+
class MySettings extends SettingsView {
|
|
1209
|
+
declare protected refs: { size: Input }
|
|
1210
|
+
|
|
1211
|
+
constructor(props: BoxProps) {
|
|
1212
|
+
super({ ...props, searchable: true })
|
|
1213
|
+
|
|
1214
|
+
this.setSections([
|
|
1215
|
+
{ id: "editor", title: "Editor", content: [
|
|
1216
|
+
new Form({ content: [
|
|
1217
|
+
new SettingItem({ title: "Font size", description: "The size of the text, in pixels.",
|
|
1218
|
+
content: this.refs.size = new Input({ type: "number", name: "fontSize", value: 14 }) }),
|
|
1219
|
+
] }),
|
|
1220
|
+
] },
|
|
1221
|
+
])
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
---
|
|
1227
|
+
|
|
1156
1228
|
## Sizers / viewport / misc
|
|
1157
1229
|
|
|
1158
1230
|
```ts
|
|
@@ -1184,7 +1256,10 @@ class ColorPicker extends VBox { // props: color, swatches?, nullable?
|
|
|
1184
1256
|
}
|
|
1185
1257
|
class Panel extends VBox { setTitle(title: string): void } // the text of its title: its icon stays
|
|
1186
1258
|
class Breadcrumbs extends HBox { setItems(elements): void }
|
|
1187
|
-
interface KeyboardProps extends BoxProps {
|
|
1259
|
+
interface KeyboardProps extends BoxProps {
|
|
1260
|
+
locale?: string // "fr-FR" (default), "de-CH", "pl"...
|
|
1261
|
+
numpadOnly?: boolean // number or date field: the pad has no "abc" key back to the letters
|
|
1262
|
+
}
|
|
1188
1263
|
class Keyboard extends HBox { // virtual keyboard, shown while an <input> has the focus
|
|
1189
1264
|
setLocale(locale: string): void
|
|
1190
1265
|
setZoom(perc: number): void
|
|
@@ -1192,6 +1267,7 @@ class Keyboard extends HBox { // virtual keyboard, shown while an
|
|
|
1192
1267
|
}
|
|
1193
1268
|
// languages: cs da de en es et fi fr ga hr hu it lt lv mt nl pl pt ro sk sl sv
|
|
1194
1269
|
// bottom row: 123 (numeric pad), the letters of the language (éè, none in en), the signs (#+=); Abc comes back
|
|
1270
|
+
// a key acts on pointerdown; the keys that draw the keyboard again (shift, 123, abc, accents, signs) on the click
|
|
1195
1271
|
class TickLine extends Component
|
|
1196
1272
|
class Header extends HBox // resizable column header; updates target style "--{name}-width"
|
|
1197
1273
|
```
|
|
@@ -1409,7 +1485,7 @@ dragManager.registerDropTarget(column, (cmd, el, infos) => {
|
|
|
1409
1485
|
|
|
1410
1486
|
## i18n (`core_i18n`)
|
|
1411
1487
|
|
|
1412
|
-
|
|
1488
|
+
Four built-in languages: `fr` (default), `en` (based on `fr`), `es` and `de` (based on `en`). Dates: `D/M/Y` in fr and es, `M/D/Y` in en, `D.M.Y` in de.
|
|
1413
1489
|
|
|
1414
1490
|
```ts
|
|
1415
1491
|
export let _tr // current translation object (proxy)
|
|
@@ -1426,7 +1502,8 @@ isLanguage(name: string): boolean
|
|
|
1426
1502
|
// search search_tip required_field invalid_format invalid_email invalid_number invalid_date
|
|
1427
1503
|
// empty_list err_403 property value copy cut paste filedrop
|
|
1428
1504
|
// diff_date_seconds/minutes/hours, date_input_formats, date_format, date_time_format,
|
|
1429
|
-
// day_short[] day_long[] month_short[] month_long[], keyboard.{next,numeric,alpha}
|
|
1505
|
+
// day_short[] day_long[] month_short[] month_long[], keyboard.{next,numeric,alpha},
|
|
1506
|
+
// settings.{search,nomatch}
|
|
1430
1507
|
```
|
|
1431
1508
|
|
|
1432
1509
|
---
|
package/package.json
CHANGED
|
@@ -35,6 +35,7 @@ export * from "./propgrid/propgrid"
|
|
|
35
35
|
export * from "./radio/radio"
|
|
36
36
|
export * from "./rating/rating"
|
|
37
37
|
export * from "./select/select"
|
|
38
|
+
export * from "./settingsview/settingsview"
|
|
38
39
|
export * from "./sizers/sizer"
|
|
39
40
|
export * from "./slider/slider"
|
|
40
41
|
export * from "./spreadsheet/spreadsheet"
|
|
@@ -239,6 +239,14 @@ function accentLabel( layout: KbLayout, upper: boolean ): string {
|
|
|
239
239
|
return ( upper ? upperKeys( label ) : label ).replace( " ", "" );
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
/**
|
|
243
|
+
* the keys that draw the keyboard again: shift, 123, abc, accents, signs
|
|
244
|
+
*/
|
|
245
|
+
|
|
246
|
+
function redrawKey( key: number ) {
|
|
247
|
+
return key==4 || key==5 || key==8 || key==10 || key==11;
|
|
248
|
+
}
|
|
249
|
+
|
|
242
250
|
|
|
243
251
|
const RE_sel = /text|password|search|tel|url/i;
|
|
244
252
|
|
|
@@ -248,6 +256,12 @@ interface KeyboardProps extends BoxProps {
|
|
|
248
256
|
* "de-CH", "pl"... "fr-FR" when it is not given
|
|
249
257
|
*/
|
|
250
258
|
locale?: string;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* on a number or date field, the keyboard stays on the pad: no "abc" key
|
|
262
|
+
* back to the letters. The other fields keep all the keys.
|
|
263
|
+
*/
|
|
264
|
+
numpadOnly?: boolean;
|
|
251
265
|
}
|
|
252
266
|
|
|
253
267
|
/**
|
|
@@ -284,7 +298,8 @@ export class Keyboard extends HBox<KeyboardProps>
|
|
|
284
298
|
|
|
285
299
|
this.hide( );
|
|
286
300
|
|
|
287
|
-
// a key: each contact, a finger or the mouse, the second tap of a quick double tap too
|
|
301
|
+
// a key: each contact, a finger or the mouse, the second tap of a quick double tap too.
|
|
302
|
+
// not the keys that draw the keyboard again, they act on the click
|
|
288
303
|
this.addDOMEvent( "pointerdown", (e) => {
|
|
289
304
|
this.handleKeyEvent( e );
|
|
290
305
|
});
|
|
@@ -352,7 +367,7 @@ export class Keyboard extends HBox<KeyboardProps>
|
|
|
352
367
|
target = target.parentNode as HTMLElement;
|
|
353
368
|
}
|
|
354
369
|
|
|
355
|
-
if( !key ) {
|
|
370
|
+
if( !key || redrawKey( key ) ) {
|
|
356
371
|
return;
|
|
357
372
|
}
|
|
358
373
|
|
|
@@ -631,6 +646,11 @@ export class Keyboard extends HBox<KeyboardProps>
|
|
|
631
646
|
|
|
632
647
|
if( this.mode=="number" || this.mode=="date" ) {
|
|
633
648
|
lines = kb_pads[this.mode];
|
|
649
|
+
|
|
650
|
+
// no key back to the letters: its place stays empty
|
|
651
|
+
if( this.props.numpadOnly ) {
|
|
652
|
+
lines = lines.map( line => line.replace( "{8}", "{9}" ) );
|
|
653
|
+
}
|
|
634
654
|
}
|
|
635
655
|
else if( this.mode=="accents" ) {
|
|
636
656
|
lines = accentLines( layout, this.upper );
|
|
@@ -765,7 +785,9 @@ export class Keyboard extends HBox<KeyboardProps>
|
|
|
765
785
|
icon,
|
|
766
786
|
autorepeat: repeat,
|
|
767
787
|
click: ( e ) => {
|
|
768
|
-
|
|
788
|
+
// the keys that draw the keyboard again act on the click: the mousedown
|
|
789
|
+
// that follows a pointerdown would go to a key no longer in the page
|
|
790
|
+
if( e.repeat || redrawKey( key ) ) {
|
|
769
791
|
this._handleKey( key )
|
|
770
792
|
}
|
|
771
793
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file settingsview.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
|
+
--settingsview-toc-width: 210px;
|
|
19
|
+
--settingsview-section-background: var( --color-primary-a10 );
|
|
20
|
+
--settingsview-section-title-background: var( --color-primary-a20 );
|
|
21
|
+
--settingsview-hover-background: color-mix( in srgb, var( --color-primary-a20 ) 60%, transparent );
|
|
22
|
+
--settingsview-marker: var( --accent-background );
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.x4settingsview {
|
|
26
|
+
min-height: 0;
|
|
27
|
+
|
|
28
|
+
> .search {
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
padding: 12px 32px 12px calc( var( --settingsview-toc-width ) + 32px );
|
|
31
|
+
border-bottom: 1px solid var( --border );
|
|
32
|
+
|
|
33
|
+
> .x4input {
|
|
34
|
+
flex: 1;
|
|
35
|
+
max-width: 900px;
|
|
36
|
+
margin: 0;
|
|
37
|
+
padding: 6px 10px;
|
|
38
|
+
border: 1px solid var( --border );
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
|
|
41
|
+
&:focus {
|
|
42
|
+
border-color: var( --settingsview-marker );
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
> .body {
|
|
48
|
+
min-height: 0;
|
|
49
|
+
align-items: stretch;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// the summary: the section at the top in the color of the text, marked on its left
|
|
53
|
+
.toc {
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
width: var( --settingsview-toc-width );
|
|
56
|
+
margin: 0;
|
|
57
|
+
padding: 12px 0;
|
|
58
|
+
border: none;
|
|
59
|
+
border-right: 1px solid var( --border );
|
|
60
|
+
border-radius: 0;
|
|
61
|
+
background: transparent;
|
|
62
|
+
|
|
63
|
+
.x4item {
|
|
64
|
+
padding: 4px 4px 4px 16px;
|
|
65
|
+
border-left: 2px solid transparent;
|
|
66
|
+
border-radius: 0;
|
|
67
|
+
--label-color: var( --text-secondary );
|
|
68
|
+
|
|
69
|
+
&:hover {
|
|
70
|
+
background: transparent;
|
|
71
|
+
--label-color: var( --text-primary );
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&.selected {
|
|
75
|
+
background: transparent;
|
|
76
|
+
border-left-color: var( --settingsview-marker );
|
|
77
|
+
--label-color: var( --text-primary );
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.sections {
|
|
84
|
+
align-items: flex-start;
|
|
85
|
+
gap: 24px;
|
|
86
|
+
min-height: 0;
|
|
87
|
+
overflow-y: auto;
|
|
88
|
+
padding: 20px 32px 32px;
|
|
89
|
+
|
|
90
|
+
> .nomatch {
|
|
91
|
+
padding: 8px;
|
|
92
|
+
--label-color: var( --text-secondary );
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// a section: a panel, its title on a band
|
|
97
|
+
.section {
|
|
98
|
+
flex-shrink: 0;
|
|
99
|
+
width: 100%;
|
|
100
|
+
max-width: 900px;
|
|
101
|
+
padding: 0 8px 10px;
|
|
102
|
+
border: 1px solid var( --border );
|
|
103
|
+
border-radius: 8px;
|
|
104
|
+
background: var( --settingsview-section-background );
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
scroll-margin-top: 20px; // brought to the top: below the padding of the view
|
|
107
|
+
|
|
108
|
+
// the last one shown: as high as the view, a click in the summary brings it to the top too
|
|
109
|
+
&.last {
|
|
110
|
+
min-height: 100%;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
> .title {
|
|
114
|
+
margin: 0 -8px 6px;
|
|
115
|
+
padding: 12px 16px;
|
|
116
|
+
border-bottom: 1px solid var( --border );
|
|
117
|
+
background: var( --settingsview-section-title-background );
|
|
118
|
+
font-size: 1.1rem;
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
--label-color: var( --text-primary );
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
> .description {
|
|
124
|
+
padding: 10px 8px 4px;
|
|
125
|
+
white-space: normal;
|
|
126
|
+
--label-color: var( --text-secondary );
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// a section is as high as its content: what it holds takes its own height,
|
|
130
|
+
// a flex child (a Form) would get none
|
|
131
|
+
> * {
|
|
132
|
+
flex: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// a form of settings: the settings set the spaces
|
|
136
|
+
> .x4form {
|
|
137
|
+
padding: 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// a setting: its name, what it does, its controls; marked on its left while one has the focus
|
|
142
|
+
.x4settingitem {
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
gap: 4px;
|
|
145
|
+
padding: 10px 8px 12px;
|
|
146
|
+
border-left: 2px solid transparent;
|
|
147
|
+
border-radius: 4px;
|
|
148
|
+
|
|
149
|
+
&:hover {
|
|
150
|
+
background: var( --settingsview-hover-background );
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
&:focus-within {
|
|
154
|
+
border-left-color: var( --settingsview-marker );
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
> .name {
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
--label-color: var( --text-primary );
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
> .description {
|
|
163
|
+
white-space: normal;
|
|
164
|
+
--label-color: var( --text-secondary );
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
> .controls {
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 10px;
|
|
170
|
+
padding-top: 4px;
|
|
171
|
+
flex-wrap: wrap;
|
|
172
|
+
|
|
173
|
+
> * {
|
|
174
|
+
margin: 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file settingsview.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, ComponentEvent, ComponentEvents } from '../../core/component';
|
|
18
|
+
import { EventCallback } from '../../core/core_events';
|
|
19
|
+
import { class_ns } from '../../core/core_tools';
|
|
20
|
+
|
|
21
|
+
import { BoxProps, HBox, VBox } from '../boxes/boxes';
|
|
22
|
+
import { Input } from '../input/input';
|
|
23
|
+
import { Label } from '../label/label';
|
|
24
|
+
import { Listbox } from '../listbox/listbox';
|
|
25
|
+
|
|
26
|
+
import { _tr } from '../../x4.js';
|
|
27
|
+
|
|
28
|
+
import "./settingsview.module.scss"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
*
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export interface SettingItemProps extends BoxProps {
|
|
35
|
+
title: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
content: Component | Component[]; // its controls
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* one setting: its name, what it does, then its controls on a line that wraps.
|
|
42
|
+
* It has no value of its own: its controls keep theirs. To read and write them
|
|
43
|
+
* together, put the items in a Form and give the controls a name.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
@class_ns( "x4" )
|
|
47
|
+
export class SettingItem extends VBox<SettingItemProps> {
|
|
48
|
+
|
|
49
|
+
constructor( props: SettingItemProps ) {
|
|
50
|
+
// the content goes under the name and the description, not in the box itself
|
|
51
|
+
super( { ...props, content: undefined } );
|
|
52
|
+
|
|
53
|
+
this.setContent( [
|
|
54
|
+
new Label( { cls: "name", text: props.title } ),
|
|
55
|
+
props.description ? new Label( { cls: "description", text: props.description } ) : null,
|
|
56
|
+
new HBox( { cls: "controls", content: props.content } ),
|
|
57
|
+
] );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* the text a search looks in: the name and the description
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
getSearchText( ) {
|
|
65
|
+
return `${this.props.title} ${this.props.description ?? ""}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* a section: a panel, its title on a band, then its content
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
export interface SettingsSection {
|
|
74
|
+
id: string;
|
|
75
|
+
title: string;
|
|
76
|
+
description?: string; // under the title
|
|
77
|
+
content: Component[]; // SettingItem, Form of SettingItem, or any component
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EvSectionChange extends ComponentEvent {
|
|
81
|
+
readonly section: string; // the id of the section now at the top
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SettingsViewProps extends BoxProps {
|
|
85
|
+
sections?: SettingsSection[];
|
|
86
|
+
searchable?: boolean; // a search field above the sections
|
|
87
|
+
searchPlaceholder?: string; // _tr.global.settings.search when missing
|
|
88
|
+
noMatch?: string; // shown when the search finds nothing; _tr.global.settings.nomatch when missing
|
|
89
|
+
sectionChange?: EventCallback<EvSectionChange>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SettingsViewEvents extends ComponentEvents {
|
|
93
|
+
sectionChange: EvSectionChange;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* text compared without case nor accents
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
function normalize( text: string ) {
|
|
101
|
+
return text.normalize( "NFD" ).replace( /[̀-ͯ]/g, "" ).toLowerCase( );
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* a page of settings, as the settings of VS Code: the summary of the sections
|
|
106
|
+
* on the left, the sections on the right, one after the other. A click in the
|
|
107
|
+
* summary brings its section to the top; the summary follows the scroll.
|
|
108
|
+
* With searchable, a field above them filters the settings.
|
|
109
|
+
*
|
|
110
|
+
* @cssvar
|
|
111
|
+
* ```
|
|
112
|
+
* --settingsview-toc-width
|
|
113
|
+
* --settingsview-section-background
|
|
114
|
+
* --settingsview-section-title-background
|
|
115
|
+
* --settingsview-hover-background
|
|
116
|
+
* --settingsview-marker
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
@class_ns( "x4" )
|
|
121
|
+
export class SettingsView<P extends SettingsViewProps = SettingsViewProps> extends VBox<P,SettingsViewEvents> {
|
|
122
|
+
|
|
123
|
+
// fields, not refs: refs belong to the classes that derive from this one
|
|
124
|
+
private _search: Input = null;
|
|
125
|
+
private _toc: Listbox;
|
|
126
|
+
private _view: VBox;
|
|
127
|
+
private _noMatch: Label;
|
|
128
|
+
|
|
129
|
+
// shown: kept by the search (isVisible is false too while the view is hidden)
|
|
130
|
+
private _sections: { section: SettingsSection, box: VBox, shown: boolean }[] = [];
|
|
131
|
+
private _current = ""; // the id of the section at the top
|
|
132
|
+
|
|
133
|
+
constructor( props: P ) {
|
|
134
|
+
super( props );
|
|
135
|
+
this.mapPropEvents( props, "sectionChange" );
|
|
136
|
+
|
|
137
|
+
this.setContent( [
|
|
138
|
+
props.searchable ? new HBox( {
|
|
139
|
+
cls: "search",
|
|
140
|
+
content: this._search = new Input( {
|
|
141
|
+
type: "text",
|
|
142
|
+
placeholder: props.searchPlaceholder ?? _tr.global.settings.search,
|
|
143
|
+
change: ( ) => this._filter( ),
|
|
144
|
+
} ),
|
|
145
|
+
} ) : null,
|
|
146
|
+
|
|
147
|
+
new HBox( {
|
|
148
|
+
cls: "body",
|
|
149
|
+
flex: true,
|
|
150
|
+
content: [
|
|
151
|
+
this._toc = new Listbox( {
|
|
152
|
+
cls: "toc",
|
|
153
|
+
items: [],
|
|
154
|
+
selectionChange: ( ev ) => this.showSection( ev.selection[0] as string ),
|
|
155
|
+
} ),
|
|
156
|
+
this._view = new VBox( { cls: "sections", flex: true } ),
|
|
157
|
+
],
|
|
158
|
+
} ),
|
|
159
|
+
] );
|
|
160
|
+
|
|
161
|
+
this._noMatch = new Label( { cls: "nomatch", text: props.noMatch ?? _tr.global.settings.nomatch, hidden: true } );
|
|
162
|
+
this._view.addDOMEvent( "scroll", ( ) => this._followScroll( ) );
|
|
163
|
+
|
|
164
|
+
// Escape empties the search
|
|
165
|
+
if( this._search ) {
|
|
166
|
+
this._search.addDOMEvent( "keydown", ( ev ) => {
|
|
167
|
+
if( ev.key == "Escape" && this._search.getValue( ) ) {
|
|
168
|
+
this._search.setValue( "" );
|
|
169
|
+
this._filter( );
|
|
170
|
+
}
|
|
171
|
+
} );
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if( props.sections ) {
|
|
175
|
+
this.setSections( props.sections );
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* the sections, in their order; for a class that builds them in its
|
|
181
|
+
* constructor, after super( )
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
setSections( sections: SettingsSection[] ) {
|
|
185
|
+
this._sections = sections.map( section => ( {
|
|
186
|
+
section,
|
|
187
|
+
shown: true,
|
|
188
|
+
box: new VBox( {
|
|
189
|
+
cls: "section",
|
|
190
|
+
content: [
|
|
191
|
+
new Label( { cls: "title", text: section.title } ),
|
|
192
|
+
section.description ? new Label( { cls: "description", text: section.description } ) : null,
|
|
193
|
+
...section.content,
|
|
194
|
+
],
|
|
195
|
+
} ),
|
|
196
|
+
} ) );
|
|
197
|
+
|
|
198
|
+
this._view.setContent( [ ...this._sections.map( s => s.box ), this._noMatch ] );
|
|
199
|
+
this._current = "";
|
|
200
|
+
this._filter( );
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* brings a section to the top of the view
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
showSection( id: string, smooth = true ) {
|
|
208
|
+
this._sections.find( s => s.section.id == id )?.box.scrollIntoView( { behavior: smooth ? "smooth" : "auto", block: "start" } );
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* the id of the section at the top of the view
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
getSection( ) {
|
|
216
|
+
return this._current;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* shows what the search finds; everything when it is empty. A section
|
|
221
|
+
* whose title or description holds the words is shown whole; otherwise
|
|
222
|
+
* only its settings that hold them, and what holds those settings.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
private _filter( ) {
|
|
226
|
+
const text = this._search ? this._search.getValue( ) : "";
|
|
227
|
+
const words = normalize( text ).split( /\s+/ ).filter( w => w );
|
|
228
|
+
const matches = ( text: string ) => {
|
|
229
|
+
const t = normalize( text );
|
|
230
|
+
return words.every( w => t.includes( w ) );
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
let found = false;
|
|
234
|
+
for( const s of this._sections ) {
|
|
235
|
+
const { section, box } = s;
|
|
236
|
+
const whole = !words.length || matches( `${section.title} ${section.description ?? ""}` );
|
|
237
|
+
let shown = whole;
|
|
238
|
+
|
|
239
|
+
for( const c of section.content ) {
|
|
240
|
+
const items = c instanceof SettingItem ? [c] : c.enumChildComponents( true ).filter( x => x instanceof SettingItem ) as SettingItem[];
|
|
241
|
+
let some = false;
|
|
242
|
+
for( const item of items ) {
|
|
243
|
+
const ok = whole || matches( item.getSearchText( ) );
|
|
244
|
+
item.show( ok );
|
|
245
|
+
some ||= ok;
|
|
246
|
+
}
|
|
247
|
+
// what holds no setting (a table, a text) is shown with its whole section only
|
|
248
|
+
c.show( whole || some );
|
|
249
|
+
shown ||= some;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
s.shown = shown;
|
|
253
|
+
box.show( shown );
|
|
254
|
+
found ||= shown;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this._noMatch.show( !found );
|
|
258
|
+
|
|
259
|
+
// the summary lists the sections shown; the last one is as high as the
|
|
260
|
+
// view at least: a click in the summary can bring every section to the top
|
|
261
|
+
const shown = this._sections.filter( s => s.shown );
|
|
262
|
+
this._sections.forEach( s => s.box.setClass( "last", s == shown[shown.length - 1] ) );
|
|
263
|
+
this._toc.clearSelection( false );
|
|
264
|
+
this._toc.setItems( shown.map( s => ( { id: s.section.id, text: s.section.title } ) ) );
|
|
265
|
+
this._current = "";
|
|
266
|
+
this._followScroll( );
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* the summary on the section at the top of the view; the first one while the view is hidden
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
private _followScroll( ) {
|
|
274
|
+
const view = this._view.getBoundingRect( );
|
|
275
|
+
|
|
276
|
+
let current = "";
|
|
277
|
+
for( const { section, box, shown } of this._sections ) {
|
|
278
|
+
if( shown && ( !current || ( view.height && box.getBoundingRect( ).top <= view.top + 40 ) ) ) {
|
|
279
|
+
current = section.id;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this._toc.select( current, false );
|
|
284
|
+
if( current != this._current ) {
|
|
285
|
+
this._current = current;
|
|
286
|
+
this.fire( "sectionChange", { section: current } );
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/core/core_i18n.ts
CHANGED
|
@@ -331,6 +331,11 @@ let fr = {
|
|
|
331
331
|
numeric: '123',
|
|
332
332
|
alpha: 'Abc'
|
|
333
333
|
},
|
|
334
|
+
|
|
335
|
+
settings: {
|
|
336
|
+
search: 'Rechercher un paramètre',
|
|
337
|
+
nomatch: 'Aucun paramètre trouvé',
|
|
338
|
+
},
|
|
334
339
|
}
|
|
335
340
|
};
|
|
336
341
|
|
|
@@ -396,6 +401,151 @@ let en = {
|
|
|
396
401
|
numeric: '123',
|
|
397
402
|
alpha: 'Abc'
|
|
398
403
|
},
|
|
404
|
+
|
|
405
|
+
settings: {
|
|
406
|
+
search: 'Search settings',
|
|
407
|
+
nomatch: 'No setting found',
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/** @ignore */
|
|
413
|
+
|
|
414
|
+
let es = {
|
|
415
|
+
global: {
|
|
416
|
+
ok: 'Aceptar',
|
|
417
|
+
cancel: 'Cancelar',
|
|
418
|
+
ignore: 'Ignorar',
|
|
419
|
+
yes: 'Sí',
|
|
420
|
+
no: 'No',
|
|
421
|
+
abort: "Anular",
|
|
422
|
+
retry: "Reintentar",
|
|
423
|
+
|
|
424
|
+
error: "Error",
|
|
425
|
+
today: "Hoy",
|
|
426
|
+
|
|
427
|
+
open: 'Abrir',
|
|
428
|
+
new: 'Nuevo',
|
|
429
|
+
delete: 'Eliminar',
|
|
430
|
+
close: 'Cerrar',
|
|
431
|
+
save: 'Guardar',
|
|
432
|
+
|
|
433
|
+
search: 'Buscar',
|
|
434
|
+
search_tip: 'Escriba el texto que desea buscar. <b>Intro</b> para iniciar la búsqueda. <b>Esc</b> para cancelar.',
|
|
435
|
+
|
|
436
|
+
required_field: "dato obligatorio",
|
|
437
|
+
invalid_format: "formato no válido",
|
|
438
|
+
invalid_email: 'dirección de correo no válida',
|
|
439
|
+
invalid_number: 'valor numérico no válido',
|
|
440
|
+
|
|
441
|
+
diff_date_seconds: '{0} segundos',
|
|
442
|
+
diff_date_minutes: '{0} minutos',
|
|
443
|
+
diff_date_hours: '{0} horas {1} minutos',
|
|
444
|
+
|
|
445
|
+
invalid_date: 'Fecha no reconocida ({0})',
|
|
446
|
+
empty_list: 'Lista vacía',
|
|
447
|
+
|
|
448
|
+
date_input_formats: 'd/m/y|d.m.y|d m y|d-m-y|dmy',
|
|
449
|
+
date_format: 'D/M/Y',
|
|
450
|
+
date_time_format: 'D/M/Y H:I:S',
|
|
451
|
+
|
|
452
|
+
day_short: [ 'dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb' ],
|
|
453
|
+
day_long: [ 'domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado' ],
|
|
454
|
+
|
|
455
|
+
month_short: [ 'ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic' ],
|
|
456
|
+
month_long: [ 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre' ],
|
|
457
|
+
|
|
458
|
+
property: 'Propiedad',
|
|
459
|
+
value: 'Valor',
|
|
460
|
+
|
|
461
|
+
err_403: `No tiene permisos suficientes para realizar esta acción`,
|
|
462
|
+
|
|
463
|
+
copy: 'Copiar',
|
|
464
|
+
cut: 'Cortar',
|
|
465
|
+
paste: 'Pegar',
|
|
466
|
+
|
|
467
|
+
filedrop: 'Suelte un archivo',
|
|
468
|
+
|
|
469
|
+
keyboard: {
|
|
470
|
+
next: 'Siguiente',
|
|
471
|
+
numeric: '123',
|
|
472
|
+
alpha: 'Abc'
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
settings: {
|
|
476
|
+
search: 'Buscar un ajuste',
|
|
477
|
+
nomatch: 'Ningún ajuste encontrado',
|
|
478
|
+
},
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
/** @ignore */
|
|
483
|
+
|
|
484
|
+
let de = {
|
|
485
|
+
global: {
|
|
486
|
+
ok: 'OK',
|
|
487
|
+
cancel: 'Abbrechen',
|
|
488
|
+
ignore: 'Ignorieren',
|
|
489
|
+
yes: 'Ja',
|
|
490
|
+
no: 'Nein',
|
|
491
|
+
abort: "Abbrechen",
|
|
492
|
+
retry: "Wiederholen",
|
|
493
|
+
|
|
494
|
+
error: "Fehler",
|
|
495
|
+
today: "Heute",
|
|
496
|
+
|
|
497
|
+
open: 'Öffnen',
|
|
498
|
+
new: 'Neu',
|
|
499
|
+
delete: 'Löschen',
|
|
500
|
+
close: 'Schließen',
|
|
501
|
+
save: 'Speichern',
|
|
502
|
+
|
|
503
|
+
search: 'Suchen',
|
|
504
|
+
search_tip: 'Geben Sie den zu suchenden Text ein. <b>Enter</b> startet die Suche. <b>Esc</b> bricht ab.',
|
|
505
|
+
|
|
506
|
+
required_field: "Pflichtangabe",
|
|
507
|
+
invalid_format: "ungültiges Format",
|
|
508
|
+
invalid_email: 'ungültige E-Mail-Adresse',
|
|
509
|
+
invalid_number: 'ungültiger Zahlenwert',
|
|
510
|
+
|
|
511
|
+
diff_date_seconds: '{0} Sekunden',
|
|
512
|
+
diff_date_minutes: '{0} Minuten',
|
|
513
|
+
diff_date_hours: '{0} Stunden {1} Minuten',
|
|
514
|
+
|
|
515
|
+
invalid_date: 'Datum nicht erkannt ({0})',
|
|
516
|
+
empty_list: 'Leere Liste',
|
|
517
|
+
|
|
518
|
+
date_input_formats: 'd.m.y|d/m/y|d m y|d-m-y|dmy',
|
|
519
|
+
date_format: 'D.M.Y',
|
|
520
|
+
date_time_format: 'D.M.Y H:I:S',
|
|
521
|
+
|
|
522
|
+
day_short: [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa' ],
|
|
523
|
+
day_long: [ 'Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag' ],
|
|
524
|
+
|
|
525
|
+
month_short: [ 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez' ],
|
|
526
|
+
month_long: [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ],
|
|
527
|
+
|
|
528
|
+
property: 'Eigenschaft',
|
|
529
|
+
value: 'Wert',
|
|
530
|
+
|
|
531
|
+
err_403: `Sie haben nicht die erforderlichen Rechte für diese Aktion`,
|
|
532
|
+
|
|
533
|
+
copy: 'Kopieren',
|
|
534
|
+
cut: 'Ausschneiden',
|
|
535
|
+
paste: 'Einfügen',
|
|
536
|
+
|
|
537
|
+
filedrop: 'Datei hier ablegen',
|
|
538
|
+
|
|
539
|
+
keyboard: {
|
|
540
|
+
next: 'Weiter',
|
|
541
|
+
numeric: '123',
|
|
542
|
+
alpha: 'Abc'
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
settings: {
|
|
546
|
+
search: 'Einstellungen durchsuchen',
|
|
547
|
+
nomatch: 'Keine Einstellung gefunden',
|
|
548
|
+
},
|
|
399
549
|
}
|
|
400
550
|
};
|
|
401
551
|
|
|
@@ -405,6 +555,13 @@ addTranslation( 'fr', fr );
|
|
|
405
555
|
createLanguage( 'en', 'fr' );
|
|
406
556
|
addTranslation( 'en', en );
|
|
407
557
|
|
|
558
|
+
// a key they lack is taken in english
|
|
559
|
+
createLanguage( 'es', 'en' );
|
|
560
|
+
addTranslation( 'es', es );
|
|
561
|
+
|
|
562
|
+
createLanguage( 'de', 'en' );
|
|
563
|
+
addTranslation( 'de', de );
|
|
564
|
+
|
|
408
565
|
selectLanguage( 'fr' ); // by default
|
|
409
566
|
|
|
410
567
|
|