From 8b08c5a5cccb5ad2a96a27f92f7a423dd18d770a Mon Sep 17 00:00:00 2001 From: Thomas Miceli Date: Mon, 3 Apr 2023 23:37:54 +0200 Subject: [PATCH] Convert Javascript to Typescript --- public/editor.js | 162 ------------------------------ public/editor.ts | 171 ++++++++++++++++++++++++++++++++ public/main.js | 136 ------------------------- public/main.ts | 140 ++++++++++++++++++++++++++ templates/base/base_header.html | 2 +- templates/pages/create.html | 2 +- templates/pages/edit.html | 2 +- tsconfig.json | 9 ++ vite.config.js | 2 +- 9 files changed, 324 insertions(+), 302 deletions(-) delete mode 100644 public/editor.js create mode 100644 public/editor.ts delete mode 100644 public/main.js create mode 100644 public/main.ts create mode 100644 tsconfig.json diff --git a/public/editor.js b/public/editor.js deleted file mode 100644 index 9cfe680..0000000 --- a/public/editor.js +++ /dev/null @@ -1,162 +0,0 @@ -import {EditorView, gutter, keymap, lineNumbers} from "@codemirror/view" -import {Compartment, EditorState, Facet, SelectionRange} from "@codemirror/state" -import {indentLess} from "@codemirror/commands"; - -document.addEventListener('DOMContentLoaded', () => { - EditorView.theme({}, {dark: true}) - - let editorsjs = [] - let editorsParentdom = document.getElementById('editors') - let allEditorsdom = document.querySelectorAll('#editors > .editor') - let firstEditordom = allEditorsdom[0] - - const txtFacet = Facet.define({ - combine(values) { - return values[0] - } - }) - let indentSize = new Compartment, wrapMode = new Compartment, indentType = new Compartment - - const newEditor = (dom, value = '') => { - let editor = new EditorView({ - doc: value, - parent: dom, - extensions: [ - lineNumbers(), gutter({class: "cm-mygutter"}), - keymap.of([{key: "Tab", run: customIndentMore, shift: indentLess}]), - indentSize.of(EditorState.tabSize.of(2)), - wrapMode.of([]), - indentType.of(txtFacet.of("space")), - ] - }) - - dom.querySelector('.editor-indent-type').onchange = (e) => { - let newTabType = e.target.value - setIndentType(editor, !['tab', 'space'].includes(newTabType) ? 'space' : newTabType) - } - - dom.querySelector('.editor-indent-size').onchange = (e) => { - let newTabSize = parseInt(e.target.value) - setIndentSize(editor, ![2, 4, 8].includes(newTabSize) ? 2 : newTabSize) - } - - dom.querySelector('.editor-wrap-mode').onchange = (e) => { - let newWrapMode = e.target.value - setLineWrapping(editor, newWrapMode === 'soft') - } - - dom.addEventListener("drop", (e) => { - e.preventDefault(); // prevent the browser from opening the dropped file - e.target.closest('.editor').querySelector('input.form-filename').value = e.dataTransfer.files[0].name - }); - - // remove editor on delete - let deleteBtns = dom.querySelector('button.delete-file') - if (deleteBtns !== null) { - deleteBtns.onclick = () => { - editorsjs.splice(editorsjs.indexOf(editor), 1); - dom.remove() - } - } - - editor.dom.addEventListener("input", function inputConfirmLeave() { - if (!editor.inView) return; // skip events outside the viewport - - editor.dom.removeEventListener("input", inputConfirmLeave); - window.onbeforeunload = () => { - return 'Are you sure you want to quit?'; - } - }); - - return editor; - } - - function getIndentation(state) { - if (indentType.get(state).value === 'tab') { - return '\t'; - } - return ' '.repeat(indentSize.get(state).value); - } - - function customIndentMore({state, dispatch}) { - let indentation = getIndentation(state) - dispatch({ - ...state.update(changeBySelectedLine(state, (line, changes) => { - changes.push({from: state.selection.ranges[0].from, insert: indentation}) - })), selection: { - anchor: state.selection.ranges[0].from + indentation.length, - head: state.selection.ranges[0].from + indentation.length, - } - }) - return true - } - - function changeBySelectedLine(state, f) { - let atLine = -1 - return state.changeByRange(range => { - let changes = [] - for (let line = state.doc.lineAt(range.from); ;) { - if (line.number > atLine) { - f(line, changes) - atLine = line.number - } - if (range.to <= line.to) break - line = state.doc.lineAt(line.number + 1) - } - let changeSet = state.changes(changes) - return { - changes, - range: new SelectionRange(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1)) - } - }) - } - - function setIndentType(view, type) { - view.dispatch({effects: indentType.reconfigure(txtFacet.of(type))}) - } - - function setIndentSize(view, size) { - view.dispatch({effects: indentSize.reconfigure(EditorState.tabSize.of(size))}) - } - - function setLineWrapping(view, enable) { - if (enable) { - view.dispatch({effects: wrapMode.reconfigure(EditorView.lineWrapping)}) - } else { - view.dispatch({effects: wrapMode.reconfigure([])}) - } - } - - let arr = [...allEditorsdom] - arr.forEach(el => { - // in case we edit the gist contents - let currEditor = newEditor(el, el.querySelector('.form-filecontent').value) - editorsjs.push(currEditor) - }) - - document.getElementById('add-file').onclick = () => { - let newEditorDom = firstEditordom.cloneNode(true) - - // reset the filename of the new cloned element - newEditorDom.querySelector('input[name="name"]').value = "" - - // removing the previous codemirror editor - let newEditorDomCM = newEditorDom.querySelector('.cm-editor') - newEditorDomCM.remove() - - // creating the new codemirror editor and append it in the editor div - editorsjs.push(newEditor(newEditorDom)) - editorsParentdom.append(newEditorDom) - } - - document.querySelector('form#create').onsubmit = () => { - let j = 0 - document.querySelectorAll('.form-filecontent').forEach((e) => { - e.value = encodeURIComponent(editorsjs[j++].state.doc.toString()) - }) - } - - document.onsubmit = () => { - window.onbeforeunload = null; - } -}) \ No newline at end of file diff --git a/public/editor.ts b/public/editor.ts new file mode 100644 index 0000000..d072f95 --- /dev/null +++ b/public/editor.ts @@ -0,0 +1,171 @@ +import {EditorView, gutter, keymap, lineNumbers} from "@codemirror/view"; +import {Compartment, EditorState, Facet, Line, SelectionRange} from "@codemirror/state"; +import {indentLess} from "@codemirror/commands"; + +document.addEventListener("DOMContentLoaded", () => { + EditorView.theme({}, {dark: true}); + + let editorsjs: EditorView[] = []; + let editorsParentdom = document.getElementById("editors")!; + let allEditorsdom = document.querySelectorAll("#editors > .editor"); + let firstEditordom = allEditorsdom[0]; + + const txtFacet = Facet.define({ + combine(values) { + return values; + }, + }); + + let indentSize = new Compartment(), + wrapMode = new Compartment(), + indentType = new Compartment(); + + const newEditor = (dom: HTMLElement, value: string = ""): EditorView => { + let editor = new EditorView({ + doc: value, + parent: dom, + extensions: [ + lineNumbers(), + gutter({class: "cm-mygutter"}), + keymap.of([{key: "Tab", run: customIndentMore, shift: indentLess}]), + indentSize.of(EditorState.tabSize.of(2)), + wrapMode.of([]), + indentType.of(txtFacet.of("space")), + ], + }); + + dom.querySelector(".editor-indent-type")!.onchange = (e) => { + let newTabType = (e.target as HTMLInputElement).value; + setIndentType(editor, !["tab", "space"].includes(newTabType) ? "space" : newTabType); + }; + + dom.querySelector(".editor-indent-size")!.onchange = (e) => { + let newTabSize = parseInt((e.target as HTMLInputElement).value); + setIndentSize(editor, ![2, 4, 8].includes(newTabSize) ? 2 : newTabSize); + }; + + dom.querySelector(".editor-wrap-mode")!.onchange = (e) => { + let newWrapMode = (e.target as HTMLInputElement).value; + setLineWrapping(editor, newWrapMode === "soft"); + }; + + dom.addEventListener("drop", (e) => { + e.preventDefault(); // prevent the browser from opening the dropped file + (e.target as HTMLInputElement) + .closest(".editor") + .querySelector("input.form-filename")!.value = + e.dataTransfer.files[0].name; + }); + + // remove editor on delete + let deleteBtns = dom.querySelector("button.delete-file"); + if (deleteBtns !== null) { + deleteBtns.onclick = () => { + editorsjs.splice(editorsjs.indexOf(editor), 1); + dom.remove(); + }; + } + + editor.dom.addEventListener("input", function inputConfirmLeave() { + if (!editor.inView) return; // skip events outside the viewport + + editor.dom.removeEventListener("input", inputConfirmLeave); + window.onbeforeunload = () => { + return "Are you sure you want to quit?"; + }; + }); + + return editor; + }; + + function getIndentation(state: EditorState): string { + // @ts-ignore + if (indentType.get(state).value === "tab") { + return "\t"; + } + // @ts-ignore + return " ".repeat(indentSize.get(state).value); + } + + function customIndentMore({state, dispatch,}: { state: EditorState; dispatch: (value: any) => void; }): boolean { + let indentation = getIndentation(state); + dispatch({ + ...state.update(changeBySelectedLine(state, (line, changes) => { + changes.push({from: state.selection.ranges[0].from, insert: indentation,}); + })), + selection: { + anchor: state.selection.ranges[0].from + indentation.length, + head: state.selection.ranges[0].from + indentation.length, + }, + }); + return true; + } + + function changeBySelectedLine(state: EditorState, f: (line: Line, changes: any[]) => void): any { + let atLine = -1; + return state.changeByRange((range) => { + let changes: any[] = []; + for (let line = state.doc.lineAt(range.from); ;) { + if (line.number > atLine) { + f(line, changes); + atLine = line.number; + } + if (range.to <= line.to) break; + line = state.doc.lineAt(line.number + 1); + } + let changeSet = state.changes(changes); + return { + changes, + // @ts-ignore + range: new SelectionRange(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1)), + }; + }); + } + + function setIndentType(view: EditorView, type: string): void { + view.dispatch({effects: indentType.reconfigure(txtFacet.of(type))}); + } + + function setIndentSize(view: EditorView, size: number): void { + view.dispatch({effects: indentSize.reconfigure(EditorState.tabSize.of(size))}); + } + + function setLineWrapping(view: EditorView, enable: boolean): void { + view.dispatch({ + effects: wrapMode.reconfigure(enable ? EditorView.lineWrapping : []), + }); + } + + let arr = Array.from(allEditorsdom); + arr.forEach((el: HTMLElement) => { + // in case we edit the gist contents + let currEditor = newEditor(el, el.querySelector(".form-filecontent")!.value); + editorsjs.push(currEditor); + }); + + document.getElementById("add-file")!.onclick = () => { + let newEditorDom = firstEditordom.cloneNode(true) as HTMLElement; + + // reset the filename of the new cloned element + newEditorDom.querySelector('input[name="name"]')!.value = ""; + + // removing the previous codemirror editor + let newEditorDomCM = newEditorDom.querySelector(".cm-editor"); + newEditorDomCM!.remove(); + + // creating the new codemirror editor and append it in the editor div + editorsjs.push(newEditor(newEditorDom)); + editorsParentdom.append(newEditorDom); + }; + + document.querySelector("form#create")!.onsubmit = () => { + let j = 0; + document.querySelectorAll(".form-filecontent").forEach((e) => { + e.value = encodeURIComponent(editorsjs[j++].state.doc.toString()); + }); + }; + + document.onsubmit = () => { + window.onbeforeunload = null; + }; +}); diff --git a/public/main.js b/public/main.js deleted file mode 100644 index 3ee9567..0000000 --- a/public/main.js +++ /dev/null @@ -1,136 +0,0 @@ -import './style.css' -import './markdown.css' -import './favicon.svg' -import 'highlight.js/styles/tokyo-night-dark.css' -import moment from 'moment' -import md from 'markdown-it' -import hljs from 'highlight.js' - -document.addEventListener('DOMContentLoaded', () => { - document.querySelectorAll('.moment-timestamp').forEach((e) => { - e.title = moment.unix(e.innerHTML).format('LLLL') - e.innerHTML = moment.unix(e.innerHTML).fromNow() - }) - - document.querySelectorAll('.moment-timestamp-date').forEach((e) => { - e.innerHTML = moment.unix(e.innerHTML).format('DD/MM/YYYY HH:mm') - }) - - let rev = document.querySelector('.revision-text') - if (rev) { - let fullRev = rev.innerHTML - let smallRev = fullRev.substring(0, 7) - rev.innerHTML = smallRev - - rev.onmouseover = () => { - rev.innerHTML = fullRev - } - rev.onmouseout = () => { - rev.innerHTML = smallRev - } - } - - document.querySelectorAll('.markdown').forEach((e) => { - e.innerHTML = md().render(e.innerHTML); - }) - - document.querySelectorAll('.table-code').forEach((el) => { - let ext = el.dataset.filename.split('.').pop() - - if (hljs.autoDetection(ext) && ext !== 'txt') { - el.querySelectorAll('td.line-code').forEach((ell) => { - ell.classList.add('language-'+ext) - hljs.highlightElement(ell); - }); - } - - // more efficient - el.addEventListener('click', event => { - if (event.target.matches('.line-num')) { - Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected')); - - event.target.nextSibling.classList.add('selected') - - let filename = el.dataset.filenameSlug - let line = event.target.textContent - let url = location.protocol + '//' + location.host + location.pathname - let hash = '#file-'+ filename + '-' +line - window.history.pushState(null, null, url+hash); - location.hash = hash; - } - }); - }); - - - let colorhash = () => { - Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected')); - let lineEl = document.querySelector(location.hash) - if (lineEl) { - lineEl.nextSibling.classList.add('selected') - } - } - - if (location.hash) { - colorhash() - } - window.onhashchange = colorhash - - document.getElementById('main-menu-button').onclick = () => { - document.getElementById('mobile-menu').classList.toggle('hidden') - } - - let tabs = document.getElementById('gist-tabs') - if (tabs) { - tabs.onchange = (e) => { - // navigate to the url in data-url - window.location.href = e.target.selectedOptions[0].dataset.url - } - } - - let gistmenutoggle = document.getElementById('gist-menu-toggle'); - if (gistmenutoggle) { - let gistmenucopy = document.getElementById('gist-menu-copy') - let gistmenubuttoncopy = document.getElementById('gist-menu-button-copy') - let gistmenuinput = document.getElementById('gist-menu-input') - let gistmenutitle = document.getElementById('gist-menu-title') - gistmenutitle.textContent = gistmenucopy.children[0].firstChild.textContent - gistmenuinput.value = gistmenucopy.children[0].dataset.link - - gistmenutoggle.onclick = () => { - gistmenucopy.classList.toggle('hidden') - } - - for (let item of gistmenucopy.children) { - item.onclick = () => { - gistmenutitle.textContent = item.firstChild.textContent - gistmenuinput.value = item.dataset.link - gistmenucopy.classList.toggle('hidden') - } - } - - gistmenubuttoncopy.onclick = () => { - let text = gistmenuinput.value - navigator.clipboard.writeText(text).then(null, function(err) { - console.error('Could not copy text: ', err); - }) - } - } - - let sortgist = document.getElementById('sort-gists-button') - if (sortgist) { - sortgist.onclick = () => { - document.getElementById('sort-gists-dropdown').classList.toggle('hidden') - } - } - - document.querySelectorAll('.copy-gist-btn').forEach((e) => { - e.onclick = () => { - navigator.clipboard.writeText(e.parentNode.querySelector('.gist-content').textContent).then(null, function (err) { - console.error('Could not copy text: ', err); - }) - } - }) - - - -}); \ No newline at end of file diff --git a/public/main.ts b/public/main.ts new file mode 100644 index 0000000..2056ae3 --- /dev/null +++ b/public/main.ts @@ -0,0 +1,140 @@ +import './style.css'; +import './markdown.css'; +import './favicon.svg'; +import 'highlight.js/styles/tokyo-night-dark.css'; +import moment from 'moment'; +import md from 'markdown-it'; +import hljs from 'highlight.js'; + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('.moment-timestamp').forEach((e: HTMLElement) => { + e.title = moment.unix(parseInt(e.innerHTML)).format('LLLL'); + e.innerHTML = moment.unix(parseInt(e.innerHTML)).fromNow(); + }); + + document.querySelectorAll('.moment-timestamp-date').forEach((e: HTMLElement) => { + e.innerHTML = moment.unix(parseInt(e.innerHTML)).format('DD/MM/YYYY HH:mm'); + }); + + const rev = document.querySelector('.revision-text'); + if (rev) { + const fullRev = rev.innerHTML; + const smallRev = fullRev.substring(0, 7); + rev.innerHTML = smallRev; + + rev.onmouseover = () => { + rev.innerHTML = fullRev; + }; + rev.onmouseout = () => { + rev.innerHTML = smallRev; + }; + } + + document.querySelectorAll('.markdown').forEach((e: HTMLElement) => { + e.innerHTML = md().render(e.innerHTML); + }); + + document.querySelectorAll('.table-code').forEach((el) => { + const ext = el.dataset.filename?.split('.').pop() || ''; + + if (hljs.autoDetection(ext) && ext !== 'txt') { + el.querySelectorAll('td.line-code').forEach((ell) => { + ell.classList.add('language-' + ext); + hljs.highlightElement(ell); + }); + } + + el.addEventListener('click', event => { + if (event.target && (event.target as HTMLElement).matches('.line-num')) { + Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected')); + + const nextSibling = (event.target as HTMLElement).nextSibling; + if (nextSibling instanceof HTMLElement) { + nextSibling.classList.add('selected'); + } + + + const filename = el.dataset.filenameSlug; + const line = (event.target as HTMLElement).textContent; + const url = location.protocol + '//' + location.host + location.pathname; + const hash = '#file-' + filename + '-' + line; + window.history.pushState(null, null, url + hash); + location.hash = hash; + } + }); + }); + + const colorhash = () => { + Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected')); + const lineEl = document.querySelector(location.hash); + if (lineEl) { + const nextSibling = lineEl.nextSibling; + if (nextSibling instanceof HTMLElement) { + nextSibling.classList.add('selected'); + } + } + }; + + if (location.hash) { + colorhash(); + } + window.onhashchange = colorhash; + + document.getElementById('main-menu-button')!.onclick = () => { + document.getElementById('mobile-menu')!.classList.toggle('hidden'); + }; + + const tabs = document.getElementById('gist-tabs'); + if (tabs) { + tabs.onchange = (e: Event) => { + const target = e.target as HTMLSelectElement; + window.location.href = target.selectedOptions[0].dataset.url || ''; + }; + } + + const gistmenutoggle = document.getElementById('gist-menu-toggle'); + if (gistmenutoggle) { + const gistmenucopy = document.getElementById('gist-menu-copy')!; + const gistmenubuttoncopy = document.getElementById('gist-menu-button-copy')!; + const gistmenuinput = document.getElementById('gist-menu-input') as HTMLInputElement; + const gistmenutitle = document.getElementById('gist-menu-title')!; + + gistmenutitle.textContent = gistmenucopy.children[0].firstChild!.textContent; + gistmenuinput.value = (gistmenucopy.children[0] as HTMLElement).dataset.link || ''; + + gistmenutoggle.onclick = () => { + gistmenucopy.classList.toggle('hidden'); + }; + + for (const item of Array.from(gistmenucopy.children)) { + (item as HTMLElement).onclick = () => { + gistmenutitle.textContent = item.firstChild!.textContent; + gistmenuinput.value = (item as HTMLElement).dataset.link || ''; + gistmenucopy.classList.toggle('hidden'); + }; + } + + gistmenubuttoncopy.onclick = () => { + const text = gistmenuinput.value; + navigator.clipboard.writeText(text).catch((err) => { + console.error('Could not copy text: ', err); + }); + }; + } + + + const sortgist = document.getElementById('sort-gists-button'); + if (sortgist) { + sortgist.onclick = () => { + document.getElementById('sort-gists-dropdown')!.classList.toggle('hidden'); + }; + } + + document.querySelectorAll('.copy-gist-btn').forEach((e: HTMLElement) => { + e.onclick = () => { + navigator.clipboard.writeText(e.parentNode!.querySelector('.gist-content')!.textContent || '').catch((err) => { + console.error('Could not copy text: ', err); + }); + }; + }); +}); diff --git a/templates/base/base_header.html b/templates/base/base_header.html index a3a2825..d370ef8 100644 --- a/templates/base/base_header.html +++ b/templates/base/base_header.html @@ -6,7 +6,7 @@ - + {{ if .htmlTitle }} {{ .htmlTitle }} - Opengist diff --git a/templates/pages/create.html b/templates/pages/create.html index a75a476..3f33799 100644 --- a/templates/pages/create.html +++ b/templates/pages/create.html @@ -65,6 +65,6 @@ - + {{ template "footer" .}} diff --git a/templates/pages/edit.html b/templates/pages/edit.html index fe74dd9..deffff2 100644 --- a/templates/pages/edit.html +++ b/templates/pages/edit.html @@ -102,6 +102,6 @@ - + {{ template "footer" .}} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4e969ec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "esModuleInterop": true + }, + "files": [ + "public/main.ts", + "public/editor.ts", + ], +} diff --git a/vite.config.js b/vite.config.js index 46d7c48..9680146 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,7 +9,7 @@ export default defineConfig({ assetsDir: 'assets', manifest: true, rollupOptions: { - input: ['./public/main.js', './public/editor.js'] + input: ['./public/main.ts', './public/editor.ts'] } } }) \ No newline at end of file