x4js 2.3.17 → 2.3.19

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.
Files changed (46) hide show
  1. package/aicli.md +3 -3
  2. package/aicontext.md +211 -47
  3. package/cli/config.mjs +14 -7
  4. package/cli/copy-plugin.mjs +8 -0
  5. package/cli/create.mjs +8 -2
  6. package/cli/dev.mjs +4 -1
  7. package/cli/less-plugin.mjs +5 -1
  8. package/cli/main.mjs +15 -5
  9. package/cli/rawfile-plugin.mjs +23 -3
  10. package/cli/templates.mjs +5 -1
  11. package/package.json +1 -1
  12. package/src/components/calendar/calendar.module.scss +16 -1
  13. package/src/components/calendar/calendar.ts +52 -9
  14. package/src/components/checkbox/checkbox.ts +1 -0
  15. package/src/components/combobox/combobox.ts +51 -23
  16. package/src/components/dialog/dialog.ts +4 -1
  17. package/src/components/form/form.ts +9 -5
  18. package/src/components/gridview/gridview.ts +352 -176
  19. package/src/components/input/input.ts +63 -38
  20. package/src/components/keyboard/keyboard.module.scss +35 -4
  21. package/src/components/keyboard/keyboard.ts +295 -80
  22. package/src/components/listbox/listbox.ts +6 -1
  23. package/src/components/messages/messages.ts +12 -3
  24. package/src/components/popup/popup.ts +19 -20
  25. package/src/components/radio/radio.ts +8 -4
  26. package/src/components/rating/rating.ts +38 -48
  27. package/src/components/slider/slider.ts +38 -8
  28. package/src/components/spreadsheet/spreadsheet.ts +167 -132
  29. package/src/components/switch/switch.ts +2 -1
  30. package/src/components/tag/tag.ts +1 -2
  31. package/src/components/textarea/textarea.module.scss +6 -2
  32. package/src/components/textarea/textarea.ts +50 -11
  33. package/src/components/textedit/textedit.ts +12 -2
  34. package/src/core/component.ts +16 -9
  35. package/src/core/core_application.ts +8 -4
  36. package/src/core/core_clipboard.ts +239 -0
  37. package/src/core/core_data.ts +118 -49
  38. package/src/core/core_dom.ts +36 -6
  39. package/src/core/core_dragdrop.ts +11 -5
  40. package/src/core/core_element.ts +1 -1
  41. package/src/core/core_pdf.ts +696 -46
  42. package/src/core/core_router.ts +42 -44
  43. package/src/core/core_shortcuts.ts +1 -1
  44. package/src/core/core_state.ts +22 -5
  45. package/src/x4.d.ts +1 -0
  46. package/src/x4.ts +1 -0
package/cli/dev.mjs CHANGED
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import esbuild from "esbuild";
6
6
  import { DEFAULT_CONFIG_FILE, expandEnv, loadConfig, loadEnvFile, resolveConfigFile, resolveTls } from "./config.mjs";
7
7
  import { createBuildOptions } from "./build-options.mjs";
8
- import { info, success, failure } from "./log.mjs";
8
+ import { info, success, failure, warning } from "./log.mjs";
9
9
 
10
10
  function openBrowser(url) {
11
11
  let command;
@@ -28,6 +28,9 @@ function openBrowser(url) {
28
28
  detached: true,
29
29
  stdio: "ignore",
30
30
  });
31
+
32
+ // no browser could be started (xdg-open missing...): the server goes on
33
+ child.on("error", (error) => warning(`cannot open the browser: ${error.message}`));
31
34
  child.unref();
32
35
  }
33
36
 
@@ -28,7 +28,11 @@ export function lessPlugin(projectRoot) {
28
28
  setup(build) {
29
29
  build.onLoad({ filter: /\.less$/ }, async (args) => {
30
30
  const source = await fs.readFile(args.path, "utf8");
31
- lessPromise ??= loadLess(projectRoot);
31
+ // not kept when it fails: less can be installed while dev runs
32
+ lessPromise ??= loadLess(projectRoot).catch((error) => {
33
+ lessPromise = undefined;
34
+ throw error;
35
+ });
32
36
  const less = await lessPromise;
33
37
 
34
38
  const result = await less.render(source, {
package/cli/main.mjs CHANGED
@@ -25,17 +25,27 @@ Usage:
25
25
  x4js build [--config <file>] [--env <file>] [--debug]
26
26
 
27
27
  Options:
28
- --config <file> Config file (default: x4.config.json)
29
- --env <file> File of NAME=value lines, for $NAME in the config
30
- -h, --help Show help
31
- -v, --version Show version`);
28
+ --config <file> Config file (default: x4.config.json)
29
+ --env <file> File of NAME=value lines, for $NAME in the config
30
+ -t, --template <name> create: template to use (default: app)
31
+ --no-install create: do not run npm install
32
+ --host <host> dev: address to listen on (default: 127.0.0.1)
33
+ --port <port> dev: port to listen on (default: the first free port from 8000)
34
+ --http, --https dev: force the protocol, whatever the config says
35
+ --open dev: open the browser once the server is ready
36
+ --debug build: not minified, with source maps
37
+ -h, --help Show help
38
+ -v, --version Show version`);
32
39
  }
33
40
 
34
41
  const version = getVersion();
35
42
  const [command, ...argv] = process.argv.slice(2);
36
43
 
44
+ // x4js <command> --help: the help, that gives the options of every command
45
+ const help = argv.includes("--help") || argv.includes("-h");
46
+
37
47
  try {
38
- switch (command) {
48
+ switch (help ? "help" : command) {
39
49
  case "create": {
40
50
  const { create } = await import("./create.mjs");
41
51
  await create(argv, { version });
@@ -8,15 +8,35 @@ export function rawFilePlugin({ filter, mime }) {
8
8
  name: "x4-raw-file",
9
9
 
10
10
  setup(build) {
11
- build.onResolve({ filter }, (args) => {
11
+ build.onResolve({ filter }, async (args) => {
12
12
  // Seulement les imports JS/TS.
13
+ // pluginData: the call of build.resolve below, which esbuild resolves itself
13
14
  if (
14
- args.kind !== "import-statement" &&
15
- args.kind !== "dynamic-import"
15
+ (args.kind !== "import-statement" &&
16
+ args.kind !== "dynamic-import") ||
17
+ args.pluginData === namespace
16
18
  ) {
17
19
  return;
18
20
  }
19
21
 
22
+ // a package: esbuild finds it in node_modules. Its file is read here
23
+ // too, in base64: Icon inserts the svg of the url as it is, a "%23"
24
+ // of a url in text would stay in it
25
+ if (!args.path.startsWith(".") && !path.isAbsolute(args.path)) {
26
+ const found = await build.resolve(args.path, {
27
+ kind: args.kind,
28
+ resolveDir: args.resolveDir,
29
+ importer: args.importer,
30
+ pluginData: namespace,
31
+ });
32
+
33
+ if (found.errors.length) {
34
+ return { errors: found.errors };
35
+ }
36
+
37
+ return { path: found.path, namespace };
38
+ }
39
+
20
40
  return {
21
41
  path: path.resolve(args.resolveDir, args.path),
22
42
  namespace,
package/cli/templates.mjs CHANGED
@@ -1,7 +1,11 @@
1
+ import { parseArgs } from "node:util";
1
2
  import { fetchTemplates } from "./templates-source.mjs";
2
3
  import { color } from "./log.mjs";
3
4
 
4
- export async function templates(_argv = [], { version = "unknown" } = {}) {
5
+ export async function templates(argv = [], { version = "unknown" } = {}) {
6
+ // takes no argument: one given is an error
7
+ parseArgs({ args: argv, options: {}, strict: true });
8
+
5
9
  const manifest = await fetchTemplates(version);
6
10
  const names = Object.keys(manifest).sort();
7
11
  const width = names.reduce((max, name) => Math.max(max, name.length), 0) + 3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.17",
3
+ "version": "2.3.19",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -41,7 +41,10 @@
41
41
  .x4calendar {
42
42
  border: 1px solid var( --border );
43
43
  padding: 4px;
44
-
44
+
45
+ // too short for its weeks (6 in some months): what does not fit is cut, not drawn over what follows
46
+ overflow: hidden;
47
+
45
48
  .month-sel {
46
49
  height: 2em;
47
50
  align-items: center;
@@ -147,6 +150,18 @@
147
150
  color: var( --calendard-day-color-hover );
148
151
  }
149
152
  }
153
+
154
+ // out of minDate..maxDate: cannot be chosen
155
+ .disabled {
156
+ color: var( --calendar-out-color );
157
+ text-decoration: line-through;
158
+ cursor: default;
159
+ }
160
+
161
+ .disabled:hover .text {
162
+ background-color: transparent;
163
+ color: var( --calendar-out-color );
164
+ }
150
165
  }
151
166
 
152
167
  .header {
@@ -78,7 +78,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
78
78
  super(props);
79
79
 
80
80
  this.mapPropEvents( props, 'change' );
81
- this.m_date = props.date ? date_clone( props.date ) : new Date();
81
+ this.m_date = this._clamp( props.date ? date_clone( props.date ) : new Date() );
82
82
 
83
83
  this._update( );
84
84
  }
@@ -130,7 +130,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
130
130
  }),
131
131
  new Flex( ),
132
132
  new Button({ icon: icon_prev, click: () => this._next(false) } ),
133
- new Button({ icon: icon_today, click: () => this.setDate(new Date()), tooltip: _tr.global.today } ),
133
+ new Button({ icon: icon_today, click: () => this.setDate( this._clamp( new Date() ) ), tooltip: _tr.global.today } ),
134
134
  new Button({ icon: icon_next, click: () => this._next(true) } )
135
135
  ]
136
136
  });
@@ -159,6 +159,9 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
159
159
  }));
160
160
 
161
161
  let cmonth = this.m_date.getMonth();
162
+ const { minDate, maxDate } = this.props;
163
+ const min_hash = minDate ? date_hash( minDate ) : null;
164
+ const max_hash = maxDate ? date_hash( maxDate ) : null;
162
165
 
163
166
  // weeks
164
167
  let first = true;
@@ -184,6 +187,13 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
184
187
  cls += ' out';
185
188
  }
186
189
 
190
+ // out of minDate..maxDate: shown, not chosen
191
+ const hash = date_hash( dte );
192
+ const off = (min_hash!==null && hash < min_hash) || (max_hash!==null && hash > max_hash);
193
+ if( off ) {
194
+ cls += ' disabled';
195
+ }
196
+
187
197
  const mkItem = ( dte: Date ) => {
188
198
  return new HBox({
189
199
  cls,
@@ -192,7 +202,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
192
202
  cls: "text",
193
203
  content: unsafeHtml( `<span>${formatIntlDate(dte, 'd')}</span>` ),
194
204
  }),
195
- dom_events: {
205
+ dom_events: off ? undefined : {
196
206
  click: () => this.select(dte)
197
207
  }
198
208
  })
@@ -230,8 +240,40 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
230
240
  */
231
241
 
232
242
  private _next(n: boolean) {
233
- this.m_date.setMonth(this.m_date.getMonth() + (n ? 1 : -1));
234
- this._update();
243
+ this._goto( this.m_date.getFullYear( ), this.m_date.getMonth() + (n ? 1 : -1) );
244
+ }
245
+
246
+ /**
247
+ * shows another month: the day is kept, not past the end of the month
248
+ * (31 January → 28 February), and the date stays between minDate and maxDate
249
+ */
250
+
251
+ private _goto( year: number, month: number ) {
252
+ const day = this.m_date.getDate( );
253
+ const d = date_clone( this.m_date );
254
+ d.setDate( 1 );
255
+ d.setFullYear( year, month ); // month 12: January of the next year
256
+ d.setDate( Math.min( day, new Date( d.getFullYear( ), d.getMonth( )+1, 0 ).getDate( ) ) );
257
+
258
+ this.m_date = this._clamp( d );
259
+ this._update( );
260
+ }
261
+
262
+ /**
263
+ * the date brought between minDate and maxDate
264
+ */
265
+
266
+ private _clamp( d: Date ) {
267
+ const { minDate, maxDate } = this.props;
268
+ if( minDate && d < minDate ) {
269
+ return date_clone( minDate );
270
+ }
271
+
272
+ if( maxDate && d > maxDate ) {
273
+ return date_clone( maxDate );
274
+ }
275
+
276
+ return d;
235
277
  }
236
278
 
237
279
  /**
@@ -246,7 +288,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
246
288
  for (let m = 0; m < 12; m++) {
247
289
  items.push(({
248
290
  text: _tr.global.month_long[m],
249
- click: () => { this.m_date.setMonth(m); this._update(); }
291
+ click: () => { this._goto( this.m_date.getFullYear( ), m ); }
250
292
  }));
251
293
  }
252
294
  }
@@ -258,7 +300,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
258
300
  for (let m = max; m >= min; m--) {
259
301
  items.push({
260
302
  text: '' + m,
261
- click: () => { this.m_date.setFullYear(m); this._update(); }
303
+ click: () => { this._goto( m, this.m_date.getMonth( ) ); }
262
304
  });
263
305
  }
264
306
  }
@@ -271,12 +313,13 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
271
313
  menu.displayAt(rc.left, rc.top);
272
314
  }
273
315
 
316
+ // a copy: the calendar and its caller do not share the object
274
317
  getDate() {
275
- return this.m_date;
318
+ return date_clone( this.m_date );
276
319
  }
277
320
 
278
321
  setDate(date: Date) {
279
- this.m_date = date;
322
+ this.m_date = date_clone( date );
280
323
  this._update();
281
324
  }
282
325
  }
@@ -71,6 +71,7 @@ export class Checkbox extends Component<CheckboxProps,CheckBoxEvents> {
71
71
  type:"checkbox",
72
72
  id: inputId,
73
73
  name: props.name,
74
+ value: props.value, // sent by a <form> of the browser when checked; a Form of x4 reads a boolean
74
75
  checked: props.checked,
75
76
  dom_events: {
76
77
  change: ( ) => this._on_change( ),
@@ -144,30 +144,15 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
144
144
  }
145
145
 
146
146
  this._popup = new DropdownList( { items: props.items, renderer: props.renderer } );
147
- const list = this._popup.getList( );
148
-
149
- const _select = ( sel: ListboxID ) => {
150
- const itm = list.getItem(sel);
151
- if( itm ) {
152
- list.select( sel, false );
153
- this._input.setValue( itm.text as string );
154
- }
155
-
156
- if( !this._prevent_close ) {
157
- this._popup.show( false );
158
- }
159
- }
160
-
161
147
  this._popup.on( "click", ( ev ) => {
162
148
  const sel = ev.context as ListboxID;
163
149
  if( sel!==undefined ) { // no empty sel
164
- _select( sel );
165
- this.fire( "selectionChange", { selection: [sel], empty: false } );
150
+ this._choose( sel, true );
166
151
  }
167
152
  });
168
153
 
169
154
  if( props.value!==undefined && props.value!==null ) {
170
- _select( props.value );
155
+ this._choose( props.value, false );
171
156
  }
172
157
 
173
158
  this._button.addDOMEvent( "click", ( ) => this._on_click( ) );
@@ -180,6 +165,27 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
180
165
  })
181
166
  }
182
167
 
168
+ /**
169
+ * the item chosen: by a click, by the arrows (the popup stays open), or by the props
170
+ */
171
+
172
+ private _choose( sel: ListboxID, notify: boolean ) {
173
+ const list = this._getList( );
174
+ const itm = list.getItem( sel );
175
+ if( itm ) {
176
+ list.select( sel, false );
177
+ this._input.setValue( itm.text as string );
178
+ }
179
+
180
+ if( !this._prevent_close ) {
181
+ this._popup.show( false );
182
+ }
183
+
184
+ if( notify ) {
185
+ this.fire( "selectionChange", { selection: [sel], empty: false } );
186
+ }
187
+ }
188
+
183
189
  private _on_key( ev: KeyboardEvent ) {
184
190
  switch( ev.key ) {
185
191
  case "Enter":
@@ -197,7 +203,13 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
197
203
  this.showDropDown( );
198
204
  }
199
205
  else {
200
- this._popup.getList().navigate( kbNav.prev );
206
+ // the item reached is chosen at once
207
+ const list = this._getList( );
208
+ list.navigate( kbNav.prev );
209
+ const [sel] = list.getSelection( );
210
+ if( sel!==undefined ) {
211
+ this._choose( sel, true );
212
+ }
201
213
  }
202
214
 
203
215
  this._prevent_close = false;
@@ -209,7 +221,13 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
209
221
  this.showDropDown( );
210
222
  }
211
223
  else {
212
- this._popup.getList().navigate( kbNav.next );
224
+ // the item reached is chosen at once
225
+ const list = this._getList( );
226
+ list.navigate( kbNav.next );
227
+ const [sel] = list.getSelection( );
228
+ if( sel!==undefined ) {
229
+ this._choose( sel, true );
230
+ }
213
231
  }
214
232
 
215
233
  this._prevent_close = false;
@@ -229,7 +247,18 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
229
247
  this.showDropDown( );
230
248
  }
231
249
 
232
- this._popup.getList().filter( this._input.getValue( ) );
250
+ // the text typed is no longer the one of the item chosen: none is chosen
251
+ const list = this._getList( );
252
+ const sel = this.getSelection( );
253
+ if( sel!==undefined ) {
254
+ const itm = list.getItem( sel );
255
+ if( !itm || itm.text!==this._input.getValue( ) ) {
256
+ list.clearSelection( false );
257
+ this.fire( "selectionChange", { selection: [], empty: true } );
258
+ }
259
+ }
260
+
261
+ list.filter( this._input.getValue( ) );
233
262
  }
234
263
 
235
264
  private _on_focusout( ) {
@@ -268,10 +297,9 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
268
297
  selectItem( index: ListboxID ) {
269
298
  const list = this._getList( );
270
299
  list.select( index );
300
+ // no item (null...): the text is emptied too
271
301
  const el = list.getItem( index );
272
- if( el ) {
273
- this.setValue( el.text as string );
274
- }
302
+ this.setValue( el ? el.text as string : "" );
275
303
  }
276
304
 
277
305
  getSelection( ) {
@@ -257,11 +257,14 @@ export class Dialog<P extends DialogProps = DialogProps, E extends DialogEvents
257
257
 
258
258
  this.on( "btnclick", ( ev ) => {
259
259
  asap( ( ) => {
260
- this.close( );
261
260
  resolve( ev.button );
261
+ this.close( );
262
262
  });
263
263
  });
264
264
 
265
+ // closed without a button (Escape, the close box): nothing was chosen
266
+ this.on( "closed", ( ) => resolve( null ) );
267
+
265
268
  this.show();
266
269
  } );
267
270
  }
@@ -117,13 +117,14 @@ export class Form<P extends FormProps = FormProps> extends Box<P> {
117
117
 
118
118
  setValidator( validator: ValidationFn ) {
119
119
 
120
+ // focusout: once, the first time
120
121
  if( !this.validator ) {
121
- this.validator = validator;
122
-
123
122
  // validation en live des valeurs
124
123
  // disable du bouton ok si pas bon
125
124
  this.addDOMEvent( "focusout", ( ) => this.validate() );
126
125
  }
126
+
127
+ this.validator = validator;
127
128
  }
128
129
 
129
130
  /**
@@ -155,11 +156,14 @@ export class Form<P extends FormProps = FormProps> extends Box<P> {
155
156
  return null;
156
157
  }
157
158
 
158
- for( let name in result ) {
159
+ // the elements already found: a select, a textarea, a rating... every radio of a group
160
+ for( const x of items ) {
161
+ const name = x.getAttribute( "name" );
159
162
  if( new_values[name] != result[name] ) {
160
- const x = this.query( `input[name="${name}"]` );
161
163
  const ifx = x.queryInterface( "form-element" ) as IFormElement;
162
- ifx.setRawValue( new_values[name] );
164
+ if( ifx ) {
165
+ ifx.setRawValue( new_values[name] );
166
+ }
163
167
  }
164
168
  }
165
169