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 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 { locale?: string } // "fr-FR" (default), "de-CH", "pl"...
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
- Two built-in languages (`fr` default, and `en`).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.21",
3
+ "version": "2.3.22",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -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
- if( e.repeat ) {
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
+ }
@@ -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