x4js 2.0.35 → 2.1.0-manual

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 (134) hide show
  1. package/README.md +21 -21
  2. package/package.json +39 -26
  3. package/src/components/base.scss +25 -89
  4. package/src/components/boxes/boxes.module.scss +54 -54
  5. package/src/components/boxes/boxes.ts +513 -513
  6. package/src/components/breadcrumb/breadcrumb.scss +56 -56
  7. package/src/components/breadcrumb/breadcrumb.ts +93 -93
  8. package/src/components/btngroup/btngroup.module.scss +40 -40
  9. package/src/components/btngroup/btngroup.ts +152 -152
  10. package/src/components/button/button.module.scss +172 -172
  11. package/src/components/button/button.ts +232 -232
  12. package/src/components/calendar/calendar.module.scss +162 -162
  13. package/src/components/calendar/calendar.ts +326 -326
  14. package/src/components/canvas/canvas.module.scss +24 -24
  15. package/src/components/canvas/canvas.ts +195 -195
  16. package/src/components/canvas/canvas_ex.ts +275 -275
  17. package/src/components/checkbox/check.svg +3 -3
  18. package/src/components/checkbox/checkbox.module.scss +141 -141
  19. package/src/components/checkbox/checkbox.ts +139 -139
  20. package/src/components/colorinput/colorinput.module.scss +64 -64
  21. package/src/components/colorinput/colorinput.ts +90 -90
  22. package/src/components/colorpicker/colorpicker.module.scss +132 -132
  23. package/src/components/colorpicker/colorpicker.ts +481 -481
  24. package/src/components/combobox/combobox.module.scss +145 -145
  25. package/src/components/combobox/combobox.ts +282 -282
  26. package/src/components/combobox/updown.svg +3 -3
  27. package/src/components/components.ts +45 -44
  28. package/src/components/dialog/dialog.module.scss +103 -105
  29. package/src/components/dialog/dialog.ts +233 -233
  30. package/src/components/filedrop/filedrop.module.scss +69 -69
  31. package/src/components/filedrop/filedrop.ts +130 -130
  32. package/src/components/form/form.module.scss +38 -38
  33. package/src/components/form/form.ts +172 -172
  34. package/src/components/gridview/gridview.module.scss +323 -337
  35. package/src/components/gridview/gridview.ts +1276 -1316
  36. package/src/components/header/header.module.scss +40 -40
  37. package/src/components/header/header.ts +141 -141
  38. package/src/components/icon/icon.module.scss +32 -32
  39. package/src/components/icon/icon.ts +165 -165
  40. package/src/components/image/image.module.scss +27 -27
  41. package/src/components/image/image.ts +168 -168
  42. package/src/components/input/input.module.scss +74 -74
  43. package/src/components/input/input.ts +537 -537
  44. package/src/components/keyboard/keyboard.module.scss +136 -136
  45. package/src/components/keyboard/keyboard.ts +549 -549
  46. package/src/components/label/label.module.scss +90 -91
  47. package/src/components/label/label.ts +101 -101
  48. package/src/components/link/link.module.scss +44 -44
  49. package/src/components/link/link.ts +87 -87
  50. package/src/components/listbox/listbox.module.scss +179 -179
  51. package/src/components/listbox/listbox.ts +596 -596
  52. package/src/components/menu/menu.module.scss +128 -128
  53. package/src/components/menu/menu.ts +174 -174
  54. package/src/components/messages/messages.module.scss +92 -146
  55. package/src/components/messages/messages.ts +237 -303
  56. package/src/components/normalize.scss +391 -391
  57. package/src/components/notification/notification.module.scss +83 -83
  58. package/src/components/notification/notification.ts +107 -107
  59. package/src/components/panel/panel.module.scss +66 -71
  60. package/src/components/panel/panel.ts +57 -57
  61. package/src/components/popup/popup.module.scss +51 -51
  62. package/src/components/popup/popup.ts +457 -457
  63. package/src/components/progress/progress.module.scss +56 -56
  64. package/src/components/progress/progress.ts +43 -43
  65. package/src/components/propgrid/progrid.module.scss +111 -111
  66. package/src/components/propgrid/propgrid.ts +300 -300
  67. package/src/components/propgrid/updown.svg +3 -3
  68. package/src/components/radio/radio.module.scss +163 -163
  69. package/src/components/radio/radio.svg +3 -3
  70. package/src/components/radio/radio.ts +141 -141
  71. package/src/components/rating/rating.module.scss +22 -22
  72. package/src/components/rating/rating.ts +131 -131
  73. package/src/components/select/select.module.scss +8 -8
  74. package/src/components/select/select.ts +134 -134
  75. package/src/components/shared.scss +141 -71
  76. package/src/components/sizers/sizer.module.scss +90 -112
  77. package/src/components/sizers/sizer.ts +131 -155
  78. package/src/components/slider/slider.module.scss +117 -117
  79. package/src/components/slider/slider.ts +197 -197
  80. package/src/components/spreadsheet/spreadsheet.module.scss +307 -307
  81. package/src/components/spreadsheet/spreadsheet.ts +1223 -1223
  82. package/src/components/switch/switch.module.scss +126 -126
  83. package/src/components/switch/switch.ts +61 -61
  84. package/src/components/tabs/tabs.module.scss +46 -67
  85. package/src/components/tabs/tabs.ts +229 -234
  86. package/src/components/textarea/textarea.module.scss +63 -63
  87. package/src/components/textarea/textarea.ts +131 -131
  88. package/src/components/textedit/textedit.module.scss +115 -115
  89. package/src/components/textedit/textedit.ts +122 -122
  90. package/src/components/themes.scss +90 -90
  91. package/src/components/tickline/tickline.module.scss +25 -25
  92. package/src/components/tickline/tickline.ts +81 -81
  93. package/src/components/tooltips/tooltips.scss +71 -71
  94. package/src/components/tooltips/tooltips.ts +120 -120
  95. package/src/components/treeview/treeview.module.scss +192 -192
  96. package/src/components/treeview/treeview.ts +484 -484
  97. package/src/components/viewport/viewport.module.scss +31 -31
  98. package/src/components/viewport/viewport.ts +41 -41
  99. package/src/core/component.ts +1299 -1299
  100. package/src/core/core_application.ts +361 -361
  101. package/src/core/core_colors.ts +512 -512
  102. package/src/core/core_data.ts +1297 -1310
  103. package/src/core/core_dom.ts +481 -481
  104. package/src/core/core_dragdrop.ts +225 -225
  105. package/src/core/core_element.ts +221 -221
  106. package/src/core/core_events.ts +214 -214
  107. package/src/core/core_i18n.ts +395 -395
  108. package/src/core/core_pdf.ts +454 -454
  109. package/src/core/core_react.ts +78 -78
  110. package/src/core/core_router.ts +296 -296
  111. package/src/core/core_state.ts +62 -62
  112. package/src/core/core_styles.ts +213 -213
  113. package/src/core/core_svg.ts +1042 -1042
  114. package/src/core/core_tools.ts +996 -996
  115. package/src/types/scss.d.ts +4 -4
  116. package/src/types/x4react.d.ts +8 -8
  117. package/src/x4.scss +19 -19
  118. package/src/x4.ts +36 -36
  119. package/src/x4tsx.d.ts +26 -26
  120. package/.vscode/launch.json +0 -14
  121. package/.vscode/settings.json +0 -2
  122. package/demo/assets/house-light.svg +0 -1
  123. package/demo/assets/radio.svg +0 -4
  124. package/demo/index.html +0 -12
  125. package/demo/main.scss +0 -23
  126. package/demo/main.ts +0 -324
  127. package/demo/package.json +0 -26
  128. package/demo/scss.d.ts +0 -4
  129. package/demo/svg.d.ts +0 -1
  130. package/demo/tsconfig.json +0 -14
  131. package/src/components/gridview/folder-open.svg +0 -1
  132. package/src/components/messages/spinner.svg +0 -1
  133. package/src/x4.d.ts +0 -10
  134. package/tsconfig.json +0 -11
@@ -1,996 +1,996 @@
1
- /**
2
- * ___ ___ __
3
- * \ \/ / / _
4
- * \ / /_| |_
5
- * / \____ _|
6
- * /__/\__\ |_|
7
- *
8
- * @file core_tools.ts
9
- * @author Etienne Cochard
10
- *
11
- * @copyright (c) 2024 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 { _tr } from "./core_i18n.js";
18
-
19
- /**
20
- * @returns true if object is a string
21
- */
22
- export function isString(val: any): val is string {
23
- return typeof val === 'string';
24
- }
25
-
26
- /**
27
- * @returns true if object is a number
28
- */
29
-
30
- export function isNumber(v: any): v is number {
31
- return typeof v === 'number' && isFinite(v);
32
- }
33
-
34
- /**
35
- * @returns true if object is an array
36
- */
37
- export function isArray(val: any): val is any[] {
38
- return val instanceof Array;
39
- }
40
-
41
- /**
42
- * @returns true if object is a function
43
- */
44
-
45
- export function isFunction(val: any): val is Function {
46
- return val instanceof Function;
47
- }
48
-
49
- /**
50
- * @returns true if object is a date
51
- */
52
-
53
- export function isDate(v: unknown): v is Date {
54
- return v instanceof Date && !isNaN(v.getTime());
55
- }
56
-
57
-
58
- export function isPlainObject(value: unknown): value is Record<string, any> {
59
- if (typeof value !== 'object' || value === null) {
60
- return false;
61
- }
62
-
63
- return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
64
- }
65
-
66
- /**
67
- * generic constructor definition
68
- */
69
-
70
- export type Constructor<P> = {
71
- new(...params: any[]): P;
72
- };
73
-
74
-
75
- /**
76
- * a way to explain that the given string may be unsafe but must be treated a string
77
- * @example
78
- * label.setText( unsafehtml`<b>Bold</b> text` );
79
- * label.setText( new UnsafeHtml("<b>Bold</b> text`" ) );
80
- */
81
-
82
- export class UnsafeHtml extends String {
83
- constructor(value: string) {
84
- super(value);
85
- }
86
- }
87
-
88
- export function unsafeHtml(x: string): UnsafeHtml {
89
- return new UnsafeHtml(x);
90
- }
91
-
92
- export function unsafe(strings: TemplateStringsArray, ...values: any[]): UnsafeHtml {
93
- const result = strings.reduce((acc, str, i) => {
94
- return acc + str + (values[i] ?? '');
95
- }, '');
96
- return unsafeHtml(result);
97
- }
98
-
99
- /**
100
- * clamp a value between min/max values
101
- */
102
-
103
- export function clamp<T>(v: T, min: T, max: T): T {
104
- if (v < min) { return min; }
105
- if (v > max) { return max; }
106
- return v;
107
- }
108
-
109
-
110
- /**
111
- * generic Rectangle
112
- */
113
-
114
- export interface IRect {
115
- left: number;
116
- top: number;
117
- height: number;
118
- width: number;
119
- }
120
-
121
- /**
122
- *
123
- */
124
-
125
- export class Rect implements IRect {
126
- left: number;
127
- top: number;
128
- height: number;
129
- width: number;
130
-
131
- constructor();
132
- constructor(l: number, t: number, w: number, h: number);
133
- constructor(l: Rect);
134
- constructor(l?: number | IRect, t?: number, w?: number, h?: number) {
135
- if (l !== undefined) {
136
- if (isNumber(l)) {
137
- this.left = l;
138
- this.top = t;
139
- this.width = w;
140
- this.height = h;
141
- }
142
- else {
143
- Object.assign(this, l);
144
- }
145
- }
146
- }
147
-
148
- get right() {
149
- return this.left + this.width;
150
- }
151
-
152
- get bottom() {
153
- return this.top + this.height;
154
- }
155
-
156
- contains(pt: Point): boolean;
157
- contains(rc: Rect): boolean;
158
- contains(arg: any): boolean {
159
- if (arg instanceof Rect) {
160
- return arg.left >= this.left && arg.right <= this.right && arg.top >= this.top && arg.bottom <= this.bottom;
161
- }
162
- else {
163
- return arg.x >= this.left && arg.x < this.right && arg.y >= this.top && arg.y < this.bottom;
164
- }
165
- }
166
-
167
- touches(rc: Rect): boolean {
168
- if (this.left > rc.right || this.right < rc.left || this.top > rc.bottom || this.bottom < rc.top) {
169
- return false;
170
- }
171
-
172
- return true;
173
- }
174
-
175
- normalize(): this {
176
- let w = this.width,
177
- h = this.height;
178
-
179
- if (w < 0) {
180
- this.left += w;
181
- this.width = -w;
182
- }
183
-
184
- if (h < 0) {
185
- this.top += h;
186
- this.height = -h;
187
- }
188
-
189
- return this;
190
- }
191
-
192
- inflate( dx: number, dy?: number ) {
193
- if( dy===undefined ) {
194
- dy = dx;
195
- }
196
-
197
- this.left -= dx;
198
- this.width += dx+dx;
199
- this.top -= dy;
200
- this.width += dy+dy;
201
-
202
- return this;
203
- }
204
-
205
- scale( scale: number ) {
206
- this.left *= scale;
207
- this.top *= scale;
208
- this.width *= scale;
209
- this.height *= scale;
210
-
211
- return this;
212
- }
213
- }
214
-
215
- /**
216
- * generic size
217
- */
218
-
219
- export interface Size {
220
- w: number;
221
- h: number;
222
- }
223
-
224
-
225
- /**
226
- * generic Point
227
- * TODO: name it IPoint
228
- */
229
-
230
- export interface Point {
231
- x: number;
232
- y: number;
233
- }
234
-
235
-
236
- /**
237
- * generic Size
238
- * TODO: name it ISize
239
- */
240
-
241
- export interface Size {
242
- w: number;
243
- h: number;
244
- }
245
-
246
- /**
247
- * center one rect inside another
248
- */
249
-
250
-
251
- export function centerRect(innerRect: IRect, outerRect: IRect, margin: number = 0): IRect {
252
-
253
- const owidth = outerRect.width - 2 * margin;
254
- const oheight = outerRect.height - 2 * margin;
255
-
256
- const ratio = innerRect.width / innerRect.height;
257
-
258
- let nwidth = owidth;
259
- let nheight = owidth / ratio;
260
-
261
- if (nheight > oheight) {
262
- nheight = oheight;
263
- nwidth = oheight * ratio;
264
- }
265
-
266
- const newLeft = outerRect.left + (outerRect.width - nwidth) / 2;
267
- const newTop = outerRect.top + (outerRect.height - nheight) / 2;
268
-
269
- return { left: newLeft, top: newTop, width: nwidth, height: nheight };
270
- }
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
- /**
286
- * @see queryInterface
287
- */
288
-
289
- export interface IComponentInterface {
290
- }
291
-
292
- // form-element
293
- export interface IFormElement extends IComponentInterface {
294
- getRawValue(): any;
295
- setRawValue(v: any): void;
296
- isValid(): boolean;
297
- }
298
-
299
- // tab-handler
300
- export interface ITabHandler extends IComponentInterface {
301
- focusNext(next: boolean): boolean; // return true to stop event
302
- }
303
-
304
- // tip-handler
305
- export interface ITipHandler extends IComponentInterface {
306
- getTip(): string;
307
- }
308
-
309
- /**
310
- *
311
- */
312
-
313
- interface Features {
314
- eyedropper: 1,
315
- }
316
-
317
- export function isFeatureAvailable(name: keyof Features): boolean {
318
- switch (name) {
319
- case "eyedropper": return "EyeDropper" in window;
320
- }
321
-
322
- return false;
323
- }
324
-
325
- export class Timer {
326
-
327
- protected _timers: Map<string, any>;
328
-
329
- /**
330
- *
331
- */
332
-
333
- setTimeout(name: string, time: number, callback: Function) {
334
- if (!this._timers) {
335
- this._timers = new Map();
336
- }
337
- else {
338
- this.clearTimeout(name);
339
- }
340
-
341
- const tm = setTimeout(callback, time);
342
- this._timers.set(name, tm);
343
-
344
- return tm;
345
- }
346
-
347
- clearTimeout(name: string) {
348
- if (this._timers && this._timers.has(name)) {
349
- clearTimeout(this._timers.get(name));
350
- this._timers.delete(name);
351
- }
352
- }
353
-
354
- /**
355
- *
356
- */
357
-
358
- setInterval(name: string, time: number, callback: Function) {
359
- if (!this._timers) {
360
- this._timers = new Map();
361
- }
362
- else {
363
- this.clearInterval(name);
364
- }
365
-
366
- const tm = setInterval(callback, time);
367
- this._timers.set(name, tm);
368
-
369
- return tm;
370
- }
371
-
372
- clearInterval(name: string) {
373
- if (this._timers && this._timers.has(name)) {
374
- clearInterval(this._timers.get(name));
375
- this._timers.delete(name);
376
- }
377
- }
378
-
379
- clearAllTimeouts() {
380
- this._timers?.forEach(t => {
381
- clearTimeout(t);
382
- });
383
-
384
- this._timers = null;
385
- }
386
- }
387
-
388
- /**
389
- *
390
- */
391
-
392
- export function asap(callback: () => void) {
393
- return requestAnimationFrame(callback);
394
- }
395
-
396
- export function oneshot(callback: () => void, ms = 0) {
397
- return setTimeout(callback, ms);
398
- }
399
-
400
-
401
- // :: STRING UTILS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
402
-
403
-
404
- /**
405
- * prepend 0 to a value to a given length
406
- * @param value
407
- * @param length
408
- */
409
-
410
- export function pad(what: any, size: number, ch: string = '0') {
411
-
412
- let value: string;
413
-
414
- if (!isString(what)) {
415
- value = '' + what;
416
- }
417
- else {
418
- value = what;
419
- }
420
-
421
- if (size > 0) {
422
- return value.padEnd(size, ch);
423
- }
424
- else {
425
- return value.padStart(-size, ch);
426
- }
427
- }
428
-
429
- /**
430
- * replace {0..9999} by given arguments
431
- * @param format string
432
- * @param args
433
- *
434
- * @example ```ts
435
- *
436
- * console.log( sprintf( 'here is arg 1 {1} and arg 0 {0}', 'argument 0', 'argument 1' ) )
437
- */
438
-
439
- export function sprintf(format: string, ...args: any[]) {
440
- return format.replace(/{(\d+)}/g, function (match, index) {
441
- return typeof args[index] != 'undefined' ? args[index] : match;
442
- });
443
- }
444
-
445
- /**
446
- * inverse of camel case
447
- * theThingToCase -> the-thing-to-case
448
- * @param {String} str
449
- */
450
-
451
- export function pascalCase(string: string): string {
452
-
453
- let result = string;
454
-
455
- result = result.replace(/([a-z])([A-Z])/g, "$1 $2");
456
- result = result.toLowerCase();
457
- result = result.replace(/[^- a-z0-9]+/g, ' ');
458
-
459
- if (result.indexOf(' ') < 0) {
460
- return result;
461
- }
462
-
463
- result = result.trim();
464
- return result.replace(/ /g, '-');
465
- }
466
-
467
- export function camelCase(text: string) {
468
- let result = text.toLowerCase();
469
- result = result.replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => {
470
- return chr.toUpperCase();
471
- });
472
- return result;
473
- }
474
-
475
- // :: DATES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
476
-
477
- let cur_locale: string = 'fr-FR';
478
-
479
- /**
480
- * change the current locale for misc translations (date...)
481
- * @param locale
482
- * TODO: ????
483
- */
484
-
485
- export function _date_set_locale(locale: string) {
486
- cur_locale = locale;
487
- }
488
-
489
- /**
490
- *
491
- * @param date
492
- * @param options
493
- * @example
494
- * let date = new Date( );
495
- * let options = { day: 'numeric', month: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric' };
496
- * let text = date_format( date, options );
497
- */
498
-
499
- export function date_format(date: Date, options?: any): string {
500
- //return new Intl.DateTimeFormat(cur_locale, options).format( date );
501
- return formatIntlDate(date);
502
- }
503
-
504
- /**
505
- * difference between 2 dates
506
- * @param date
507
- * @param options
508
- * @return text
509
- */
510
-
511
- export function date_diff(date1: Date, date2: Date, options?: any): string {
512
-
513
- let dt = (date1.getTime() - date2.getTime()) / 1000;
514
-
515
- // seconds
516
- let sec = dt;
517
- if (sec < 60) {
518
- return sprintf(_tr.global.diff_date_seconds, Math.round(sec));
519
- }
520
-
521
- // minutes
522
- let min = Math.floor(sec / 60);
523
- if (min < 60) {
524
- return sprintf(_tr.global.diff_date_minutes, Math.round(min));
525
- }
526
-
527
- // hours
528
- let hrs = Math.floor(min / 60);
529
- return sprintf(_tr.global.diff_date_hours, hrs, min % 60);
530
- }
531
-
532
- /**
533
- * format a date to sql format Y-M-D
534
- * @param date
535
- * @param withHours
536
- */
537
-
538
- export function date_to_sql(date: Date, withHours: boolean) {
539
-
540
- if (withHours) {
541
- return formatIntlDate(date, 'Y-M-D H:I:S');
542
- }
543
- else {
544
- return formatIntlDate(date, 'Y-M-D');
545
- }
546
- }
547
-
548
- /**
549
- * construct a date from an utc date time (sql format)
550
- * YYYY-MM-DD HH:MM:SS
551
- */
552
-
553
- export function date_sql_utc(date: string): Date {
554
- let result = new Date(date + ' GMT');
555
- return result;
556
- }
557
-
558
-
559
-
560
- /**
561
- * return a number that is a representation of the date
562
- * this number can be compared with another hash
563
- */
564
-
565
- export function date_hash(date: Date): number {
566
- return date.getFullYear() << 16 | date.getMonth() << 8 | date.getDate();
567
- }
568
-
569
- /**
570
- * return a copy of a date
571
- */
572
-
573
- export function date_clone(date: Date): Date {
574
- return new Date(date.getTime());
575
- }
576
-
577
- /**
578
- * return the week number of a date
579
- */
580
-
581
- export function date_calc_weeknum(date: Date): number {
582
- const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
583
- const pastDaysOfYear = (date.valueOf() - firstDayOfYear.valueOf()) / 86400000;
584
- return Math.floor((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
585
- }
586
-
587
-
588
-
589
- /**
590
- * parse a date according to the given format
591
- * @param value - string date to parse
592
- * @param fmts - format list - i18 tranlation by default
593
- * allowed format specifiers:
594
- * d: date (1 or 2 digits)
595
- * D: date (2 digits)
596
- * m: month (1 or 2 digits)
597
- * M: month (2 digits)
598
- * y: year (2 to 4 digits)
599
- * Y: year (2 digits)
600
- * YY: year (4 digits)
601
- * h: hours (1 or 2 digits)
602
- * H: hours (2 digits)
603
- * i: minutes (1 or 2 digits)
604
- * I: minutes (2 digits)
605
- * s: seconds (1 or 2 digits)
606
- * S: seconds (2 digits)
607
- * <space>: 1 or more spaces
608
- * any other char: <0 or more spaces><the char><0 or more spaces>
609
- * each specifiers is separated from other by a pipe (|)
610
- * more specific at first
611
- * @example
612
- * 'd/m/y|d m Y|dmy|y-m-d h:i:s|y-m-d|YY-M-D'
613
- */
614
-
615
- export function parseIntlDate(value: string, fmts: string = _tr.global.date_input_formats): Date {
616
-
617
- let formats = fmts.split('|');
618
- for (let fmatch of formats) {
619
-
620
- //review: could do that only once & keep result
621
- //review: add hours, minutes, seconds
622
-
623
- let smatch = '';
624
- for (let i = 0; i < fmatch.length; i++) {
625
- const c = fmatch[i];
626
-
627
- if (c == 'd') {
628
- smatch += '(?<day>\\d{1,2})';
629
- }
630
- else if (c == 'D') {
631
- smatch += '(?<day>\\d{2})';
632
- }
633
- else if (c == 'm') {
634
- smatch += '(?<month>\\d{1,2})';
635
- }
636
- else if (c == 'M') {
637
- smatch += '(?<month>\\d{2})';
638
- }
639
- else if (c == 'y') {
640
- smatch += '(?<year>\\d{1,4})';
641
- }
642
- else if (c == 'Y') {
643
- if (fmatch[i + 1] == 'Y') {
644
- smatch += '(?<year>\\d{4})';
645
- i++;
646
- }
647
- else {
648
- smatch += '(?<year>\\d{2})';
649
- }
650
- }
651
- else if (c == 'h') {
652
- smatch += '(?<hour>\\d{1,2})';
653
- }
654
- else if (c == 'H') {
655
- smatch += '(?<hour>\\d{2})';
656
- }
657
- else if (c == 'i') {
658
- smatch += '(?<min>\\d{1,2})';
659
- }
660
- else if (c == 'I') {
661
- smatch += '(?<min>\\d{2})';
662
- }
663
- else if (c == 's') {
664
- smatch += '(?<sec>\\d{1,2})';
665
- }
666
- else if (c == 'S') {
667
- smatch += '(?<sec>\\d{2})';
668
- }
669
- else if (c == ' ') {
670
- smatch += '\\s+';
671
- }
672
- else {
673
- smatch += '\\s*\\' + c + '\\s*';
674
- }
675
- }
676
-
677
- let rematch = new RegExp('^' + smatch + '$', 'm');
678
-
679
- let match = rematch.exec(value);
680
-
681
- if (match) {
682
- const now = new Date();
683
-
684
- let d = parseInt(match.groups.day ?? '1');
685
- let m = parseInt(match.groups.month ?? '1');
686
- let y = parseInt(match.groups.year ?? now.getFullYear() + '');
687
- let h = parseInt(match.groups.hour ?? '0');
688
- let i = parseInt(match.groups.min ?? '0');
689
- let s = parseInt(match.groups.sec ?? '0');
690
-
691
- if (y > 0 && y < 100) {
692
- y += 2000;
693
- }
694
-
695
- let result = new Date(y, m - 1, d, h, i, s, 0);
696
-
697
- // we test the vdate validity (without adjustments)
698
- // without this test, date ( 0, 0, 0) is accepted and transformed to 1969/11/31 (not fun)
699
- let ty = result.getFullYear(),
700
- tm = result.getMonth() + 1,
701
- td = result.getDate();
702
-
703
- if (ty != y || tm != m || td != d) {
704
- //debugger;
705
- return null;
706
- }
707
-
708
- return result;
709
- }
710
- }
711
-
712
- return null;
713
- }
714
-
715
- /**
716
- * format a date as string
717
- * @param date - date to format
718
- * @param fmt - format
719
- * format specifiers:
720
- * - d: date (no pad)
721
- * - D: 2 digits date padded with 0
722
- * - j: day of week short mode 'mon'
723
- * - J: day of week long mode 'monday'
724
- * - w: week number
725
- * - m: month (no pad)
726
- * - M: 2 digits month padded with 0
727
- * - o: month short mode 'jan'
728
- * - O: month long mode 'january'
729
- * - y or Y: year
730
- * - h: hour (24 format)
731
- * - H: 2 digits hour (24 format) padded with 0
732
- * - i: minutes
733
- * - I: 2 digits minutes padded with 0
734
- * - s: seconds
735
- * - S: 2 digits seconds padded with 0
736
- * - a: am or pm
737
- * - anything else is inserted
738
- * - if you need to insert some text, put it between {}
739
- *
740
- * @example
741
- *
742
- * 01/01/1970 11:25:00 with '{this is my demo date formatter: }H-i*M'
743
- * "this is my demo date formatter: 11-25*january"
744
- */
745
-
746
- export function formatIntlDate(date: Date, fmt: string = _tr.global.date_format, utc = false) {
747
-
748
- if (!date) {
749
- return '';
750
- }
751
-
752
- let now = {
753
- year: utc ? date.getUTCFullYear() : date.getFullYear(),
754
- month: utc ? date.getUTCMonth() + 1 : date.getMonth() + 1,
755
- day: utc ? date.getUTCDate() : date.getDate(),
756
- wday: utc ? date.getUTCDay() : date.getDay(),
757
- hours: utc ? date.getUTCHours() : date.getHours(),
758
- minutes: utc ? date.getUTCMinutes() : date.getMinutes(),
759
- seconds: utc ? date.getUTCSeconds() : date.getSeconds(),
760
- milli: utc ? date.getUTCMilliseconds() : date.getMilliseconds()
761
- };
762
-
763
-
764
- let result = '';
765
- let esc = 0;
766
-
767
- for (let c of fmt) {
768
-
769
- if (c == '{') {
770
- if (++esc == 1) {
771
- continue;
772
- }
773
- }
774
- else if (c == '}') {
775
- if (--esc == 0) {
776
- continue;
777
- }
778
- }
779
-
780
- if (esc) {
781
- result += c;
782
- continue;
783
- }
784
-
785
- if (c == 'd') {
786
- result += now.day;
787
- }
788
- else if (c == 'D') {
789
- result += pad(now.day, -2);
790
- }
791
- else if (c == 'j') { // day short
792
- result += _tr.global.day_short[now.wday];
793
- }
794
- else if (c == 'J') { // day long
795
- result += _tr.global.day_long[now.wday];
796
- }
797
- else if (c == 'w') { // week
798
- result += date_calc_weeknum(date);
799
- }
800
- else if (c == 'W') { // week
801
- result += pad(date_calc_weeknum(date), -2);
802
- }
803
- else if (c == 'm') {
804
- result += now.month;
805
- }
806
- else if (c == 'M') {
807
- result += pad(now.month, -2);
808
- }
809
- else if (c == 'o') { // month short
810
- result += _tr.global.month_short[now.month - 1];
811
- }
812
- else if (c == 'O') { // month long
813
- result += _tr.global.month_long[now.month - 1];
814
- }
815
- else if (c == 'y' || c == 'Y') {
816
- result += pad(now.year, -4);
817
- }
818
- else if (c == 'a' || c == 'A') {
819
- result += now.hours < 12 ? 'am' : 'pm';
820
- }
821
- else if (c == 'h') {
822
- result += now.hours;
823
- }
824
- else if (c == 'H') {
825
- result += pad(now.hours, -2);
826
- }
827
- else if (c == 'i') {
828
- result += now.minutes;
829
- }
830
- else if (c == 'I') {
831
- result += pad(now.minutes, -2);
832
- }
833
- else if (c == 's') {
834
- result += now.seconds;
835
- }
836
- else if (c == 'S') {
837
- result += pad(now.seconds, -2);
838
- }
839
- else if (c == 'l') {
840
- result += now.milli;
841
- }
842
- else if (c == 'L') {
843
- result += pad(now.milli, -3);
844
- }
845
- else {
846
- result += c;
847
- }
848
- }
849
-
850
- return result;
851
- }
852
-
853
- /**
854
- * calc an age from 2 dates
855
- * @param birth
856
- * @param ref
857
- * @returns age in years
858
- */
859
- export function calcAge(birth: Date, ref?: Date) {
860
- if (ref === undefined) {
861
- ref = new Date();
862
- }
863
-
864
- if (!birth) {
865
- return 0;
866
- }
867
-
868
- let age = ref.getFullYear() - birth.getFullYear();
869
- if (ref.getMonth() < birth.getMonth() || (ref.getMonth() == birth.getMonth() && ref.getDate() < birth.getDate())) {
870
- age--;
871
- }
872
-
873
- return age;
874
- }
875
-
876
-
877
- // :: MISC ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
878
-
879
- // TODO: mmmmm
880
- export function beep() {
881
- const snd = new Audio("data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
882
- snd.play();
883
- }
884
-
885
-
886
- let sb_width_cache = -1;
887
- let window_zoom = -1;
888
-
889
- /**
890
- * compute scrollbar size
891
- */
892
-
893
- export function getScrollbarSize() {
894
-
895
- if (sb_width_cache < 0) {
896
- let outerDiv = document.createElement('div');
897
- outerDiv.style.cssText = 'overflow:auto;position:absolute;top:0;width:100px;height:100px';
898
-
899
- let innerDiv = document.createElement('div');
900
- innerDiv.style.width = '200px';
901
- innerDiv.style.height = '200px';
902
-
903
- outerDiv.appendChild(innerDiv);
904
- document.body.appendChild(outerDiv);
905
-
906
- sb_width_cache = outerDiv.offsetWidth - outerDiv.clientWidth;
907
- document.body.removeChild(outerDiv);
908
- }
909
-
910
- return sb_width_cache;
911
- }
912
-
913
- export function getGlobalZoom( ) {
914
- if( window_zoom<0 ) {
915
- const style = window.getComputedStyle(document.body);
916
- const matrix = style.transform;
917
-
918
- if (matrix !== 'none') {
919
- const values = matrix.split('(')[1].split(')')[0].split(',');
920
- window_zoom = parseFloat(values[0]);
921
- }
922
- else {
923
- window_zoom = parseFloat( style.zoom) || 1;
924
- }
925
- }
926
-
927
- return window_zoom;
928
- }
929
-
930
- export function getSystemMetrics( part: "scrollbar" | "zoom" ) {
931
- switch( part ) {
932
- case "scrollbar": return getScrollbarSize( );
933
- case "zoom": return getGlobalZoom( );
934
- }
935
-
936
- return 0;
937
- }
938
-
939
-
940
-
941
-
942
-
943
-
944
-
945
- /**
946
- *
947
- */
948
-
949
- export const x4_class_ns_sym = Symbol("class-ns");
950
- export function class_ns(ns: string) {
951
- return function (constructor: Function) {
952
- (constructor as any)[x4_class_ns_sym] = ns;
953
- }
954
- }
955
-
956
- /**
957
- *
958
- */
959
-
960
- export function setWaitCursor(wait: boolean) {
961
- document.body.style.cursor = wait ? "wait" : "default";
962
- }
963
-
964
- /**
965
- * return the focusable elements from a given node
966
- */
967
-
968
- export function getFocusableElements(root: Element) {
969
- const els = [
970
- 'button:not([tabindex="-1"]):not([disabled])',
971
- '[href]',
972
- 'input:not([disabled])',
973
- 'select:not([disabled])',
974
- 'textarea:not([disabled])',
975
- '[tabindex]:not([tabindex="-1"])'
976
- ]
977
-
978
- const focusable = Array.from(root.querySelectorAll(els.join(',')));
979
- return focusable.filter(x => (x as HTMLElement).offsetParent != null); // check visibility
980
- }
981
-
982
- /**
983
- *
984
- */
985
-
986
- export enum kbNav {
987
- first,
988
- prev,
989
- pgdn,
990
- pgup,
991
- next,
992
- last,
993
- left,
994
- right,
995
- }
996
-
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file core_tools.ts
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2024 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 { _tr } from "./core_i18n.js";
18
+
19
+ /**
20
+ * @returns true if object is a string
21
+ */
22
+ export function isString(val: any): val is string {
23
+ return typeof val === 'string';
24
+ }
25
+
26
+ /**
27
+ * @returns true if object is a number
28
+ */
29
+
30
+ export function isNumber(v: any): v is number {
31
+ return typeof v === 'number' && isFinite(v);
32
+ }
33
+
34
+ /**
35
+ * @returns true if object is an array
36
+ */
37
+ export function isArray(val: any): val is any[] {
38
+ return val instanceof Array;
39
+ }
40
+
41
+ /**
42
+ * @returns true if object is a function
43
+ */
44
+
45
+ export function isFunction(val: any): val is Function {
46
+ return val instanceof Function;
47
+ }
48
+
49
+ /**
50
+ * @returns true if object is a date
51
+ */
52
+
53
+ export function isDate(v: unknown): v is Date {
54
+ return v instanceof Date && !isNaN(v.getTime());
55
+ }
56
+
57
+
58
+ export function isPlainObject(value: unknown): value is Record<string, any> {
59
+ if (typeof value !== 'object' || value === null) {
60
+ return false;
61
+ }
62
+
63
+ return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
64
+ }
65
+
66
+ /**
67
+ * generic constructor definition
68
+ */
69
+
70
+ export type Constructor<P> = {
71
+ new(...params: any[]): P;
72
+ };
73
+
74
+
75
+ /**
76
+ * a way to explain that the given string may be unsafe but must be treated a string
77
+ * @example
78
+ * label.setText( unsafehtml`<b>Bold</b> text` );
79
+ * label.setText( new UnsafeHtml("<b>Bold</b> text`" ) );
80
+ */
81
+
82
+ export class UnsafeHtml extends String {
83
+ constructor(value: string) {
84
+ super(value);
85
+ }
86
+ }
87
+
88
+ export function unsafeHtml(x: string): UnsafeHtml {
89
+ return new UnsafeHtml(x);
90
+ }
91
+
92
+ export function unsafe(strings: TemplateStringsArray, ...values: any[]): UnsafeHtml {
93
+ const result = strings.reduce((acc, str, i) => {
94
+ return acc + str + (values[i] ?? '');
95
+ }, '');
96
+ return unsafeHtml(result);
97
+ }
98
+
99
+ /**
100
+ * clamp a value between min/max values
101
+ */
102
+
103
+ export function clamp<T>(v: T, min: T, max: T): T {
104
+ if (v < min) { return min; }
105
+ if (v > max) { return max; }
106
+ return v;
107
+ }
108
+
109
+
110
+ /**
111
+ * generic Rectangle
112
+ */
113
+
114
+ export interface IRect {
115
+ left: number;
116
+ top: number;
117
+ height: number;
118
+ width: number;
119
+ }
120
+
121
+ /**
122
+ *
123
+ */
124
+
125
+ export class Rect implements IRect {
126
+ left: number;
127
+ top: number;
128
+ height: number;
129
+ width: number;
130
+
131
+ constructor();
132
+ constructor(l: number, t: number, w: number, h: number);
133
+ constructor(l: Rect);
134
+ constructor(l?: number | IRect, t?: number, w?: number, h?: number) {
135
+ if (l !== undefined) {
136
+ if (isNumber(l)) {
137
+ this.left = l;
138
+ this.top = t;
139
+ this.width = w;
140
+ this.height = h;
141
+ }
142
+ else {
143
+ Object.assign(this, l);
144
+ }
145
+ }
146
+ }
147
+
148
+ get right() {
149
+ return this.left + this.width;
150
+ }
151
+
152
+ get bottom() {
153
+ return this.top + this.height;
154
+ }
155
+
156
+ contains(pt: Point): boolean;
157
+ contains(rc: Rect): boolean;
158
+ contains(arg: any): boolean {
159
+ if (arg instanceof Rect) {
160
+ return arg.left >= this.left && arg.right <= this.right && arg.top >= this.top && arg.bottom <= this.bottom;
161
+ }
162
+ else {
163
+ return arg.x >= this.left && arg.x < this.right && arg.y >= this.top && arg.y < this.bottom;
164
+ }
165
+ }
166
+
167
+ touches(rc: Rect): boolean {
168
+ if (this.left > rc.right || this.right < rc.left || this.top > rc.bottom || this.bottom < rc.top) {
169
+ return false;
170
+ }
171
+
172
+ return true;
173
+ }
174
+
175
+ normalize(): this {
176
+ let w = this.width,
177
+ h = this.height;
178
+
179
+ if (w < 0) {
180
+ this.left += w;
181
+ this.width = -w;
182
+ }
183
+
184
+ if (h < 0) {
185
+ this.top += h;
186
+ this.height = -h;
187
+ }
188
+
189
+ return this;
190
+ }
191
+
192
+ inflate( dx: number, dy?: number ) {
193
+ if( dy===undefined ) {
194
+ dy = dx;
195
+ }
196
+
197
+ this.left -= dx;
198
+ this.width += dx+dx;
199
+ this.top -= dy;
200
+ this.width += dy+dy;
201
+
202
+ return this;
203
+ }
204
+
205
+ scale( scale: number ) {
206
+ this.left *= scale;
207
+ this.top *= scale;
208
+ this.width *= scale;
209
+ this.height *= scale;
210
+
211
+ return this;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * generic size
217
+ */
218
+
219
+ export interface Size {
220
+ w: number;
221
+ h: number;
222
+ }
223
+
224
+
225
+ /**
226
+ * generic Point
227
+ * TODO: name it IPoint
228
+ */
229
+
230
+ export interface Point {
231
+ x: number;
232
+ y: number;
233
+ }
234
+
235
+
236
+ /**
237
+ * generic Size
238
+ * TODO: name it ISize
239
+ */
240
+
241
+ export interface Size {
242
+ w: number;
243
+ h: number;
244
+ }
245
+
246
+ /**
247
+ * center one rect inside another
248
+ */
249
+
250
+
251
+ export function centerRect(innerRect: IRect, outerRect: IRect, margin: number = 0): IRect {
252
+
253
+ const owidth = outerRect.width - 2 * margin;
254
+ const oheight = outerRect.height - 2 * margin;
255
+
256
+ const ratio = innerRect.width / innerRect.height;
257
+
258
+ let nwidth = owidth;
259
+ let nheight = owidth / ratio;
260
+
261
+ if (nheight > oheight) {
262
+ nheight = oheight;
263
+ nwidth = oheight * ratio;
264
+ }
265
+
266
+ const newLeft = outerRect.left + (outerRect.width - nwidth) / 2;
267
+ const newTop = outerRect.top + (outerRect.height - nheight) / 2;
268
+
269
+ return { left: newLeft, top: newTop, width: nwidth, height: nheight };
270
+ }
271
+
272
+
273
+
274
+
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+
284
+
285
+ /**
286
+ * @see queryInterface
287
+ */
288
+
289
+ export interface IComponentInterface {
290
+ }
291
+
292
+ // form-element
293
+ export interface IFormElement extends IComponentInterface {
294
+ getRawValue(): any;
295
+ setRawValue(v: any): void;
296
+ isValid(): boolean;
297
+ }
298
+
299
+ // tab-handler
300
+ export interface ITabHandler extends IComponentInterface {
301
+ focusNext(next: boolean): boolean; // return true to stop event
302
+ }
303
+
304
+ // tip-handler
305
+ export interface ITipHandler extends IComponentInterface {
306
+ getTip(): string;
307
+ }
308
+
309
+ /**
310
+ *
311
+ */
312
+
313
+ interface Features {
314
+ eyedropper: 1,
315
+ }
316
+
317
+ export function isFeatureAvailable(name: keyof Features): boolean {
318
+ switch (name) {
319
+ case "eyedropper": return "EyeDropper" in window;
320
+ }
321
+
322
+ return false;
323
+ }
324
+
325
+ export class Timer {
326
+
327
+ protected _timers: Map<string, any>;
328
+
329
+ /**
330
+ *
331
+ */
332
+
333
+ setTimeout(name: string, time: number, callback: Function) {
334
+ if (!this._timers) {
335
+ this._timers = new Map();
336
+ }
337
+ else {
338
+ this.clearTimeout(name);
339
+ }
340
+
341
+ const tm = setTimeout(callback, time);
342
+ this._timers.set(name, tm);
343
+
344
+ return tm;
345
+ }
346
+
347
+ clearTimeout(name: string) {
348
+ if (this._timers && this._timers.has(name)) {
349
+ clearTimeout(this._timers.get(name));
350
+ this._timers.delete(name);
351
+ }
352
+ }
353
+
354
+ /**
355
+ *
356
+ */
357
+
358
+ setInterval(name: string, time: number, callback: Function) {
359
+ if (!this._timers) {
360
+ this._timers = new Map();
361
+ }
362
+ else {
363
+ this.clearInterval(name);
364
+ }
365
+
366
+ const tm = setInterval(callback, time);
367
+ this._timers.set(name, tm);
368
+
369
+ return tm;
370
+ }
371
+
372
+ clearInterval(name: string) {
373
+ if (this._timers && this._timers.has(name)) {
374
+ clearInterval(this._timers.get(name));
375
+ this._timers.delete(name);
376
+ }
377
+ }
378
+
379
+ clearAllTimeouts() {
380
+ this._timers?.forEach(t => {
381
+ clearTimeout(t);
382
+ });
383
+
384
+ this._timers = null;
385
+ }
386
+ }
387
+
388
+ /**
389
+ *
390
+ */
391
+
392
+ export function asap(callback: () => void) {
393
+ return requestAnimationFrame(callback);
394
+ }
395
+
396
+ export function oneshot(callback: () => void, ms = 0) {
397
+ return setTimeout(callback, ms);
398
+ }
399
+
400
+
401
+ // :: STRING UTILS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
402
+
403
+
404
+ /**
405
+ * prepend 0 to a value to a given length
406
+ * @param value
407
+ * @param length
408
+ */
409
+
410
+ export function pad(what: any, size: number, ch: string = '0') {
411
+
412
+ let value: string;
413
+
414
+ if (!isString(what)) {
415
+ value = '' + what;
416
+ }
417
+ else {
418
+ value = what;
419
+ }
420
+
421
+ if (size > 0) {
422
+ return value.padEnd(size, ch);
423
+ }
424
+ else {
425
+ return value.padStart(-size, ch);
426
+ }
427
+ }
428
+
429
+ /**
430
+ * replace {0..9999} by given arguments
431
+ * @param format string
432
+ * @param args
433
+ *
434
+ * @example ```ts
435
+ *
436
+ * console.log( sprintf( 'here is arg 1 {1} and arg 0 {0}', 'argument 0', 'argument 1' ) )
437
+ */
438
+
439
+ export function sprintf(format: string, ...args: any[]) {
440
+ return format.replace(/{(\d+)}/g, function (match, index) {
441
+ return typeof args[index] != 'undefined' ? args[index] : match;
442
+ });
443
+ }
444
+
445
+ /**
446
+ * inverse of camel case
447
+ * theThingToCase -> the-thing-to-case
448
+ * @param {String} str
449
+ */
450
+
451
+ export function pascalCase(string: string): string {
452
+
453
+ let result = string;
454
+
455
+ result = result.replace(/([a-z])([A-Z])/g, "$1 $2");
456
+ result = result.toLowerCase();
457
+ result = result.replace(/[^- a-z0-9]+/g, ' ');
458
+
459
+ if (result.indexOf(' ') < 0) {
460
+ return result;
461
+ }
462
+
463
+ result = result.trim();
464
+ return result.replace(/ /g, '-');
465
+ }
466
+
467
+ export function camelCase(text: string) {
468
+ let result = text.toLowerCase();
469
+ result = result.replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => {
470
+ return chr.toUpperCase();
471
+ });
472
+ return result;
473
+ }
474
+
475
+ // :: DATES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
476
+
477
+ let cur_locale: string = 'fr-FR';
478
+
479
+ /**
480
+ * change the current locale for misc translations (date...)
481
+ * @param locale
482
+ * TODO: ????
483
+ */
484
+
485
+ export function _date_set_locale(locale: string) {
486
+ cur_locale = locale;
487
+ }
488
+
489
+ /**
490
+ *
491
+ * @param date
492
+ * @param options
493
+ * @example
494
+ * let date = new Date( );
495
+ * let options = { day: 'numeric', month: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric' };
496
+ * let text = date_format( date, options );
497
+ */
498
+
499
+ export function date_format(date: Date, options?: any): string {
500
+ //return new Intl.DateTimeFormat(cur_locale, options).format( date );
501
+ return formatIntlDate(date);
502
+ }
503
+
504
+ /**
505
+ * difference between 2 dates
506
+ * @param date
507
+ * @param options
508
+ * @return text
509
+ */
510
+
511
+ export function date_diff(date1: Date, date2: Date, options?: any): string {
512
+
513
+ let dt = (date1.getTime() - date2.getTime()) / 1000;
514
+
515
+ // seconds
516
+ let sec = dt;
517
+ if (sec < 60) {
518
+ return sprintf(_tr.global.diff_date_seconds, Math.round(sec));
519
+ }
520
+
521
+ // minutes
522
+ let min = Math.floor(sec / 60);
523
+ if (min < 60) {
524
+ return sprintf(_tr.global.diff_date_minutes, Math.round(min));
525
+ }
526
+
527
+ // hours
528
+ let hrs = Math.floor(min / 60);
529
+ return sprintf(_tr.global.diff_date_hours, hrs, min % 60);
530
+ }
531
+
532
+ /**
533
+ * format a date to sql format Y-M-D
534
+ * @param date
535
+ * @param withHours
536
+ */
537
+
538
+ export function date_to_sql(date: Date, withHours: boolean) {
539
+
540
+ if (withHours) {
541
+ return formatIntlDate(date, 'Y-M-D H:I:S');
542
+ }
543
+ else {
544
+ return formatIntlDate(date, 'Y-M-D');
545
+ }
546
+ }
547
+
548
+ /**
549
+ * construct a date from an utc date time (sql format)
550
+ * YYYY-MM-DD HH:MM:SS
551
+ */
552
+
553
+ export function date_sql_utc(date: string): Date {
554
+ let result = new Date(date + ' GMT');
555
+ return result;
556
+ }
557
+
558
+
559
+
560
+ /**
561
+ * return a number that is a representation of the date
562
+ * this number can be compared with another hash
563
+ */
564
+
565
+ export function date_hash(date: Date): number {
566
+ return date.getFullYear() << 16 | date.getMonth() << 8 | date.getDate();
567
+ }
568
+
569
+ /**
570
+ * return a copy of a date
571
+ */
572
+
573
+ export function date_clone(date: Date): Date {
574
+ return new Date(date.getTime());
575
+ }
576
+
577
+ /**
578
+ * return the week number of a date
579
+ */
580
+
581
+ export function date_calc_weeknum(date: Date): number {
582
+ const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
583
+ const pastDaysOfYear = (date.valueOf() - firstDayOfYear.valueOf()) / 86400000;
584
+ return Math.floor((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
585
+ }
586
+
587
+
588
+
589
+ /**
590
+ * parse a date according to the given format
591
+ * @param value - string date to parse
592
+ * @param fmts - format list - i18 tranlation by default
593
+ * allowed format specifiers:
594
+ * d: date (1 or 2 digits)
595
+ * D: date (2 digits)
596
+ * m: month (1 or 2 digits)
597
+ * M: month (2 digits)
598
+ * y: year (2 to 4 digits)
599
+ * Y: year (2 digits)
600
+ * YY: year (4 digits)
601
+ * h: hours (1 or 2 digits)
602
+ * H: hours (2 digits)
603
+ * i: minutes (1 or 2 digits)
604
+ * I: minutes (2 digits)
605
+ * s: seconds (1 or 2 digits)
606
+ * S: seconds (2 digits)
607
+ * <space>: 1 or more spaces
608
+ * any other char: <0 or more spaces><the char><0 or more spaces>
609
+ * each specifiers is separated from other by a pipe (|)
610
+ * more specific at first
611
+ * @example
612
+ * 'd/m/y|d m Y|dmy|y-m-d h:i:s|y-m-d|YY-M-D'
613
+ */
614
+
615
+ export function parseIntlDate(value: string, fmts: string = _tr.global.date_input_formats): Date {
616
+
617
+ let formats = fmts.split('|');
618
+ for (let fmatch of formats) {
619
+
620
+ //review: could do that only once & keep result
621
+ //review: add hours, minutes, seconds
622
+
623
+ let smatch = '';
624
+ for (let i = 0; i < fmatch.length; i++) {
625
+ const c = fmatch[i];
626
+
627
+ if (c == 'd') {
628
+ smatch += '(?<day>\\d{1,2})';
629
+ }
630
+ else if (c == 'D') {
631
+ smatch += '(?<day>\\d{2})';
632
+ }
633
+ else if (c == 'm') {
634
+ smatch += '(?<month>\\d{1,2})';
635
+ }
636
+ else if (c == 'M') {
637
+ smatch += '(?<month>\\d{2})';
638
+ }
639
+ else if (c == 'y') {
640
+ smatch += '(?<year>\\d{1,4})';
641
+ }
642
+ else if (c == 'Y') {
643
+ if (fmatch[i + 1] == 'Y') {
644
+ smatch += '(?<year>\\d{4})';
645
+ i++;
646
+ }
647
+ else {
648
+ smatch += '(?<year>\\d{2})';
649
+ }
650
+ }
651
+ else if (c == 'h') {
652
+ smatch += '(?<hour>\\d{1,2})';
653
+ }
654
+ else if (c == 'H') {
655
+ smatch += '(?<hour>\\d{2})';
656
+ }
657
+ else if (c == 'i') {
658
+ smatch += '(?<min>\\d{1,2})';
659
+ }
660
+ else if (c == 'I') {
661
+ smatch += '(?<min>\\d{2})';
662
+ }
663
+ else if (c == 's') {
664
+ smatch += '(?<sec>\\d{1,2})';
665
+ }
666
+ else if (c == 'S') {
667
+ smatch += '(?<sec>\\d{2})';
668
+ }
669
+ else if (c == ' ') {
670
+ smatch += '\\s+';
671
+ }
672
+ else {
673
+ smatch += '\\s*\\' + c + '\\s*';
674
+ }
675
+ }
676
+
677
+ let rematch = new RegExp('^' + smatch + '$', 'm');
678
+
679
+ let match = rematch.exec(value);
680
+
681
+ if (match) {
682
+ const now = new Date();
683
+
684
+ let d = parseInt(match.groups.day ?? '1');
685
+ let m = parseInt(match.groups.month ?? '1');
686
+ let y = parseInt(match.groups.year ?? now.getFullYear() + '');
687
+ let h = parseInt(match.groups.hour ?? '0');
688
+ let i = parseInt(match.groups.min ?? '0');
689
+ let s = parseInt(match.groups.sec ?? '0');
690
+
691
+ if (y > 0 && y < 100) {
692
+ y += 2000;
693
+ }
694
+
695
+ let result = new Date(y, m - 1, d, h, i, s, 0);
696
+
697
+ // we test the vdate validity (without adjustments)
698
+ // without this test, date ( 0, 0, 0) is accepted and transformed to 1969/11/31 (not fun)
699
+ let ty = result.getFullYear(),
700
+ tm = result.getMonth() + 1,
701
+ td = result.getDate();
702
+
703
+ if (ty != y || tm != m || td != d) {
704
+ //debugger;
705
+ return null;
706
+ }
707
+
708
+ return result;
709
+ }
710
+ }
711
+
712
+ return null;
713
+ }
714
+
715
+ /**
716
+ * format a date as string
717
+ * @param date - date to format
718
+ * @param fmt - format
719
+ * format specifiers:
720
+ * - d: date (no pad)
721
+ * - D: 2 digits date padded with 0
722
+ * - j: day of week short mode 'mon'
723
+ * - J: day of week long mode 'monday'
724
+ * - w: week number
725
+ * - m: month (no pad)
726
+ * - M: 2 digits month padded with 0
727
+ * - o: month short mode 'jan'
728
+ * - O: month long mode 'january'
729
+ * - y or Y: year
730
+ * - h: hour (24 format)
731
+ * - H: 2 digits hour (24 format) padded with 0
732
+ * - i: minutes
733
+ * - I: 2 digits minutes padded with 0
734
+ * - s: seconds
735
+ * - S: 2 digits seconds padded with 0
736
+ * - a: am or pm
737
+ * - anything else is inserted
738
+ * - if you need to insert some text, put it between {}
739
+ *
740
+ * @example
741
+ *
742
+ * 01/01/1970 11:25:00 with '{this is my demo date formatter: }H-i*M'
743
+ * "this is my demo date formatter: 11-25*january"
744
+ */
745
+
746
+ export function formatIntlDate(date: Date, fmt: string = _tr.global.date_format, utc = false) {
747
+
748
+ if (!date) {
749
+ return '';
750
+ }
751
+
752
+ let now = {
753
+ year: utc ? date.getUTCFullYear() : date.getFullYear(),
754
+ month: utc ? date.getUTCMonth() + 1 : date.getMonth() + 1,
755
+ day: utc ? date.getUTCDate() : date.getDate(),
756
+ wday: utc ? date.getUTCDay() : date.getDay(),
757
+ hours: utc ? date.getUTCHours() : date.getHours(),
758
+ minutes: utc ? date.getUTCMinutes() : date.getMinutes(),
759
+ seconds: utc ? date.getUTCSeconds() : date.getSeconds(),
760
+ milli: utc ? date.getUTCMilliseconds() : date.getMilliseconds()
761
+ };
762
+
763
+
764
+ let result = '';
765
+ let esc = 0;
766
+
767
+ for (let c of fmt) {
768
+
769
+ if (c == '{') {
770
+ if (++esc == 1) {
771
+ continue;
772
+ }
773
+ }
774
+ else if (c == '}') {
775
+ if (--esc == 0) {
776
+ continue;
777
+ }
778
+ }
779
+
780
+ if (esc) {
781
+ result += c;
782
+ continue;
783
+ }
784
+
785
+ if (c == 'd') {
786
+ result += now.day;
787
+ }
788
+ else if (c == 'D') {
789
+ result += pad(now.day, -2);
790
+ }
791
+ else if (c == 'j') { // day short
792
+ result += _tr.global.day_short[now.wday];
793
+ }
794
+ else if (c == 'J') { // day long
795
+ result += _tr.global.day_long[now.wday];
796
+ }
797
+ else if (c == 'w') { // week
798
+ result += date_calc_weeknum(date);
799
+ }
800
+ else if (c == 'W') { // week
801
+ result += pad(date_calc_weeknum(date), -2);
802
+ }
803
+ else if (c == 'm') {
804
+ result += now.month;
805
+ }
806
+ else if (c == 'M') {
807
+ result += pad(now.month, -2);
808
+ }
809
+ else if (c == 'o') { // month short
810
+ result += _tr.global.month_short[now.month - 1];
811
+ }
812
+ else if (c == 'O') { // month long
813
+ result += _tr.global.month_long[now.month - 1];
814
+ }
815
+ else if (c == 'y' || c == 'Y') {
816
+ result += pad(now.year, -4);
817
+ }
818
+ else if (c == 'a' || c == 'A') {
819
+ result += now.hours < 12 ? 'am' : 'pm';
820
+ }
821
+ else if (c == 'h') {
822
+ result += now.hours;
823
+ }
824
+ else if (c == 'H') {
825
+ result += pad(now.hours, -2);
826
+ }
827
+ else if (c == 'i') {
828
+ result += now.minutes;
829
+ }
830
+ else if (c == 'I') {
831
+ result += pad(now.minutes, -2);
832
+ }
833
+ else if (c == 's') {
834
+ result += now.seconds;
835
+ }
836
+ else if (c == 'S') {
837
+ result += pad(now.seconds, -2);
838
+ }
839
+ else if (c == 'l') {
840
+ result += now.milli;
841
+ }
842
+ else if (c == 'L') {
843
+ result += pad(now.milli, -3);
844
+ }
845
+ else {
846
+ result += c;
847
+ }
848
+ }
849
+
850
+ return result;
851
+ }
852
+
853
+ /**
854
+ * calc an age from 2 dates
855
+ * @param birth
856
+ * @param ref
857
+ * @returns age in years
858
+ */
859
+ export function calcAge(birth: Date, ref?: Date) {
860
+ if (ref === undefined) {
861
+ ref = new Date();
862
+ }
863
+
864
+ if (!birth) {
865
+ return 0;
866
+ }
867
+
868
+ let age = ref.getFullYear() - birth.getFullYear();
869
+ if (ref.getMonth() < birth.getMonth() || (ref.getMonth() == birth.getMonth() && ref.getDate() < birth.getDate())) {
870
+ age--;
871
+ }
872
+
873
+ return age;
874
+ }
875
+
876
+
877
+ // :: MISC ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
878
+
879
+ // TODO: mmmmm
880
+ export function beep() {
881
+ const snd = new Audio("data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
882
+ snd.play();
883
+ }
884
+
885
+
886
+ let sb_width_cache = -1;
887
+ let window_zoom = -1;
888
+
889
+ /**
890
+ * compute scrollbar size
891
+ */
892
+
893
+ export function getScrollbarSize() {
894
+
895
+ if (sb_width_cache < 0) {
896
+ let outerDiv = document.createElement('div');
897
+ outerDiv.style.cssText = 'overflow:auto;position:absolute;top:0;width:100px;height:100px';
898
+
899
+ let innerDiv = document.createElement('div');
900
+ innerDiv.style.width = '200px';
901
+ innerDiv.style.height = '200px';
902
+
903
+ outerDiv.appendChild(innerDiv);
904
+ document.body.appendChild(outerDiv);
905
+
906
+ sb_width_cache = outerDiv.offsetWidth - outerDiv.clientWidth;
907
+ document.body.removeChild(outerDiv);
908
+ }
909
+
910
+ return sb_width_cache;
911
+ }
912
+
913
+ export function getGlobalZoom( ) {
914
+ if( window_zoom<0 ) {
915
+ const style = window.getComputedStyle(document.body);
916
+ const matrix = style.transform;
917
+
918
+ if (matrix !== 'none') {
919
+ const values = matrix.split('(')[1].split(')')[0].split(',');
920
+ window_zoom = parseFloat(values[0]);
921
+ }
922
+ else {
923
+ window_zoom = parseFloat( style.zoom) || 1;
924
+ }
925
+ }
926
+
927
+ return window_zoom;
928
+ }
929
+
930
+ export function getSystemMetrics( part: "scrollbar" | "zoom" ) {
931
+ switch( part ) {
932
+ case "scrollbar": return getScrollbarSize( );
933
+ case "zoom": return getGlobalZoom( );
934
+ }
935
+
936
+ return 0;
937
+ }
938
+
939
+
940
+
941
+
942
+
943
+
944
+
945
+ /**
946
+ *
947
+ */
948
+
949
+ export const x4_class_ns_sym = Symbol("class-ns");
950
+ export function class_ns(ns: string) {
951
+ return function (constructor: Function) {
952
+ (constructor as any)[x4_class_ns_sym] = ns;
953
+ }
954
+ }
955
+
956
+ /**
957
+ *
958
+ */
959
+
960
+ export function setWaitCursor(wait: boolean) {
961
+ document.body.style.cursor = wait ? "wait" : "default";
962
+ }
963
+
964
+ /**
965
+ * return the focusable elements from a given node
966
+ */
967
+
968
+ export function getFocusableElements(root: Element) {
969
+ const els = [
970
+ 'button:not([tabindex="-1"]):not([disabled])',
971
+ '[href]',
972
+ 'input:not([disabled])',
973
+ 'select:not([disabled])',
974
+ 'textarea:not([disabled])',
975
+ '[tabindex]:not([tabindex="-1"])'
976
+ ]
977
+
978
+ const focusable = Array.from(root.querySelectorAll(els.join(',')));
979
+ return focusable.filter(x => (x as HTMLElement).offsetParent != null); // check visibility
980
+ }
981
+
982
+ /**
983
+ *
984
+ */
985
+
986
+ export enum kbNav {
987
+ first,
988
+ prev,
989
+ pgdn,
990
+ pgup,
991
+ next,
992
+ last,
993
+ left,
994
+ right,
995
+ }
996
+