diff options
Diffstat (limited to 'Finder.js')
-rw-r--r-- | Finder.js | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/Finder.js b/Finder.js new file mode 100644 index 0000000..b330dae --- /dev/null +++ b/Finder.js @@ -0,0 +1,202 @@ +import { createMenuItem } from './create.js' + +class Find { + /** + * @type {HTMLSpanElement[]} + */ + highlightSpans + /** + * @type {Number|undefined} + */ + currentIndex + /** + * @param {Node} editor + */ + constructor(editor) { + this.editor = editor + editor.addEventListener('input', () => this.findAll()) + this.search = '' + } + + get search() { + return this._search + } + + set search(newSearch) { + if (this._search === newSearch) { + return + } + this._search = newSearch + this.findAll() + } + + get currentHighlight() { + if (this.currentIndex !== undefined) { + return this.highlightSpans[this.currentIndex] + } + } + + /** + * @param {Range} range + */ + highlight(range) { + const highlight = document.createElement('span') + highlight.style.backgroundColor = 'green' + range.surroundContents(highlight) + this.highlightSpans.push(highlight) + return highlight + } + + /** + * + * @param {boolean} preserveIndex + */ + clear(preserveIndex = false) { + this.highlightSpans?.forEach(element => { + element.parentNode?.replaceChild(element.firstChild ?? document.createTextNode(""), element) + }) + this.editor.normalize() + this.highlightSpans = [] + + if (!preserveIndex) { + this.currentIndex = undefined + } + } + + /** + * + * @param {boolean} preserveIndex + */ + findAll(preserveIndex = false) { + this.clear(preserveIndex) + + if (this.search === "") { + return + } + + var textNode = this.editor.firstChild + var content = textNode?.textContent ?? "" + var index = content.indexOf(this.search) + + while (textNode !== null && index !== -1) { + const range = document.createRange() + + range.setStart(textNode, index) + range.setEnd(textNode, index + this.search.length) + var highlight = this.highlight(range) + + textNode = highlight.nextSibling + content = textNode?.textContent ?? "" + index = content.indexOf(this.search) + } + } + + /** + * + */ + selectCurrent() { + const highlight = this.currentHighlight + if (highlight !== undefined) { + const range = document.createRange() + range.selectNode(highlight) + + const selection = window.getSelection() + selection?.removeAllRanges() + selection?.addRange(range) + + highlight.scrollIntoView({ behavior: 'smooth' }) + } + } + /** + * + */ + findNext() { + this.currentIndex = this.normalizeIndex((this.currentIndex ?? -1) + 1) + this.selectCurrent() + } + + /** + * + */ + findPrev() { + this.currentIndex = this.normalizeIndex((this.currentIndex ?? 0) - 1) + this.selectCurrent() + } + + /** + * @param {number} index + */ + normalizeIndex(index) { + return ((index % this.highlightSpans.length) + this.highlightSpans.length) % this.highlightSpans.length + } + + /** + * + * @param {string} text + */ + replace(text = "") { + const highlight = this.currentHighlight + if (highlight !== undefined) { + this.selectCurrent() + highlight.textContent = text + this.findAll(true) + } + } + + /** + * + * @param {string} text + */ + replaceAll(text = "") { + this.highlightSpans.forEach(element => { + element.textContent = text + }) + this.findAll(true) + } +} + +/** + * @param {HTMLMenuElement} menu + * @param {Node} editor + */ +export default function createFinder(menu, editor) { + const menuitem = createMenuItem(menu) + + const find = document.createElement('input') + find.placeholder = "Find" + find.type = "search" + const findPrev = document.createElement('button') + findPrev.textContent = "\u2191" + const findNext = document.createElement('button') + findNext.textContent = "\u2193" + const replace = document.createElement('input') + replace.placeholder = "Replace" + replace.type = "search" + const replaceAll = document.createElement('button') + replaceAll.textContent = "Replace All" + const replaceNext = document.createElement('button') + replaceNext.textContent = "Replace" + menuitem.append(find, findPrev, findNext, replace, replaceNext, replaceAll) + + const finder = new Find(editor) + + find.addEventListener("input", async function () { + finder.search = this.value + }) + + findPrev.addEventListener('click', async function () { + finder.findPrev() + }) + + findNext.addEventListener('click', async function () { + finder.findNext() + }) + + replaceNext.addEventListener('click', async function () { + finder.replace(replace.value ?? "") + }) + + replaceAll.addEventListener('click', async function () { + finder.replaceAll(replace.value ?? "") + }) +} |