@rokkit/core 1.0.0-next.100

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.
@@ -0,0 +1,233 @@
1
+ import { defaultFields } from './constants'
2
+ import { isExpanded, hasChildren, getAttribute } from './mapping'
3
+ import { equals } from 'ramda'
4
+
5
+ /**
6
+ * Checks if a specifc attribute of an item matches a value.
7
+ *
8
+ * @param {Object} item - The item.
9
+ * @param {string} attr - The attribute to check.
10
+ */
11
+ function isMatch(item, attr, value) {
12
+ const itemValue = attr ? getAttribute(item, attr) : item
13
+ return equals(itemValue, value)
14
+ }
15
+
16
+ /**
17
+ * Traverses the tree to find an item by value.
18
+ * @param {Array} items - The items array.
19
+ * @param {Object} fields - The fields mapping.
20
+ * @param {any} value - The value to find.
21
+ * @param {Array} position - The current position in the tree.
22
+ * @returns {Object} The found item, or null if not found.
23
+ */
24
+ function findInChildren(item, index, fields, value, attr, position) {
25
+ if (hasChildren(item, fields)) {
26
+ return findItemByValue(value, item[fields.children], fields.fields ?? fields, attr, [
27
+ ...position,
28
+ index
29
+ ])
30
+ }
31
+ return null
32
+ }
33
+ /**
34
+ * Traverses the tree to find an item by value.
35
+ * @param {Array} items - The items array.
36
+ * @param {Object} fields - The fields mapping.
37
+ * @param {any} value - The value to find.
38
+ * @param {Array} position - The current position in the tree.
39
+ * @returns {Object} The found item, or null if not found.
40
+ */
41
+ export function findItemByValue(value, items, fields = defaultFields, attr = null, position = []) {
42
+ for (let i = 0; i < items.length; i++) {
43
+ if (isMatch(items[i], attr, value)) {
44
+ return { item: items[i], position: [...position, i], fields }
45
+ }
46
+
47
+ const foundInChildren = findInChildren(items[i], i, fields, value, attr, position)
48
+ if (foundInChildren) return foundInChildren
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ /**
55
+ * Gets an item from an items array using an index array.
56
+ * @param {Array} indices - The index array.
57
+ * @param {Array} items - The items array.
58
+ * @param {Object} fields - The fields configuration.
59
+ * @returns {Object} The item.
60
+ */
61
+ export function findItemByIndexArray(indices, items, fields) {
62
+ let item = items[indices[0]]
63
+ let levelFields = fields
64
+ for (let level = 1; level < indices.length; level++) {
65
+ if (hasChildren(item, levelFields)) {
66
+ item = item[levelFields.children][indices[level]]
67
+ levelFields = levelFields.fields ?? levelFields
68
+ } else {
69
+ return null
70
+ }
71
+ }
72
+ return { item, position: indices, fields: levelFields }
73
+ }
74
+
75
+ /**
76
+ *
77
+ * @param {Array<integer>} position
78
+ * @param {Array<*>} items
79
+ * @param {import('./types').FieldMapping} fields
80
+ * @returns
81
+ */
82
+ export function findNearestItemBefore(position, items, fields) {
83
+ if (items.length === 0) return null
84
+ if (position.length === 0) return { item: items[0], position: [0], fields }
85
+
86
+ let index = position[position.length - 1]
87
+ let result = null
88
+ if (index > 0) {
89
+ index -= 1
90
+ if (position.length === 1) {
91
+ return findLastVisibleChild(items[index], [index], fields)
92
+ }
93
+
94
+ const sibling = findItemByIndexArray([...position.slice(0, -1), index], items, fields)
95
+ result = findLastVisibleChild(sibling.item, sibling.position, sibling.fields)
96
+ } else {
97
+ result = findItemByIndexArray(position.slice(0, -1), items, fields)
98
+ }
99
+ return result
100
+ }
101
+
102
+ /**
103
+ * Returns the next sibling of the current item.
104
+ *
105
+ * @param {*} parent
106
+ * @param {Array<integer>} position
107
+ * @param {import('./types').FieldMapping} fields
108
+ * @returns
109
+ */
110
+ export function findLastVisibleChild(parent, position, fields) {
111
+ if (isExpanded(parent, fields)) {
112
+ const children = parent[fields.children]
113
+ return findLastVisibleChild(
114
+ children[children.length - 1],
115
+ position.concat(children.length - 1),
116
+ fields.fields ?? fields
117
+ )
118
+ }
119
+ return { item: parent, position, fields }
120
+ }
121
+
122
+ /**
123
+ * Returns the next item in the tree.
124
+ * @param {Array} position - The position of the current item.
125
+ * @param {Array} items - The items in the tree.
126
+ * @param {Object} fields - The fields mapping.
127
+ * @returns {Object|null} The next item or null if there is none.
128
+ */
129
+ export function findNearestItemAfter(position, items, fields) {
130
+ if (items.length === 0) return null
131
+ if (position.length === 0) return { item: items[0], position: [0], fields }
132
+
133
+ const current = findItemByIndexArray(position, items, fields)
134
+ let result = null
135
+ if (isExpanded(current.item, current.fields)) {
136
+ result = getFirstChild(current, position)
137
+ } else if (position.length === 1) {
138
+ result = getNextSiblingAtRoot(position, items, fields)
139
+ } else {
140
+ result = getNextSiblingOrAncestor(position, items, fields)
141
+ }
142
+ return result
143
+ }
144
+
145
+ /**
146
+ * Returns the next child of the current item.
147
+ * @param {Object} current - The current item.
148
+ * @param {Array} position - The position of the current item.
149
+ * @returns {Object} The next child.
150
+ */
151
+ function getFirstChild(current, position) {
152
+ return {
153
+ item: current.item[current.fields.children][0],
154
+ position: position.concat(0),
155
+ fields: current.fields.fields ?? current.fields
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Returns the next sibling of the current item at the root level.
161
+ * @param {Array} position - The position of the current item.
162
+ * @param {Array} items - The items in the tree.
163
+ * @param {Object} fields - The fields mapping.
164
+ * @returns {Object|null} The next sibling or null if there is none.
165
+ */
166
+ function getNextSiblingAtRoot(position, items, fields) {
167
+ if (position[0] < items.length - 1) {
168
+ return {
169
+ item: items[position[0] + 1],
170
+ position: [position[0] + 1],
171
+ fields
172
+ }
173
+ }
174
+ return null
175
+ }
176
+
177
+ /**
178
+ * Returns the next sibling of the current item or the next item in the ancestor.
179
+ * @param {Array} position - The position of the current item.
180
+ * @param {Array} items - The items in the tree.
181
+ * @param {Object} fields - The fields mapping.
182
+ * @returns {Object|null} The next sibling or ancestor or null if there is none.
183
+ */
184
+ function getNextSiblingOrAncestor(position, items, fields) {
185
+ let index = position[position.length - 1]
186
+ let parent = findItemByIndexArray(position.slice(0, -1), items, fields)
187
+ let children = parent.item[parent.fields.children]
188
+ if (index < children.length - 1) {
189
+ index += 1
190
+ const sibling = findItemByIndexArray([...position.slice(0, -1), index], items, fields)
191
+ return { item: sibling.item, position: sibling.position, fields }
192
+ } else {
193
+ while (index === children.length - 1) {
194
+ index = position[position.length - 1]
195
+ position = position.slice(0, -1)
196
+ if (position.length === 0) return null
197
+ parent = findItemByIndexArray(position, items, fields)
198
+ children = parent.item[parent.fields.children]
199
+ }
200
+ return {
201
+ item: children[index + 1],
202
+ position: [...position, index + 1],
203
+ fields
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Creates a mapped list from an items array and a fields mapping.
210
+ * @param {Array<Object>} items - The items array.
211
+ * @param {import('./types').FieldMapping} fields - The fields mapping.
212
+ * @returns {Object} The mapped list.
213
+ */
214
+ export function mappedList(items, fields) {
215
+ const findByValue = (value) => findItemByValue(value, items, fields)
216
+ const findByAttribute = (value, attr) => findItemByValue(value, items, fields, attr)
217
+ const findByIndexArray = (index) => findItemByIndexArray(index, items, fields)
218
+ const previous = (position) => findNearestItemBefore(position, items, fields)
219
+ const next = (position) => findNearestItemAfter(position, items, fields)
220
+
221
+ const update = (newItems, newFields) => {
222
+ items = newItems
223
+ fields = newFields
224
+ }
225
+ return {
226
+ findByValue,
227
+ findByAttribute,
228
+ findByIndexArray,
229
+ previous,
230
+ next,
231
+ update
232
+ }
233
+ }
package/src/mapping.js ADDED
@@ -0,0 +1,138 @@
1
+ import { defaultFields } from './constants'
2
+ import { toString, isObject } from './utils'
3
+
4
+ /**
5
+ * Get the component to be used to render the item.
6
+ * If the component is null or undefined, it will return the default component.
7
+ *
8
+ * @param {object|string} value
9
+ * @param {import('./types.js').FieldMapping} fields
10
+ * @param {import('./types.js').ComponentMap} using
11
+ */
12
+ export function getComponent(value, fields, using) {
13
+ return fields.component && isObject(value)
14
+ ? using[value[fields.component]] ?? using.default
15
+ : using.default
16
+ }
17
+
18
+ /**
19
+ * Get the icon for the item. If the icon is an object, it will use the state to determine which icon to use.
20
+ *
21
+ * @param {object|string} value
22
+ * @param {import('./types.js').FieldMapping} fields
23
+ * @returns {string}
24
+ */
25
+ export function getIcon(value, fields = defaultFields) {
26
+ if (fields.icon === undefined || typeof (value ?? '') !== 'object') return null
27
+
28
+ const name = getIconFromObject(value, fields)
29
+ return fields.iconPrefix ? [fields.iconPrefix, name].join('-') : name
30
+ }
31
+
32
+ /**
33
+ * Get the icon for the item. If the icon is an object, it will use the state to determine which icon to use.
34
+ *
35
+ * @param {object} value
36
+ * @param {import('./types.js').FieldMapping} fields
37
+ * @returns {string}
38
+ */
39
+ function getIconFromObject(value, fields) {
40
+ if (typeof value[fields.icon] === 'object') return value[fields.icon][value[fields.state]]
41
+ return value[fields.icon]
42
+ }
43
+
44
+ /**
45
+ * Get the value for the item. If the value is an object,
46
+ * it will use the field mapping to determine which attribute to get.
47
+ *
48
+ * @param {*} node
49
+ * @param {import('./types').FieldMapping} fields
50
+ * @returns {*}
51
+ */
52
+ export function getValue(node, fields = defaultFields) {
53
+ return typeof node === 'object' && node !== null ? node[fields.value] ?? node[fields.text] : node
54
+ }
55
+
56
+ /**
57
+ * Get the text for the item. If the text is an object,
58
+ * it will use the field mapping to determine which attribute to get.
59
+ *
60
+ * @param {*} node
61
+ * @param {import('./types').FieldMapping} fields
62
+ * @returns {string}
63
+ */
64
+ export function getText(node, fields = defaultFields) {
65
+ const value = isObject(node) ? node[fields.text] : node
66
+ return value
67
+ }
68
+
69
+ /**
70
+ * Get the formatted text for the item. If the text is an object, use the field mapping to determine
71
+ * which attribute to get currency. Use the formatter or identity function to format the text.
72
+ *
73
+ * @param {*} node
74
+ * @param {import('./types').FieldMapping} fields
75
+ * @param {Function} formatter
76
+ * @returns {Function}
77
+ */
78
+ export function getFormattedText(node, fields = defaultFields, formatter = toString) {
79
+ const value = isObject(node) ? node[fields.text] : node
80
+ const currency = getAttribute(node, fields.currency)
81
+ const formatValue = typeof formatter === 'function' ? formatter : toString
82
+
83
+ return currency ? formatValue(value, currency) : formatValue(value)
84
+ }
85
+ /**
86
+ * Gets the attribute from the node
87
+ * @param {*} node
88
+ * @param {string} attr
89
+ * @returns {*}
90
+ */
91
+ export function getAttribute(node, attr) {
92
+ return typeof node === 'object' && node !== null && attr !== null ? node[attr] : null
93
+ }
94
+ /**
95
+ * Check if the current item is a parent
96
+ *
97
+ * @param {*} item
98
+ * @param {import('./types').FieldMapping} fields
99
+ * @returns {boolean}
100
+ */
101
+ export function hasChildren(item, fields) {
102
+ return (
103
+ item !== null &&
104
+ typeof item === 'object' &&
105
+ fields.children in item &&
106
+ Array.isArray(item[fields.children])
107
+ )
108
+ }
109
+
110
+ /**
111
+ * Check if the current item is a parent and is expanded
112
+ *
113
+ * @param {*} item
114
+ * @param {import('./types').FieldMapping} fields
115
+ * @returns {boolean}
116
+ */
117
+ export function isExpanded(item, fields) {
118
+ if (item === null) return false
119
+ if (!hasChildren(item, fields)) return false
120
+ if (fields.isOpen in item) {
121
+ return item[fields.isOpen]
122
+ }
123
+ return false
124
+ }
125
+
126
+ /**
127
+ * Verify if at least one item has children
128
+ *
129
+ * @param {Array<*>} items
130
+ * @param {import('./types').FieldMapping} fields
131
+ * @returns {boolean}
132
+ */
133
+ export function isNested(items, fields) {
134
+ for (let i = 0; i < items.length; i++) {
135
+ if (hasChildren(items[i], fields)) return true
136
+ }
137
+ return false
138
+ }
package/src/nested.js ADDED
@@ -0,0 +1,56 @@
1
+ import { omit } from 'ramda'
2
+ import { defaultFields } from './constants'
3
+
4
+ /**
5
+ * Flattens a nested list of items
6
+ *
7
+ * @param {Array} items
8
+ * @param {import('./types).FieldMapping} fields
9
+ * @param {number} level
10
+ * @returns {Array}
11
+ */
12
+ export function flattenNestedList(items, fields = defaultFields, level = 0) {
13
+ fields = { ...defaultFields, ...fields }
14
+ let data = []
15
+ items.forEach((item) => {
16
+ const children = item[fields.children] ?? []
17
+ data = [
18
+ ...data,
19
+ {
20
+ ...omit([fields.children], item),
21
+ [fields.level]: level,
22
+ [fields.parent]: children.length > 0
23
+ },
24
+ ...flattenNestedList(children, fields, level + 1)
25
+ ]
26
+ })
27
+ return data
28
+ }
29
+
30
+ /**
31
+ * Matches a path slug to a value in the menu
32
+ *
33
+ * @param {string} slug
34
+ * @param {Array} data
35
+ * @param {import('./types').FieldMapping} fields
36
+ * @returns {any}
37
+ */
38
+ export function findValueFromPath(slug, data, fields) {
39
+ fields = { ...defaultFields, ...fields }
40
+ const keys = slug.split('/')
41
+ let items = data
42
+ let value = null
43
+
44
+ keys.forEach((key, index) => {
45
+ const match = items.find((item) => item[fields.key] === key)
46
+ if (match) {
47
+ if (index < keys.length - 1) {
48
+ match[fields.isOpen] = true
49
+ items = match[fields.children]
50
+ } else {
51
+ value = match
52
+ }
53
+ }
54
+ })
55
+ return value
56
+ }
package/src/string.js ADDED
@@ -0,0 +1,97 @@
1
+ import { filter } from 'ramda'
2
+ /**
3
+ * Capitalizes the first letter of input string
4
+ *
5
+ * @param {String} str
6
+ * @returns {String}
7
+ */
8
+ export function toInitCapCase(text) {
9
+ return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()
10
+ }
11
+
12
+ /**
13
+ * Convert a hyphen separated string to PascalCase
14
+ *
15
+ * @param {String} text
16
+ * @returns
17
+ */
18
+ export function toPascalCase(text) {
19
+ return text
20
+ .split('-')
21
+ .map((part) => toInitCapCase(part))
22
+ .join('')
23
+ }
24
+
25
+ /**
26
+ * Convert a PascalCase string to snake case with separator as hyphen
27
+ *
28
+ * @param {string} text
29
+ * @returns {string}
30
+ */
31
+ export function toHyphenCase(text) {
32
+ return text
33
+ .replace(/\s+/, '-')
34
+ .replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
35
+ .replace(/^-/, '')
36
+ }
37
+
38
+ /**
39
+ * Sort by splitting hyphen separated strings while keeping strings with same number of parts together
40
+ *
41
+ * @param {String} a hyphen separates string
42
+ * @param {String} b hyphen separates string
43
+ * @param {string} separator - separator to split the string
44
+ * @returns {Number} -1, 0, 1 based on comparison
45
+ */
46
+ export function sortByParts(a, b, separator = '-') {
47
+ const partsOfA = a.split(separator)
48
+ const partsOfB = b.split(separator)
49
+
50
+ let result = compareStrings(partsOfA[0], partsOfB[0])
51
+ if (result === 0) result = partsOfA.length - partsOfB.length
52
+ if (result === 0) result = compareStrings(a, b)
53
+ return result
54
+ }
55
+
56
+ /**
57
+ * Simple comparison for two strings
58
+ *
59
+ * @param {String} a
60
+ * @param {String} b
61
+ * @returns
62
+ */
63
+ export function compareStrings(a, b) {
64
+ return a > b ? 1 : a < b ? -1 : 0
65
+ }
66
+
67
+ /**
68
+ * Generates a unique id from current timestamp
69
+ *
70
+ * @returns {String} timestamp based unique id
71
+ */
72
+ export function uniqueId(prefix = '', separator = '-') {
73
+ const pair = prefix && prefix.length > 0 ? [prefix] : []
74
+ pair.push(Date.now().toString(36))
75
+ return pair.join(separator)
76
+ }
77
+
78
+ /**
79
+ * Removes undefined and null values from the input object.
80
+ *
81
+ * @param {Object} obj
82
+ * @returns {Object}
83
+ */
84
+ export function compact(obj) {
85
+ return filter((x) => x !== undefined && x !== null, obj)
86
+ }
87
+
88
+ /**
89
+ * Converts an input number into it's hexadecimal representation, with optional left padded zeroes based on the `size`
90
+ *
91
+ * @param {number} value
92
+ * @param {number} size
93
+ * @returns
94
+ */
95
+ export function toHexString(value, size = 2) {
96
+ return value.toString(16).padStart(size, '0')
97
+ }