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 { monaco } from "./monaco" |
||||
|
import { PlaceholderContentWidget } from "./PlaceholderContentWidget" |
||||
|
import { judgeFile } from "./utils" |
||||
|
import type { Ref } from "vue" |
||||
|
|
||||
const defaultOptions: monaco.editor.IStandaloneEditorConstructionOptions = { |
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, |
fontSize: 14, |
||||
readOnly: false, |
readOnly: false, |
||||
theme: "vs-light", |
theme: "vs-light", |
||||
fontFamily: "Cascadia Mono, Consolas, 'Courier New', monospace", |
fontFamily: "Cascadia Mono, Consolas, 'Courier New', monospace", |
||||
lineHeight: 22, |
|
||||
scrollBeyondLastLine: false, |
scrollBeyondLastLine: false, |
||||
|
lineHeight: 22, |
||||
automaticLayout: true, |
automaticLayout: true, |
||||
minimap: { |
minimap: { |
||||
enabled: false, |
enabled: false, |
||||
}, |
}, |
||||
|
}, |
||||
|
modelOptions: {}, |
||||
} |
} |
||||
|
|
||||
function getOptions(opt = {}) { |
const assign = (curOpt, opt, parenyKey: string[] = [], config = { arrayExtend: "concat" }) => { |
||||
return { |
for (const key in opt) { |
||||
...defaultOptions, |
if (opt[key] !== undefined) { |
||||
...opt, |
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 |
||||
|
} |
||||
|
|
||||
|
function getOptions(opt = {}, config = { arrayExtend: "concat" }): IOptions { |
||||
|
const curOptions = structuredClone(defaultOptions) |
||||
|
assign(curOptions, opt, [], config) |
||||
|
return curOptions |
||||
} |
} |
||||
|
|
||||
export function useMonacoEditor(editor: HTMLDivElement) { |
export function useMonacoEditor(editorElement: Ref<HTMLDivElement | undefined>, opts: Partial<IOptions>) { |
||||
|
let curOption = getOptions(opts) as IOptions |
||||
let editor: monaco.editor.IStandaloneCodeEditor | null = null |
let editor: monaco.editor.IStandaloneCodeEditor | null = null |
||||
let placeholderWidget: PlaceholderContentWidget | 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 { |
return { |
||||
|
setValue, |
||||
scrollTop() { |
scrollTop() { |
||||
|
editor?.setScrollTop(0) |
||||
} |
}, |
||||
|
updateOption, |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue