x4js 2.3.20 → 2.3.21

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.
@@ -85,7 +85,12 @@ export function addTranslation( name: string, ...parts: any[] ) {
85
85
  _patch( lang.src_translations, p );
86
86
  } );
87
87
 
88
- lang.translations = _proxyfy( lang.src_translations, lang.base, true );
88
+ lang.translations = _proxyfy( lang.src_translations, lang.base, [] );
89
+
90
+ // the current language: _tr sees the new translations at once
91
+ if( name==current.name ) {
92
+ current.translations = lang.translations;
93
+ }
89
94
  }
90
95
 
91
96
  /**
@@ -117,96 +122,71 @@ function _patch( obj: any, by: any ) {
117
122
  /**
118
123
  * create a proxy for all sub objects
119
124
  * (deep traverse)
125
+ * @param path where obj is, from _tr: [] for _tr itself, ["global"] for _tr.global
120
126
  */
121
127
 
122
- function _proxyfy( obj: any, base: any, root: any ) {
128
+ function _proxyfy( obj: any, base: string, path: string[] ) {
123
129
 
124
130
  const result: any = {}
125
131
 
126
132
  for( const n in obj ) {
127
133
  if( typeof obj[n]!=="string" && !Array.isArray(obj[n])) {
128
- result[n] = _proxyfy( obj[n], base, false );
134
+ result[n] = _proxyfy( obj[n], base, [...path, n] );
129
135
  }
130
136
  else {
131
137
  result[n] = obj[n];
132
138
  }
133
139
  }
134
140
 
135
- return _mk_proxy( result, base, root );
141
+ return _mk_proxy( result, base, path );
136
142
  }
137
143
 
138
144
 
139
145
  /**
140
146
  * create a proxy for the given object
147
+ * what is not found is asked to the base language, at the same path
141
148
  */
142
149
 
143
- function _mk_proxy( obj: any, base: string, root: boolean ) : any {
150
+ function _mk_proxy( obj: any, base: string, path: string[] ) : any {
144
151
  return new Proxy( obj, {
145
152
  get: (target, prop) => {
146
- if( root ) {
147
- req_path = [prop];
148
- }
149
- else {
150
- req_path.push( prop );
153
+ const value = target[prop];
154
+
155
+ // a symbol is not a translation
156
+ if( value!==undefined || typeof prop==="symbol" ) {
157
+ return value;
151
158
  }
152
159
 
153
- let value = target[prop];
154
- if( value===undefined ) {
155
- if( base ) {
156
- value = _findBaseTrans( base );
157
- }
160
+ const asked = [...path, prop];
158
161
 
159
- if( value===undefined ) {
160
- console.error( "I18N error: unable to find", '_tr.'+req_path.join('.') );
161
- }
162
+ // not in this language: in its base, which looks in its own base
163
+ if( base ) {
164
+ return _findBaseTrans( base, asked );
162
165
  }
163
166
 
164
- return value;
167
+ console.error( "I18N error: unable to find", '_tr.'+asked.join('.') );
168
+ return undefined;
165
169
  }
166
170
  });
167
171
  }
168
172
 
169
-
170
173
  /**
171
- * when we ask for _tr.xxx
172
- * reqpath is set to [xxx]
173
- *
174
- * then when we try to get _tr.xxx.yyy
175
- * reqpath is [xxx,yyy]
176
- * if yyy is not found, we try with base langage for the full reqpath
177
- * until no base found
174
+ * the translation at path in the language base
175
+ * its proxies look in their own base: the last language of the chain tells what is missing
178
176
  */
179
177
 
180
- let req_path: (string | symbol)[];
178
+ function _findBaseTrans( base: string, path: string[] ) {
181
179
 
182
- /**
183
- *
184
- */
185
-
186
- function _findBaseTrans( base: any ) {
187
-
188
- while( base ) {
189
- const lang = languages[base];
190
- let trans = lang.translations;
191
- let value;
192
-
193
- for( const p of req_path ) {
194
- value = trans[p];
195
- if( value===undefined ) {
196
- break;
197
- }
180
+ let trans = languages[base].translations;
198
181
 
199
- trans = value;
200
- }
201
-
202
- if( value!==undefined ) {
203
- return trans;
182
+ for( const p of path ) {
183
+ trans = trans[p];
184
+ if( trans===undefined ) {
185
+ return undefined;
204
186
  }
205
-
206
- base = lang.base;
207
187
  }
208
188
 
209
- return undefined;
189
+ return trans;
210
190
  }
211
191
 
212
192
  /**
@@ -320,7 +300,7 @@ let fr = {
320
300
 
321
301
  diff_date_seconds: '{0} secondes',
322
302
  diff_date_minutes: '{0} minutes',
323
- diff_date_hours: '{0} heures',
303
+ diff_date_hours: '{0} heures {1} minutes',
324
304
 
325
305
  invalid_date: 'Date non reconnue ({0})',
326
306
  empty_list: 'Liste vide',
@@ -385,7 +365,7 @@ let en = {
385
365
 
386
366
  diff_date_seconds: '{0} seconds',
387
367
  diff_date_minutes: '{0} minutes',
388
- diff_date_hours: '{0} hours',
368
+ diff_date_hours: '{0} hours {1} minutes',
389
369
 
390
370
  invalid_date: 'Unrecognized date({0})',
391
371
  empty_list: 'Empty list',
@@ -835,7 +835,13 @@ function transformSegments( segs: PathSegment[], m: Matrix ): PathSegment[] {
835
835
 
836
836
  function svgSource( icon: string ): string {
837
837
  while (icon.startsWith('var:')) {
838
- icon = document.documentElement.style.getPropertyValue(icon.substring(4)).trim()
838
+ // "var:home" or "var:--home": the variable --home, set on <html>
839
+ let name = icon.substring(4)
840
+ if (!name.startsWith('--')) {
841
+ name = '--' + name
842
+ }
843
+
844
+ icon = document.documentElement.style.getPropertyValue(name).trim()
839
845
  }
840
846
 
841
847
  if (icon.startsWith('data:image/svg+xml;base64,')) {
@@ -1297,13 +1303,19 @@ export class X4PDFBuilder {
1297
1303
  }
1298
1304
 
1299
1305
  const image = (jpegBytes: Uint8Array, x: number, y: number, w: number, h: number) => {
1306
+ // the size is in the frame header (SOF): the segments are followed, an EXIF
1307
+ // thumbnail inside a segment is skipped
1300
1308
  let imgWidth = 0, imgHeight = 0
1301
- for (let i = 0; i < jpegBytes.length - 1; i++) {
1302
- if (jpegBytes[i] === 0xFF && (jpegBytes[i + 1] === 0xC0 || jpegBytes[i + 1] === 0xC2)) {
1309
+ let i = 2
1310
+ while (i + 9 < jpegBytes.length && jpegBytes[i] === 0xFF) {
1311
+ const marker = jpegBytes[i + 1]
1312
+ if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) {
1303
1313
  imgHeight = (jpegBytes[i + 5] << 8) | jpegBytes[i + 6]
1304
1314
  imgWidth = (jpegBytes[i + 7] << 8) | jpegBytes[i + 8]
1305
1315
  break
1306
1316
  }
1317
+
1318
+ i += 2 + ((jpegBytes[i + 2] << 8) | jpegBytes[i + 3])
1307
1319
  }
1308
1320
 
1309
1321
  const imgName = `/Im${imageCount++}`
@@ -91,6 +91,11 @@ function sequenceOf( ev: KeyboardEvent ): string {
91
91
  key = ev.code[3].toLowerCase( );
92
92
  }
93
93
 
94
+ // the space bar ( " " ): written "Space" in a sequence
95
+ if( key===" " ) {
96
+ key = "space";
97
+ }
98
+
94
99
  return join( { shift: ev.shiftKey, ctrl: ev.ctrlKey, cmd: ev.metaKey, alt: ev.altKey, key } );
95
100
  }
96
101
 
@@ -158,7 +163,6 @@ const key_names: Record<string,string> = {
158
163
  arrowdown: "↓",
159
164
  escape: "Esc",
160
165
  delete: "Del",
161
- " ": "Space",
162
166
  };
163
167
 
164
168
  /**
@@ -121,33 +121,42 @@ export function isUnitLess( name: string ) {
121
121
  return unitless[name] ? true : false;
122
122
  }
123
123
 
124
+ // one sheet for the page, shared by all the Stylesheet: a name is one rule,
125
+ // whatever instance sets it
126
+ let page_sheet: CSSStyleSheet = null;
127
+ const page_rules = new Map<string, CSSRule>( );
128
+
129
+ function pageSheet( ): CSSStyleSheet {
130
+ if( !page_sheet ) {
131
+ // found by its id: a title would make it a "preferred" style sheet,
132
+ // of which a page applies only one
133
+ let dom = document.getElementById( 'x4-dynamic-css' ) as HTMLStyleElement;
134
+ if( !dom ) {
135
+ dom = document.createElement( 'style' );
136
+ dom.id = 'x4-dynamic-css';
137
+ document.head.appendChild( dom );
138
+ }
139
+
140
+ page_sheet = dom.sheet;
141
+ }
142
+
143
+ return page_sheet;
144
+ }
145
+
124
146
  /**
125
- *
147
+ * Rules added to the page while it runs, in one <style id="x4-dynamic-css">.
148
+ * @deprecated the CSS variables do it: setStyleVariable( ) on a component,
149
+ * var( --name ) in its stylesheet
126
150
  */
127
151
 
128
152
  export class Stylesheet {
129
153
 
130
154
  readonly _sheet: CSSStyleSheet;
131
- readonly _rules: Map<string, number> = new Map( );
155
+ readonly _rules: Map<string, CSSRule>;
132
156
 
133
157
  constructor() {
134
-
135
- function getStyleSheet( name: string ) : CSSStyleSheet {
136
- for(let i=0; i<document.styleSheets.length; i++) {
137
- let sheet = document.styleSheets[i];
138
- if(sheet.title === name ) {
139
- return sheet;
140
- }
141
- }
142
- }
143
-
144
- this._sheet = getStyleSheet( 'x4-dynamic-css' );
145
- if( !this._sheet ) {
146
- const dom = document.createElement( 'style' );
147
- dom.setAttribute('id', 'x4-dynamic-css' );
148
- document.head.appendChild(dom);
149
- this._sheet = dom.sheet
150
- }
158
+ this._sheet = pageSheet( );
159
+ this._rules = page_rules;
151
160
  }
152
161
 
153
162
  /**
@@ -156,20 +165,31 @@ export class Stylesheet {
156
165
  * @param {string} definition - css definition of the rule
157
166
  * @example
158
167
  * setRule('xbody', "body { background-color: #ff0000; }" );
168
+ * @deprecated a CSS variable: setStyleVariable( ) on a component, var( --name ) in its stylesheet
159
169
  */
160
170
 
161
171
  public setRule(name: string, definition: any ) {
162
172
 
163
173
  if( isString(definition) ) {
164
- let index = this._rules.get( name );
165
- if (index !== undefined) {
166
- this._sheet.deleteRule(index);
174
+ const sheet = this._sheet;
175
+ const old = this._rules.get( name );
176
+
177
+ // in the place of the rule of that name, else at the end
178
+ let index = sheet.cssRules.length;
179
+ if( old ) {
180
+ const at = Array.prototype.indexOf.call( sheet.cssRules, old );
181
+ if( at>=0 ) {
182
+ index = at;
183
+ }
167
184
  }
168
- else {
169
- index = this._sheet.cssRules.length;
185
+
186
+ // inserted first: a definition that is not valid throws, the old rule stays
187
+ sheet.insertRule( definition, index );
188
+ if( old && sheet.cssRules[index+1]===old ) {
189
+ sheet.deleteRule( index+1 );
170
190
  }
171
191
 
172
- this._rules.set( name, this._sheet.insertRule( definition, index) );
192
+ this._rules.set( name, sheet.cssRules[index] );
173
193
  }
174
194
  else {
175
195
  let idx = 1;
@@ -203,6 +223,7 @@ export class Stylesheet {
203
223
  * ```
204
224
  * let color = Component.getCss( ).getVar( 'button-color' );
205
225
  * ```
226
+ * @deprecated getStyleVariable( ) of a component, or getComputedStyle( document.documentElement ).getPropertyValue( "--name" )
206
227
  */
207
228
 
208
229
  public static getVar( name: string ) : any {
@@ -156,7 +156,7 @@ class SvgItem {
156
156
  * @param cap
157
157
  */
158
158
 
159
- strokeCap( cap: "butt" | "round" | "sqaure" ) {
159
+ strokeCap( cap: "butt" | "round" | "square" ) {
160
160
  return this.setAttr( "stroke-linecap", cap );
161
161
  }
162
162
 
@@ -218,12 +218,13 @@ class SvgItem {
218
218
  */
219
219
 
220
220
  getNumAttr( name: string ) {
221
- const a = this._dom.getAttribute( name )
222
- if( a=='' ) {
221
+ // not set or empty: 0; "1.5" is 1.5
222
+ const a = this._dom.getAttribute( name );
223
+ if( !a ) {
223
224
  return 0;
224
225
  }
225
226
 
226
- return parseInt( a );
227
+ return parseFloat( a );
227
228
  }
228
229
 
229
230
  /**
@@ -271,7 +272,9 @@ class SvgItem {
271
272
  */
272
273
 
273
274
  addClass( cls: string ): this {
274
- if( !cls ) return;
275
+ if( !cls ) {
276
+ return this;
277
+ }
275
278
 
276
279
  cls = cls.trim();
277
280
  if( cls.indexOf(' ')>=0 ) {
@@ -484,12 +487,13 @@ export class SvgPath extends SvgItem {
484
487
 
485
488
  /**
486
489
  * draw an arc
487
- * Draws an elliptical arc from the current point to a new point.
490
+ * A circular arc around (x, y), from the angle start to end in degrees: 0 is the top,
491
+ * 90 the right (clockwise). It starts a new subpath at its first point.
488
492
  *
489
- * @param x - The x-coordinate of the center of the ellipse.
490
- * @param y - The y-coordinate of the center of the ellipse.
493
+ * @param x - The x-coordinate of the center.
494
+ * @param y - The y-coordinate of the center.
491
495
  * @param r - The radius of the arc.
492
- * @param start - The start angle of the arc in degrees (0 is right, 90 is down).
496
+ * @param start - The start angle of the arc in degrees (0 is the top, 90 is the right).
493
497
  * @param end - The end angle of the arc in degrees.
494
498
  * @param clockwise - If `true`, the arc is drawn clockwise; otherwise, counter-clockwise. Defaults to `true`.
495
499
  *
@@ -612,12 +616,16 @@ export class SvgIcon extends SvgItem {
612
616
  constructor( svg: string ) {
613
617
  super( "svg" );
614
618
 
619
+ // a data url is decoded when it is encoded ("data:image/svg+xml,%3Csvg..."), a raw svg never
615
620
  if( svg.startsWith("data:image/svg+xml,") ) {
616
621
  svg = svg.substring( 19 );
622
+ if( !svg.startsWith( "<" ) ) {
623
+ svg = decodeURIComponent( svg );
624
+ }
617
625
  }
618
626
 
619
627
  const parser = new DOMParser();
620
- const doc = parser.parseFromString( decodeURIComponent(svg), "image/svg+xml");
628
+ const doc = parser.parseFromString( svg, "image/svg+xml");
621
629
 
622
630
  const parserErrorElement = doc.querySelector("parsererror");
623
631
  if( parserErrorElement ) {
@@ -629,11 +637,9 @@ export class SvgIcon extends SvgItem {
629
637
  this._dom.setAttribute( svgRoot.attributes[i].name, svgRoot.attributes[i].value );
630
638
  }
631
639
 
632
- for( let i=0; i<svgRoot.childNodes.length; i++) {
633
- const child = svgRoot.childNodes[i];
634
- if (child.nodeType === 1) {
635
- this._dom.appendChild(child);
636
- }
640
+ // the elements, all of them: each one moved leaves the list
641
+ for( const child of Array.from( svgRoot.children ) ) {
642
+ this._dom.appendChild( child );
637
643
  }
638
644
  }
639
645
  }
@@ -707,7 +713,9 @@ export class SvgGradient extends SvgItem {
707
713
  * @returns The current `SvgGradient` instance for chaining.
708
714
  */
709
715
  addStop( offset: number_or_perc, color: string ): this {
710
- this._dom.insertAdjacentHTML( "beforeend", `<stop offset="${offset}%" stop-color="${color}"></stop>`);
716
+ // a number is a percentage: 20 is "20%"
717
+ const o = isString( offset ) ? offset : offset + "%";
718
+ this._dom.insertAdjacentHTML( "beforeend", `<stop offset="${o}" stop-color="${color}"></stop>`);
711
719
  return this;
712
720
  }
713
721
  }
@@ -124,8 +124,8 @@ export function unsafeHtml(x: string): UnsafeHtml {
124
124
  export function unsafe(strings: TemplateStringsArray, ...values: any[]): UnsafeHtml {
125
125
 
126
126
  const resolve = ( value: any ) : string | UnsafeHtml => {
127
- let v: string;
128
- if( !value ) {
127
+ // nothing to write: null, undefined (after the last part of the template)
128
+ if( value===undefined || value===null ) {
129
129
  return "";
130
130
  }
131
131
 
@@ -137,7 +137,8 @@ export function unsafe(strings: TemplateStringsArray, ...values: any[]): UnsafeH
137
137
  return value.toString();
138
138
  }
139
139
 
140
- return sanitizeHtml(value);
140
+ // a number, a boolean...: its text, escaped
141
+ return sanitizeHtml( String( value ) );
141
142
  }
142
143
 
143
144
  const result = strings.reduce((acc, str, i) => {
@@ -561,7 +562,8 @@ export function date_format(date: Date, options?: any): string {
561
562
 
562
563
  export function date_diff(date1: Date, date2: Date, options?: any): string {
563
564
 
564
- let dt = (date1.getTime() - date2.getTime()) / 1000;
565
+ // the gap, whichever date comes first
566
+ let dt = Math.abs(date1.getTime() - date2.getTime()) / 1000;
565
567
 
566
568
  // seconds
567
569
  let sec = dt;
@@ -575,7 +577,7 @@ export function date_diff(date1: Date, date2: Date, options?: any): string {
575
577
  return sprintf(_tr.global.diff_date_minutes, Math.round(min));
576
578
  }
577
579
 
578
- // hours
580
+ // hours, and the minutes left
579
581
  let hrs = Math.floor(min / 60);
580
582
  return sprintf(_tr.global.diff_date_hours, hrs, min % 60);
581
583
  }
@@ -626,13 +628,20 @@ export function date_clone(date: Date): Date {
626
628
  }
627
629
 
628
630
  /**
629
- * return the week number of a date
631
+ * return the week number of a date, ISO 8601: weeks from monday,
632
+ * the week 1 holds the first thursday of the year (the 29/12/2025 is in the week 1 of 2026)
630
633
  */
631
634
 
632
635
  export function date_calc_weeknum(date: Date): number {
633
- const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
634
- const pastDaysOfYear = (date.valueOf() - firstDayOfYear.valueOf()) / 86400000;
635
- return Math.floor((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
636
+ // in UTC: a day of 23 or 25 hours (summer time) does not move the weeks
637
+ const d = new Date( Date.UTC( date.getFullYear( ), date.getMonth( ), date.getDate( ) ) );
638
+
639
+ // the thursday of its week gives the year of the week
640
+ const day = d.getUTCDay( ) || 7; // monday 1 .. sunday 7
641
+ d.setUTCDate( d.getUTCDate( ) + 4 - day );
642
+
643
+ const jan1 = Date.UTC( d.getUTCFullYear( ), 0, 1 );
644
+ return Math.ceil( ( ( d.getTime( ) - jan1 ) / 86400000 + 1 ) / 7 );
636
645
  }
637
646
 
638
647