const constants = { configLocation: '.config.ini', defaultConfig: `\ defaultDocumentPath = "untitled.txt" ` } function main() { Util.syncAnnotation = $.Annotation.define() app.open() } function App() { const self = this this.open = () => { let nav = document.createElement('nav') this.path = new PathPrompt() nav.appendChild(this.path.dom) document.body.appendChild(nav) this.editor = new $.EditorView({ state: this.createState({ doc: localStorage[this.path.value] }), parent: document.body, dispatchTransactions: transactions => { if(this.views.length > 1) { for(let i = 1; i < this.views.length; i++) { transactions.forEach(transaction => Util.syncDispatch(transaction, this.editor, this.views[i].editor)) } } else { this.editor.update(transactions) } } }) this.views.push(this.editor) this.editor.focus() } this.saveDocument = () => { localStorage[this.path.value] = this.editor.state.doc.toString() } this.openDocument = () => { this.editor.setState(this.createDocumentState()) } this.createDocumentState = () => { return this.createState({ doc: localStorage[this.path.value] }) } this.refreshState = () => { let newState = this.createState({ doc: app.editor.state.doc.toString() }) this.editor.setState(newState) return newState } const openConfig = () => { let config = {} if(localStorage[constants.configLocation]) { config = parseConfig(localStorage[constants.configLocation]) } else { config = parseConfig(constants.defaultConfig) localStorage[constants.configLocation] = constants.defaultConfig } return config } const parseConfig = (text) => { let data = {} for(let line of text.split('\n')) { if(line.length == 0 || line[0] == ';') continue let delimiterIndex = line.indexOf('=') if(delimiterIndex == -1) continue let key = line.slice(0, delimiterIndex).trim() let value = line.slice(delimiterIndex + 1).trim() if(value[0] == '"') { let quoteEnd = value.lastIndexOf('"') value = value.slice(1, quoteEnd == 0 ? undefined : quoteEnd) } data[key] = value } return data } this.createState = ({ doc, extraKeys = [], hasHistory = true }) => { let keymap = [ ...$.closeBracketsKeymap, ...$.defaultKeymap, ...$.searchKeymap, ...$.foldKeymap, ...$.completionKeymap, ...$.lintKeymap, $.indentWithTab, { key: 'Ctrl-s', run(state, event) { event.preventDefault() self.saveDocument() } }, { key: 'Ctrl-o', run(state, event) { let mainSelection = self.editor.state.selection.main if(!mainSelection.empty) self.path.set(self.editor.state.sliceDoc(mainSelection.from, mainSelection.to).trim() ) self.openDocument() }, shift(state, event) { event.preventDefault() let mainSelection = self.editor.state.selection.main if(!mainSelection.empty) { let url = new URL(window.location) url.hash = self.editor.state.sliceDoc(mainSelection.from, mainSelection.to).trim() window.open(url.href) } } }, { key: 'Ctrl-r', run(state, event) { event.preventDefault() let mainSelection = self.editor.state.selection.main if(!mainSelection.empty) self.path.dom.value = self.editor.state.sliceDoc(mainSelection.from, mainSelection.to) self.path.dom.select() } }, { key: 'Ctrl-l', run(state, event) { self.editor.setState(self.createState({ doc: Object.keys(localStorage).join('\n') })) } }, { key: 'Ctrl-b', run(state, event) { app.views.push(new ChildWindow()) } }, ...extraKeys ] if(hasHistory) { keymap = keymap.concat($.historyKeymap) } let extensions = [ $.lineNumbers(), $.highlightActiveLineGutter(), $.highlightSpecialChars(), $.foldGutter(), $.drawSelection(), $.dropCursor(), $.EditorState.allowMultipleSelections.of(true), $.indentOnInput(), $.syntaxHighlighting($.defaultHighlightStyle, { fallback: true }), $.bracketMatching(), $.closeBrackets(), $.autocompletion(), $.rectangularSelection(), $.crosshairCursor(), $.highlightActiveLine(), $.highlightSelectionMatches(), $.keymap.of(keymap) ] if(hasHistory) { extensions.push($.history()) } let state = $.EditorState.create({ doc, extensions }) return state } this.config = openConfig() this.views = [] } function PathPrompt() { this.set = value => document.title = window.location.hash = this.value = this.dom.value = value this.dom = document.createElement('input') this.dom.addEventListener('focusout', (event) => { this.dom.value = this.value }) this.dom.addEventListener('keydown', (event) => { switch(event.key) { case 'Enter': event.preventDefault() this.set(this.dom.value) sessionStorage.lastDocument = this.value // app.openDocument() app.editor.focus() break case 'Escape': event.preventDefault() app.editor.focus() break } }) if(window.location.hash !== '') { this.set(decodeURI(window.location.hash.slice(1) ) ) } else { this.set(sessionStorage.lastDocument ?? app.config.defaultDocumentPath) } } function ChildWindow(index) { let self = this let state = app.createState({ doc: app.refreshState().doc, hasHistory: false, extraKeys: [ { key: 'Mod-z', run: () => $.undo(app.editor) }, { key: 'Mod-y', mac: "Mod-Shift-z", run: () => $.redo(app.editor) } ] }) this.index = app.views.length this.window = window.open('about:blank', '_blank') console.log(this) this.window.document.head.appendChild(document.getElementById('stylesheet').cloneNode(true)) this.editor = new $.EditorView({ state, parent: this.window.document.body, dispatch: transaction => { for(let i = 0; i < app.views.length; i++) { if(i == this.index) continue Util.syncDispatch(transaction, this.editor, app.editor) } } }) } const Util = {} { Util.syncDispatch = (transaction, source, target) => { source.update([ transaction ]) if(!transaction.changes.empty && !transaction.annotation(Util.syncAnnotation)) { let annotations = [ Util.syncAnnotation.of(true) ] let userEvent = transaction.annotation($.Transaction.userEvent) if(userEvent) annotations.push($.Transaction.userEvent.of(userEvent)) target.dispatch({ changes: transaction.changes, annotations }) } } } window.app = new App() window.addEventListener('load', main)