site: implement search functionality
This commit is contained in:
parent
3ae6b02672
commit
08be6b17f5
8 changed files with 257 additions and 2 deletions
|
@ -52,9 +52,11 @@
|
|||
}
|
||||
|
||||
dl,
|
||||
form,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
input,
|
||||
ol,
|
||||
p,
|
||||
ul {
|
||||
|
@ -72,6 +74,7 @@ body {
|
|||
}
|
||||
|
||||
dl,
|
||||
form,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
|
@ -127,6 +130,65 @@ img {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
font: inherit;
|
||||
border: var(--border-thin);
|
||||
padding: 0 0.1875rem;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type=submit]:active {
|
||||
background-color: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
form[name=search] {
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
padding-right: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
form[name=search] input[type=submit] {
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
form[name=search] input[name=tags] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#tags-suggestions {
|
||||
position: absolute;
|
||||
|
||||
background-color: var(--bg);
|
||||
width: 100%;
|
||||
border: var(--border-thin);
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 0 0.25rem 0.5rem -0.25rem;
|
||||
padding-left: 0;
|
||||
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.suggestion-hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#tags-suggestions li {
|
||||
padding-left: 0.1875rem;
|
||||
padding-right: 0.1875rem;
|
||||
}
|
||||
|
||||
body > header {
|
||||
border-bottom: var(--border-thin);
|
||||
font-size: 1.25rem;
|
||||
|
@ -168,10 +230,18 @@ main {
|
|||
padding-bottom: var(--gap);
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.layout-viewport {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
@ -263,16 +333,23 @@ main {
|
|||
line-height: 0;
|
||||
}
|
||||
|
||||
.accent {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
span.tag-kind-a,
|
||||
a.tag-kind-a:link,
|
||||
a.tag-kind-a:visited {
|
||||
color: var(--tag-kind-a);
|
||||
}
|
||||
|
||||
span.tag-kind-c,
|
||||
a.tag-kind-c:link,
|
||||
a.tag-kind-c:visited {
|
||||
color: var(--tag-kind-c);
|
||||
}
|
||||
|
||||
span.tag-kind-r,
|
||||
a.tag-kind-r:link,
|
||||
a.tag-kind-r:visited {
|
||||
color: var(--tag-kind-r);
|
||||
|
|
5
public/js/alpinejs@3.14.9/dist/cdn.min.js
vendored
Normal file
5
public/js/alpinejs@3.14.9/dist/cdn.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
97
public/js/app.js
Normal file
97
public/js/app.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
function getCurrentWord(s, cursor) {
|
||||
return s.substring(s.lastIndexOf(' ', cursor - 1) + 1, cursor);
|
||||
}
|
||||
|
||||
function mySplice(s, idx, del, n) {
|
||||
return s.substring(0, idx) + n + s.substring(idx + del);
|
||||
}
|
||||
|
||||
function suggestionPosition(el) {
|
||||
// el.parentNode is ul#tags-suggestion, which include a template. That's why
|
||||
// a -1 is needed here.
|
||||
const idx = [...el.parentNode.children].indexOf(el);
|
||||
return idx <= 0 ? null : idx - 1;
|
||||
}
|
||||
|
||||
function currentSuggestionElement(idx) {
|
||||
if (idx === null)
|
||||
return null;
|
||||
return document.querySelector("#tags-suggestions").children[idx + 1];
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('tagsSuggestions', () => ({
|
||||
suggestions: [],
|
||||
currentSuggestion: null,
|
||||
inputCursor: 0,
|
||||
query: '',
|
||||
|
||||
reset() {
|
||||
this.suggestions = [];
|
||||
this.currentSuggestion = null;
|
||||
this.inputCursor = 0;
|
||||
this.query = '';
|
||||
},
|
||||
|
||||
async fetchSuggestions(evt) {
|
||||
this.inputCursor = evt.target.selectionStart;
|
||||
this.query = getCurrentWord(evt.target.value, this.inputCursor);
|
||||
if (!this.query) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({q: this.query});
|
||||
const response = await fetch(`${endpoints.search}?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
this.suggestions = await response.json();
|
||||
},
|
||||
|
||||
completeSuggestion(evt) {
|
||||
if (this.currentSuggestion === null)
|
||||
return;
|
||||
|
||||
const value = this.$refs.tags.value;
|
||||
const pos = this.inputCursor - this.query.length;
|
||||
const tag = this.suggestions[this.currentSuggestion].display;
|
||||
|
||||
this.$refs.tags.value = mySplice(value, pos, this.query.length,
|
||||
tag + ' ');
|
||||
this.reset();
|
||||
this.$refs.tags.focus();
|
||||
|
||||
// Don't switch focus if we're completing after a Tab press.
|
||||
if (evt.type === "keydown" && evt.keyCode === 9)
|
||||
evt.preventDefault();
|
||||
},
|
||||
|
||||
selectCurrentSuggestion(evt) {
|
||||
this.currentSuggestion = suggestionPosition(evt.currentTarget);
|
||||
this.completeSuggestion(evt);
|
||||
},
|
||||
|
||||
setCurrentSuggestion(evt, amount = 0) {
|
||||
currentSuggestionElement(this.currentSuggestion)
|
||||
?.classList.remove("suggestion-hover");
|
||||
|
||||
if (evt === 'mouseenter')
|
||||
this.currentSuggestion = suggestionPosition(evt.currentTarget);
|
||||
else {
|
||||
const len = this.suggestions.length;
|
||||
let cur = this.currentSuggestion;
|
||||
|
||||
if (cur === null)
|
||||
cur = amount > 0 ? -1 : amount < 0 ? len : 0;
|
||||
this.currentSuggestion = (cur + amount + len) % len;
|
||||
}
|
||||
|
||||
currentSuggestionElement(this.currentSuggestion)
|
||||
?.classList.add("suggestion-hover");
|
||||
}
|
||||
}));
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue