fenton-jekyll-boilerplate 0.0.5 → 0.0.6
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.
- checksums.yaml +4 -4
- data/assets/js/main.js +18 -0
- data/assets/js/modules/animation.js +41 -0
- data/assets/js/modules/click-blocks.js +35 -0
- data/assets/js/modules/events.js +19 -0
- data/assets/js/modules/focus.js +76 -0
- data/assets/js/modules/nav-expand.js +51 -0
- data/assets/js/modules/nav-mobile.js +104 -0
- data/assets/js/modules/nav-sticky.js +54 -0
- data/assets/js/modules/query.js +42 -0
- data/assets/js/modules/resizing.js +43 -0
- data/assets/js/modules/string.js +49 -0
- data/assets/js/search.js +154 -0
- metadata +13 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0d1a8a76ec9b945ff3ab8e850ea6874390b5abbb02d68d11a750427142810de
|
4
|
+
data.tar.gz: a72394148f74d7fa8bf7a4cc751e44978945095982ce351b1f3285bddc96dec2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f6b74abfacb10010e0445fbe914d29e8757de93b42c746df7f9e34bf4e77a5f9bbf78215cf8b7aad5b2be336fa365a3d47fc2772afa2adb55e5efc0bdf9aaf72
|
7
|
+
data.tar.gz: 528baee39ddef889baf502beeef69fd884b4df3a1bd8d12c0088ca75b35712c8de1429bbe9318a351272582dfa6c3f39a19c5c612f540de3d9fdf41e517c23c3
|
data/assets/js/main.js
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { setNavigationTree, setNavigationItem } from './modules/nav-expand.js';
|
4
|
+
import { setClickableBlocks } from './modules/click-blocks.js';
|
5
|
+
import { addResizedEvent } from './modules/resizing.js';
|
6
|
+
import { addStickyNavigation } from './modules/nav-sticky.js';
|
7
|
+
import { addMobileNavigation } from './modules/nav-mobile.js';
|
8
|
+
import { addIntersectionObserver } from './modules/animation.js';
|
9
|
+
|
10
|
+
setNavigationTree('details.sub-nav');
|
11
|
+
setNavigationItem('.site-nav a', 'current-item')
|
12
|
+
setClickableBlocks('data-destination');
|
13
|
+
|
14
|
+
var resizedEventName = addResizedEvent();
|
15
|
+
|
16
|
+
addStickyNavigation('.site-header', '.site-nav', '.site-nav > ul', resizedEventName);
|
17
|
+
addMobileNavigation('.navigation-icon', '.site-nav', resizedEventName);
|
18
|
+
addIntersectionObserver('.post-list .list-item, main img, main .note, main blockquote');
|
@@ -0,0 +1,41 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { qsa } from './query.js';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Assists animation by setting "--shown" CSS property
|
7
|
+
*
|
8
|
+
* When an item is visible in the viewport, it will have --shown: 1
|
9
|
+
* Otherwise it will be --shown: 0
|
10
|
+
* This allows CSS transitions and calculated properties to animate elements
|
11
|
+
*
|
12
|
+
* Example
|
13
|
+
* transition: all 0.2s ease-in;
|
14
|
+
* scale: calc(0.75 + (var(--shown, 1) * 0.25));
|
15
|
+
*
|
16
|
+
* @param {string} listItemQuery
|
17
|
+
*/
|
18
|
+
function addIntersectionObserver(listItemQuery) {
|
19
|
+
function handleIntersection(entries, observer) {
|
20
|
+
for (var entry of entries) {
|
21
|
+
var value = entry.isIntersecting ? 1 : 0;
|
22
|
+
entry.target.style.setProperty('--shown', value);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
var options = {
|
27
|
+
root: null,
|
28
|
+
rootMargin: '0px',
|
29
|
+
threshold: 0
|
30
|
+
};
|
31
|
+
|
32
|
+
var observer = new IntersectionObserver(handleIntersection, options)
|
33
|
+
|
34
|
+
var items = qsa(listItemQuery);
|
35
|
+
|
36
|
+
for (var i = 0; i < items.length; i++) {
|
37
|
+
observer.observe(items[i]);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
export { addIntersectionObserver };
|
@@ -0,0 +1,35 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { qsa } from './query.js';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Makes an entire block clickable based on a data-attribute, usually "data-destination"
|
7
|
+
*
|
8
|
+
* Example: You have a list of blog posts, including featured images. If you make the title
|
9
|
+
* clickable, clicks on the image won't open the blog. Adding links to the images means
|
10
|
+
* keyboard users have to tab twice as much to get through the list.
|
11
|
+
*
|
12
|
+
* Use clickable blocks to allow keyboard users to tab through the real links, but still
|
13
|
+
* capture clicks elsewhere on the block.
|
14
|
+
*
|
15
|
+
* @param {string} dataAttributeName
|
16
|
+
*/
|
17
|
+
function setClickableBlocks(dataAttributeName) {
|
18
|
+
var listItems = qsa('[' + dataAttributeName + ']');
|
19
|
+
|
20
|
+
for(var i = 0; i < listItems.length; i++) {
|
21
|
+
var listItem = listItems[i];
|
22
|
+
listItem.style.cursor = 'pointer';
|
23
|
+
listItem.addEventListener('click', function (e) {
|
24
|
+
var location = this.getAttribute(dataAttributeName);
|
25
|
+
|
26
|
+
if (location) {
|
27
|
+
e.preventDefault();
|
28
|
+
document.location = location;
|
29
|
+
return false;
|
30
|
+
}
|
31
|
+
});
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
export { setClickableBlocks };
|
@@ -0,0 +1,19 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
/**
|
4
|
+
*
|
5
|
+
* @param {string} name
|
6
|
+
* @param {{[key: string]: any}} detail
|
7
|
+
* @param {Document | HTMLElement} [target]
|
8
|
+
*/
|
9
|
+
function raiseEvent(name, detail, target) {
|
10
|
+
if (!target) {
|
11
|
+
target = document;
|
12
|
+
}
|
13
|
+
|
14
|
+
const event = new CustomEvent(name, { detail: detail});
|
15
|
+
document.dispatchEvent(event);
|
16
|
+
console.log('Event Raised', name, detail);
|
17
|
+
}
|
18
|
+
|
19
|
+
export { raiseEvent };
|
@@ -0,0 +1,76 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { qsa } from './query.js';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Gets first, last, and all focusable elements in the target
|
7
|
+
*
|
8
|
+
* For the supplied element, finds all the elements that can receive keyboard focus.
|
9
|
+
*
|
10
|
+
* Examples: a, button, input, textarea, select, and other valid interactive items
|
11
|
+
* that haven't been disabled or hidden.
|
12
|
+
*
|
13
|
+
* @param {HTMLElement} target element
|
14
|
+
* @returns {{first: HTMLElement, last: HTMLElement, all: HTMLElement[]}}
|
15
|
+
*/
|
16
|
+
function getFocusableElement(target) {
|
17
|
+
var focusElements = Array.from(
|
18
|
+
qsa('a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])', target)
|
19
|
+
).filter(function(el) {
|
20
|
+
return !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden');
|
21
|
+
});
|
22
|
+
|
23
|
+
return {
|
24
|
+
first: focusElements[0],
|
25
|
+
last: focusElements[focusElements.length -1],
|
26
|
+
all: focusElements
|
27
|
+
};
|
28
|
+
}
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Mechanism to trap focus
|
32
|
+
*
|
33
|
+
* @param {KeyboardEvent} event
|
34
|
+
* @param {HTMLElement} focusItem
|
35
|
+
* @returns
|
36
|
+
*/
|
37
|
+
function trapFocus(event, focusItem) {
|
38
|
+
switch (event.code.toLowerCase()) {
|
39
|
+
case 'tab':
|
40
|
+
event.preventDefault();
|
41
|
+
focusItem.focus();
|
42
|
+
return false;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Mechanism to trap focus (TAB)
|
48
|
+
*
|
49
|
+
* @param {KeyboardEvent} event
|
50
|
+
* @param {HTMLElement} focusItem
|
51
|
+
* @returns
|
52
|
+
*/
|
53
|
+
function trapFocusForward(event, focusItem) {
|
54
|
+
if (event.shiftKey) {
|
55
|
+
return;
|
56
|
+
}
|
57
|
+
|
58
|
+
trapFocus(event, focusItem);
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Mechanism to trap tab (SHIFT + TAB)
|
63
|
+
*
|
64
|
+
* @param {KeyboardEvent} event
|
65
|
+
* @param {HTMLElement} focusItem
|
66
|
+
* @returns
|
67
|
+
*/
|
68
|
+
function trapReverseFocus(event, focusItem) {
|
69
|
+
if (!event.shiftKey) {
|
70
|
+
return;
|
71
|
+
}
|
72
|
+
|
73
|
+
trapFocus(event, focusItem);
|
74
|
+
}
|
75
|
+
|
76
|
+
export { getFocusableElement, trapFocusForward, trapReverseFocus };
|
@@ -0,0 +1,51 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { qs, qsa } from './query.js';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Sets the navigation based on the current page
|
7
|
+
*
|
8
|
+
* Example: You have a navigation tree with an "About" page with several child items.
|
9
|
+
* When the user is on the "About" page, or any child pages, the navigation should
|
10
|
+
* be automatically expanded so the user can orient themselves within the site.
|
11
|
+
*
|
12
|
+
* @param {string} className
|
13
|
+
*/
|
14
|
+
function setNavigationTree(className) {
|
15
|
+
var summaries = qsa(className);
|
16
|
+
var site = document.location.origin;
|
17
|
+
var location = document.location.pathname;
|
18
|
+
|
19
|
+
for (var i = 0; i < summaries.length; i++) {
|
20
|
+
var summary = summaries[i];
|
21
|
+
var anchorElement = /** @type {HTMLAnchorElement} */(qs('a', summary));
|
22
|
+
var address = anchorElement.href.replace(site, '');
|
23
|
+
|
24
|
+
if (location.startsWith(address)){
|
25
|
+
summary.setAttribute('open', 'open');
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Highlights the current navigation item
|
32
|
+
*
|
33
|
+
* @param {string} navQuery
|
34
|
+
* @param {string} selectedClass
|
35
|
+
*/
|
36
|
+
function setNavigationItem(navQuery, selectedClass) {
|
37
|
+
var anchors = qsa(navQuery);
|
38
|
+
var site = document.location.origin;
|
39
|
+
var location = document.location.pathname;
|
40
|
+
|
41
|
+
for (var j = 0; j < anchors.length; j++) {
|
42
|
+
var anchor = /** @type {HTMLAnchorElement} */ (anchors[j]);
|
43
|
+
var href = anchor.href.replace(site, '');
|
44
|
+
|
45
|
+
if (href === location) {
|
46
|
+
anchor.classList.add(selectedClass);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
export { setNavigationTree, setNavigationItem };
|
@@ -0,0 +1,104 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { qs } from './query.js';
|
4
|
+
import { getFocusableElement, trapFocusForward, trapReverseFocus } from './focus.js';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Provides an overlay with the navigation for mobile users.
|
8
|
+
*
|
9
|
+
* Example: You have site navigation on the page, but demote it (closer to the footer) on mobile to avoid
|
10
|
+
* the content being pushed below the fold. You provide an icon that bookmarks to the
|
11
|
+
* navigation.
|
12
|
+
*
|
13
|
+
* The mobile navigation intercepts the bookmark link and opens the navigation in a modal
|
14
|
+
* overlay, trapping keyboard focus until the overlay is closed.
|
15
|
+
*
|
16
|
+
* @param {string} iconSelector
|
17
|
+
* @param {string} navigationSelector
|
18
|
+
*/
|
19
|
+
function addMobileNavigation(iconSelector, navigationSelector, resizedEventName) {
|
20
|
+
var icon = qs(iconSelector);
|
21
|
+
var originalIcon = icon.innerHTML;
|
22
|
+
var overlay = document.createElement('div');
|
23
|
+
var dataOpen = 'data-open';
|
24
|
+
|
25
|
+
icon.addEventListener('keydown', function(e) {
|
26
|
+
if (icon.getAttribute(dataOpen) === dataOpen) {
|
27
|
+
var focusElements = getFocusableElement(overlay);
|
28
|
+
trapFocusForward(e, focusElements.first);
|
29
|
+
trapReverseFocus(e, focusElements.last);
|
30
|
+
}
|
31
|
+
});
|
32
|
+
|
33
|
+
function handleIconInteraction() {
|
34
|
+
if (icon.dataset.open == dataOpen) {
|
35
|
+
closeMobileMenu();
|
36
|
+
} else {
|
37
|
+
openMobileMenu();
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
function openMobileMenu(){
|
42
|
+
document.body.style.overflow = 'hidden';
|
43
|
+
var navigation = qs(navigationSelector);
|
44
|
+
|
45
|
+
overlay.innerHTML = navigation.outerHTML;
|
46
|
+
overlay.className = 'overlay';
|
47
|
+
overlay.style.display = 'block';
|
48
|
+
|
49
|
+
// Modal Accessibility
|
50
|
+
var title = qs('.site-nav-title', overlay);
|
51
|
+
title.setAttribute('id', 'modal-title');
|
52
|
+
title.setAttribute('tabindex', '-1');
|
53
|
+
overlay.setAttribute('role', 'dialog');
|
54
|
+
overlay.setAttribute('aria-modal', 'true');
|
55
|
+
overlay.setAttribute('aria-labelled-by', 'modal-title');
|
56
|
+
|
57
|
+
// Trap Focus to Visible Overlay
|
58
|
+
var focusElements = getFocusableElement(overlay);
|
59
|
+
|
60
|
+
focusElements.first.addEventListener('keydown', function(e) {
|
61
|
+
trapReverseFocus(e, icon);
|
62
|
+
})
|
63
|
+
focusElements.last.addEventListener('keydown', function(e) {
|
64
|
+
trapFocusForward(e, icon);
|
65
|
+
});
|
66
|
+
|
67
|
+
icon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg"
|
68
|
+
width="40" height="40" viewBox="0 0 24 24" stroke-width="1.5"
|
69
|
+
fill="none" stroke-linecap="round" stroke-linejoin="round">
|
70
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
71
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
72
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
73
|
+
</svg>`;
|
74
|
+
|
75
|
+
document.body.appendChild(overlay);
|
76
|
+
icon.setAttribute(dataOpen, dataOpen);
|
77
|
+
title.focus();
|
78
|
+
}
|
79
|
+
|
80
|
+
function closeMobileMenu() {
|
81
|
+
document.body.style.overflow = 'auto';
|
82
|
+
|
83
|
+
if (icon.getAttribute(dataOpen) === dataOpen) {
|
84
|
+
overlay.innerHTML = '';
|
85
|
+
overlay.style.display = 'none';
|
86
|
+
document.body.removeChild(overlay);
|
87
|
+
}
|
88
|
+
|
89
|
+
icon.innerHTML = originalIcon;
|
90
|
+
icon.removeAttribute(dataOpen);
|
91
|
+
}
|
92
|
+
|
93
|
+
icon.addEventListener('click', function (e) {
|
94
|
+
e.preventDefault();
|
95
|
+
handleIconInteraction();
|
96
|
+
return false;
|
97
|
+
});
|
98
|
+
|
99
|
+
document.addEventListener(resizedEventName, function () {
|
100
|
+
closeMobileMenu();
|
101
|
+
})
|
102
|
+
}
|
103
|
+
|
104
|
+
export { addMobileNavigation };
|
@@ -0,0 +1,54 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { qs, qsa } from './query.js';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Makes an existing navigation element sticky
|
7
|
+
*
|
8
|
+
* Example: If the existing navigation is not as tall as the content, the
|
9
|
+
* navigation will stick to the top, allowing the user to see it as
|
10
|
+
* they scroll through the article
|
11
|
+
*
|
12
|
+
* @param {string} headerSelector
|
13
|
+
* @param {string} navigationSelector
|
14
|
+
* @param {string} navigationListSelector
|
15
|
+
*/
|
16
|
+
function addStickyNavigation(headerSelector, navigationSelector, navigationListSelector, resizedEventName) {
|
17
|
+
function setNavigationMode() {
|
18
|
+
var header = qs(headerSelector);
|
19
|
+
var navigation = qs(navigationSelector);
|
20
|
+
var navigationList = qs(navigationListSelector);
|
21
|
+
|
22
|
+
var buffer = 50;
|
23
|
+
var className = 'sticky';
|
24
|
+
|
25
|
+
var dimensions = {
|
26
|
+
browserHeight: window.innerHeight,
|
27
|
+
browserWidth: window.innerWidth,
|
28
|
+
headerHeight: header.clientHeight,
|
29
|
+
navigationHeight: navigationList.clientHeight
|
30
|
+
};
|
31
|
+
|
32
|
+
// Only enable sticky mode if the menu will fit vertically
|
33
|
+
// && where the browser is more than 860px wide
|
34
|
+
if (dimensions.navigationHeight < ((dimensions.browserHeight - dimensions.headerHeight) - buffer)
|
35
|
+
&& dimensions.browserWidth > 860) {
|
36
|
+
console.log('Navigation: Sticky Mode');
|
37
|
+
navigation.classList.add(className)
|
38
|
+
navigation.style.top = dimensions.headerHeight.toString() + 'px';
|
39
|
+
} else {
|
40
|
+
console.log('Navigation: Fixed Mode');
|
41
|
+
navigation.classList.remove(className);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
setNavigationMode();
|
46
|
+
|
47
|
+
document.addEventListener(resizedEventName, function(e) {
|
48
|
+
if (e.detail && e.detail.change && e.detail.change.height != 0) {
|
49
|
+
setNavigationMode();
|
50
|
+
}
|
51
|
+
});
|
52
|
+
}
|
53
|
+
|
54
|
+
export { addStickyNavigation };
|
@@ -0,0 +1,42 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Utility for query selector
|
5
|
+
*
|
6
|
+
* @param {string} query
|
7
|
+
* @param {HTMLElement | null} [container]
|
8
|
+
* @returns {HTMLElement}
|
9
|
+
*/
|
10
|
+
function qs(query, container) {
|
11
|
+
var target = (container)
|
12
|
+
? container
|
13
|
+
: document;
|
14
|
+
|
15
|
+
/** @type {HTMLElement | null} */
|
16
|
+
var result = target.querySelector(query);
|
17
|
+
|
18
|
+
if (result) {
|
19
|
+
return result;
|
20
|
+
}
|
21
|
+
|
22
|
+
throw new Error(`No element ${query}`);
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Utility for query selector all
|
27
|
+
*
|
28
|
+
* @param {string} query
|
29
|
+
* @param {HTMLElement | null} [container]
|
30
|
+
* @returns {NodeListOf<any>}
|
31
|
+
*/
|
32
|
+
function qsa(query, container) {
|
33
|
+
var target = (container)
|
34
|
+
? container
|
35
|
+
: document;
|
36
|
+
|
37
|
+
/** @type {NodeListOf<HTMLElement>} */
|
38
|
+
var result = target.querySelectorAll(query);
|
39
|
+
return result;
|
40
|
+
}
|
41
|
+
|
42
|
+
export { qs, qsa };
|
@@ -0,0 +1,43 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { raiseEvent } from './events.js';
|
4
|
+
|
5
|
+
var resizeEventName = 'resize';
|
6
|
+
var resizedEventName = 'resized';
|
7
|
+
|
8
|
+
var width = window.innerWidth;
|
9
|
+
var height = window.innerHeight;
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Adds a de-bounced "resized" event, so you can listen to:
|
13
|
+
* document.addEventListener('resized', <handler>);
|
14
|
+
*
|
15
|
+
* @returns {string}
|
16
|
+
*/
|
17
|
+
function addResizedEvent() {
|
18
|
+
var debounce = null;
|
19
|
+
|
20
|
+
function resizeEnd(e) {
|
21
|
+
window.clearTimeout(debounce);
|
22
|
+
debounce = window.setTimeout(raiseResizeEvent, 500);
|
23
|
+
}
|
24
|
+
|
25
|
+
function raiseResizeEvent() {
|
26
|
+
var change = {
|
27
|
+
width: window.innerWidth - width,
|
28
|
+
height: window.innerHeight - height
|
29
|
+
};
|
30
|
+
|
31
|
+
width = window.innerWidth;
|
32
|
+
height = window.innerHeight;
|
33
|
+
|
34
|
+
|
35
|
+
raiseEvent(resizedEventName, { change: change });
|
36
|
+
}
|
37
|
+
|
38
|
+
window.addEventListener(resizeEventName, resizeEnd);
|
39
|
+
|
40
|
+
return resizedEventName;
|
41
|
+
}
|
42
|
+
|
43
|
+
export { addResizedEvent };
|
@@ -0,0 +1,49 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Looks for a search within a string
|
5
|
+
*
|
6
|
+
* @param {string} string
|
7
|
+
* @param {string} search
|
8
|
+
* @returns
|
9
|
+
*/
|
10
|
+
function contains(string, search) {
|
11
|
+
return string.indexOf(search) > -1;
|
12
|
+
}
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Simplifies a string to plain lower case, removing diacritic characters and hyphens
|
16
|
+
* This means a search for "co-op" will be found in "COOP" and "Café" will be found in "cafe"
|
17
|
+
* @param {string} string
|
18
|
+
* @returns
|
19
|
+
*/
|
20
|
+
function sanitise(string) {
|
21
|
+
// @ts-ignore
|
22
|
+
if (String.prototype.normalize) {
|
23
|
+
// Reduces diacritic characters to plain characters
|
24
|
+
string.trim().normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/-/g, '');
|
25
|
+
}
|
26
|
+
|
27
|
+
// Some browsers can't normalise strings
|
28
|
+
return string.trim().toLowerCase().replace(/-/g, '');
|
29
|
+
}
|
30
|
+
|
31
|
+
/**
|
32
|
+
* Sets a minimum length for a search
|
33
|
+
* @param {string} string
|
34
|
+
* @returns
|
35
|
+
*/
|
36
|
+
function isLongEnough(string) {
|
37
|
+
return string.length > 1;
|
38
|
+
}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Splits a sentence into individual search terms
|
42
|
+
* @param {string} string
|
43
|
+
* @returns
|
44
|
+
*/
|
45
|
+
function explode(string) {
|
46
|
+
return string.split(' ').filter(isLongEnough).map(sanitise);
|
47
|
+
}
|
48
|
+
|
49
|
+
export { contains, sanitise, explode };
|
data/assets/js/search.js
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
// @ts-check
|
2
|
+
|
3
|
+
import { qs } from './modules/query.js';
|
4
|
+
import { raiseEvent } from './modules/events.js';
|
5
|
+
import { contains, sanitise, explode } from './modules/string.js';
|
6
|
+
|
7
|
+
var haystack = [];
|
8
|
+
var needles = [];
|
9
|
+
var currentQuery = null;
|
10
|
+
|
11
|
+
var ready = false;
|
12
|
+
var scrolled = false;
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Performs the search
|
16
|
+
* @param {string} s
|
17
|
+
* @returns
|
18
|
+
*/
|
19
|
+
function search(s) {
|
20
|
+
needles = [];
|
21
|
+
|
22
|
+
// Clean the input
|
23
|
+
var cleanQuery = sanitise(s);
|
24
|
+
|
25
|
+
if (currentQuery === cleanQuery) {
|
26
|
+
return;
|
27
|
+
}
|
28
|
+
|
29
|
+
raiseEvent('searched', { search: s });
|
30
|
+
|
31
|
+
currentQuery = cleanQuery;
|
32
|
+
|
33
|
+
var queryTerms = explode(currentQuery);
|
34
|
+
|
35
|
+
for (var i = 0; i < haystack.length; i++) {
|
36
|
+
var item = haystack[i];
|
37
|
+
|
38
|
+
item.score = 0;
|
39
|
+
|
40
|
+
var title = sanitise(item.title);
|
41
|
+
var category = sanitise(item.category);
|
42
|
+
var tags = sanitise(item.tags);
|
43
|
+
|
44
|
+
for (var j = 0; j < queryTerms.length; j++) {
|
45
|
+
var term = queryTerms[j];
|
46
|
+
|
47
|
+
if (contains(title, term)) {
|
48
|
+
item.score = item.score + 10;
|
49
|
+
}
|
50
|
+
|
51
|
+
if (contains(category, term)) {
|
52
|
+
item.score = item.score + 5;
|
53
|
+
}
|
54
|
+
|
55
|
+
if (contains(tags, term)) {
|
56
|
+
item.score = item.score + 5;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
if (item.score > 0) {
|
61
|
+
needles.push(item);
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
needles.sort(function (a, b){
|
66
|
+
return b.score - a.score;
|
67
|
+
});
|
68
|
+
|
69
|
+
var results = qs('#site-search-results');
|
70
|
+
|
71
|
+
if (results == null) {
|
72
|
+
throw new Error('Cannot find #site-search-results');
|
73
|
+
}
|
74
|
+
|
75
|
+
var ol = document.createElement('ol');
|
76
|
+
ol.className = 'site-search-results';
|
77
|
+
|
78
|
+
var limit = Math.min(needles.length, 12)
|
79
|
+
|
80
|
+
for (var i = 0; i < limit; i++) {
|
81
|
+
var needle = needles[i];
|
82
|
+
|
83
|
+
var a = document.createElement('a');
|
84
|
+
a.innerHTML = needle.title;
|
85
|
+
a.href = needle.url;
|
86
|
+
|
87
|
+
var li = document.createElement('li');
|
88
|
+
li.appendChild(a);
|
89
|
+
|
90
|
+
ol.appendChild(li);
|
91
|
+
}
|
92
|
+
|
93
|
+
var h2 = document.createElement('h2');
|
94
|
+
h2.innerHTML = needles.length === 0
|
95
|
+
? results.dataset.emptytitle || 'No Results'
|
96
|
+
: results.dataset.title || 'Results';
|
97
|
+
|
98
|
+
results.innerHTML = '';
|
99
|
+
results.appendChild(h2);
|
100
|
+
results.appendChild(ol);
|
101
|
+
}
|
102
|
+
|
103
|
+
var debounceTimer;
|
104
|
+
|
105
|
+
function debounceSearch() {
|
106
|
+
var input = /** @type {HTMLInputElement} */(qs('#site-search-query'));
|
107
|
+
|
108
|
+
if (input == null) {
|
109
|
+
throw new Error('Cannot find #site-search-query');
|
110
|
+
}
|
111
|
+
|
112
|
+
var s = input.value;
|
113
|
+
|
114
|
+
window.clearTimeout(debounceTimer);
|
115
|
+
debounceTimer = window.setTimeout(function () {
|
116
|
+
if (ready) {
|
117
|
+
search(s);
|
118
|
+
}
|
119
|
+
}, 400);
|
120
|
+
}
|
121
|
+
|
122
|
+
fetch('/search.json')
|
123
|
+
.then(function (response) {
|
124
|
+
return response.json();
|
125
|
+
})
|
126
|
+
.then(function (data) {
|
127
|
+
haystack = data;
|
128
|
+
ready = true;
|
129
|
+
|
130
|
+
var siteSearch = qs('#site-search');
|
131
|
+
var siteSearchQuery = qs('#site-search-query');
|
132
|
+
|
133
|
+
if (siteSearch == null || siteSearchQuery == null) {
|
134
|
+
throw new Error('Cannot find #site-search or #site-search-query');
|
135
|
+
}
|
136
|
+
|
137
|
+
siteSearch.addEventListener('submit', function (e) {
|
138
|
+
e.preventDefault();
|
139
|
+
debounceSearch();
|
140
|
+
return false;
|
141
|
+
});
|
142
|
+
|
143
|
+
siteSearchQuery.addEventListener('keyup', function (e) {
|
144
|
+
e.preventDefault();
|
145
|
+
if (!scrolled) {
|
146
|
+
scrolled = true;
|
147
|
+
this.scrollIntoView(true);
|
148
|
+
}
|
149
|
+
debounceSearch();
|
150
|
+
return false;
|
151
|
+
});
|
152
|
+
|
153
|
+
console.log('Search ready');
|
154
|
+
});
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fenton-jekyll-boilerplate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steve Fenton
|
@@ -70,6 +70,18 @@ files:
|
|
70
70
|
- assets/icons/favicon-16x16.png
|
71
71
|
- assets/icons/favicon-32x32.png
|
72
72
|
- assets/icons/favicon.ico
|
73
|
+
- assets/js/main.js
|
74
|
+
- assets/js/modules/animation.js
|
75
|
+
- assets/js/modules/click-blocks.js
|
76
|
+
- assets/js/modules/events.js
|
77
|
+
- assets/js/modules/focus.js
|
78
|
+
- assets/js/modules/nav-expand.js
|
79
|
+
- assets/js/modules/nav-mobile.js
|
80
|
+
- assets/js/modules/nav-sticky.js
|
81
|
+
- assets/js/modules/query.js
|
82
|
+
- assets/js/modules/resizing.js
|
83
|
+
- assets/js/modules/string.js
|
84
|
+
- assets/js/search.js
|
73
85
|
homepage: https://jekyll.stevefenton.co.uk/
|
74
86
|
licenses:
|
75
87
|
- Apache-2.0
|