import {EditorView, gutter, keymap, lineNumbers} from "@codemirror/view"; import {Compartment, EditorState, Facet, Line, SelectionRange} from "@codemirror/state"; import {defaultKeymap, 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}, ...defaultKeymap, ]), indentSize.of(EditorState.tabSize.of(2)), wrapMode.of([]), indentType.of(txtFacet.of("space")), ], }); let mdpreview = dom.querySelector(".md-preview") as HTMLElement; let formfilename = dom.querySelector(".form-filename"); // check if file ends with .md on pageload if (formfilename!.value.endsWith(".md")) { mdpreview!.classList.remove("hidden"); } else { mdpreview!.classList.add("hidden"); } // event if the filename ends with .md; trigger event formfilename!.onkeyup = (e) => { let filename = (e.target as HTMLInputElement).value; if (filename.endsWith(".md")) { mdpreview!.classList.remove("hidden"); } else { mdpreview!.classList.add("hidden"); } }; // @ts-ignore const baseUrl = window.opengist_base_url || ''; let previewShown = false; mdpreview.onclick = () => { previewShown = !previewShown; let divpreview = dom.querySelector("div.preview") as HTMLElement; let cmeditor = dom.querySelector(".cm-editor") as HTMLElement; if (!previewShown) { divpreview!.classList.add("hidden"); cmeditor!.classList.remove("hidden-important"); return; } else { const formData = new FormData(); formData.append('content', editor.state.doc.toString()); let csrf = document.querySelector('form#create input[name="_csrf"]').value fetch(`${baseUrl}/preview`, { method: 'POST', credentials: 'same-origin', body: formData, headers: { 'X-CSRF-Token': csrf } }).then(r => r.text()).then(r => { let divpreview = dom.querySelector("div.preview") as HTMLElement; divpreview!.innerHTML = r; divpreview!.classList.remove("hidden"); cmeditor!.classList.add("hidden-important"); }) } } 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(); checkForFirstDeleteButton(); }; } 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); }); checkForFirstDeleteButton(); 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); showDeleteButton(newEditorDom); }; document.querySelector("form#create")!.onsubmit = () => { let j = 0; document.querySelectorAll(".form-filecontent").forEach((e) => { e.value = encodeURIComponent(editorsjs[j++].state.doc.toString()); }); }; document.getElementById('gist-metadata-btn')!.onclick = (el) => { let metadata = document.getElementById('gist-metadata')!; metadata.classList.toggle('hidden'); let btn = el.target as HTMLButtonElement; if (btn.innerText.endsWith('▼')) { btn.innerText = btn.innerText.replace('▼', '▲'); } else { btn.innerText = btn.innerText.replace('▲', '▼'); } } function checkForFirstDeleteButton() { let deleteBtn = editorsParentdom.querySelector("button.delete-file")!; if (editorsjs.length === 1) { deleteBtn.classList.add("hidden"); deleteBtn.previousElementSibling.classList.remove("rounded-l-md"); deleteBtn.previousElementSibling.classList.add("rounded-md"); } else { deleteBtn.classList.remove("hidden"); deleteBtn.previousElementSibling.classList.add("rounded-l-md"); deleteBtn.previousElementSibling.classList.remove("rounded-md"); } } function showDeleteButton(editorDom: HTMLElement) { let deleteBtn = editorDom.querySelector("button.delete-file")!; deleteBtn.classList.remove("hidden"); deleteBtn.previousElementSibling.classList.add("rounded-l-md"); deleteBtn.previousElementSibling.classList.remove("rounded-md"); checkForFirstDeleteButton(); } document.onsubmit = () => { window.onbeforeunload = null; }; });