@rokkit/core 1.0.0-next.16 → 1.0.0-next.160

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 (75) hide show
  1. package/README.md +121 -1
  2. package/package.json +31 -39
  3. package/src/calendar.js +44 -0
  4. package/src/colors/extra.json +16 -0
  5. package/src/colors/index.ts +24 -0
  6. package/src/colors/syntax.json +42 -0
  7. package/src/colors/tailwind.json +275 -0
  8. package/src/connector.js +34 -0
  9. package/src/constants.js +165 -107
  10. package/src/events.js +32 -0
  11. package/src/field-mapper.js +147 -0
  12. package/src/index.js +19 -27
  13. package/src/key-event-map.js +35 -0
  14. package/src/mapped-items.js +22 -0
  15. package/src/mapping.js +21 -0
  16. package/src/nested.js +28 -0
  17. package/src/string.js +97 -0
  18. package/src/theme.ts +208 -0
  19. package/src/ticks.js +26 -0
  20. package/src/types.js +160 -0
  21. package/src/utils.js +272 -0
  22. package/src/vite/icon-collections.js +73 -0
  23. package/src/vite/index.js +1 -0
  24. package/LICENSE +0 -21
  25. package/src/Accordion.svelte +0 -80
  26. package/src/Alerts.svelte +0 -39
  27. package/src/DropDown.svelte +0 -82
  28. package/src/DropSearch.svelte +0 -67
  29. package/src/EditableTabs.svelte +0 -31
  30. package/src/Icon.svelte +0 -15
  31. package/src/List-Discard.svelte +0 -48
  32. package/src/List.svelte +0 -65
  33. package/src/ListActions.svelte +0 -35
  34. package/src/NavTabs.svelte +0 -0
  35. package/src/NestedList.svelte +0 -77
  36. package/src/Overlay.svelte +0 -4
  37. package/src/PageNavigator.svelte +0 -94
  38. package/src/ResponsiveGrid.svelte +0 -73
  39. package/src/Scrollable.svelte +0 -8
  40. package/src/Searchable.svelte +0 -19
  41. package/src/Sidebar.svelte +0 -5
  42. package/src/Slider.svelte +0 -17
  43. package/src/SpinList.svelte +0 -48
  44. package/src/SplitPane.svelte +0 -109
  45. package/src/SplitView.svelte +0 -44
  46. package/src/Splitter.svelte +0 -95
  47. package/src/TabItem.svelte +0 -27
  48. package/src/TabItems.svelte +0 -34
  49. package/src/Tabs.svelte +0 -49
  50. package/src/Tree.svelte +0 -45
  51. package/src/actions/dismissable.js +0 -24
  52. package/src/actions/fillable.js +0 -114
  53. package/src/actions/hierarchy.js +0 -180
  54. package/src/actions/index.js +0 -7
  55. package/src/actions/navigable.js +0 -43
  56. package/src/actions/navigator.js +0 -179
  57. package/src/actions/pannable.js +0 -50
  58. package/src/actions/swipeable.js +0 -56
  59. package/src/actions/themeable.js +0 -23
  60. package/src/items/Collapsible.svelte +0 -51
  61. package/src/items/Connector.svelte +0 -26
  62. package/src/items/Link.svelte +0 -18
  63. package/src/items/Node.svelte +0 -52
  64. package/src/items/Pill.svelte +0 -33
  65. package/src/items/Separator.svelte +0 -1
  66. package/src/items/Summary.svelte +0 -27
  67. package/src/items/Text.svelte +0 -21
  68. package/src/items/index.js +0 -8
  69. package/src/list.js +0 -14
  70. package/src/mocks/Custom.svelte +0 -7
  71. package/src/mocks/index.js +0 -10
  72. package/src/stores/alerts.js +0 -3
  73. package/src/stores/index.js +0 -6
  74. package/src/stores/persist.js +0 -63
  75. package/src/stores/theme.js +0 -34
package/src/constants.js CHANGED
@@ -1,102 +1,149 @@
1
- export const defaultIcons = [
1
+ export { defaultColors, syntaxColors, shades, defaultPalette } from './colors/index'
2
+ export const DATA_IMAGE_REGEX = /^data:image\/(jpeg|png|gif|bmp|webp|svg\+xml)/i
3
+
4
+ // ─── Snippet names ────────────────────────────────────────────────────────────
5
+
6
+ export const ITEM_SNIPPET = 'itemContent'
7
+ export const GROUP_SNIPPET = 'groupContent'
8
+ /**
9
+ * @deprecated Use BASE_FIELDS from @rokkit/core instead.
10
+ * Retained for legacy ListController/FieldMapper/Proxy consumers (Toolbar, Table).
11
+ * Will be removed when those components migrate to Wrapper+Navigator.
12
+ * @type {import('./types).FieldMapping} Fields
13
+ */
14
+ export const DEFAULT_FIELDS = {
15
+ id: 'id',
16
+ href: 'href',
17
+ icon: 'icon',
18
+ text: 'text',
19
+ value: 'value',
20
+ keywords: 'keywords',
21
+ children: 'children',
22
+ iconPrefix: null,
23
+ image: 'image',
24
+ summary: 'summary',
25
+ notes: 'notes',
26
+ props: 'props',
27
+ target: 'target',
28
+ state: 'state',
29
+ level: 'level',
30
+ parent: 'parent',
31
+ currency: 'currency',
32
+ label: 'label',
33
+ component: 'component', // to be deprecated in favour of snippet
34
+ snippet: 'snippet',
35
+ disabled: 'disabled',
36
+ /* flag fields */
37
+ deleted: '_deleted',
38
+ expanded: '_expanded',
39
+ selected: '_selected'
40
+ }
41
+
42
+ // ─── BASE_FIELDS ─────────────────────────────────────────────────────────────
43
+ // Canonical field mapping for ProxyItem and all Wrapper+Navigator components.
44
+ // Semantic keys map to common raw data keys for backward compatibility.
45
+
46
+ export const BASE_FIELDS = {
47
+ // Identity
48
+ id: 'id',
49
+ value: 'value',
50
+ // Display
51
+ label: 'label',
52
+ icon: 'icon',
53
+ avatar: 'image',
54
+ subtext: 'description',
55
+ tooltip: 'title',
56
+ badge: 'badge',
57
+ shortcut: 'shortcut',
58
+ // Structure
59
+ children: 'children',
60
+ type: 'type',
61
+ snippet: 'snippet',
62
+ href: 'href',
63
+ hrefTarget: 'target',
64
+ // State
65
+ disabled: 'disabled',
66
+ expanded: 'expanded',
67
+ selected: 'selected'
68
+ }
69
+
70
+ export const DEFAULT_ICONS = [
2
71
  'accordion-opened',
3
72
  'accordion-closed',
4
73
  'action-remove',
74
+ 'action-cancel',
75
+ 'action-retry',
5
76
  'action-add',
6
77
  'action-clear',
7
78
  'action-search',
8
79
  'action-close',
9
- 'action-close-filled',
80
+ 'action-copy',
81
+ 'action-copysuccess',
82
+ 'action-check',
83
+ 'action-pin',
84
+ 'action-save',
85
+ 'action-unpin',
10
86
  'node-opened',
11
87
  'node-closed',
88
+ 'folder-opened',
89
+ 'folder-closed',
12
90
  'selector-opened',
13
91
  'selector-closed',
92
+ 'menu-opened',
93
+ 'menu-closed',
14
94
  'checkbox-checked',
15
95
  'checkbox-unchecked',
16
96
  'checkbox-unknown',
17
97
  'rating-filled',
18
98
  'rating-empty',
99
+ 'rating-half',
19
100
  'radio-off',
20
101
  'radio-on',
21
102
  'mode-dark',
22
103
  'mode-light',
104
+ 'mode-system',
23
105
  'navigate-left',
24
106
  'navigate-right',
25
107
  'navigate-up',
26
- 'navigate-down'
108
+ 'navigate-down',
109
+ 'palette-hex',
110
+ 'palette-presets',
111
+ 'state-error',
112
+ 'state-warning',
113
+ 'state-success',
114
+ 'state-info',
115
+ 'state-unknown',
116
+ 'badge-fail',
117
+ 'badge-warn',
118
+ 'badge-pass',
119
+ 'badge-unknown',
120
+ 'sort-none',
121
+ 'sort-ascending',
122
+ 'sort-descending',
123
+ 'validity-failed',
124
+ 'validity-passed',
125
+ 'validity-unknown',
126
+ 'validity-warning',
127
+ 'alert-clear',
128
+ 'alert-unread',
129
+ 'align-horizontal-left',
130
+ 'align-horizontal-right',
131
+ 'align-horizontal-center',
132
+ 'align-vertical-top',
133
+ 'align-vertical-bottom',
134
+ 'align-vertical-middle',
135
+ 'view-code',
136
+ 'view-chart'
27
137
  ]
28
138
 
29
- // export const defaultIcons = [
30
- // 'accordion-opened',
31
- // 'accordion-closed',
32
- // 'action-remove',
33
- // 'action-add',
34
- // 'action-clear',
35
- // 'action-search',
36
- // 'action-close',
37
- // 'node-opened',
38
- // 'node-closed',
39
- // 'checkbox-checked',
40
- // 'checkbox-unchecked',
41
- // 'checkbox-unknown',
42
- // 'rating-filled',
43
- // 'rating-empty',
44
- // 'rating-half',
45
- // 'radio-off',
46
- // 'radio-on',
47
- // 'folder-closed',
48
- // 'folder-opened',
49
- // 'navigate-up',
50
- // 'navigate-down',
51
- // 'navigate-left',
52
- // 'navigate-right',
53
- // 'selector-closed',
54
- // 'selector-opened',
55
- // 'mode-dark',
56
- // 'mode-light'
57
- // ]
58
-
59
- export const defaultOptions = {
139
+ export const DEFAULT_OPTIONS = {
60
140
  id: 'id',
61
141
  label: 'label',
62
142
  value: 'value',
63
143
  checked: 'checked'
64
144
  }
65
145
 
66
- /**
67
- * Structure to map custom fields for rendering
68
- *
69
- * @typedef FieldMapping
70
- * @property {string} [id='id'] - Unique id for the item
71
- * @property {string} [text='text'] - Attribute to identify the text to render
72
- * @property {string} [url='url'] - Attribute to identify a URL
73
- * @property {string} [icon='icon'] - Attribute to identify an icon class to render
74
- * @property {string} [image='image'] - Attribute to identify an image to render
75
- * @property {string} [children='children'] - Attribute to identify children of the current item
76
- * @property {string} [summary='summary']
77
- * @property {string} [notes='notes']
78
- * @property {string} [props='props']
79
- * @property {string} [isOpen='_open'] - Attribute to identify if the current item is open
80
- * @property {string} [isDeleted='_deleted'] - Attribute to identify if the current item is deleted
81
- * @property {FieldMapping} [fields] - Field mapping to be used on children in the next level
82
- */
83
- export const defaultFields = {
84
- id: 'id',
85
- url: 'url',
86
- text: 'text',
87
- children: 'data',
88
- icon: 'icon',
89
- image: 'image',
90
- component: 'component',
91
- summary: 'summary',
92
- notes: 'notes',
93
- props: 'props',
94
- target: 'target',
95
- isOpen: '_open',
96
- isDeleted: '_deleted'
97
- }
98
-
99
- export const defaultKeyMap = {
146
+ export const DEFAULT_KEYMAP = {
100
147
  ArrowRight: 'open',
101
148
  ArrowLeft: 'close',
102
149
  ArrowDown: 'down',
@@ -105,45 +152,56 @@ export const defaultKeyMap = {
105
152
  Escape: 'deselect'
106
153
  }
107
154
 
155
+ export const DEFAULT_THEME_MAPPING = {
156
+ surface: 'slate',
157
+ primary: 'orange',
158
+ secondary: 'pink',
159
+ accent: 'sky',
160
+ success: 'green',
161
+ warning: 'yellow',
162
+ danger: 'red',
163
+ error: 'red',
164
+ info: 'cyan'
165
+ }
166
+
167
+ export const TONE_MAP = {
168
+ z0: 50,
169
+ z1: 100,
170
+ z2: 200,
171
+ z3: 300,
172
+ z4: 400,
173
+ z5: 500,
174
+ z6: 600,
175
+ z7: 700,
176
+ z8: 800,
177
+ z9: 900,
178
+ z10: 950
179
+ }
180
+ /**
181
+ * Splits an icon name into its group and key components.
182
+ * @param {string} name - The icon name to split.
183
+ * @returns {{ group: string, key: string, value: string }} An object containing the group, key, and value of the icon name.
184
+ */
185
+ function splitIconName(name) {
186
+ const parts = name.split('-')
187
+ const group = parts.slice(0, parts.length - 1).join('-')
188
+ return { group, key: parts[parts.length - 1], value: name }
189
+ }
190
+
191
+ /**
192
+ * Generate a state icon mapping from a list of icon names
193
+ *
194
+ * @param {string[]} icons
195
+ * @returns {import('./types').StateIcons}
196
+ */
108
197
  export function stateIconsFromNames(icons) {
109
- return icons
110
- .map((k) => [...k.split('-')])
111
- .reduce(
112
- (acc, parts) => ({
113
- ...acc,
114
- [parts[0]]: { ...acc[parts[0]], [parts[1]]: parts.join('-') }
115
- }),
116
- {}
117
- )
198
+ return icons.map(splitIconName).reduce(
199
+ (acc, { group, key, value }) => ({
200
+ ...acc,
201
+ [group]: { ...acc[group], [key]: value }
202
+ }),
203
+ {}
204
+ )
118
205
  }
119
206
 
120
- export const defaultStateIcons = stateIconsFromNames(defaultIcons)
121
- // export const defaultStateIcons = {
122
- // accordion: {
123
- // opened: 'accordion-opened',
124
- // closed: 'accordion-closed'
125
- // },
126
- // item: {
127
- // remove: 'item-remove',
128
- // add: 'item-add',
129
- // clear: 'item-clear',
130
- // search: 'item-search'
131
- // },
132
- // node: {
133
- // opened: 'node-opened',
134
- // closed: 'node-closed'
135
- // },
136
- // list: {
137
- // selector: 'list-selector'
138
- // },
139
- // checkbox: {
140
- // checked: 'checkbox-checked',
141
- // unchecked: 'checkbox-unchecked',
142
- // unknown: 'checkbox-unknown'
143
- // },
144
- // rating: { filled: 'rating-filled', empty: 'rating-empty' },
145
- // radio: {
146
- // off: 'radio-off',
147
- // on: 'radio-on'
148
- // }
149
- // }
207
+ export const DEFAULT_STATE_ICONS = stateIconsFromNames(DEFAULT_ICONS)
package/src/events.js ADDED
@@ -0,0 +1,32 @@
1
+ import { noop } from './utils.js'
2
+
3
+ /**
4
+ * Creates an emitter object from the given properties.
5
+ *
6
+ * - Filters attributes that start with 'on' and are functions,
7
+ * - Returns an object with keys that are the event names (without the 'on' prefix)
8
+ * - If a default event is not present in the props, it will be set to a no-op function.
9
+ *
10
+ * @param {Object} props - The properties object to filter.
11
+ * @param {Array<string>} defaults - An array of default events.
12
+ * @returns {import('./types.js').EventHandlers} The emitter object.
13
+ */
14
+ export function createEmitter(props, defaults = []) {
15
+ const emit = {}
16
+
17
+ // Filter and add functions that start with 'on'
18
+ Object.entries(props)
19
+ .filter(([key, value]) => key.startsWith('on') && typeof value === 'function')
20
+ .forEach(([key, value]) => {
21
+ emit[key.slice(2)] = value
22
+ })
23
+
24
+ // Add default events with no-op function if not present
25
+ defaults.forEach((event) => {
26
+ if (!emit[event]) {
27
+ emit[event] = noop
28
+ }
29
+ })
30
+
31
+ return emit
32
+ }
@@ -0,0 +1,147 @@
1
+ import { isNil, has, omit } from 'ramda'
2
+ import { DEFAULT_FIELDS } from './constants.js'
3
+ import { isObject } from './utils.js'
4
+
5
+ export class FieldMapper {
6
+ #fields = { ...DEFAULT_FIELDS }
7
+ #childMapper = null
8
+
9
+ constructor(fields = DEFAULT_FIELDS) {
10
+ this.#updateFields(fields)
11
+ }
12
+
13
+ #updateFields(fields) {
14
+ Object.keys(fields).forEach((key) => {
15
+ this.#fields[key] = fields[key]
16
+ })
17
+ this.hasIcon = has(this.#fields.icon)
18
+ this.hasImage = has(this.#fields.image)
19
+ this.hasValue = has(this.#fields.value)
20
+ this.hasLabel = has(this.#fields.label)
21
+ this.hasComponent = has(this.#fields.component)
22
+ this.hasCurrency = has(this.#fields.currency)
23
+ this.withPrefix = (x) => [this.#fields.iconPrefix, x].join('-').replace(/^-+/g, '')
24
+ this.excludeFlags = omit([
25
+ this.#fields.isDeleted,
26
+ this.#fields.isHidden,
27
+ this.#fields.isSelected,
28
+ this.#fields.isFiltered,
29
+ this.#fields.isOpen
30
+ ])
31
+ }
32
+
33
+ getChildMapper() {
34
+ if (!this.#childMapper) {
35
+ this.#childMapper = new FieldMapper(this.fields.fields ?? this.fields)
36
+ }
37
+ return this.#childMapper
38
+ }
39
+ /**
40
+ * @private
41
+ */
42
+ prop(fieldName, value) {
43
+ if (isNil(value)) return null
44
+
45
+ if (typeof value === 'object') {
46
+ return value[this.fields[fieldName]]
47
+ }
48
+
49
+ return fieldName === 'label' ? value : null
50
+ }
51
+
52
+ /**
53
+ * Gets a mapped attribute from the original item
54
+ *
55
+ * @param {string} fieldName - Name of the field to get
56
+ * @returns {any|null} - The attribute value or null if not found
57
+ */
58
+ get(fieldName, value, defaultValue = null) {
59
+ return this.prop(fieldName, value) ?? defaultValue
60
+ }
61
+
62
+ get fields() {
63
+ return this.#fields
64
+ }
65
+
66
+ set fields(fields) {
67
+ this.#updateFields(fields)
68
+ }
69
+
70
+ getIcon(value) {
71
+ if (!this.hasIcon(value)) return null
72
+ const icon = value[this.fields.icon]
73
+ if (isObject(icon)) return this.withPrefix(icon[value[this.fields.state]])
74
+ return this.withPrefix(icon)
75
+ }
76
+
77
+ getValue(value) {
78
+ if (this.hasValue(value)) {
79
+ return value[this.fields.value]
80
+ }
81
+ return value
82
+ }
83
+
84
+ getFormattedText(value, formatter) {
85
+ const text = this.get('label', value)
86
+
87
+ if (isNil(text)) return ''
88
+
89
+ if (typeof formatter !== 'function') return text.toString()
90
+
91
+ if (this.hasCurrency(value)) {
92
+ return formatter(text, this.get('currency', value))
93
+ }
94
+ return formatter(text)
95
+ }
96
+
97
+ hasChildren(item) {
98
+ return (
99
+ !isNil(item) &&
100
+ has(this.fields.children, item) &&
101
+ Array.isArray(item[this.fields.children]) &&
102
+ item[this.fields.children].length > 0
103
+ )
104
+ }
105
+
106
+ isNested(items) {
107
+ return Array.isArray(items) && items.some((item) => this.hasChildren(item))
108
+ }
109
+
110
+ getChildren(item) {
111
+ return this.hasChildren(item) ? item[this.fields.children] : []
112
+ }
113
+
114
+ /**
115
+ * Finds children by an index path
116
+ *
117
+ * @param {Array<Object>} items
118
+ * @param {Array<number>} path
119
+ * @returns {Array<Object>}
120
+ */
121
+ getChildrenByPath(items, path = []) {
122
+ const result = path.reduce(
123
+ (children, index) => children?.[index]?.[this.fields.children],
124
+ items
125
+ )
126
+
127
+ if (result === undefined) throw new Error('Invalid path')
128
+ return result
129
+ }
130
+
131
+ /**
132
+ * Finds an item by an index path
133
+ *
134
+ * @param {Array<Object>} items
135
+ * @param {Array<number>} path
136
+ * @returns {Object|null}
137
+ */
138
+ getItemByPath(items, path = []) {
139
+ const result = path.reduce(
140
+ (item, index, i) => (i === 0 ? items?.[index] : item?.[this.fields.children]?.[index]),
141
+ /* skipcq: JS-W1042 default undefined is needed */ undefined
142
+ )
143
+
144
+ if (result === undefined) throw new Error('Invalid path')
145
+ return result
146
+ }
147
+ }
package/src/index.js CHANGED
@@ -1,27 +1,19 @@
1
- export * from './constants'
2
- export * from './items'
3
-
4
- export { default as Icon } from './Icon.svelte'
5
- // export { default as List } from './List-Discard.svelte'
6
- export { default as Tree } from './Tree.svelte'
7
- export { default as Accordion } from './Accordion.svelte'
8
- export { default as List } from './List.svelte'
9
- export { default as Searchable } from './Searchable.svelte'
10
- export { default as Scrollable } from './Scrollable.svelte'
11
- export { default as NestedList } from './NestedList.svelte'
12
- export { default as Tabs } from './Tabs.svelte'
13
-
14
- export { default as Alerts } from './Alerts.svelte'
15
- export { default as Slider } from './Slider.svelte'
16
- export { default as DropDown } from './DropDown.svelte'
17
- export { default as DropSearch } from './DropSearch.svelte'
18
- export { default as SpinList } from './SpinList.svelte'
19
-
20
- export { default as Sidebar } from './Sidebar.svelte'
21
- export { default as SplitPane } from './SplitPane.svelte'
22
- export { default as Splitter } from './Splitter.svelte'
23
- export { default as SplitView } from './SplitView.svelte'
24
- export { default as Overlay } from './Overlay.svelte'
25
-
26
- export { default as PageNavigator } from './PageNavigator.svelte'
27
- export { default as ResponsiveGrid } from './ResponsiveGrid.svelte'
1
+ // skipcq: JS-E1004 - Needed for exposing JS Doc types
2
+ export * from './types.js'
3
+ // skipcq: JS-E1004 - Needed for exposing all constants
4
+ export * from './constants.js'
5
+ // skipcq: JS-E1004 - Needed for exposing all functions
6
+ export * from './utils.js'
7
+ // skipcq: JS-E1004 - Needed for exposing all functions
8
+ export * from './nested.js'
9
+ // skipcq: JS-E1004 - Needed for exposing all functions
10
+ export * from './string.js'
11
+ // skipcq: JS-E1004 - Needed for exposing all functions
12
+ export * from './theme.js'
13
+ export { FieldMapper } from './field-mapper.js'
14
+ export { getItemAtIndex, getIndexForItem } from './mapped-items.js'
15
+ export { createEmitter } from './events.js'
16
+ export { getLineTypes } from './connector.js'
17
+ export { weekdays, getCalendarDays } from './calendar.js'
18
+ export { generateTicks } from './ticks.js'
19
+ export { getNestedFields } from './mapping.js'
@@ -0,0 +1,35 @@
1
+ import { find, toPairs } from 'ramda'
2
+
3
+ /**
4
+ * Class to manage key event mappings.
5
+ */
6
+ export class KeyEventMap {
7
+ constructor() {
8
+ this.mapping = {}
9
+ }
10
+
11
+ /**
12
+ * Add a new key mapping.
13
+ * @param {string} eventName - The event name.
14
+ * @param {Array<string>|RegExp} keys - The keys to match.
15
+ */
16
+ add(eventName, keys) {
17
+ if (!Array.isArray(keys) && !(keys instanceof RegExp)) {
18
+ throw new Error('Keys must be an array or a regular expression')
19
+ }
20
+ this.mapping[eventName] = keys
21
+ }
22
+
23
+ /**
24
+ * Get the event name for a given key.
25
+ * @param {string} key - The key to match.
26
+ * @returns {string|null} - The event name or null if no match is found.
27
+ */
28
+ getEventForKey(key) {
29
+ const matchEvent = ([_, keys]) =>
30
+ (Array.isArray(keys) && keys.includes(key)) || (keys instanceof RegExp && keys.test(key))
31
+
32
+ const event = find(matchEvent, toPairs(this.mapping))
33
+ return event ? event[0] : null
34
+ }
35
+ }
@@ -0,0 +1,22 @@
1
+ import { equals, isNil } from 'ramda'
2
+
3
+ /**
4
+ * Get an item at a specific index
5
+ * @param {Array<any>} items
6
+ * @param {any} index
7
+ * @returns
8
+ */
9
+ export function getItemAtIndex(items, index) {
10
+ if (isNil(index)) return null
11
+ return index >= 0 && index < items.length ? items[index] : null
12
+ }
13
+
14
+ /**
15
+ * Get the index for an item in an array
16
+ * @param {Array<any} items
17
+ * @param {any} item
18
+ * @returns
19
+ */
20
+ export function getIndexForItem(items, item) {
21
+ return items.findIndex((i) => equals(i, item))
22
+ }
package/src/mapping.js ADDED
@@ -0,0 +1,21 @@
1
+ import { DEFAULT_FIELDS } from './constants.js'
2
+
3
+ /**
4
+ * Gets the attribute from the node
5
+ * @param {*} node
6
+ * @param {string} attr
7
+ * @returns {*}
8
+ */
9
+ export function getAttribute(node, attr) {
10
+ return typeof node === 'object' && node !== null && attr !== null ? node[attr] : null
11
+ }
12
+
13
+ /**
14
+ * Fetches the fieldmapping for a child node
15
+ *
16
+ * @param {import('./types').FieldMapping} fields
17
+ * @returns {import('./types').FieldMapping}
18
+ */
19
+ export function getNestedFields(fields) {
20
+ return { ...DEFAULT_FIELDS, ...(fields.fields ?? fields) }
21
+ }
package/src/nested.js ADDED
@@ -0,0 +1,28 @@
1
+ import { omit } from 'ramda'
2
+ import { DEFAULT_FIELDS } from './constants.js'
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 = DEFAULT_FIELDS, level = 0) {
13
+ fields = { ...DEFAULT_FIELDS, ...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
+ }