@rokkit/actions 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.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/package.json +56 -0
- package/src/delegate.js +34 -0
- package/src/dismissable.js +33 -0
- package/src/fillable.js +106 -0
- package/src/hierarchy.js +156 -0
- package/src/index.js +13 -0
- package/src/lib/constants.js +35 -0
- package/src/lib/event-manager.js +50 -0
- package/src/lib/index.js +5 -0
- package/src/lib/internal.js +185 -0
- package/src/lib/viewport.js +123 -0
- package/src/navigable.js +46 -0
- package/src/navigator.js +182 -0
- package/src/pannable.js +67 -0
- package/src/swipeable.js +150 -0
- package/src/switchable.js +52 -0
- package/src/themeable.js +42 -0
- package/src/traversable.js +385 -0
- package/src/types.js +132 -0
- package/src/utils.js +24 -0
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
const { gap = 0 } = options
|
|
12
|
+
let { minSize = 40, maxVisible = 0, visibleSize } = options
|
|
13
|
+
let current = { lower: 0, upper: 0 }
|
|
14
|
+
const bounds = writable({ lower: 0, upper: 0 })
|
|
15
|
+
const space = writable({
|
|
16
|
+
before: 0,
|
|
17
|
+
after: 0
|
|
18
|
+
})
|
|
19
|
+
let items = null
|
|
20
|
+
let averageSize = minSize
|
|
21
|
+
let visibleCount = maxVisible
|
|
22
|
+
let value = null
|
|
23
|
+
let cache = []
|
|
24
|
+
let index = -1
|
|
25
|
+
|
|
26
|
+
const updateBounds = ({ lower, upper }) => {
|
|
27
|
+
const previous = get(bounds)
|
|
28
|
+
if (maxVisible > 0) {
|
|
29
|
+
const visible = calculateSum(cache, lower, upper, averageSize, gap)
|
|
30
|
+
space.update((state) => (state = { ...state, visible }))
|
|
31
|
+
}
|
|
32
|
+
if (previous.lower !== lower) {
|
|
33
|
+
const before = calculateSum(cache, 0, lower, averageSize)
|
|
34
|
+
space.update((state) => (state = { ...state, before }))
|
|
35
|
+
}
|
|
36
|
+
if (previous.upper !== upper) {
|
|
37
|
+
const after = calculateSum(cache, upper, cache.length, averageSize)
|
|
38
|
+
space.update((state) => (state = { ...state, after }))
|
|
39
|
+
}
|
|
40
|
+
if (previous.lower !== lower || previous.upper !== upper) {
|
|
41
|
+
bounds.set({ lower, upper })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const update = (data) => {
|
|
46
|
+
// const previous = get(bounds)
|
|
47
|
+
|
|
48
|
+
data = {
|
|
49
|
+
start: current.lower,
|
|
50
|
+
end: current.upper,
|
|
51
|
+
value,
|
|
52
|
+
...data
|
|
53
|
+
}
|
|
54
|
+
items = data.items ?? items
|
|
55
|
+
minSize = data.minSize ?? minSize
|
|
56
|
+
maxVisible = data.maxVisible ?? maxVisible
|
|
57
|
+
visibleSize = data.visibleSize ?? visibleSize
|
|
58
|
+
|
|
59
|
+
if (items.length !== cache.length) {
|
|
60
|
+
cache = Array.from({ length: items.length }).fill(null)
|
|
61
|
+
if (items.length === 0) index = -1
|
|
62
|
+
}
|
|
63
|
+
current = { lower: data.start, upper: data.end }
|
|
64
|
+
|
|
65
|
+
cache = updateSizes(cache, data.sizes ?? [], current.lower)
|
|
66
|
+
averageSize =
|
|
67
|
+
cache.length === 0
|
|
68
|
+
? minSize
|
|
69
|
+
: calculateSum(cache, 0, cache.length, averageSize) / cache.length
|
|
70
|
+
|
|
71
|
+
let visible = calculateSum(cache, current.lower, current.upper, averageSize, gap)
|
|
72
|
+
|
|
73
|
+
if (maxVisible > 0) {
|
|
74
|
+
visibleCount = maxVisible
|
|
75
|
+
} else {
|
|
76
|
+
while (visible < visibleSize) visible += averageSize
|
|
77
|
+
while (visible - averageSize > visibleSize) visible -= averageSize
|
|
78
|
+
visibleCount = Math.ceil(visible / averageSize)
|
|
79
|
+
}
|
|
80
|
+
current = fixViewportForVisibileCount(current, cache.length, visibleCount)
|
|
81
|
+
|
|
82
|
+
// recalculate the lower, upper bounds based on current index
|
|
83
|
+
if (items.length > 0 && data.value && data.value !== value) {
|
|
84
|
+
index = items.findIndex((item) => item === data.value)
|
|
85
|
+
if (index > -1) {
|
|
86
|
+
value = data.value
|
|
87
|
+
current = fitIndexInViewport(index, current, visibleCount)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
updateBounds(current)
|
|
91
|
+
}
|
|
92
|
+
const moveByOffset = (offset) => {
|
|
93
|
+
if (cache.length > 0) {
|
|
94
|
+
index = Math.max(0, Math.min(index + offset, cache.length - 1))
|
|
95
|
+
current = fitIndexInViewport(index, current, visibleCount)
|
|
96
|
+
updateBounds(current)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const scrollTo = (position) => {
|
|
101
|
+
const start = Math.round(position / averageSize)
|
|
102
|
+
if (start !== current.lower) update({ start })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
update(options)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
bounds: pick(['subscribe'], bounds),
|
|
109
|
+
space: pick(['subscribe'], space),
|
|
110
|
+
get index() {
|
|
111
|
+
return index
|
|
112
|
+
},
|
|
113
|
+
update,
|
|
114
|
+
scrollTo,
|
|
115
|
+
moveByOffset,
|
|
116
|
+
next: () => moveByOffset(1),
|
|
117
|
+
previous: () => moveByOffset(-1),
|
|
118
|
+
nextPage: () => moveByOffset(visibleCount),
|
|
119
|
+
previousPage: () => moveByOffset(-visibleCount),
|
|
120
|
+
first: () => moveByOffset(-cache.length),
|
|
121
|
+
last: () => moveByOffset(cache.length + 1)
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/navigable.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { handleAction, getKeyboardActions } from './utils'
|
|
2
|
+
|
|
3
|
+
const defaultOptions = { horizontal: true, nested: false, enabled: true }
|
|
4
|
+
/**
|
|
5
|
+
* A svelte action function that captures keyboard evvents and emits event for corresponding movements.
|
|
6
|
+
*
|
|
7
|
+
* @param {HTMLElement} node
|
|
8
|
+
* @param {import('./types').NavigableOptions} options
|
|
9
|
+
* @returns {import('./types').SvelteActionReturn}
|
|
10
|
+
*/
|
|
11
|
+
export function navigable(node, options) {
|
|
12
|
+
options = { ...defaultOptions, ...options }
|
|
13
|
+
|
|
14
|
+
let listening = false
|
|
15
|
+
const handlers = {
|
|
16
|
+
previous: () => node.dispatchEvent(new CustomEvent('previous')),
|
|
17
|
+
next: () => node.dispatchEvent(new CustomEvent('next')),
|
|
18
|
+
collapse: () => node.dispatchEvent(new CustomEvent('collapse')),
|
|
19
|
+
expand: () => node.dispatchEvent(new CustomEvent('expand')),
|
|
20
|
+
select: () => node.dispatchEvent(new CustomEvent('select'))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let actions = {} //getKeyboardActions(node, { horizontal, nested })
|
|
24
|
+
const handleKeydown = (event) => handleAction(actions, event)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Update the listeners based on the input configuration.
|
|
28
|
+
* @param {import('./types').NavigableOptions} input
|
|
29
|
+
*/
|
|
30
|
+
function updateListeners(input) {
|
|
31
|
+
options = { ...options, ...input }
|
|
32
|
+
if (listening) node.removeEventListener('keydown', handleKeydown)
|
|
33
|
+
|
|
34
|
+
actions = getKeyboardActions(node, input, handlers)
|
|
35
|
+
if (input.enabled) node.addEventListener('keydown', handleKeydown)
|
|
36
|
+
|
|
37
|
+
listening = input.enabled
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
updateListeners(options)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
update: (config) => updateListeners(config),
|
|
44
|
+
destroy: () => updateListeners({ enabled: false })
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/navigator.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { handleAction } from './utils'
|
|
2
|
+
import { noop, isNested, hasChildren, isExpanded } from '@rokkit/core'
|
|
3
|
+
import {
|
|
4
|
+
moveNext,
|
|
5
|
+
movePrevious,
|
|
6
|
+
pathFromIndices,
|
|
7
|
+
indicesFromPath,
|
|
8
|
+
getCurrentNode
|
|
9
|
+
} from './hierarchy'
|
|
10
|
+
import { mapKeyboardEventsToActions } from './lib'
|
|
11
|
+
/**
|
|
12
|
+
* Keyboard navigation for Lists and NestedLists. The data is either nested or not and is not
|
|
13
|
+
* expected to switch from nested to simple list or vice-versa.
|
|
14
|
+
*
|
|
15
|
+
* @param {HTMLElement} element - Root element for the actionn
|
|
16
|
+
* @param {import('./types').NavigatorOptions} options - Configuration options for the action
|
|
17
|
+
* @returns
|
|
18
|
+
*/
|
|
19
|
+
export function navigator(element, options) {
|
|
20
|
+
const { fields, enabled = true, vertical = true, idPrefix = 'id-' } = options
|
|
21
|
+
let items = [],
|
|
22
|
+
path = null,
|
|
23
|
+
currentNode = null
|
|
24
|
+
|
|
25
|
+
if (!enabled) return { destroy: noop }
|
|
26
|
+
|
|
27
|
+
const update = (input) => {
|
|
28
|
+
const previousNode = currentNode
|
|
29
|
+
items = input.items
|
|
30
|
+
path = pathFromIndices(input.indices ?? [], items, fields)
|
|
31
|
+
currentNode = getCurrentNode(path)
|
|
32
|
+
|
|
33
|
+
if (previousNode !== currentNode && currentNode) {
|
|
34
|
+
const indices = indicesFromPath(path)
|
|
35
|
+
const current = element.querySelector(`#${idPrefix}${indices.join('-')}`)
|
|
36
|
+
if (current) {
|
|
37
|
+
current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const next = () => {
|
|
43
|
+
const previousNode = currentNode
|
|
44
|
+
path = moveNext(path, items, fields)
|
|
45
|
+
currentNode = getCurrentNode(path)
|
|
46
|
+
if (previousNode !== currentNode) moveTo(element, path, currentNode, idPrefix)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const previous = () => {
|
|
50
|
+
const previousNode = currentNode
|
|
51
|
+
path = movePrevious(path)
|
|
52
|
+
if (path.length > 0) {
|
|
53
|
+
currentNode = getCurrentNode(path)
|
|
54
|
+
if (previousNode !== currentNode) moveTo(element, path, currentNode, idPrefix)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const select = () => {
|
|
58
|
+
if (currentNode) emit('select', element, indicesFromPath(path), currentNode)
|
|
59
|
+
}
|
|
60
|
+
const collapse = () => {
|
|
61
|
+
if (currentNode) {
|
|
62
|
+
const expanded = isExpanded(currentNode, path[path.length - 1].fields)
|
|
63
|
+
if (expanded) {
|
|
64
|
+
toggle()
|
|
65
|
+
} else if (path.length > 0) {
|
|
66
|
+
path = path.slice(0, -1)
|
|
67
|
+
currentNode = getCurrentNode(path)
|
|
68
|
+
select()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const expand = () => {
|
|
73
|
+
if (currentNode && hasChildren(currentNode, path[path.length - 1].fields)) {
|
|
74
|
+
toggle()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function toggle() {
|
|
78
|
+
const expanded = isExpanded(currentNode, path[path.length - 1].fields)
|
|
79
|
+
const event = expanded ? 'collapse' : 'expand'
|
|
80
|
+
currentNode[path[path.length - 1].fields.isOpen] = !expanded
|
|
81
|
+
emit(event, element, indicesFromPath(path), currentNode)
|
|
82
|
+
}
|
|
83
|
+
const handlers = { next, previous, select, collapse, expand }
|
|
84
|
+
|
|
85
|
+
update(options)
|
|
86
|
+
|
|
87
|
+
const nested = isNested(items, fields)
|
|
88
|
+
const actions = mapKeyboardEventsToActions(handlers, {
|
|
89
|
+
horizontal: !vertical,
|
|
90
|
+
nested
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const handleKeyDown = (event) => handleAction(actions, event)
|
|
94
|
+
|
|
95
|
+
const handleClick = (event) => {
|
|
96
|
+
event.stopPropagation()
|
|
97
|
+
const target = findParentWithDataPath(event.target, element)
|
|
98
|
+
const indices = !target
|
|
99
|
+
? []
|
|
100
|
+
: target.dataset.path
|
|
101
|
+
.split(',')
|
|
102
|
+
.filter((item) => item !== '')
|
|
103
|
+
.map((item) => Number(item))
|
|
104
|
+
|
|
105
|
+
if (indices.length > 0 && event.target.tagName !== 'DETAIL') {
|
|
106
|
+
path = pathFromIndices(indices, items, fields)
|
|
107
|
+
currentNode = getCurrentNode(path)
|
|
108
|
+
if (hasChildren(currentNode, path[path.length - 1].fields)) {
|
|
109
|
+
currentNode[path[path.length - 1].fields.isOpen] =
|
|
110
|
+
!currentNode[path[path.length - 1].fields.isOpen]
|
|
111
|
+
const eventName = currentNode[path[path.length - 1].fields.isOpen] ? 'expand' : 'collapse'
|
|
112
|
+
emit(eventName, element, indices, currentNode)
|
|
113
|
+
} else if (currentNode !== null) emit('select', element, indices, currentNode)
|
|
114
|
+
emit('move', element, indices, currentNode)
|
|
115
|
+
// emit('select', element, indices, currentNode)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
element.addEventListener('keydown', handleKeyDown)
|
|
120
|
+
element.addEventListener('click', handleClick)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
update,
|
|
124
|
+
destroy() {
|
|
125
|
+
element.removeEventListener('keydown', handleKeyDown)
|
|
126
|
+
element.removeEventListener('click', handleClick)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Move to the element with the given path
|
|
133
|
+
*
|
|
134
|
+
* @param {HTMLElement} element
|
|
135
|
+
* @param {*} path
|
|
136
|
+
* @param {*} currentNode
|
|
137
|
+
* @param {*} idPrefix
|
|
138
|
+
*/
|
|
139
|
+
export function moveTo(element, path, currentNode, idPrefix) {
|
|
140
|
+
const indices = indicesFromPath(path)
|
|
141
|
+
const current = element.querySelector(`#${idPrefix}${indices.join('-')}`)
|
|
142
|
+
if (current) current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
143
|
+
|
|
144
|
+
emit('move', element, indices, currentNode)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find the parent element with data-path attribute
|
|
149
|
+
*
|
|
150
|
+
* @param {HTMLElement} element
|
|
151
|
+
* @param {HTMLElement} root
|
|
152
|
+
* @returns {HTMLElement}
|
|
153
|
+
*/
|
|
154
|
+
export function findParentWithDataPath(element, root) {
|
|
155
|
+
if (element.hasAttribute('data-path')) return element
|
|
156
|
+
let parent = element.parentNode
|
|
157
|
+
|
|
158
|
+
while (parent && parent !== root && !parent.hasAttribute('data-path')) {
|
|
159
|
+
parent = parent.parentNode
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return parent !== root ? parent : null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Emit a custom event on the element with the path and node as detail
|
|
167
|
+
*
|
|
168
|
+
* @param {string} event
|
|
169
|
+
* @param {HTMLElement} element
|
|
170
|
+
* @param {Array<integer>} indices
|
|
171
|
+
* @param {*} node
|
|
172
|
+
*/
|
|
173
|
+
function emit(event, element, indices, node) {
|
|
174
|
+
element.dispatchEvent(
|
|
175
|
+
new CustomEvent(event, {
|
|
176
|
+
detail: {
|
|
177
|
+
path: indices,
|
|
178
|
+
node
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
}
|
package/src/pannable.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { omit } from 'ramda'
|
|
2
|
+
import { removeListeners, setupListeners } from './lib'
|
|
3
|
+
/**
|
|
4
|
+
* Makes an element pannable with mouse or touch events.
|
|
5
|
+
*
|
|
6
|
+
* @param {HTMLElement} node The DOM element to apply the panning action.
|
|
7
|
+
* @returns {import('./types').SvelteActionReturn}
|
|
8
|
+
*/
|
|
9
|
+
export function pannable(node) {
|
|
10
|
+
let coords = { x: 0, y: 0 }
|
|
11
|
+
const listeners = {
|
|
12
|
+
primary: {
|
|
13
|
+
mousedown: start,
|
|
14
|
+
touchstart: start
|
|
15
|
+
},
|
|
16
|
+
secondary: {
|
|
17
|
+
mousemove: move,
|
|
18
|
+
mouseup: stop,
|
|
19
|
+
touchmove: move,
|
|
20
|
+
touchend: stop
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function start(event) {
|
|
25
|
+
coords = handleEvent(node, event, 'panstart', coords)
|
|
26
|
+
setupListeners(window, listeners.secondary)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function move(event) {
|
|
30
|
+
coords = handleEvent(node, event, 'panmove', coords)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stop(event) {
|
|
34
|
+
coords = handleEvent(node, event, 'panend', coords)
|
|
35
|
+
removeListeners(window, listeners.secondary)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setupListeners(node, listeners.primary)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
destroy: () => removeListeners(node, listeners.primary)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handles the panning event.
|
|
47
|
+
*
|
|
48
|
+
* @param {HTMLElement} node - The node where the event is dispatched.
|
|
49
|
+
* @param {Event} event - The event object.
|
|
50
|
+
* @param {string} name - The name of the event.
|
|
51
|
+
* @param {import('./types').Coords} coords - The previous coordinates of the event.
|
|
52
|
+
*/
|
|
53
|
+
function handleEvent(node, event, name, coords) {
|
|
54
|
+
const x = event.clientX || event.touches[0].clientX
|
|
55
|
+
const y = event.clientY || event.touches[0].clientY
|
|
56
|
+
const detail = { x, y }
|
|
57
|
+
|
|
58
|
+
if (name === 'panmove') {
|
|
59
|
+
detail.dx = x - coords.x
|
|
60
|
+
detail.dy = y - coords.y
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
event.stopPropagation()
|
|
64
|
+
event.preventDefault()
|
|
65
|
+
node.dispatchEvent(new CustomEvent(name, { detail }))
|
|
66
|
+
return omit(['dx', 'dy'], detail)
|
|
67
|
+
}
|
package/src/swipeable.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { removeListeners, setupListeners } from './lib'
|
|
2
|
+
|
|
3
|
+
const defaultOptions = {
|
|
4
|
+
horizontal: true,
|
|
5
|
+
vertical: false,
|
|
6
|
+
threshold: 100,
|
|
7
|
+
enabled: true,
|
|
8
|
+
minSpeed: 300
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A svelte action function that captures swipe actions and emits event for corresponding movements.
|
|
13
|
+
*
|
|
14
|
+
* @param {HTMLElement} node
|
|
15
|
+
* @param {import(./types).SwipeableOptions} options
|
|
16
|
+
* @returns {import('./types').SvelteActionReturn}
|
|
17
|
+
*/
|
|
18
|
+
export function swipeable(node, options = defaultOptions) {
|
|
19
|
+
const track = {}
|
|
20
|
+
let listeners = {}
|
|
21
|
+
|
|
22
|
+
const updateListeners = (props) => {
|
|
23
|
+
removeListeners(node, listeners)
|
|
24
|
+
listeners = getListeners(node, props, track)
|
|
25
|
+
setupListeners(node, listeners, props)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
options = { ...defaultOptions, ...options }
|
|
29
|
+
updateListeners(options)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
update: (data) => {
|
|
33
|
+
options = { ...options, ...data }
|
|
34
|
+
updateListeners(options)
|
|
35
|
+
},
|
|
36
|
+
destroy() {
|
|
37
|
+
removeListeners(node, listeners)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns the listeners for the swipeable action.
|
|
44
|
+
* @param {HTMLElement} node - The node where the event is dispatched.
|
|
45
|
+
* @param {import(./types).SwipeableOptions} options - The options for the swipe.
|
|
46
|
+
* @param {import(./types).TouchTracker} track - The tracking object.
|
|
47
|
+
* @returns {import(./types).Listeners}
|
|
48
|
+
*/
|
|
49
|
+
function getListeners(node, options, track) {
|
|
50
|
+
if (!options.enabled) return {}
|
|
51
|
+
|
|
52
|
+
const listeners = {
|
|
53
|
+
touchend: (e) => touchEnd(e, node, options, track),
|
|
54
|
+
touchstart: (e) => touchStart(e, track),
|
|
55
|
+
mousedown: (e) => touchStart(e, track),
|
|
56
|
+
mouseup: (e) => touchEnd(e, node, options, track)
|
|
57
|
+
}
|
|
58
|
+
return listeners
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handles the touch start event.
|
|
63
|
+
*
|
|
64
|
+
* @param {Event} event
|
|
65
|
+
* @param {import(./types).TouchTracker} track
|
|
66
|
+
*/
|
|
67
|
+
function touchStart(event, track) {
|
|
68
|
+
const touch = event.touches ? event.touches[0] : event
|
|
69
|
+
track.startX = touch.clientX
|
|
70
|
+
track.startY = touch.clientY
|
|
71
|
+
track.startTime = new Date().getTime()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handles the touch end event and triggers a swipe event if the criteria are met.
|
|
76
|
+
*
|
|
77
|
+
* @param {Event} event - The event object representing the touch or mouse event.
|
|
78
|
+
* @param {HTMLElement} node - The HTML element on which the swipe event will be dispatched.
|
|
79
|
+
* @param {object} options - Configuration options for determining swipe behavior.
|
|
80
|
+
* @param {object} track - An object tracking the start point and time of the touch or swipe action.
|
|
81
|
+
*/
|
|
82
|
+
function touchEnd(event, node, options, track) {
|
|
83
|
+
const { distance, duration } = getTouchMetrics(event, track)
|
|
84
|
+
if (!isSwipeFastEnough(distance, duration, options.minSpeed)) return
|
|
85
|
+
|
|
86
|
+
const swipeDetails = getSwipeDetails(distance, options)
|
|
87
|
+
if (!swipeDetails.isValid) return
|
|
88
|
+
node.dispatchEvent(new CustomEvent(`swipe${swipeDetails.direction}`))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Calculates and returns the distance and duration of the swipe.
|
|
93
|
+
*
|
|
94
|
+
* @param {Event} event - The event object that initiated the touchEnd.
|
|
95
|
+
* @param {object} track - The tracking object holding the start of the touch action.
|
|
96
|
+
* @returns {{distance: {x: number, y: number}, duration: number}} The distance swiped (x and y) and the duration of the swipe.
|
|
97
|
+
*/
|
|
98
|
+
function getTouchMetrics(event, track) {
|
|
99
|
+
const touch = event.changedTouches ? event.changedTouches[0] : event
|
|
100
|
+
const distX = touch.clientX - track.startX
|
|
101
|
+
const distY = touch.clientY - track.startY
|
|
102
|
+
const duration = (new Date().getTime() - track.startTime) / 1000
|
|
103
|
+
return { distance: { x: distX, y: distY }, duration }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Checks if the swipe was fast enough according to the minimum speed requirement.
|
|
108
|
+
*
|
|
109
|
+
* @param {{x: number, y: number}} distance - The distance of the swipe action.
|
|
110
|
+
* @param {number} duration - The duration of the swipe action in seconds.
|
|
111
|
+
* @param {number} minSpeed - The minimum speed threshold for the swipe action.
|
|
112
|
+
* @returns {boolean} True if the swipe is fast enough, otherwise false.
|
|
113
|
+
*/
|
|
114
|
+
function isSwipeFastEnough(distance, duration, minSpeed) {
|
|
115
|
+
const speed = Math.max(Math.abs(distance.x), Math.abs(distance.y)) / duration
|
|
116
|
+
return speed > minSpeed
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Determines swipe validity and direction based on horizontal/vertical preferences and thresholds.
|
|
121
|
+
*
|
|
122
|
+
* @param {{x: number, y: number}} distance - The distance of the swipe.
|
|
123
|
+
* @param {object} options - Configuration options such as direction preferences and thresholds.
|
|
124
|
+
* @returns {{isValid: boolean, direction?: string}} Object indicating whether the swipe is valid, and if so, its direction.
|
|
125
|
+
*/
|
|
126
|
+
function getSwipeDetails(distance, options) {
|
|
127
|
+
const isHorizontalSwipe = options.horizontal && Math.abs(distance.x) >= options.threshold
|
|
128
|
+
const isVerticalSwipe = options.vertical && Math.abs(distance.y) >= options.threshold
|
|
129
|
+
if (isHorizontalSwipe || isVerticalSwipe) {
|
|
130
|
+
return {
|
|
131
|
+
isValid: true,
|
|
132
|
+
direction: getSwipeDirection(distance.x, distance.y)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { isValid: false }
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Returns the swipe direction based on the distance in the x and y axis.
|
|
139
|
+
*
|
|
140
|
+
* @param {number} distX - The distance in the x axis.
|
|
141
|
+
* @param {number} distY - The distance in the y axis.
|
|
142
|
+
* @returns {string} The swipe direction.
|
|
143
|
+
*/
|
|
144
|
+
function getSwipeDirection(distX, distY) {
|
|
145
|
+
if (Math.abs(distX) > Math.abs(distY)) {
|
|
146
|
+
return distX > 0 ? 'Right' : 'Left'
|
|
147
|
+
} else {
|
|
148
|
+
return distY > 0 ? 'Down' : 'Up'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { removeListeners, setupListeners } from './lib'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A switchable action that allows the user to cycle through a list of options
|
|
5
|
+
*
|
|
6
|
+
* @param {HTMLElement} node
|
|
7
|
+
* @param {Object} data
|
|
8
|
+
*/
|
|
9
|
+
export function switchable(node, data) {
|
|
10
|
+
let index = 0
|
|
11
|
+
let { value, options, disabled } = data
|
|
12
|
+
|
|
13
|
+
const update = (input) => {
|
|
14
|
+
value = input.value === null || input.value === undefined ? options[0] : input.value
|
|
15
|
+
options = input.options
|
|
16
|
+
disabled = input.disabled
|
|
17
|
+
index = options.indexOf(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const toggle = (increment = 1) => {
|
|
21
|
+
index = (index + increment) % options.length
|
|
22
|
+
value = options[index]
|
|
23
|
+
node.dispatchEvent(new CustomEvent('change', { detail: value }))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const listeners = getEventHandlers(options, toggle)
|
|
27
|
+
|
|
28
|
+
update(data)
|
|
29
|
+
setupListeners(node, listeners, { enabled: !disabled })
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
update,
|
|
33
|
+
destroy: () => removeListeners(node, listeners)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns a keydown handler for the switchable component
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} options
|
|
40
|
+
*/
|
|
41
|
+
function getEventHandlers(options, toggle) {
|
|
42
|
+
const keydown = (e) => {
|
|
43
|
+
if ([' ', 'Enter', 'ArrowRight', 'ArrowLeft'].includes(e.key)) {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
e.stopPropagation()
|
|
46
|
+
|
|
47
|
+
toggle(e.key === 'ArrowLeft' ? options.length - 1 : 1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { keydown, click: () => toggle(1) }
|
|
52
|
+
}
|
package/src/themeable.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { theme } from '@rokkit/stores'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A svelte action function that adds theme classes to the element
|
|
5
|
+
*
|
|
6
|
+
* @param {HTMLElement} node
|
|
7
|
+
*/
|
|
8
|
+
export function themable(node) {
|
|
9
|
+
let previous = {}
|
|
10
|
+
|
|
11
|
+
theme.subscribe((data) => {
|
|
12
|
+
switchClass(node, data.name, previous.name)
|
|
13
|
+
switchClass(node, data.mode, previous.mode)
|
|
14
|
+
// switchPalette(node, data.palette, previous.palette)
|
|
15
|
+
previous = data
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Switch the class on the node
|
|
21
|
+
*
|
|
22
|
+
* @param {HTMLElement} node
|
|
23
|
+
* @param {string} current
|
|
24
|
+
* @param {string} previous
|
|
25
|
+
* @returns
|
|
26
|
+
*/
|
|
27
|
+
function switchClass(node, current, previous) {
|
|
28
|
+
if (current && current !== previous) {
|
|
29
|
+
node.classList.remove(previous)
|
|
30
|
+
node.classList.add(current)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// function switchPalette(node, current, previous) {
|
|
35
|
+
// Object.keys(current).map((key) => {
|
|
36
|
+
// if (!equals(current[key], previous[key])) {
|
|
37
|
+
// Object.keys(current[key]).map((shade) => {
|
|
38
|
+
// node.style.setProperty(`--${key}-${shade}`, current[key][shade])
|
|
39
|
+
// })
|
|
40
|
+
// }
|
|
41
|
+
// })
|
|
42
|
+
// }
|