@soleil-se/app-util 5.12.3 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ All notable changes to this project will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
+ ## [5.14.0] - 2026-04-24
11
+
12
+ - `localizedCompareBy` now accepts objects with a `key` and optional `order` (`'asc'` | `'desc'`,
13
+ defaults to `'asc'`) in addition to plain strings, enabling per-property sort direction.
14
+ Existing string-based usage is unchanged.
15
+
16
+ ## [5.13.0] - 2026-01-16
17
+
18
+ - Add comparator functions `localizedCompare` and `localizedCompareBy` to sort strings and objects
19
+ by a specific property using localized string comparison since String.localeCompare does not
20
+ work properly with nordic characters in Rhino.
21
+
10
22
  ## [5.12.3] - 2025-12-03
11
23
 
12
24
  - Even better type definitions for Svelte render functions.
package/common/index.d.ts CHANGED
@@ -79,3 +79,6 @@ export const isOffline: boolean;
79
79
  * @constant {boolean}
80
80
  */
81
81
  export const isOnline: boolean;
82
+ import { localizedCompare } from "./localized-compare";
83
+ import { localizedCompareBy } from "./localized-compare";
84
+ export { localizedCompare, localizedCompareBy };
package/common/index.js CHANGED
@@ -3,6 +3,7 @@ import app from '@sitevision/api/common/app';
3
3
 
4
4
  import { nativeRequire } from '../server';
5
5
  import getLegacyRouteUri from './legacy/getRouteUri';
6
+ import { localizedCompare, localizedCompareBy } from './localized-compare';
6
7
 
7
8
  /**
8
9
  * Regex for selecting leading slashes
@@ -150,7 +151,7 @@ export function getRouteUri(route = '', params = undefined) {
150
151
  */
151
152
  export function getViewUri(route = '', params = undefined) {
152
153
  if (!router?.getUrl) {
153
- console.warn('[@soleil-api/webapp-util] getViewUri requires router.getUrl support.');
154
+ console.warn('[@soleil-se/app-util] getViewUri requires router.getUrl support.');
154
155
  return undefined;
155
156
  }
156
157
 
@@ -183,3 +184,5 @@ export function setAppProps(data) {
183
184
  export function getAppProps(key) {
184
185
  return key ? appProps[key] : appProps;
185
186
  }
187
+
188
+ export { localizedCompare, localizedCompareBy };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Compares two strings in a localized manner, taking into account special characters.
3
+ * The order is defined as: a-z å ä æ ö ø (case-insensitive, with lowercase before uppercase).
4
+ * Characters not in this set are compared based on their Unicode code points after removing accents.
5
+ * @param {string} a - The first string to compare.
6
+ * @param {string} b - The second string to compare.
7
+ * @returns {number} Negative if a < b, positive if a > b, zero if equal.
8
+ */
9
+ export function localizedCompare(a: string, b: string): number;
10
+ /**
11
+ * Creates a comparator function for sorting objects by a specific property or properties.
12
+ * The property values are compared using localized string comparison.
13
+ * When given an array of properties, sorts by the first property, then by the second if equal, etc.
14
+ * Each property can be a string or an object with a `key` and optional `order` ('asc' | 'desc', defaults to 'asc').
15
+ * @param {string|{key: string, order?: 'asc'|'desc'}|Array<string|{key: string, order?: 'asc'|'desc'}>} prop - The property name(s) to compare by.
16
+ * @returns {(a: Object, b: Object) => number} A comparator function that accepts two objects and returns their sort order.
17
+ * @example
18
+ * arr.sort(localizedCompareBy('title'))
19
+ * arr.sort(localizedCompareBy(['lastName', 'firstName']))
20
+ * arr.sort(localizedCompareBy({ key: 'firstName', order: 'desc' }))
21
+ * arr.sort(localizedCompareBy([{ key: 'lastName', order: 'asc' }, { key: 'firstName', order: 'desc' }]))
22
+ */
23
+ export function localizedCompareBy(prop: string | {
24
+ key: string;
25
+ order?: 'asc' | 'desc';
26
+ } | Array<string | {
27
+ key: string;
28
+ order?: 'asc' | 'desc';
29
+ }>): (a: any, b: any) => number;
@@ -0,0 +1,111 @@
1
+ const charOrder = 'abcdefghijklmnopqrstuvwxyzåäæöø';
2
+
3
+ function normalizeChar(char) {
4
+ // Remove accents from unknown characters (é → e, ñ → n)
5
+ return char.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
6
+ }
7
+
8
+ function getCharInfo(char) {
9
+ const lowerChar = char.toLowerCase();
10
+ let index = charOrder.indexOf(lowerChar);
11
+ let isAccented = false;
12
+
13
+ // If not found, try normalized version
14
+ if (index === -1) {
15
+ const normalized = normalizeChar(lowerChar);
16
+ index = charOrder.indexOf(normalized);
17
+
18
+ if (index !== -1) {
19
+ isAccented = true;
20
+ } else {
21
+ // Completely unknown character
22
+ index = charOrder.length + char.charCodeAt(0);
23
+ }
24
+ }
25
+
26
+ return {
27
+ baseIndex: index,
28
+ isAccented,
29
+ isLower: char === char.toLowerCase() && char !== char.toUpperCase(),
30
+ accentCode: isAccented ? char.normalize('NFD').charCodeAt(1) || 0 : 0,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Compares two strings in a localized manner, taking into account special characters.
36
+ * The order is defined as: a-z å ä æ ö ø (case-insensitive, with lowercase before uppercase).
37
+ * Characters not in this set are compared based on their Unicode code points after removing accents.
38
+ * @param {string} a - The first string to compare.
39
+ * @param {string} b - The second string to compare.
40
+ * @returns {number} Negative if a < b, positive if a > b, zero if equal.
41
+ */
42
+ export function localizedCompare(a, b) {
43
+ // Handle null/undefined
44
+ if (a == null) return b == null ? 0 : -1;
45
+ if (b == null) return 1;
46
+
47
+ // Ensure strings
48
+ const strA = String(a);
49
+ const strB = String(b);
50
+
51
+ const minLength = Math.min(strA.length, strB.length);
52
+
53
+ for (let i = 0; i < minLength; i += 1) {
54
+ const infoA = getCharInfo(strA[i]);
55
+ const infoB = getCharInfo(strB[i]);
56
+
57
+ // 1. Compare base character (case and accent insensitive)
58
+ if (infoA.baseIndex !== infoB.baseIndex) {
59
+ return infoA.baseIndex - infoB.baseIndex;
60
+ }
61
+
62
+ // 2. Non-accented before accented (e.g., 'e' < 'é')
63
+ if (infoA.isAccented !== infoB.isAccented) {
64
+ return infoA.isAccented ? 1 : -1;
65
+ }
66
+
67
+ // 3. If both accented, compare accent types (é vs è)
68
+ if (infoA.isAccented && infoA.accentCode !== infoB.accentCode) {
69
+ return infoA.accentCode - infoB.accentCode;
70
+ }
71
+
72
+ // 4. Lowercase before uppercase (e.g., 'e' < 'E')
73
+ if (infoA.isLower !== infoB.isLower) {
74
+ return infoA.isLower ? -1 : 1;
75
+ }
76
+ }
77
+
78
+ return strA.length - strB.length;
79
+ }
80
+
81
+ /**
82
+ * Creates a comparator function for sorting objects by a specific property or properties.
83
+ * The property values are compared using localized string comparison.
84
+ * When given an array of properties, sorts by the first property, then by the second if equal, etc.
85
+ * Each property can be a string or an object with a `key` and optional `order` ('asc' | 'desc', defaults to 'asc').
86
+ * @param {string|{key: string, order?: 'asc'|'desc'}|Array<string|{key: string, order?: 'asc'|'desc'}>} prop - The property name(s) to compare by.
87
+ * @returns {(a: Object, b: Object) => number} A comparator function that accepts two objects and returns their sort order.
88
+ * @example
89
+ * arr.sort(localizedCompareBy('title'))
90
+ * arr.sort(localizedCompareBy(['lastName', 'firstName']))
91
+ * arr.sort(localizedCompareBy({ key: 'firstName', order: 'desc' }))
92
+ * arr.sort(localizedCompareBy([{ key: 'lastName', order: 'asc' }, { key: 'firstName', order: 'desc' }]))
93
+ */
94
+ export function localizedCompareBy(prop) {
95
+ const props = (Array.isArray(prop) ? prop : [prop])
96
+ .map((p) => (typeof p === 'string'
97
+ ? { key: p, order: 'asc' }
98
+ : { key: p.key, order: p.order ?? 'asc' }
99
+ ));
100
+
101
+ return (a, b) => {
102
+ for (let i = 0; i < props.length; i += 1) {
103
+ const { key, order } = props[i];
104
+ const result = localizedCompare(a[key], b[key]);
105
+ if (result !== 0) {
106
+ return order === 'desc' ? -result : result;
107
+ }
108
+ }
109
+ return 0;
110
+ };
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleil-se/app-util",
3
- "version": "5.12.3",
3
+ "version": "5.14.0",
4
4
  "description": "Utility functions for WebApps, RESTApps and Widgets in Sitevision.",
5
5
  "main": "./common/index.js",
6
6
  "author": "Soleil AB",
@@ -23,7 +23,7 @@
23
23
  "@sitevision/api": "*"
24
24
  },
25
25
  "scripts": {
26
- "create-type-definitions": "node ../../utils/createTypeDefinitions.js ./common/index.js ./client/index.js ./client/svelte/index.js ./client/svelte/3/index.js ./client/svelte/4/index.js ./client/svelte/5/index.js ./server/index.js ./server/svelte/index.js ./server/svelte/3/index.js ./server/svelte/4/index.js ./server/svelte/5/index.js ./server/app-data/index.js ./server/global-app-data/index.js"
26
+ "create-type-definitions": "node ../../utils/createTypeDefinitions.js ./common/index.js ./common/localized-compare/index.js ./client/index.js ./client/svelte/index.js ./client/svelte/3/index.js ./client/svelte/4/index.js ./client/svelte/5/index.js ./server/index.js ./server/svelte/index.js ./server/svelte/3/index.js ./server/svelte/4/index.js ./server/svelte/5/index.js ./server/app-data/index.js ./server/global-app-data/index.js"
27
27
  },
28
- "gitHead": "9b1f44728b2ba84db68acdad31699822067ced13"
28
+ "gitHead": "1685a5874b85ff0d0b005c815b7207cde49b3e72"
29
29
  }
@@ -1,6 +0,0 @@
1
- /**
2
- * Require a module natively.
3
- * @param {string} module
4
- * @returns any
5
- */
6
- export function nativeRequire(module: string): any;