Skip to content

Commit

Permalink
Better keyboard interaction (#44)
Browse files Browse the repository at this point in the history
* Use keyboard and mouse events to control highlight

* Escape to hide results

* Make the test page a lil better

* Bump version number
  • Loading branch information
jameslittle230 authored Jun 15, 2020
1 parent 187885e commit 6bdb91a
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 32 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "stork-search"
version = "0.7.0"
version = "0.7.1"
authors = ["James Little <[email protected]>"]
edition = "2018"
documentation = "https://stork-search.net/docs"
Expand Down
10 changes: 10 additions & 0 deletions js/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ export function setText(elem, text) {
elem.appendChild(textNode);
}
}

export function existsBeyondContainerBounds(elem, container) {
var elemBoundingBox = elem.getBoundingClientRect();
var containerBoundingBox = container.getBoundingClientRect();

return (
elemBoundingBox.bottom > containerBoundingBox.bottom ||
elemBoundingBox.top < containerBoundingBox.top
);
}
117 changes: 97 additions & 20 deletions js/entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Configuration, calculateOverriddenConfig } from "./config";
import { assert, htmlToElement } from "./util";
import { create, add, clear, setText } from "./dom";
import {
create,
add,
clear,
setText,
existsBeyondContainerBounds
} from "./dom";
import { generateListItem } from "./pencil";

interface Result {
Expand All @@ -16,6 +22,14 @@ interface ElementMap {
message?: Element;
}

interface RenderOptions {
shouldScrollTo: boolean;
}

const defaultRenderOptions: RenderOptions = {
shouldScrollTo: false
};

export class Entity {
name: string;
url: string;
Expand All @@ -26,6 +40,8 @@ export class Entity {
progress = 0;
hitCount = 0;
query = "";
resultsVisible = false;
hoverSelectEnabled = true;

// render options
scrollAnchorPoint: "start" | "end" = "end";
Expand Down Expand Up @@ -71,18 +87,80 @@ export class Entity {
return null;
}

changeHighlightedResult(delta: number): number {
setResultsVisible(val: boolean): void {
const prev = this.resultsVisible;
this.resultsVisible = val;

if (val !== prev) {
this.render();
}
}

changeHighlightedResult(options: {
by: number | null;
to: number | null;
}): number {
const previousValue = this.highlightedResult;

const intendedIdx = this.highlightedResult + delta;
const intendedIdx = (() => {
if (typeof options.to === "number") {
return options.to;
} else if (typeof options.by === "number") {
return this.highlightedResult + options.by;
} else {
return 0;
}
})();

options.to !== null
? options.to
: this.highlightedResult + (options.by || 0);

const resolvedIdx = Math.max(
0,
Math.min(this.results.length - 1, intendedIdx)
);
this.highlightedResult = resolvedIdx;

this.highlightedResult = resolvedIdx;
this.scrollAnchorPoint = previousValue < resolvedIdx ? "end" : "start";

let targetForScrollTo = null;

for (let i = 0; i < this.results.length; i++) {
const element = this.elements.list?.children[i];
if (!element) {
continue;
}

const highlightedClassName = "selected";

if (i == resolvedIdx) {
element.classList.add(highlightedClassName);
targetForScrollTo = element;
} else {
element.classList.remove(highlightedClassName);
}
}

// using options.by as a proxy for keyboard selection
if (typeof options.by === "number") {
this.hoverSelectEnabled = false;
if (targetForScrollTo) {
if (
existsBeyondContainerBounds(
targetForScrollTo as HTMLElement,
this.elements.list
)
) {
(targetForScrollTo as HTMLElement).scrollIntoView({
behavior: "smooth",
block: this.scrollAnchorPoint,
inline: "nearest"
});
}
}
}

return resolvedIdx;
}

Expand Down Expand Up @@ -121,7 +199,7 @@ export class Entity {
setText(this.elements.message, message);

// Render results
if (this.results?.length > 0) {
if (this.results?.length > 0 && this.resultsVisible) {
// Create list if it doesn't exist
if (!this.elements.list) {
this.elements.list = create("ul", {
Expand All @@ -131,9 +209,11 @@ export class Entity {
}

clear(this.elements.list);
this.elements.list?.addEventListener("mousemove", event => {
this.hoverSelectEnabled = true;
});

// Render each result
let targetForScrollTo: ChildNode | null = null;
for (let i = 0; i < this.results.length; i++) {
const result = this.results[i];
const generateOptions = {
Expand All @@ -146,30 +226,27 @@ export class Entity {
generateListItem(generateOptions)
);

if (this.elements.list && elementToInsert) {
const insertedElement = this.elements.list.appendChild(
if (elementToInsert) {
const insertedElement = this.elements.list?.appendChild(
elementToInsert
);
if (i === this.highlightedResult) {
targetForScrollTo = insertedElement;
}
}
}

if (targetForScrollTo) {
(targetForScrollTo as HTMLElement).scrollIntoView({
behavior: "smooth",
block: this.scrollAnchorPoint,
inline: "nearest"
});
insertedElement?.addEventListener("mousemove", event => {
if (this.hoverSelectEnabled) {
if (i !== this.highlightedResult) {
this.changeHighlightedResult({ by: null, to: i });
}
}
});
}
}
} else if (this.elements.list) {
this.elements.output.removeChild(this.elements.list);
delete this.elements.list;
}

// Remove output's contents if there's no query
if (!this.query || this.query.length === 0) {
if (!this.query || this.query.length === 0 || !this.resultsVisible) {
delete this.elements.message;
delete this.elements.list;
clear(this.elements.output);
Expand Down
19 changes: 13 additions & 6 deletions js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,42 +54,48 @@ function handleInputEvent(event) {
* (keypress event doesn't work here)
*/
function handleKeyDownEvent(event) {
console.log(event);
const LEFT = 37;
const UP = 38;
const RIGHT = 39;
const DOWN = 40;
const RETURN = 13;
const SPACE = 32;
const ESC = 27;

const name = event.target.getAttribute("data-stork");
const entity = entities[name];

if (![LEFT, UP, RIGHT, DOWN, RETURN, ESC].includes(event.keyCode)) {
console.log(entity.resultsVisible);
entity.setResultsVisible(true);
return;
}

const resultNodeArray = Array.from(
entity.elements.list ? entity.elements.list.childNodes : []
).filter(n => n.className == "stork-result");

switch (event.keyCode) {
case DOWN:
entity.changeHighlightedResult(+1);
entity.changeHighlightedResult({ by: +1 });
break;

case UP:
entity.changeHighlightedResult(-1);
entity.changeHighlightedResult({ by: -1 });
break;

case RETURN:
Array.from(resultNodeArray[entity.highlightedResult].childNodes)
.filter(n => n.href)[0] // get the `a` element
.click();

case ESC:
entity.setResultsVisible(false);
break;

default:
return;
}

console.log(entities[name]);
entities[name].render();
}

function performSearch(name) {
Expand All @@ -116,6 +122,7 @@ function performSearch(name) {

entities[name].results = results.results;
entities[name].hitCount = results.total_hit_count;
entities[name].highlightedResult = 0;

// Mutate the result URL, like we do when there's a url prefix or suffix
const urlPrefix = results.url_prefix || "";
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stork-search",
"version": "0.7.0",
"version": "0.7.1",
"description": "Impossibly fast web search, made for static sites.",
"main": "index.js",
"repository": {
Expand Down
1 change: 0 additions & 1 deletion test/static/basic.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
border-bottom: 1px solid hsla(0, 0%, 90%, 1);
}

.stork-result:hover,
.stork-result.selected {
background: rgb(151, 226, 245);
}
Expand Down
9 changes: 7 additions & 2 deletions test/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
.search-wrap {
max-width: 1200px;
display: flex;
justify-content: space-between;
flex-direction: column;
}

.stork-wrapper {
max-width: 550px;
width: 100%;
margin-bottom: 3rem;
}

.stork-output-visible {
z-index: 200;
}

.subtitle {
Expand Down

0 comments on commit 6bdb91a

Please sign in to comment.