-
-
Notifications
You must be signed in to change notification settings - Fork 642
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Autocomplete loadout hashtags in loadout drawer
- Loading branch information
1 parent
92f2bae
commit b4be32a
Showing
11 changed files
with
257 additions
and
107 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { createCustomEvent, CursorOffset, Editor, SearchResult } from '@textcomplete/core'; | ||
import { calculateElementOffset, getLineHeightPx } from '@textcomplete/utils'; | ||
import getCaretCoordinates from 'app/utils/textarea-caret'; | ||
import { update } from 'undate'; | ||
|
||
/** | ||
* An Editor for `textcomplete` so that we can show the completion dropdown | ||
* on <input type="text" /> elements too. | ||
*/ | ||
export class InputTextEditor extends Editor { | ||
constructor(private readonly el: HTMLInputElement) { | ||
super(); | ||
this.startListening(); | ||
} | ||
|
||
destroy(): this { | ||
super.destroy(); | ||
this.stopListening(); | ||
return this; | ||
} | ||
|
||
/** | ||
* @implements {@link Editor#applySearchResult} | ||
*/ | ||
applySearchResult(searchResult: SearchResult): void { | ||
const beforeCursor = this.getBeforeCursor(); | ||
if (beforeCursor !== null) { | ||
const replace = searchResult.replace(beforeCursor, this.getAfterCursor()); | ||
this.el.focus(); // Clicking a dropdown item removes focus from the element. | ||
if (Array.isArray(replace)) { | ||
// Commit a type crime because the update code works with both input and textarea | ||
// even though the types don't advertise it | ||
update(this.el as any, replace[0], replace[1]); | ||
if (this.el) { | ||
this.el.dispatchEvent(createCustomEvent('input')); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @implements {@link Editor#getCursorOffset} | ||
*/ | ||
getCursorOffset(): CursorOffset { | ||
const elOffset = calculateElementOffset(this.el); | ||
const cursorPosition = getCaretCoordinates(this.el, this.el.selectionEnd ?? 0); | ||
const lineHeight = getLineHeightPx(this.el); | ||
const top = elOffset.top + lineHeight; | ||
const left = elOffset.left + cursorPosition.left; | ||
const clientTop = this.el.getBoundingClientRect().top; | ||
if (this.el.dir !== 'rtl') { | ||
return { top, left, lineHeight, clientTop }; | ||
} else { | ||
const right = document.documentElement ? document.documentElement.clientWidth - left : 0; | ||
return { top, right, lineHeight, clientTop }; | ||
} | ||
} | ||
|
||
/** | ||
* @implements {@link Editor#getBeforeCursor} | ||
*/ | ||
getBeforeCursor(): string | null { | ||
return this.el.selectionStart !== this.el.selectionEnd | ||
? null | ||
: this.el.value.substring(0, this.el.selectionEnd ?? undefined); | ||
} | ||
|
||
private getAfterCursor(): string { | ||
return this.el.value.substring(this.el.selectionEnd ?? 0); | ||
} | ||
|
||
private onInput = () => { | ||
this.emitChangeEvent(); | ||
}; | ||
|
||
private onKeydown = (e: KeyboardEvent) => { | ||
const code = this.getCode(e); | ||
let event; | ||
if (code === 'UP' || code === 'DOWN') { | ||
event = this.emitMoveEvent(code); | ||
} else if (code === 'ENTER') { | ||
event = this.emitEnterEvent(); | ||
} else if (code === 'ESC') { | ||
event = this.emitEscEvent(); | ||
} | ||
if (event?.defaultPrevented) { | ||
e.preventDefault(); | ||
} | ||
}; | ||
|
||
private startListening(): void { | ||
this.el.addEventListener('input', this.onInput); | ||
this.el.addEventListener('keydown', this.onKeydown); | ||
} | ||
|
||
private stopListening(): void { | ||
this.el.removeEventListener('input', this.onInput); | ||
this.el.removeEventListener('keydown', this.onKeydown); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
@use '../../variables.scss' as *; | ||
|
||
.dropdownMenu { | ||
color: black; | ||
background-color: white; | ||
border-radius: 4px; | ||
overflow: hidden; | ||
list-style: none; | ||
padding: 0; | ||
margin: 0; | ||
|
||
:global(.textcomplete-header), | ||
:global(.textcomplete-footer) { | ||
display: none; | ||
} | ||
li { | ||
padding: 4px 8px; | ||
&:nth-child(2) { | ||
border-top: none; | ||
} | ||
&:hover { | ||
background-color: #e8a534; | ||
} | ||
} | ||
:global(.active) { | ||
background-color: #e8a534; | ||
} | ||
a { | ||
color: black; | ||
font-size: 12px; | ||
&:hover { | ||
cursor: pointer; | ||
} | ||
text-decoration-line: none; | ||
} | ||
|
||
@include phone-portrait { | ||
a { | ||
font-size: 16px; | ||
} | ||
li { | ||
padding: 8px 10px; | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { StrategyProps, Textcomplete } from '@textcomplete/core'; | ||
import { TextareaEditor } from '@textcomplete/textarea'; | ||
import { getHashtagsFromNote } from 'app/inventory/note-hashtags'; | ||
import clsx from 'clsx'; | ||
import { useEffect } from 'react'; | ||
import { InputTextEditor } from './InputTextEditor'; | ||
|
||
import styles from './text-complete.m.scss'; | ||
|
||
function createTagsCompleter( | ||
textArea: React.RefObject<HTMLTextAreaElement | HTMLInputElement>, | ||
tags: string[] | ||
): StrategyProps { | ||
return { | ||
match: /#(\w*)$/, | ||
search: (term, callback) => { | ||
const termLower = term.toLowerCase(); | ||
// need to build this list from the element ref, because relying | ||
// on liveNotes state would re-instantiate Textcomplete every keystroke | ||
const existingTags = getHashtagsFromNote(textArea.current!.value).map((t) => t.toLowerCase()); | ||
const possibleTags: string[] = []; | ||
for (const t of tags) { | ||
const tagLower = t.toLowerCase(); | ||
// don't suggest duplicate tags | ||
if (existingTags.includes(tagLower)) { | ||
continue; | ||
} | ||
// favor startswith | ||
if (tagLower.startsWith('#' + termLower)) { | ||
possibleTags.unshift(t); | ||
// over full text search | ||
} else if (tagLower.includes(termLower)) { | ||
possibleTags.push(t); | ||
} | ||
} | ||
callback(possibleTags); | ||
}, | ||
replace: (key) => `${key} `, | ||
// to-do: for major tags, gonna use this to show what the notes icon will change to | ||
// template: (key) => `<img src="${url}"/> <small>:${key}:</small>`, | ||
}; | ||
} | ||
|
||
/** | ||
* Autocomplete a list of hashtags in this <textarea /> or <input type="text" />. | ||
* `tags` must have a stable object identity when using this hook (unless the set of tags changes). | ||
* selectors should ensure this, useMemo doesn't guarantee it per contract but works now. | ||
*/ | ||
export function useAutocomplete( | ||
textArea: React.RefObject<HTMLTextAreaElement | HTMLInputElement>, | ||
tags: string[] | ||
) { | ||
useEffect(() => { | ||
if (textArea.current) { | ||
const isInput = textArea.current instanceof HTMLInputElement; | ||
const editor = isInput | ||
? new InputTextEditor(textArea.current) | ||
: new TextareaEditor(textArea.current); | ||
const textcomplete = new Textcomplete(editor, [createTagsCompleter(textArea, tags)], { | ||
dropdown: { | ||
className: clsx(styles.dropdownMenu, 'textcomplete-dropdown'), | ||
}, | ||
}); | ||
return () => { | ||
textcomplete.destroy(); | ||
}; | ||
} | ||
}, [tags, textArea]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.