mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
240 lines
7.1 KiB
TypeScript
240 lines
7.1 KiB
TypeScript
import type { DocAndNode } from '@spyglassmc/core'
|
|
import { fileUtil } from '@spyglassmc/core'
|
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
import { useLocale } from '../../contexts/index.js'
|
|
import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx'
|
|
import { useLocalStorage } from '../../hooks/index.js'
|
|
import { getSourceFormats, getSourceIndent, getSourceIndents, parseSource, stringifySource } from '../../services/index.js'
|
|
import { Store } from '../../Store.js'
|
|
import { message } from '../../Utils.js'
|
|
import { Btn, BtnMenu } from '../index.js'
|
|
|
|
interface Editor {
|
|
getValue(): string
|
|
setValue(value: string): void
|
|
configure(indent: string, format: string): void
|
|
select(): void
|
|
}
|
|
|
|
type SourcePanelProps = {
|
|
docAndNode: DocAndNode | undefined,
|
|
doCopy?: number,
|
|
doDownload?: number,
|
|
doImport?: number,
|
|
copySuccess: () => unknown,
|
|
onError: (message: string | Error) => unknown,
|
|
}
|
|
export function SourcePanel({ docAndNode, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) {
|
|
const { locale } = useLocale()
|
|
const { service } = useSpyglass()
|
|
const [indent, setIndent] = useState(Store.getIndent())
|
|
const [format, setFormat] = useState(Store.getFormat())
|
|
const [sort, setSort] = useLocalStorage('misode_output_sort', 'schema')
|
|
const [highlighting, setHighlighting] = useState(Store.getHighlighting())
|
|
const [braceLoaded, setBraceLoaded] = useState(false)
|
|
const download = useRef<HTMLAnchorElement>(null)
|
|
const retransform = useRef<Function>(() => {})
|
|
const onImport = useRef<() => Promise<void>>(async () => {})
|
|
|
|
const textarea = useRef<HTMLTextAreaElement>(null)
|
|
const editor = useRef<Editor>()
|
|
|
|
const getSerializedOutput = useCallback((text: string) => {
|
|
// TODO: implement sort
|
|
// if (sort === 'alphabetically') {
|
|
// data = sortData(data)
|
|
// }
|
|
return stringifySource(text, format, indent)
|
|
}, [indent, format, sort])
|
|
|
|
const text = useDocAndNode(docAndNode)?.doc.getText()
|
|
|
|
useEffect(() => {
|
|
retransform.current = () => {
|
|
if (!editor.current || text === undefined) {
|
|
return
|
|
}
|
|
try {
|
|
const output = getSerializedOutput(text)
|
|
editor.current.setValue(output)
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
e.message = `Error getting JSON output: ${e.message}`
|
|
onError(e)
|
|
} else {
|
|
onError(`Error getting JSON output: ${message(e)}`)
|
|
}
|
|
console.error(e)
|
|
editor.current.setValue('')
|
|
}
|
|
}
|
|
|
|
onImport.current = async () => {
|
|
if (!editor.current) return
|
|
const value = editor.current.getValue()
|
|
if (value.length === 0) return
|
|
if (!service || !docAndNode) return
|
|
try {
|
|
const text = await parseSource(value, format)
|
|
await service.writeFile(docAndNode.doc.uri, text)
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
e.message = `Error importing: ${e.message}`
|
|
onError(e)
|
|
} else {
|
|
onError(`Error importing: ${message(e)}`)
|
|
}
|
|
console.error(e)
|
|
}
|
|
}
|
|
}, [service, docAndNode, text, indent, format, sort, highlighting])
|
|
|
|
useEffect(() => {
|
|
if (highlighting) {
|
|
setBraceLoaded(false)
|
|
editor.current = {
|
|
getValue() { return ''},
|
|
setValue() {},
|
|
configure() {},
|
|
select() {},
|
|
}
|
|
import('brace').then(async (brace) => {
|
|
await Promise.all([
|
|
import('brace/mode/json'),
|
|
import('brace/mode/yaml'),
|
|
])
|
|
const braceEditor = brace.edit('editor')
|
|
braceEditor.setOptions({
|
|
fontSize: 14,
|
|
showFoldWidgets: false,
|
|
highlightSelectedWord: false,
|
|
scrollPastEnd: 0.5,
|
|
})
|
|
braceEditor.$blockScrolling = Infinity
|
|
braceEditor.on('blur', () => onImport.current())
|
|
braceEditor.getSession().setMode('ace/mode/json')
|
|
|
|
editor.current = {
|
|
getValue() {
|
|
return braceEditor.getSession().getValue()
|
|
},
|
|
setValue(value) {
|
|
braceEditor.getSession().setValue(value)
|
|
},
|
|
configure(indent, format) {
|
|
braceEditor.setOption('useSoftTabs', indent !== 'tabs')
|
|
braceEditor.setOption('tabSize', indent === 'tabs' ? 4 : getSourceIndent(indent))
|
|
braceEditor.getSession().setMode(`ace/mode/${format}`)
|
|
},
|
|
select() {
|
|
braceEditor.selectAll()
|
|
},
|
|
}
|
|
setBraceLoaded(true)
|
|
})
|
|
} else {
|
|
editor.current = {
|
|
getValue() {
|
|
if (!textarea.current) return ''
|
|
return textarea.current.value
|
|
},
|
|
setValue(value: string) {
|
|
if (!textarea.current) return
|
|
textarea.current.value = value
|
|
},
|
|
configure() {},
|
|
select() {},
|
|
}
|
|
}
|
|
}, [highlighting])
|
|
|
|
useEffect(() => {
|
|
if (retransform.current && text !== undefined) {
|
|
retransform.current()
|
|
}
|
|
}, [text])
|
|
|
|
useEffect(() => {
|
|
if (!editor.current || !retransform.current) return
|
|
if (!highlighting || braceLoaded) {
|
|
editor.current.configure(indent, format === 'snbt' ? 'yaml' : format)
|
|
retransform.current()
|
|
}
|
|
}, [indent, format, sort, highlighting, braceLoaded])
|
|
|
|
useEffect(() => {
|
|
if (doCopy && text !== undefined) {
|
|
navigator.clipboard.writeText(getSerializedOutput(text)).then(() => {
|
|
copySuccess()
|
|
})
|
|
}
|
|
}, [doCopy, text])
|
|
|
|
useEffect(() => {
|
|
if (doDownload && docAndNode && text !== undefined && download.current) {
|
|
const content = encodeURIComponent(getSerializedOutput(text))
|
|
download.current.setAttribute('href', `data:text/json;charset=utf-8,${content}`)
|
|
const fileName = fileUtil.basename(docAndNode.doc.uri)
|
|
download.current.setAttribute('download', fileName)
|
|
download.current.click()
|
|
}
|
|
}, [doDownload])
|
|
|
|
useEffect(() => {
|
|
if (doImport && editor.current) {
|
|
editor.current.setValue('')
|
|
editor.current.select()
|
|
}
|
|
}, [doImport])
|
|
|
|
const changeIndent = (value: string) => {
|
|
Store.setIndent(value)
|
|
setIndent(value)
|
|
}
|
|
|
|
const changeFormat = (value: string) => {
|
|
Store.setFormat(value)
|
|
setFormat(value)
|
|
}
|
|
|
|
const changeHighlighting = (value: boolean) => {
|
|
Store.setHighlighting(value)
|
|
setHighlighting(value)
|
|
}
|
|
|
|
const importFromClipboard = useCallback(async () => {
|
|
if (editor.current) {
|
|
const text = await navigator.clipboard.readText()
|
|
editor.current.setValue(text)
|
|
onImport.current()
|
|
}
|
|
}, [editor, onImport])
|
|
|
|
return <>
|
|
<div class="controls source-controls">
|
|
{window.matchMedia('(pointer: coarse)').matches && <>
|
|
<Btn icon="paste" onClick={importFromClipboard} />
|
|
</>}
|
|
<BtnMenu icon="gear" tooltip={locale('output_settings')}>
|
|
{getSourceIndents().map(key =>
|
|
<Btn label={locale(`indentation.${key}`)} active={indent === key}
|
|
onClick={() => changeIndent(key)}/>
|
|
)}
|
|
<hr />
|
|
{getSourceFormats().map(key =>
|
|
<Btn label={locale(`format.${key}`)} active={format === key}
|
|
onClick={() => changeFormat(key)} />)}
|
|
<hr />
|
|
<Btn icon={sort === 'alphabetically' ? 'square_fill' : 'square'} label={locale('sort_alphabetically')}
|
|
onClick={() => setSort(sort === 'alphabetically' ? 'schema' : 'alphabetically')} />
|
|
<Btn icon={highlighting ? 'square_fill' : 'square'} label={locale('highlighting')}
|
|
onClick={() => changeHighlighting(!highlighting)} />
|
|
</BtnMenu>
|
|
</div>
|
|
{highlighting
|
|
? <pre id="editor" class="source"></pre>
|
|
: <textarea ref={textarea} class="source" spellcheck={false} autocorrect="off" onBlur={onImport.current}></textarea>}
|
|
<a ref={download} style="display: none;"></a>
|
|
</>
|
|
}
|