aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Finder.js202
-rw-r--r--README.md16
-rw-r--r--create.js158
-rw-r--r--index.css69
-rw-r--r--index.html12
-rw-r--r--index.js24
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)
+