23 changed files with 527 additions and 151 deletions
@ -0,0 +1,66 @@ |
|||
import { inject } from "inversify" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
import { broadcast } from "utils/main" |
|||
|
|||
class TabsCommand { |
|||
constructor( |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
) { |
|||
this.listenerTabActive = this.listenerTabActive.bind(this) |
|||
this._Tabs.events.on("update", this.listenerTabActive) |
|||
} |
|||
|
|||
bindElement(rect) { |
|||
this._Tabs.updateRect(rect) |
|||
} |
|||
|
|||
reload() { |
|||
this._WindowManager.getMainWindow()?.reload() |
|||
} |
|||
|
|||
sync() { |
|||
this.listenerTabActive() |
|||
if (!this.getAllTabs().length) { |
|||
this.add("about:blank") |
|||
} |
|||
} |
|||
|
|||
listenerTabActive() { |
|||
broadcast("TabsCommand.update", this.getAllTabs()) |
|||
} |
|||
|
|||
add(url) { |
|||
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!) |
|||
} |
|||
|
|||
nagivate(index: number, url: string) { |
|||
this._Tabs.navigate(+index, url) |
|||
} |
|||
|
|||
setActive(index) { |
|||
this._Tabs.changeActive(index) |
|||
} |
|||
|
|||
closeTab(e) { |
|||
this._Tabs.remove(e.body.active) |
|||
} |
|||
|
|||
closeAll() { |
|||
this._Tabs.closeAll() |
|||
} |
|||
|
|||
getAllTabs() { |
|||
return this._Tabs._tabs.map(v => ({ |
|||
url: v.url, |
|||
showUrl: v.showUrl, |
|||
title: v.title, |
|||
favicons: v.favicons, |
|||
isActive: v.isActive, |
|||
})) |
|||
} |
|||
} |
|||
|
|||
export { TabsCommand } |
|||
export default TabsCommand |
|||
@ -0,0 +1,10 @@ |
|||
<template> |
|||
<div class="w-[calc((100%-4*20px)/5)] <lg:w-[calc((100%-2*20px)/3)]" shadow> |
|||
<div p-2 text-lg font-bold>MarkdownUtils</div> |
|||
<div p-2 pt-0 text-sm>这是一个导航站</div> |
|||
<div flex gap="10px" px-4 py-2 tex border-t="1px solid #E5E5E5"> |
|||
<div cursor="pointer" text-sm leading-1 py-2 px-3 border="1px solid #E5E5E5" rounded @click="$router.push('browser')">查看</div> |
|||
<div cursor="pointer" text-sm leading-1 py-2 px-3 border="1px solid #E5E5E5" rounded>访问</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,105 @@ |
|||
<script lang="ts" setup> |
|||
import { useMonacoEditor, IOptions } from "./hook" |
|||
|
|||
const props = withDefaults( |
|||
defineProps<{ |
|||
readonly?: boolean |
|||
modelValue?: string |
|||
filename?: string |
|||
placeholder?: string |
|||
modelOptions?: IOptions["modelOptions"] |
|||
editorOptions?: IOptions["editorOptions"] |
|||
}>(), |
|||
{ |
|||
readonly: false, |
|||
modelValue: "", |
|||
filename: "", |
|||
}, |
|||
) |
|||
|
|||
const emit = defineEmits<{ |
|||
(e: "update:modelValue", code: string): void |
|||
(e: "change", code: string): void |
|||
(e: "cursor:position", position: [number, number]): void |
|||
}>() |
|||
|
|||
const editorRef = ref<HTMLDivElement>() |
|||
const { updateOption, setValue } = useMonacoEditor(editorRef, { |
|||
placeholder: "请输入一些文本测试", |
|||
content: props.modelValue, |
|||
filename: props.filename, |
|||
modelOptions: props.modelOptions, |
|||
editorOptions: props.editorOptions, |
|||
onCursorChange(e) { |
|||
emit("cursor:position", [e.position.lineNumber, e.position.column]) |
|||
}, |
|||
onDidChangeContent(code) { |
|||
emit("update:modelValue", code) |
|||
emit("change", code) |
|||
}, |
|||
}) |
|||
watch( |
|||
() => props.modelValue, |
|||
() => { |
|||
setValue(props.modelValue) |
|||
}, |
|||
) |
|||
watch( |
|||
() => props.filename, |
|||
() => { |
|||
updateOption({ |
|||
filename: props.filename, |
|||
}) |
|||
}, |
|||
) |
|||
watch( |
|||
() => props.editorOptions, |
|||
() => { |
|||
updateOption({ |
|||
editorOptions: props.editorOptions, |
|||
}) |
|||
}, |
|||
{ deep: true }, |
|||
) |
|||
watch( |
|||
() => props.modelOptions, |
|||
() => { |
|||
updateOption({ |
|||
modelOptions: props.modelOptions, |
|||
}) |
|||
}, |
|||
{ deep: true }, |
|||
) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="monaco-wrapper"> |
|||
<div ref="editorRef" class="monaco-editor"></div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="scss" scoped> |
|||
.monaco-wrapper { |
|||
height: 100%; |
|||
position: relative; |
|||
|
|||
.monaco-editor { |
|||
height: 100%; |
|||
} |
|||
|
|||
.monaco-bg { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
pointer-events: none; |
|||
opacity: 0.1; |
|||
overflow: hidden; |
|||
|
|||
.monaco-logo { |
|||
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,33 +1,209 @@ |
|||
import { monaco } from "./monaco" |
|||
import { PlaceholderContentWidget } from "./PlaceholderContentWidget" |
|||
import { judgeFile } from "./utils" |
|||
import type { Ref } from "vue" |
|||
|
|||
const defaultOptions: monaco.editor.IStandaloneEditorConstructionOptions = { |
|||
fontSize: 14, |
|||
readOnly: false, |
|||
theme: "vs-light", |
|||
fontFamily: "Cascadia Mono, Consolas, 'Courier New', monospace", |
|||
lineHeight: 22, |
|||
scrollBeyondLastLine: false, |
|||
automaticLayout: true, |
|||
minimap: { |
|||
enabled: false, |
|||
function useResizeObserver(el: HTMLDivElement, callback: ResizeObserverCallback) { |
|||
const isSupported = window && "ResizeObserver" in window |
|||
let observer: ResizeObserver | undefined |
|||
const cleanup = () => { |
|||
if (observer) { |
|||
observer.disconnect() |
|||
observer = undefined |
|||
} |
|||
} |
|||
const stopWatch = watch( |
|||
() => el, |
|||
el => { |
|||
cleanup() |
|||
if (isSupported && window && el) { |
|||
observer = new ResizeObserver(callback) |
|||
observer!.observe(el, {}) |
|||
} |
|||
}, |
|||
{ immediate: true }, |
|||
) |
|||
const stop = () => { |
|||
cleanup() |
|||
stopWatch() |
|||
} |
|||
function tryOnScopeDispose(fn: () => void) { |
|||
if (getCurrentScope()) { |
|||
onScopeDispose(fn) |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
tryOnScopeDispose(() => { |
|||
stop() |
|||
}) |
|||
} |
|||
|
|||
export interface IOptions { |
|||
placeholder: string |
|||
filename: string |
|||
content: string |
|||
editorOptions: monaco.editor.IEditorOptions & monaco.editor.IGlobalEditorOptions |
|||
modelOptions: monaco.editor.ITextModelUpdateOptions |
|||
onCursorChange?: (e: monaco.editor.ICursorPositionChangedEvent) => void |
|||
onDidChangeContent?: (str: string) => void |
|||
} |
|||
|
|||
const defaultOptions: IOptions = { |
|||
placeholder: "请输入文本", |
|||
filename: "temp", |
|||
content: "", |
|||
editorOptions: { |
|||
fontSize: 14, |
|||
readOnly: false, |
|||
theme: "vs-light", |
|||
fontFamily: "Cascadia Mono, Consolas, 'Courier New', monospace", |
|||
scrollBeyondLastLine: false, |
|||
lineHeight: 22, |
|||
automaticLayout: true, |
|||
minimap: { |
|||
enabled: false, |
|||
}, |
|||
}, |
|||
modelOptions: {}, |
|||
} |
|||
|
|||
function getOptions(opt = {}) { |
|||
return { |
|||
...defaultOptions, |
|||
...opt, |
|||
const assign = (curOpt, opt, parenyKey: string[] = [], config = { arrayExtend: "concat" }) => { |
|||
for (const key in opt) { |
|||
if (opt[key] !== undefined) { |
|||
if (typeof opt[key] === "function" && curOpt[key] !== undefined && typeof curOpt[key] !== "function") { |
|||
opt[key] = opt[key](curOpt[key]) |
|||
} |
|||
if (typeof opt[key] === "object" && !Array.isArray(opt[key]) && !Array.isArray(curOpt[key])) { |
|||
parenyKey.push(key) |
|||
assign(curOpt[key], opt[key], parenyKey, config) |
|||
} else if (typeof opt[key] === "object" && Array.isArray(opt[key]) && Array.isArray(curOpt[key])) { |
|||
if (config.arrayExtend === "concat") { |
|||
curOpt[key] = curOpt[key].concat(opt[key]) |
|||
} else { |
|||
curOpt[key] = opt[key] |
|||
} |
|||
} else if (curOpt[key] !== undefined && typeof curOpt[key] !== typeof opt[key]) { |
|||
throw new Error(`Type of ${parenyKey.join(",") + "." + key} is not match`) |
|||
} else { |
|||
curOpt[key] = opt[key] |
|||
} |
|||
} |
|||
} |
|||
return curOpt |
|||
} |
|||
|
|||
export function useMonacoEditor(editor: HTMLDivElement) { |
|||
function getOptions(opt = {}, config = { arrayExtend: "concat" }): IOptions { |
|||
const curOptions = structuredClone(defaultOptions) |
|||
assign(curOptions, opt, [], config) |
|||
return curOptions |
|||
} |
|||
|
|||
export function useMonacoEditor(editorElement: Ref<HTMLDivElement | undefined>, opts: Partial<IOptions>) { |
|||
let curOption = getOptions(opts) as IOptions |
|||
let editor: monaco.editor.IStandaloneCodeEditor | null = null |
|||
let placeholderWidget: PlaceholderContentWidget | null = null |
|||
|
|||
const updateOption = (opts: Partial<IOptions>) => { |
|||
if (!editor) return |
|||
curOption = assign(curOption, opts) |
|||
if (Object.hasOwn(opts, "placeholder")) { |
|||
if (placeholderWidget) { |
|||
placeholderWidget.dispose() |
|||
placeholderWidget = null |
|||
} |
|||
if (opts.placeholder) { |
|||
placeholderWidget = new PlaceholderContentWidget(opts.placeholder, editor) |
|||
} |
|||
} |
|||
if (Object.hasOwn(opts, "modelOptions") && opts.modelOptions) { |
|||
const model = editor.getModel() |
|||
model?.updateOptions(opts.modelOptions) |
|||
} |
|||
if (Object.hasOwn(opts, "editorOptions") && opts.editorOptions) { |
|||
editor.updateOptions(opts.editorOptions) |
|||
} |
|||
if (Object.hasOwn(opts, "filename")) { |
|||
updateModel(curOption.filename, curOption.content) |
|||
} |
|||
if (Object.hasOwn(opts, "content")) { |
|||
console.log("无法通过updateOption修改content") |
|||
} |
|||
} |
|||
|
|||
let isInnerChange = "waitting" // waitting, out, in
|
|||
const setValue = (content: string) => { |
|||
if (isInnerChange === "waitting") { |
|||
isInnerChange = "out" |
|||
} |
|||
if (editor && isInnerChange === "out") { |
|||
editor.setValue(content) |
|||
} else { |
|||
isInnerChange = "waitting" |
|||
} |
|||
} |
|||
function updateModel(name: string, content: string) { |
|||
if (editor) { |
|||
const oldModel = editor.getModel() //获取旧模型
|
|||
const file = judgeFile(name) |
|||
// 这样定义的话model无法清除
|
|||
// monaco.editor.createModel("const a = 111","typescript", monaco.Uri.parse('file://root/file3.ts'))
|
|||
const model: monaco.editor.ITextModel = monaco.editor.createModel(content ?? "", file?.language ?? "txt") |
|||
model.updateOptions(curOption.modelOptions) |
|||
model.onDidChangeContent(() => { |
|||
if (model) { |
|||
if (isInnerChange === "out") { |
|||
isInnerChange = "waitting" |
|||
return |
|||
} |
|||
isInnerChange = "in" |
|||
const code = model.getValue() |
|||
curOption.onDidChangeContent?.(code) |
|||
} |
|||
}) |
|||
if (oldModel) { |
|||
oldModel.dispose() |
|||
} |
|||
editor.setModel(model) |
|||
} |
|||
} |
|||
|
|||
tryOnMounted(() => { |
|||
if (editorElement.value && !editor) { |
|||
editor = monaco.editor.create(editorElement.value, curOption.editorOptions) as monaco.editor.IStandaloneCodeEditor |
|||
editor.onDidChangeCursorPosition(e => { |
|||
curOption.onCursorChange?.(e) |
|||
}) |
|||
if (!curOption.content) { |
|||
placeholderWidget = new PlaceholderContentWidget(curOption.placeholder, editor) |
|||
} else { |
|||
if (isInnerChange === "waitting") { |
|||
isInnerChange = "out" |
|||
} |
|||
} |
|||
updateModel(curOption.filename, curOption.content) |
|||
useResizeObserver(editorElement.value, () => { |
|||
editor!.layout() |
|||
}) |
|||
} |
|||
}) |
|||
|
|||
tryOnUnmounted(() => { |
|||
if (editor) { |
|||
const oldModel = editor.getModel() |
|||
if (oldModel) { |
|||
oldModel.dispose() |
|||
} |
|||
editor.dispose() |
|||
editor = null |
|||
} |
|||
}) |
|||
|
|||
return { |
|||
setValue, |
|||
scrollTop() { |
|||
|
|||
} |
|||
editor?.setScrollTop(0) |
|||
}, |
|||
updateOption, |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue