@rokkit/actions 1.0.0-next.44 → 1.0.0-next.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/actions",
3
- "version": "1.0.0-next.44",
3
+ "version": "1.0.0-next.48",
4
4
  "description": "Contains generic actions that can be used in various components.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -13,18 +13,18 @@
13
13
  "access": "public"
14
14
  },
15
15
  "devDependencies": {
16
- "@sveltejs/vite-plugin-svelte": "^2.4.3",
16
+ "@sveltejs/vite-plugin-svelte": "^2.4.5",
17
17
  "@testing-library/svelte": "^4.0.3",
18
18
  "@types/ramda": "^0.29.3",
19
- "@vitest/coverage-v8": "^0.33.0",
20
- "@vitest/ui": "~0.33.0",
19
+ "@vitest/coverage-v8": "^0.34.4",
20
+ "@vitest/ui": "~0.34.4",
21
21
  "jsdom": "^22.1.0",
22
- "svelte": "^4.1.1",
23
- "typescript": "^5.1.6",
22
+ "svelte": "^4.2.0",
23
+ "typescript": "^5.2.2",
24
24
  "validators": "latest",
25
- "vite": "^4.4.7",
26
- "vitest": "~0.33.0",
27
- "shared-config": "1.0.0-next.44"
25
+ "vite": "^4.4.9",
26
+ "vitest": "~0.34.4",
27
+ "shared-config": "1.0.0-next.48"
28
28
  },
29
29
  "files": [
30
30
  "src/**/*.js",
@@ -0,0 +1,34 @@
1
+ import { EventManager } from './lib'
2
+ /**
3
+ * Svelte action function for forwarding keyboard events from a parent element to a child.
4
+ * The child is selected using a CSS selector passed in the options object.
5
+ * Optionally, you can specify which keyboard events you want to forward: "keydown", "keyup", and/or "keypress".
6
+ * By default, all three events are forwarded.
7
+ * The action returns an object with a destroy method.
8
+ * The destroy method removes all event listeners from the parent.
9
+ *
10
+ * @param {HTMLElement} element - The parent element from which keyboard events will be forwarded.
11
+ * @param {import('./types').PushDownOptions} options - The options object.
12
+ * @returns {{destroy: Function}}
13
+ */
14
+ export function delegateKeyboardEvents(
15
+ element,
16
+ { selector, events = ['keydown', 'keyup', 'keypress'] }
17
+ ) {
18
+ const child = element.querySelector(selector)
19
+ const handlers = {}
20
+ const manager = EventManager(element)
21
+
22
+ function forwardEvent(event) {
23
+ child.dispatchEvent(new KeyboardEvent(event.type, event))
24
+ }
25
+
26
+ if (child) {
27
+ events.forEach((event) => (handlers[event] = forwardEvent))
28
+ manager.update(handlers)
29
+ }
30
+
31
+ return {
32
+ destroy: () => manager.reset()
33
+ }
34
+ }
package/src/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import './types'
2
+ export * from './lib/constants'
3
+ export * from './lib'
2
4
  export { fillable } from './fillable'
3
5
  export { pannable } from './pannable'
4
6
  export { navigable } from './navigable'
@@ -6,3 +8,5 @@ export { navigator } from './navigator'
6
8
  export { dismissable } from './dismissable'
7
9
  export { themable } from './themeable'
8
10
  export { swipeable } from './swipeable'
11
+ export { delegateKeyboardEvents } from './delegate'
12
+ export { virtualListViewport } from './lib/viewport'
@@ -0,0 +1,35 @@
1
+ export const dimensionAttributes = {
2
+ vertical: {
3
+ scroll: 'scrollTop',
4
+ offset: 'offsetHeight',
5
+ paddingStart: 'paddingTop',
6
+ paddingEnd: 'paddingBottom',
7
+ size: 'height'
8
+ },
9
+ horizontal: {
10
+ scroll: 'scrollLeft',
11
+ offset: 'offsetWidth',
12
+ paddingStart: 'paddingLeft',
13
+ paddingEnd: 'paddingRight',
14
+ size: 'width'
15
+ }
16
+ }
17
+
18
+ export const defaultResizerOptions = {
19
+ horizontal: false,
20
+ minSize: 40,
21
+ minVisible: 1,
22
+ maxVisible: null,
23
+ availableSize: 200,
24
+ start: 0
25
+ }
26
+
27
+ export const defaultVirtualListOptions = {
28
+ itemSelector: 'virtual-list-item',
29
+ contentSelector: 'virtual-list-content',
30
+ enabled: true,
31
+ horizontal: false,
32
+ start: 0,
33
+ limit: null,
34
+ index: null
35
+ }
@@ -6,27 +6,28 @@ export function EventManager(element, handlers = {}) {
6
6
 
7
7
  function activate() {
8
8
  if (!listening) {
9
- for (const event in handlers) {
10
- element.addEventListener(event, handlers[event])
11
- }
9
+ Object.entries(handlers).forEach(([event, handler]) =>
10
+ element.addEventListener(event, handler)
11
+ )
12
12
  listening = true
13
13
  }
14
14
  }
15
- function destroy() {
15
+ function reset() {
16
16
  if (listening) {
17
- for (const event in handlers) {
18
- element.removeEventListener(event, handlers[event])
19
- }
17
+ Object.entries(handlers).forEach(([event, handler]) =>
18
+ element.removeEventListener(event, handler)
19
+ )
20
20
  listening = false
21
21
  }
22
22
  }
23
- function update(enabled, newHandlers = handlers) {
23
+ function update(newHandlers = handlers, enabled = true) {
24
24
  if (listening !== enabled || handlers !== newHandlers) {
25
- destroy()
25
+ reset()
26
26
  handlers = newHandlers
27
+ // console.log(listening, enabled, handlers)
27
28
  if (enabled) activate()
28
29
  }
29
30
  }
30
31
 
31
- return { activate, destroy, update }
32
+ return { activate, reset, update }
32
33
  }
package/src/lib/index.js CHANGED
@@ -1,2 +1,4 @@
1
+ // export * from './constants'
1
2
  export * from './internal'
2
3
  export * from './event-manager'
4
+ export * from './viewport'
@@ -111,3 +111,79 @@ export function handleItemClick(element, current) {
111
111
  }
112
112
  return current
113
113
  }
114
+
115
+ /**
116
+ * Caclulates sum of array values between the given bounds.
117
+ * If a value is null, the default size is used.
118
+ * @param {Array<number|null>} sizes
119
+ * @param {number} lower
120
+ * @param {number} upper
121
+ * @param {number} [defaultSize]
122
+ * @returns {number}
123
+ */
124
+ export function calculateSum(sizes, lower, upper, defaultSize = 40, gap = 0) {
125
+ return (
126
+ sizes
127
+ .slice(lower, upper)
128
+ .map((size) => size ?? defaultSize)
129
+ .reduce((acc, size) => acc + size + gap, 0) - gap
130
+ )
131
+ }
132
+
133
+ /**
134
+ * Updates the sizes array with the given values.
135
+ *
136
+ * @param {Array<number|null>} sizes
137
+ * @param {Array<number>} values
138
+ * @param {number} [offset]
139
+ * @returns {Array<number|null>}
140
+ */
141
+ export function updateSizes(sizes, values, offset = 0) {
142
+ let result = [
143
+ ...sizes.slice(0, offset),
144
+ ...values,
145
+ ...sizes.slice(offset + values.length)
146
+ ]
147
+
148
+ return result
149
+ }
150
+
151
+ /**
152
+ * Adjusts the viewport to ensure that the bounds contain the given number of items.
153
+ *
154
+ * @param {import('../types').Bounds} current
155
+ * @param {number} count
156
+ * @param {number} visibleCount
157
+ * @returns {import('../types').Bounds}
158
+ */
159
+ export function fixViewportForVisibileCount(current, count, visibleCount) {
160
+ let { lower, upper } = current
161
+ if (lower < 0) lower = 0
162
+ if (lower + visibleCount > count) {
163
+ upper = count
164
+ lower = Math.max(0, upper - visibleCount)
165
+ } else if (lower + visibleCount !== upper) {
166
+ upper = lower + visibleCount
167
+ }
168
+ return { lower, upper }
169
+ }
170
+
171
+ /**
172
+ * Adjusts the viewport to ensure the given index is visible.
173
+ *
174
+ * @param {number} index
175
+ * @param {import('../types').Bounds} current
176
+ * @param {number} visibleCount
177
+ * @returns {import('../types').Bounds}
178
+ */
179
+ export function fitIndexInViewport(index, current, visibleCount) {
180
+ let { lower, upper } = current
181
+ if (index >= upper) {
182
+ upper = index + 1
183
+ lower = upper - visibleCount
184
+ } else if (index < lower) {
185
+ lower = index
186
+ upper = lower + visibleCount
187
+ }
188
+ return { lower, upper }
189
+ }
@@ -0,0 +1,127 @@
1
+ import { writable, get } from 'svelte/store'
2
+ import { pick } from 'ramda'
3
+ import {
4
+ updateSizes,
5
+ calculateSum,
6
+ fixViewportForVisibileCount,
7
+ fitIndexInViewport
8
+ } from './internal'
9
+
10
+ export function virtualListViewport(options) {
11
+ let { minSize = 40, maxVisible = 0, visibleSize, gap = 0 } = options
12
+ let current = { lower: 0, upper: 0 }
13
+ const bounds = writable({ lower: 0, upper: 0 })
14
+ const space = writable({
15
+ before: 0,
16
+ after: 0
17
+ })
18
+ let items
19
+ let averageSize = minSize
20
+ let visibleCount = maxVisible
21
+ let value = null
22
+ let cache = []
23
+ let index = -1
24
+
25
+ const update = (data) => {
26
+ // const previous = get(bounds)
27
+
28
+ data = {
29
+ start: current.lower,
30
+ end: current.upper,
31
+ value,
32
+ ...data
33
+ }
34
+ items = data.items ?? items
35
+ minSize = data.minSize ?? minSize
36
+ maxVisible = data.maxVisible ?? maxVisible
37
+ visibleSize = data.visibleSize ?? visibleSize
38
+
39
+ if (items.length !== cache.length) {
40
+ cache = Array.from({ length: items.length }).fill(null)
41
+ if (items.length == 0) index = -1
42
+ }
43
+ current = { lower: data.start, upper: data.end }
44
+
45
+ cache = updateSizes(cache, data.sizes ?? [], current.lower)
46
+ averageSize =
47
+ cache.length == 0
48
+ ? minSize
49
+ : calculateSum(cache, 0, cache.length, averageSize) / cache.length
50
+
51
+ let visible = calculateSum(
52
+ cache,
53
+ current.lower,
54
+ current.upper,
55
+ averageSize,
56
+ gap
57
+ )
58
+
59
+ if (maxVisible > 0) {
60
+ visibleCount = maxVisible
61
+ } else {
62
+ while (visible < visibleSize) visible += averageSize
63
+ while (visible - averageSize > visibleSize) visible -= averageSize
64
+ visibleCount = Math.ceil(visible / averageSize)
65
+ }
66
+ current = fixViewportForVisibileCount(current, cache.length, visibleCount)
67
+
68
+ // recalculate the lower, upper bounds based on current index
69
+ if (items.length > 0 && data.value && data.value !== value) {
70
+ index = items.findIndex((item) => item === data.value)
71
+ if (index > -1) {
72
+ value = data.value
73
+ current = fitIndexInViewport(index, current, visibleCount)
74
+ }
75
+ }
76
+ updateBounds(current)
77
+ }
78
+ const moveByOffset = (offset) => {
79
+ if (cache.length > 0) {
80
+ index = Math.max(0, Math.min(index + offset, cache.length - 1))
81
+ current = fitIndexInViewport(index, current, visibleCount)
82
+ updateBounds(current)
83
+ }
84
+ }
85
+ const updateBounds = ({ lower, upper }) => {
86
+ const previous = get(bounds)
87
+ if (maxVisible > 0) {
88
+ let visible = calculateSum(cache, lower, upper, averageSize, gap)
89
+ space.update((value) => (value = { ...value, visible }))
90
+ }
91
+ if (previous.lower !== lower) {
92
+ let before = calculateSum(cache, 0, lower, averageSize)
93
+ space.update((value) => (value = { ...value, before }))
94
+ }
95
+ if (previous.upper !== upper) {
96
+ let after = calculateSum(cache, upper, cache.length, averageSize)
97
+ space.update((value) => (value = { ...value, after }))
98
+ }
99
+ if (previous.lower !== lower || previous.upper !== upper) {
100
+ bounds.set({ lower, upper })
101
+ }
102
+ }
103
+
104
+ const scrollTo = (position) => {
105
+ const start = Math.round(position / averageSize)
106
+ if (start !== current.lower) update({ start })
107
+ }
108
+
109
+ update(options)
110
+
111
+ return {
112
+ bounds: pick(['subscribe'], bounds),
113
+ space: pick(['subscribe'], space),
114
+ get index() {
115
+ return index
116
+ },
117
+ update,
118
+ scrollTo,
119
+ moveByOffset,
120
+ next: () => moveByOffset(1),
121
+ previous: () => moveByOffset(-1),
122
+ nextPage: () => moveByOffset(visibleCount),
123
+ previousPage: () => moveByOffset(-visibleCount),
124
+ first: () => moveByOffset(-cache.length),
125
+ last: () => moveByOffset(cache.length + 1)
126
+ }
127
+ }
package/src/navigator.js CHANGED
@@ -63,8 +63,7 @@ export function navigator(element, options) {
63
63
  if (currentNode) {
64
64
  const collapse = isExpanded(currentNode, path[path.length - 1].fields)
65
65
  if (collapse) {
66
- currentNode[path[path.length - 1].fields.isOpen] = false
67
- emit('collapse', element, indicesFromPath(path), currentNode)
66
+ toggle()
68
67
  } else if (path.length > 0) {
69
68
  path = path.slice(0, -1)
70
69
  currentNode = getCurrentNode(path)
@@ -74,10 +73,15 @@ export function navigator(element, options) {
74
73
  }
75
74
  const expand = () => {
76
75
  if (currentNode && hasChildren(currentNode, path[path.length - 1].fields)) {
77
- currentNode[path[path.length - 1].fields.isOpen] = true
78
- emit('expand', element, indicesFromPath(path), currentNode)
76
+ toggle()
79
77
  }
80
78
  }
79
+ function toggle() {
80
+ const expanded = isExpanded(currentNode, path[path.length - 1].fields)
81
+ const event = expanded ? 'collapse' : 'expand'
82
+ currentNode[path[path.length - 1].fields.isOpen] = !expanded
83
+ emit(event, element, indicesFromPath(path), currentNode)
84
+ }
81
85
  const handlers = { next, previous, select, collapse, expand }
82
86
 
83
87
  update(options)
package/src/pannable.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { removeListeners, setupListeners } from './lib'
2
-
3
2
  /**
4
3
  * Handle drag and move events
5
4
  *
@@ -1,10 +1,10 @@
1
1
  import { mappedList, isNested } from '@rokkit/core'
2
2
  import { pick } from 'ramda'
3
3
  import {
4
- getClosestAncestorWithAttribute,
4
+ // getClosestAncestorWithAttribute,
5
5
  mapKeyboardEventsToActions,
6
6
  emit,
7
- handleItemClick,
7
+ // handleItemClick,
8
8
  EventManager
9
9
  } from './lib'
10
10
 
@@ -32,7 +32,6 @@ export function traversable(element, options) {
32
32
  const result = content[direction](current.position)
33
33
  if (result) {
34
34
  current = result
35
-
36
35
  checkAndEmit('move')
37
36
  }
38
37
  }
@@ -70,11 +69,14 @@ export function traversable(element, options) {
70
69
  }
71
70
 
72
71
  const update = (data) => {
73
- options = { ...defaultOptions, ...options, ...data }
72
+ options = { ...defaultOptions, ...options, value: current.item, ...data }
74
73
  options.nested = isNested(options.items, options.fields)
75
74
  content.update(options.items, options.fields)
76
75
  handlers = mapKeyboardEventsToActions(actions, options)
77
- manager.update(options.enabled, listeners)
76
+ manager.update(listeners, options.enabled)
77
+ if (options.value !== null) {
78
+ current = { ...current, ...content.findByValue(options.value) }
79
+ }
78
80
  // current = handleValueChange(element, data, content, current)
79
81
  }
80
82
 
@@ -82,7 +84,7 @@ export function traversable(element, options) {
82
84
 
83
85
  return {
84
86
  update,
85
- destroy: () => manager.destroy()
87
+ destroy: () => manager.reset()
86
88
  }
87
89
  }
88
90
 
package/src/types.js CHANGED
@@ -116,3 +116,15 @@
116
116
  * @property {number} startY - The start Y position of the touch.
117
117
  * @property {number} startTime - The start time of the touch.
118
118
  */
119
+
120
+ /**
121
+ * @typedef {Object} PushDownOptions
122
+ * @property {string} selector - The CSS selector for the child element to which keyboard events will be forwarded.
123
+ * @property {Array<string>} [events=['keydown', 'keyup', 'keypress']] - The keyboard events to forward.
124
+ */
125
+
126
+ /**
127
+ * @typedef {Object} Bounds
128
+ * @property {number} lower
129
+ * @property {number} upper
130
+ */