@rokkit/actions 1.0.0-next.35

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Jerry Thomas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # Core Components
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@rokkit/actions",
3
+ "version": "1.0.0-next.35",
4
+ "description": "Contains generic actions that can be used in various components.",
5
+ "author": "Jerry Thomas <me@jerrythomas.name>",
6
+ "license": "MIT",
7
+ "main": "index.js",
8
+ "svelte": "src/index.js",
9
+ "module": "src/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "type": "module",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "devDependencies": {
16
+ "@sveltejs/vite-plugin-svelte": "^2.4.2",
17
+ "@testing-library/svelte": "^4.0.3",
18
+ "@vitest/coverage-v8": "^0.32.3",
19
+ "@vitest/ui": "~0.32.3",
20
+ "jsdom": "^22.1.0",
21
+ "svelte": "^4.0.1",
22
+ "typescript": "^5.1.6",
23
+ "validators": "latest",
24
+ "vite": "^4.3.9",
25
+ "vitest": "~0.32.3",
26
+ "shared-config": "1.0.0-next.35"
27
+ },
28
+ "files": [
29
+ "src/**/*.js",
30
+ "src/**/*.svelte",
31
+ "!src/mocks",
32
+ "!src/**/*.spec.js"
33
+ ],
34
+ "exports": {
35
+ "./src": "./src",
36
+ "./package.json": "./package.json",
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./src/index.js"
40
+ }
41
+ },
42
+ "dependencies": {
43
+ "@rokkit/stores": "latest"
44
+ },
45
+ "scripts": {
46
+ "format": "prettier --write .",
47
+ "lint": "eslint --fix .",
48
+ "test:ct": "playwright test -c playwright.config.js",
49
+ "test:ci": "vitest run",
50
+ "test:ui": "vitest --ui",
51
+ "test": "vitest",
52
+ "coverage": "vitest run --coverage",
53
+ "latest": "pnpm upgrade --latest && pnpm test:ci",
54
+ "release": "tsc && pnpm publish --access public"
55
+ }
56
+ }
@@ -0,0 +1,24 @@
1
+ const KEYCODE_ESC = 27
2
+
3
+ export function dismissable(node) {
4
+ const handleClick = (event) => {
5
+ if (node && !node.contains(event.target) && !event.defaultPrevented) {
6
+ node.dispatchEvent(new CustomEvent('dismiss', node))
7
+ }
8
+ }
9
+ const keyup = (event) => {
10
+ if (event.keyCode === KEYCODE_ESC) {
11
+ node.dispatchEvent(new CustomEvent('dismiss', node))
12
+ }
13
+ }
14
+
15
+ document.addEventListener('click', handleClick, true)
16
+ document.addEventListener('keyup', keyup, true)
17
+
18
+ return {
19
+ destroy() {
20
+ document.removeEventListener('click', handleClick, true)
21
+ document.removeEventListener('keyup', keyup, true)
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @typedef FillOptions
3
+ * @property {Array<string>} options available options to fill
4
+ * @property {integer} current index of option to be filled
5
+ * @property {boolean} check validate filled values
6
+ */
7
+ /**
8
+ * Action for filling a <del>?</del> element in html block.
9
+ *
10
+ * @param {*} node
11
+ * @param {FillOptions} options
12
+ * @returns
13
+ */
14
+ export function fillable(node, { options, current, check }) {
15
+ let data = { options, current, check }
16
+ let blanks = node.getElementsByTagName('del')
17
+
18
+ function click(event) {
19
+ if ('?' !== event.target.innerHTML) {
20
+ clear(event, node)
21
+ }
22
+ }
23
+
24
+ initialize(blanks, click)
25
+
26
+ return {
27
+ update({ options, current }) {
28
+ data.options = options
29
+ data.current = current
30
+ data.check = check
31
+
32
+ fill(blanks, data.options, data.current)
33
+ if (data.check) validate(blanks, data)
34
+ },
35
+ destroy() {
36
+ Object.keys(blanks).map((ref) => {
37
+ blanks[ref].removeEventListener('click', click)
38
+ })
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Initialize empty fillable element style and add listener for click
45
+ *
46
+ * @param {*} blanks
47
+ * @param {*} click
48
+ */
49
+ function initialize(blanks, click) {
50
+ Object.keys(blanks).map((ref) => {
51
+ blanks[ref].addEventListener('click', click)
52
+ blanks[ref].classList.add('empty')
53
+ blanks[ref].name = 'fill-' + ref
54
+ blanks[ref]['data-index'] = ref
55
+ })
56
+ }
57
+
58
+ /**
59
+ * Fill current blank with provided option
60
+ *
61
+ * @param {*} blanks
62
+ * @param {*} options
63
+ * @param {*} current
64
+ */
65
+ function fill(blanks, options, current) {
66
+ if (current > -1 && current < Object.keys(blanks).length) {
67
+ let index = options.findIndex(({ actualIndex }) => actualIndex == current)
68
+ if (index > -1) {
69
+ blanks[current].innerHTML = options[index].value
70
+ blanks[current].classList.remove('empty')
71
+ blanks[current].classList.add('filled')
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Clear all fillable elements
78
+ *
79
+ * @param {*} event
80
+ * @param {*} node
81
+ */
82
+ function clear(event, node) {
83
+ event.target.innerHTML = '?'
84
+ event.target.classList.remove('filled')
85
+ event.target.classList.remove('pass')
86
+ event.target.classList.remove('fail')
87
+ event.target.classList.add('empty')
88
+ node.dispatchEvent(
89
+ new CustomEvent('remove', {
90
+ detail: {
91
+ index: event.target.name.split('-')[1],
92
+ value: event.target['data-index']
93
+ }
94
+ })
95
+ )
96
+ }
97
+
98
+ /**
99
+ * Validate the filled values
100
+ *
101
+ * @param {*} blanks
102
+ * @param {*} data
103
+ */
104
+ function validate(blanks, data) {
105
+ Object.keys(blanks).map((ref) => {
106
+ let index = data.options.findIndex(({ actualIndex }) => actualIndex == ref)
107
+ if (index > -1)
108
+ blanks[ref].classList.add(
109
+ data.options[index].expectedIndex == data.options[index].actualIndex
110
+ ? 'pass'
111
+ : 'fail'
112
+ )
113
+ })
114
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * A part of the path to node in hierarchy
3
+ *
4
+ * @typedef PathFragment
5
+ * @property {integer} index - Index to item in array
6
+ * @property {Array<*>} items - Array of items
7
+ * @property {import('../constants').FieldMapping} fields - Field mapping for the data
8
+ */
9
+
10
+ /**
11
+ * Check if the current item is a parent
12
+ *
13
+ * @param {*} item
14
+ * @param {import('../constants').FieldMapping} fields
15
+ * @returns {boolean}
16
+ */
17
+ export function hasChildren(item, fields) {
18
+ return (
19
+ typeof item === 'object' &&
20
+ fields.children in item &&
21
+ Array.isArray(item[fields.children])
22
+ )
23
+ }
24
+
25
+ /**
26
+ * Check if the current item is a parent and is expanded
27
+ *
28
+ * @param {*} item
29
+ * @param {import('../constants').FieldMapping} fields
30
+ * @returns {boolean}
31
+ */
32
+ export function isExpanded(item, fields) {
33
+ if (item == null) return false
34
+ if (!hasChildren(item, fields)) return false
35
+ if (fields.isOpen in item) {
36
+ return item[fields.isOpen]
37
+ }
38
+ return false
39
+ }
40
+
41
+ /**
42
+ * Verify if at least one item has children
43
+ *
44
+ * @param {Array<*>} items
45
+ * @param {import('../constants').FieldMapping} fields
46
+ * @returns {boolean}
47
+ */
48
+ export function isNested(items, fields) {
49
+ for (let i = 0; i < items.length; i++) {
50
+ if (hasChildren(items[i], fields)) return true
51
+ }
52
+ return false
53
+ }
54
+
55
+ /**
56
+ * Navigate to last visible child in the hirarchy starting with the provided path
57
+ *
58
+ * @param {Array<PathFragment>} path - path to a node in the hierarchy
59
+ * @returns
60
+ */
61
+ export function navigateToLastVisibleChild(path) {
62
+ let current = path[path.length - 1]
63
+ // console.log(current)
64
+ while (isExpanded(current.items[current.index], current.fields)) {
65
+ const items = current.items[current.index][current.fields.children]
66
+ const level = {
67
+ items,
68
+ index: items.length - 1,
69
+ fields: current.fields.fields ?? current.fields
70
+ }
71
+ path.push(level)
72
+ current = level
73
+ }
74
+
75
+ return path
76
+ }
77
+
78
+ /**
79
+ * Navigate to the next item
80
+ *
81
+ * @param {Array<PathFragment>} path - path to a node in the hierarchy
82
+ * @param {Array<*>} items - array of items
83
+ * @param {import('../constants').FieldMapping} fields - field mapping
84
+ * @returns
85
+ */
86
+ export function moveNext(path, items, fields) {
87
+ if (path.length === 0) {
88
+ return [{ index: 0, items, fields }]
89
+ }
90
+
91
+ const current = path[path.length - 1]
92
+ if (isExpanded(current.items[current.index], current.fields)) {
93
+ path.push({
94
+ items: current.items[current.index][current.fields.children],
95
+ index: 0,
96
+ fields: current.fields.fields || current.fields
97
+ })
98
+ } else if (current.index < current.items.length - 1) {
99
+ current.index++
100
+ } else {
101
+ let level = path.length - 2
102
+ while (level >= 0) {
103
+ const parent = path[level]
104
+ if (parent.index < parent.items.length - 1) {
105
+ parent.index++
106
+ path = path.slice(0, level + 1)
107
+ break
108
+ }
109
+ level--
110
+ }
111
+ }
112
+ return path
113
+ }
114
+
115
+ /**
116
+ * Navigate to the previous item
117
+ *
118
+ * @param {Array<PathFragment>} path - path to a node in the hierarchy
119
+ * @returns
120
+ */
121
+ export function movePrevious(path) {
122
+ if (path.length === 0) return []
123
+
124
+ const current = path[path.length - 1]
125
+
126
+ if (current.index == 0) {
127
+ if (path.length > 1) path.pop()
128
+ return path
129
+ }
130
+
131
+ current.index--
132
+ if (isExpanded(current.items[current.index], current.fields)) {
133
+ return navigateToLastVisibleChild(path)
134
+ }
135
+ return path
136
+ }
137
+
138
+ /**
139
+ *
140
+ * @param {Array<integer>} indices
141
+ * @param {Array<*>} items
142
+ * @param {import('../constants').FieldMapping} fields
143
+ * @returns
144
+ */
145
+ export function pathFromIndices(indices, items, fields) {
146
+ let path = []
147
+ let fragment
148
+ indices.map((index, level) => {
149
+ if (level === 0) {
150
+ fragment = { index, items, fields }
151
+ } else {
152
+ fragment = {
153
+ index,
154
+ items: fragment.items[fragment.index][fragment.fields.children],
155
+ fields: fragment.fields.fields ?? fragment.fields
156
+ }
157
+ }
158
+ path.push(fragment)
159
+ })
160
+ return path
161
+ }
162
+
163
+ export function indicesFromPath(path) {
164
+ return path.map(({ index }) => index)
165
+ }
166
+ export function getCurrentNode(path) {
167
+ if (path.length === 0) return null
168
+ const lastIndex = path.length - 1
169
+ return path[lastIndex].items[path[lastIndex].index]
170
+ }
171
+
172
+ export function findItem(items, indices, fields) {
173
+ let item = items[indices[0]]
174
+ let levelFields = fields
175
+ for (let level = 1; level < indices.length; level++) {
176
+ item = item[levelFields.children][indices[level]]
177
+ levelFields = levelFields.fields ?? levelFields
178
+ }
179
+ return item
180
+ }
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { fillable } from './fillable'
2
+ export { pannable } from './pannable'
3
+ export { navigable } from './navigable'
4
+ export { navigator } from './navigator'
5
+ export { dismissable } from './dismissable'
6
+ export { themable } from './themeable'
7
+ export { swipeable } from './swipeable'
@@ -0,0 +1,42 @@
1
+ export function navigable(
2
+ node,
3
+ { horizontal = true, nested = false, enabled = true } = {}
4
+ ) {
5
+ if (!enabled) return { destroy() {} }
6
+ const previous = () => node.dispatchEvent(new CustomEvent('previous'))
7
+ const next = () => node.dispatchEvent(new CustomEvent('next'))
8
+ const collapse = () => node.dispatchEvent(new CustomEvent('collapse'))
9
+ const expand = () => node.dispatchEvent(new CustomEvent('expand'))
10
+ const select = () => node.dispatchEvent(new CustomEvent('select'))
11
+
12
+ const movement = horizontal
13
+ ? { ArrowLeft: previous, ArrowRight: next }
14
+ : { ArrowUp: previous, ArrowDown: next }
15
+ const change = nested
16
+ ? horizontal
17
+ ? { ArrowUp: collapse, ArrowDown: expand }
18
+ : { ArrowLeft: collapse, ArrowRight: expand }
19
+ : {}
20
+ const actions = {
21
+ Enter: select,
22
+ ' ': select,
23
+ ...movement,
24
+ ...change
25
+ }
26
+
27
+ function handleKeydown(event) {
28
+ if (actions[event.key]) {
29
+ event.preventDefault()
30
+ event.stopPropagation()
31
+ actions[event.key]()
32
+ }
33
+ }
34
+
35
+ node.addEventListener('keydown', handleKeydown)
36
+
37
+ return {
38
+ destroy() {
39
+ node.removeEventListener('keydown', handleKeydown)
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,179 @@
1
+ import {
2
+ moveNext,
3
+ movePrevious,
4
+ isNested,
5
+ hasChildren,
6
+ pathFromIndices,
7
+ indicesFromPath,
8
+ getCurrentNode
9
+ } from './hierarchy'
10
+ /**
11
+ * @typedef NavigatorOptions
12
+ * @property {Array<*>} items - An array containing the data set to navigate
13
+ * @property {boolean} [vertical=true] - Identifies whether navigation shoud be vertical or horizontal
14
+ * @property {string} [idPrefix='id-'] - id prefix used for identifying individual node
15
+ * @property {import('../constants').FieldMapping} fields - Field mapping to identify attributes to be used for state and identification of children
16
+ */
17
+
18
+ /**
19
+ * Keyboard navigation for Lists and NestedLists. The data is either nested or not and is not
20
+ * expected to switch from nested to simple list or vice-versa.
21
+ *
22
+ * @param {HTMLElement} node - The node on which the action is to be used on
23
+ * @param {NavigatorOptions} options - Configuration options for the action
24
+ * @returns
25
+ */
26
+ export function navigator(node, options) {
27
+ const { fields, enabled = true, vertical = true, idPrefix = 'id-' } = options
28
+ let items, path, currentNode
29
+
30
+ if (!enabled) return { destroy: () => {} }
31
+
32
+ const update = (options) => {
33
+ items = options.items
34
+ path = pathFromIndices(options.indices ?? [], items, fields)
35
+ currentNode = getCurrentNode(path)
36
+ }
37
+
38
+ const next = () => {
39
+ const previousNode = currentNode
40
+ path = moveNext(path, items, fields)
41
+ currentNode = getCurrentNode(path)
42
+
43
+ if (previousNode !== currentNode) moveTo(node, path, currentNode, idPrefix)
44
+ }
45
+ const previous = () => {
46
+ const previousNode = currentNode
47
+ path = movePrevious(path)
48
+ if (path.length > 0) {
49
+ currentNode = getCurrentNode(path)
50
+ if (previousNode !== currentNode)
51
+ moveTo(node, path, currentNode, idPrefix)
52
+ }
53
+ }
54
+ const select = () => {
55
+ if (currentNode)
56
+ node.dispatchEvent(
57
+ new CustomEvent('select', {
58
+ detail: { path: indicesFromPath(path), node: currentNode }
59
+ })
60
+ )
61
+ }
62
+ const collapse = () => {
63
+ if (currentNode) {
64
+ const collapse =
65
+ hasChildren(currentNode, path[path.length - 1].fields) &&
66
+ currentNode[path[path.length - 1].fields.isOpen]
67
+ if (collapse) {
68
+ currentNode[path[path.length - 1].fields.isOpen] = false
69
+ node.dispatchEvent(
70
+ new CustomEvent('collapse', {
71
+ detail: { path: indicesFromPath(path), node: currentNode }
72
+ })
73
+ )
74
+ } else if (path.length > 0) {
75
+ path = path.slice(0, -1)
76
+ currentNode = getCurrentNode(path)
77
+ select()
78
+ }
79
+ }
80
+ }
81
+ const expand = () => {
82
+ if (currentNode && hasChildren(currentNode, path[path.length - 1].fields)) {
83
+ currentNode[path[path.length - 1].fields.isOpen] = true
84
+ node.dispatchEvent(
85
+ new CustomEvent('expand', {
86
+ detail: { path: indicesFromPath(path), node: currentNode }
87
+ })
88
+ )
89
+ }
90
+ }
91
+
92
+ update(options)
93
+
94
+ const nested = isNested(items, fields)
95
+ const movement = vertical
96
+ ? { ArrowDown: next, ArrowUp: previous }
97
+ : { ArrowRight: next, ArrowLeft: previous }
98
+ const states = !nested
99
+ ? {}
100
+ : vertical
101
+ ? { ArrowRight: expand, ArrowLeft: collapse }
102
+ : { ArrowDown: expand, ArrowUp: collapse }
103
+ const actions = { ...movement, Enter: select, ...states }
104
+
105
+ const handleKeyDown = (event) => {
106
+ if (actions[event.key]) {
107
+ event.preventDefault()
108
+ event.stopPropagation()
109
+ actions[event.key]()
110
+ }
111
+ }
112
+
113
+ const handleClick = (event) => {
114
+ let target = findParentWithDataPath(event.target)
115
+ let indices = !target
116
+ ? []
117
+ : target.dataset.path
118
+ .split(',')
119
+ .filter((item) => item !== '')
120
+ .map((item) => +item)
121
+
122
+ if (indices.length > 0 && event.target.tagName != 'DETAIL') {
123
+ path = pathFromIndices(indices, items, fields)
124
+ currentNode = getCurrentNode(path)
125
+ if (hasChildren(currentNode, path[path.length - 1].fields)) {
126
+ currentNode[path[path.length - 1].fields.isOpen] =
127
+ !currentNode[path[path.length - 1].fields.isOpen]
128
+ const event = currentNode[path[path.length - 1].fields.isOpen]
129
+ ? 'expand'
130
+ : 'collapse'
131
+ node.dispatchEvent(
132
+ new CustomEvent(event, {
133
+ detail: { path: indices, node: currentNode }
134
+ })
135
+ )
136
+ }
137
+ node.dispatchEvent(
138
+ new CustomEvent('select', {
139
+ detail: { path: indices, node: currentNode }
140
+ })
141
+ )
142
+ }
143
+ }
144
+
145
+ node.addEventListener('keydown', handleKeyDown)
146
+ node.addEventListener('click', handleClick)
147
+
148
+ return {
149
+ update,
150
+ destroy() {
151
+ node.removeEventListener('keydown', handleKeyDown)
152
+ node.removeEventListener('click', handleClick)
153
+ }
154
+ }
155
+ }
156
+
157
+ export function moveTo(node, path, currentNode, idPrefix) {
158
+ const indices = indicesFromPath(path)
159
+
160
+ let current = node.querySelector('#' + idPrefix + indices.join('-'))
161
+ if (current) current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
162
+
163
+ node.dispatchEvent(
164
+ new CustomEvent('move', {
165
+ detail: { path: indices, node: currentNode }
166
+ })
167
+ )
168
+ }
169
+
170
+ export function findParentWithDataPath(element) {
171
+ if (element.hasAttribute('data-path')) return element
172
+ let parent = element.parentNode
173
+
174
+ while (parent && !parent.hasAttribute('data-path')) {
175
+ parent = parent.parentNode
176
+ }
177
+
178
+ return parent
179
+ }
@@ -0,0 +1,57 @@
1
+ // pannable.js
2
+ /**
3
+ * Handle drag and move events
4
+ *
5
+ * @param {*} node
6
+ * @returns
7
+ */
8
+ export function pannable(node) {
9
+ let x
10
+ let y
11
+
12
+ function track(event, name, delta = {}) {
13
+ x = event.clientX || event.touches[0].clientX
14
+ y = event.clientY || event.touches[0].clientY
15
+ event.stopPropagation()
16
+ event.preventDefault()
17
+ node.dispatchEvent(
18
+ new CustomEvent(name, {
19
+ detail: { x, y, ...delta }
20
+ })
21
+ )
22
+ }
23
+
24
+ function handleMousedown(event) {
25
+ track(event, 'panstart')
26
+ window.addEventListener('mousemove', handleMousemove)
27
+ window.addEventListener('mouseup', handleMouseup)
28
+ window.addEventListener('touchmove', handleMousemove, { passive: false })
29
+ window.addEventListener('touchend', handleMouseup)
30
+ }
31
+
32
+ function handleMousemove(event) {
33
+ const dx = (event.clientX || event.touches[0].clientX) - x
34
+ const dy = (event.clientY || event.touches[0].clientY) - y
35
+
36
+ track(event, 'panmove', { dx, dy })
37
+ }
38
+
39
+ function handleMouseup(event) {
40
+ track(event, 'panend')
41
+
42
+ window.removeEventListener('mousemove', handleMousemove)
43
+ window.removeEventListener('mouseup', handleMouseup)
44
+ window.removeEventListener('touchmove', handleMousemove)
45
+ window.removeEventListener('touchend', handleMouseup)
46
+ }
47
+
48
+ node.addEventListener('mousedown', handleMousedown)
49
+ node.addEventListener('touchstart', handleMousedown, { passive: false })
50
+
51
+ return {
52
+ destroy() {
53
+ node.removeEventListener('mousedown', handleMousedown)
54
+ node.removeEventListener('touchstart', handleMousedown)
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,65 @@
1
+ export function swipeable(
2
+ node,
3
+ {
4
+ horizontal = true,
5
+ vertical = false,
6
+ threshold = 100,
7
+ enabled = true,
8
+ minSpeed = 300
9
+ } = {}
10
+ ) {
11
+ if (!enabled) return { destroy() {} }
12
+
13
+ let startX
14
+ let startY
15
+ let startTime
16
+
17
+ function touchStart(event) {
18
+ const touch = event.touches ? event.touches[0] : event
19
+ startX = touch.clientX
20
+ startY = touch.clientY
21
+ startTime = new Date().getTime()
22
+ }
23
+
24
+ function touchEnd(event) {
25
+ const touch = event.changedTouches ? event.changedTouches[0] : event
26
+ const distX = touch.clientX - startX
27
+ const distY = touch.clientY - startY
28
+ const duration = (new Date().getTime() - startTime) / 1000
29
+ const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
30
+
31
+ if (horizontal && speed > minSpeed) {
32
+ if (Math.abs(distX) > Math.abs(distY) && Math.abs(distX) >= threshold) {
33
+ if (distX > 0 && distX / duration > minSpeed) {
34
+ node.dispatchEvent(new CustomEvent('swipeRight'))
35
+ } else {
36
+ node.dispatchEvent(new CustomEvent('swipeLeft'))
37
+ }
38
+ }
39
+ }
40
+
41
+ if (vertical && speed > minSpeed) {
42
+ if (Math.abs(distY) > Math.abs(distX) && Math.abs(distY) >= threshold) {
43
+ if (distY > 0) {
44
+ node.dispatchEvent(new CustomEvent('swipeDown'))
45
+ } else {
46
+ node.dispatchEvent(new CustomEvent('swipeUp'))
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ node.addEventListener('touchstart', touchStart)
53
+ node.addEventListener('touchend', touchEnd)
54
+ node.addEventListener('mousedown', touchStart)
55
+ node.addEventListener('mouseup', touchEnd)
56
+
57
+ return {
58
+ destroy() {
59
+ node.removeEventListener('touchstart', touchStart)
60
+ node.removeEventListener('touchend', touchEnd)
61
+ node.removeEventListener('mousedown', touchStart)
62
+ node.removeEventListener('mouseup', touchEnd)
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,23 @@
1
+ import { theme } from '@rokkit/stores'
2
+
3
+ /**
4
+ * Sets theme level classes based on the theme store
5
+ *
6
+ * @param {HTMLElement} node
7
+ */
8
+ export function themable(node) {
9
+ let previous = {}
10
+
11
+ theme.subscribe((data) => {
12
+ if (data.name && data.name !== previous.name) {
13
+ node.classList.remove(previous.name)
14
+ node.classList.add(data.name)
15
+ }
16
+ if (data.mode && data.mode !== previous.mode) {
17
+ node.classList.remove(previous.mode)
18
+ node.classList.add(data.mode)
19
+ }
20
+
21
+ previous = data
22
+ })
23
+ }