henry-search 1.0.2 → 1.0.4
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/dist/HenrySearch.css +1 -1
- package/dist/HenrySearch.js +26700 -25558
- package/dist/HenrySearch.umd.cjs +88 -127
- package/package.json +6 -2
- package/src/assets/main-icons-sprite.svg +14 -2
- package/src/components/Autocomplete.vue +141 -0
- package/src/components/PAccordion.vue +25 -2
- package/src/components/PTabs.vue +98 -0
- package/src/components/Search.vue +180 -2032
- package/src/components/SearchDetail.vue +160 -0
- package/src/components/SearchHistory.vue +40 -0
- package/src/components/SearchTab.vue +707 -0
- package/src/components/constants.js +1 -0
- package/src/composables/FocusableElements.js +13 -0
- package/src/composables/UseDirectionalKeys.js +105 -0
- package/src/composables/focusWithArrows.js +52 -0
- package/src/composables/formatDate.js +15 -0
- package/src/composables/slugify.js +10 -0
- package/src/main.js +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const INSTANT_SEARCH_INDEX_NAME = "archived_performances"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { toValue } from 'vue'
|
|
2
|
+
|
|
3
|
+
const FOCUSABLE_SELECTOR = `:not([tabindex^="-"]):not([disabled]):is(a[href],audio[controls],button,details summary,input,map area[href],select,svg a[xlink\:href],[tabindex],textarea,video[controls])`
|
|
4
|
+
|
|
5
|
+
export default function focusableElements(element, includeSelf = true) {
|
|
6
|
+
element = toValue(element)
|
|
7
|
+
const elements = [...element.querySelectorAll(FOCUSABLE_SELECTOR)]
|
|
8
|
+
if (includeSelf && element.matches(FOCUSABLE_SELECTOR)) {
|
|
9
|
+
elements.unshift(element)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return elements
|
|
13
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { toValue, onMounted } from 'vue'
|
|
2
|
+
|
|
3
|
+
import focusableElements from './FocusableElements.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default key filters. These should be functions that 'filter out' what
|
|
7
|
+
* elements should be considered when a key is hit.
|
|
8
|
+
*/
|
|
9
|
+
const KEY_FILTERS = {
|
|
10
|
+
ArrowRight: elements => elements.filter(({ x, top, bottom }) => x > 0 && top < 0 && bottom > 0),
|
|
11
|
+
ArrowLeft: elements => elements.filter(({ x, top, bottom }) => x < 0 && top < 0 && bottom > 0),
|
|
12
|
+
ArrowDown: elements => elements.filter(({ y, left, right }) => y > 0 && left < 0 && right > 0),
|
|
13
|
+
ArrowUp: elements => elements.filter(({ y, left, right }) => y < 0 && left < 0 && right > 0),
|
|
14
|
+
Home: elements => elements.length && elements.slice(0, 1),
|
|
15
|
+
End: elements => elements.length && elements.slice(-1),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves where the element is drawn on screen (client rects).
|
|
20
|
+
* Adds in x and y for the center of the element.
|
|
21
|
+
*/
|
|
22
|
+
const getElementRects = el => {
|
|
23
|
+
const rects = el.getClientRects()[0]
|
|
24
|
+
|
|
25
|
+
if (!rects || !rects.left) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
bottom: rects.bottom,
|
|
31
|
+
height: rects.height,
|
|
32
|
+
left: rects.left,
|
|
33
|
+
right: rects.right,
|
|
34
|
+
top: rects.top,
|
|
35
|
+
width: rects.width,
|
|
36
|
+
x: rects.left + rects.width / 2,
|
|
37
|
+
y: rects.top + rects.height / 2,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calls getElementRects for every element in nodeList, and adjust the
|
|
43
|
+
* values to be relative to origin for simpler follow up math. Also
|
|
44
|
+
* calculates the distance between the two elements' centers.
|
|
45
|
+
*/
|
|
46
|
+
const augmentElementRects = (nodeList, origin) => {
|
|
47
|
+
const elements = []
|
|
48
|
+
origin = getElementRects(origin)
|
|
49
|
+
|
|
50
|
+
if (!origin) {
|
|
51
|
+
return elements
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
nodeList.forEach(el => {
|
|
55
|
+
const rects = getElementRects(el)
|
|
56
|
+
if (rects === null) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
rects.bottom -= origin.y
|
|
61
|
+
rects.left -= origin.x
|
|
62
|
+
rects.right -= origin.x
|
|
63
|
+
rects.top -= origin.y
|
|
64
|
+
rects.x -= origin.x
|
|
65
|
+
rects.y -= origin.y
|
|
66
|
+
|
|
67
|
+
const distance = Math.sqrt(rects.x * rects.x + rects.y * rects.y)
|
|
68
|
+
|
|
69
|
+
elements.push({ el, ...rects, distance })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return elements
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* useDirectionalKeys() takes any number of rootElements that it searches for
|
|
77
|
+
* focusable elements and handles directional arrow moves between. Also
|
|
78
|
+
* supports Home and End.
|
|
79
|
+
*/
|
|
80
|
+
export default function useDirectionalKeys (element) {
|
|
81
|
+
const findTarget = (el, key) => {
|
|
82
|
+
const elements = augmentElementRects(focusableElements(element), el)
|
|
83
|
+
if (key in KEY_FILTERS) {
|
|
84
|
+
return KEY_FILTERS[key](elements).reduce(
|
|
85
|
+
(closest, el) => {
|
|
86
|
+
return el.distance < closest.distance ? el : closest
|
|
87
|
+
},
|
|
88
|
+
{ distance: Infinity }
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handler = evt => {
|
|
95
|
+
const newTarget = findTarget(evt.target, evt.key)
|
|
96
|
+
|
|
97
|
+
if (newTarget?.el) {
|
|
98
|
+
evt.preventDefault()
|
|
99
|
+
evt.stopPropagation()
|
|
100
|
+
newTarget.el.focus()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
element.addEventListener('keydown', handler)
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ref, toValue, watchEffect } from 'vue'
|
|
2
|
+
|
|
3
|
+
export default function focusWithArrows(
|
|
4
|
+
elements,
|
|
5
|
+
{
|
|
6
|
+
includeHomeEnd = true,
|
|
7
|
+
} = {}
|
|
8
|
+
) {
|
|
9
|
+
|
|
10
|
+
const KEY_BEHAVIORS = {
|
|
11
|
+
ArrowLeft: i => Math.max(0, i - 1),
|
|
12
|
+
ArrowRight: i => Math.min(handledElements.length - 1, i + 1),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (includeHomeEnd) {
|
|
16
|
+
KEY_BEHAVIORS.Home = () => 0
|
|
17
|
+
KEY_BEHAVIORS.End = () => handledElements.length - 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const focus = ref(null)
|
|
21
|
+
|
|
22
|
+
const handleKeydown = event => {
|
|
23
|
+
if (event.key in KEY_BEHAVIORS) {
|
|
24
|
+
event.preventDefault()
|
|
25
|
+
const i = handledElements.indexOf(event.target)
|
|
26
|
+
|
|
27
|
+
focus.value = handledElements[KEY_BEHAVIORS[event.key](i)]
|
|
28
|
+
focus.value.focus()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handledElements = []
|
|
33
|
+
|
|
34
|
+
watchEffect(() => {
|
|
35
|
+
(toValue(elements) || [])
|
|
36
|
+
.filter(elem => !handledElements.includes(elem))
|
|
37
|
+
.forEach(elem => {
|
|
38
|
+
elem.addEventListener('keydown', handleKeydown)
|
|
39
|
+
elem.addEventListener('focus', event => {
|
|
40
|
+
focus.value = event.target
|
|
41
|
+
event.target.addEventListener('blur', () => focus.value = null, { once: true })
|
|
42
|
+
})
|
|
43
|
+
handledElements.push(elem)
|
|
44
|
+
|
|
45
|
+
if (document.activeElement === elem) {
|
|
46
|
+
focus.value = elem
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return focus
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default function formatDate(unix_timestamp, detail = false) {
|
|
2
|
+
// multiplied by 1000 so that the argument is in milliseconds, not seconds
|
|
3
|
+
const date = new Date(unix_timestamp * 1000)
|
|
4
|
+
const days = ["Sun", "Mon", "Tues", "Weds", "Thurs", "Fri", "Sat"]
|
|
5
|
+
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
|
6
|
+
const hour = date.getHours() + 1 > 12 ? date.getHours() - 11 : date.getHours() + 1
|
|
7
|
+
const amPm = date.getHours() + 1 > 12 ? "pm" : "am"
|
|
8
|
+
const minutes = "0" + date.getMinutes()
|
|
9
|
+
|
|
10
|
+
if (detail) {
|
|
11
|
+
return `${days[date.getDate()]}, ${(months[date.getMonth()]).substring(0, 3)} ${date.getDate()}, ${hour}:${minutes.substr(-2)}${amPm} EDT`
|
|
12
|
+
} else {
|
|
13
|
+
return `${date.getMonth() + 1}-${date.getDate() + 1}-${date.getFullYear()}`
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default function slugify(str) {
|
|
2
|
+
return String(str)
|
|
3
|
+
.normalize('NFKD') // split accented characters into their base characters and diacritical marks
|
|
4
|
+
.replace(/[\u0300-\u036f]/g, '') // remove all the accents, which happen to be all in the \u03xx UNICODE block.
|
|
5
|
+
.trim() // trim leading or trailing whitespace
|
|
6
|
+
.toLowerCase() // convert to lowercase
|
|
7
|
+
.replace(/[^a-z0-9 -]/g, '') // remove non-alphanumeric characters
|
|
8
|
+
.replace(/\s+/g, '-') // replace spaces with hyphens
|
|
9
|
+
.replace(/-+/g, '-'); // remove consecutive hyphens
|
|
10
|
+
}
|
package/src/main.js
CHANGED
|
@@ -5,6 +5,8 @@ import { createApp } from 'vue'
|
|
|
5
5
|
// createApp(App).mount('#app')
|
|
6
6
|
|
|
7
7
|
import Search from './components/Search.vue'
|
|
8
|
+
import SearchDetail from './components/SearchDetail.vue'
|
|
9
|
+
import SearchHistory from './components/SearchHistory.vue'
|
|
8
10
|
|
|
9
11
|
export default {
|
|
10
12
|
|
|
@@ -16,4 +18,4 @@ export default {
|
|
|
16
18
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export { Search }
|
|
21
|
+
export { Search, SearchDetail, SearchHistory }
|