diff options
-rw-r--r-- | Finder.js | 202 | ||||
-rw-r--r-- | README.md | 16 | ||||
-rw-r--r-- | create.js | 158 | ||||
-rw-r--r-- | index.css | 69 | ||||
-rw-r--r-- | index.html | 12 | ||||
-rw-r--r-- | index.js | 24 |
6 files changed, 481 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 ?? "") + }) +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0544c29 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Translate + +This a simple text editor which run in the browser. + +## Installation + +This is a static website. Copy all the files to the webroot of your webserver. + +## Execution + +To debug this, you can setup a simple localhost server for development and testing. + +```bash +python -m http.server +``` + diff --git a/create.js b/create.js new file mode 100644 index 0000000..3782290 --- /dev/null +++ b/create.js @@ -0,0 +1,158 @@ +//@ts-check + +/** + * Create The menubar + * @returns {HTMLMenuElement} + */ +export function createMenu() { + const header = document.createElement('header') + document.body.append(header) + + const menu = document.createElement('menu') + header.append(menu) + menu.role = 'menubar' + return menu +} + + +/** + * @param {HTMLSelectElement} select + * @param {string} value + */ +function addSelectOption(select, value, text = value) { + const opt = document.createElement("option") + opt.textContent = text + opt.value = value + select.add(opt) +} + +/** + * @param {HTMLMenuElement} menu + * @returns {HTMLLIElement} + */ +export function createMenuItem(menu) { + const menuItem = document.createElement('li') + menu.append(menuItem) + menuItem.role = 'menuitem' + return menuItem +} + +export function createEditor() { + const editor = document.createElement('pre') + document.body.appendChild(editor) + editor.contentEditable = 'true' + editor.id = "editor" + return editor +} + +/** + * @param {HTMLMenuElement} menu + * @param {HTMLPreElement} editor + */ +export function createFontSize(menu, editor) { + const menuitem = createMenuItem(menu) + const fontSize = document.createElement('select') + menuitem.appendChild(fontSize) + Object.entries({ + ['xx-small']: 'Very Very Small', + ['x-small']: 'Very Very Small', + small: 'Small', + medium: 'Medium', + large: 'Large', + ['x-large']: 'Very Large', + ['xx-large']: 'Very Very Large', + ['xxx-large']: 'Very Very Very Large', + }).forEach(([key, value]) => { + addSelectOption(fontSize, key, value) + }) + fontSize.value = 'medium' + fontSize.addEventListener('change', async function () { + editor.style.fontSize = fontSize.value + }) +} + +/** + * @param {HTMLMenuElement} menu + */ +export function createPrinter(menu) { + const menuitem = createMenuItem(menu) + const print = document.createElement('button') + menuitem.appendChild(print) + print.textContent = 'Print' + print.addEventListener('click', async function () { + window.print() + }) +} + +/** + * @param {HTMLMenuElement} menu + * @param {HTMLPreElement} editor + * @param {HTMLInputElement} fileName + */ +export function createSaveFile(menu, editor, fileName) { + const menuitem = createMenuItem(menu) + const save = document.createElement('button') + menuitem.appendChild(save) + save.textContent = 'Save' + save.addEventListener('click', + async function () { + saveFile(editor.textContent ?? "", fileName.value) + } + ) +} + +/** + * @param {HTMLMenuElement} menu + * @param {HTMLPreElement} editor + * @param {HTMLInputElement} fileName + */ +export function createOpenFile(menu, editor, fileName) { + const menuitem = createMenuItem(menu) + const fileInput = document.createElement('input') + fileInput.type = 'file' + fileInput.id = 'fileInput' + const label = document.createElement('label') + label.textContent = 'Open' + label.htmlFor = fileInput.id + fileInput.addEventListener('change', async function () { + const files = this.files ?? new FileList() + const file = files[0] + editor.textContent = await file.text() + fileName.value = file.name + }) + menuitem.appendChild(label) + menuitem.appendChild(fileInput) +} + +/** + * @param {HTMLMenuElement} menu + */ +export async function createFileName(menu) { + const menuitem = createMenuItem(menu) + const fileName = document.createElement('input') + menuitem.append(fileName) + fileName.type = "text" + fileName.value = "untitled" + async function title() { + return document.title = `Text Editor - ${this.value}` + } + fileName.addEventListener("input", title) + await title.call(fileName) + return fileName +} + +/** + * @param {BlobPart} content + * @param {string} filename + */ +function saveFile(content, filename) { + const blob = new Blob([content], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) +} diff --git a/index.css b/index.css new file mode 100644 index 0000000..2d8cbc6 --- /dev/null +++ b/index.css @@ -0,0 +1,69 @@ +*{ + box-sizing: inherit; +} + +@media print{ + header { + display: none; + } +} + +html { + box-sizing: border-box; +} + +menu > li { + display: inline; + list-style: none; + padding: 0.5rem; +} + +@media screen{ + html { + color-scheme: light dark; + height: 90vh; + } + body { + display: grid; + grid: "head" fit-content(1rem) "main" auto; + height: 100%; + } + + header { + grid-area: head; + color-scheme: light; + background-color: rgb(117,117,117); + } + + #fileInput{ + height: 0; + width: 0; + padding: 0; + opacity: 0; + } + + label[for="fileInput"] { + text-align: center; + } + + button{ + color: inherit; + background: inherit; + font: inherit; + border: none; + } + + :is(button,label[for="fileInput"]):hover { + background-color: dimgray; + } + + + + #editor { + grid-area: main; + border: 1px solid; + height: 100%; + padding: 1rem; + overflow: scroll; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..0fb7f39 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>Text Editor</title> + <link rel="stylesheet" href="index.css"> + <script type=module src="index.js"></script> +</head> + +</html>
\ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..f894b12 --- /dev/null +++ b/index.js @@ -0,0 +1,24 @@ +//@ts-check + +import { + createEditor, createFileName, createFontSize, createMenu, + createOpenFile, createPrinter, createSaveFile +} from './create.js' +import createFinder from './Finder.js' + +/** + * The Main function + */ +async function main() { + const menu = createMenu() + const editor = createEditor() + const fileName = await createFileName(menu) + createOpenFile(menu, editor, fileName) + createSaveFile(menu, editor, fileName) + createPrinter(menu) + createFontSize(menu, editor) + createFinder(menu, editor) +} + +window.addEventListener("load", main) + |