diff --git a/.gitignore b/.gitignore index aa00657..764e2d4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules dev dist build +tmp diff --git a/plugin.json b/plugin.json index 8558256..37acc2b 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "plugin-sample-vite-svelte", "author": "frostime", "url": "https://github.com/siyuan-note/plugin-sample-vite-svelte", - "version": "0.0.6", + "version": "0.1.3", "minAppVersion": "2.9.0", "displayName": { "en_US": "Plugin sample with vite and svelte", diff --git a/src/index.ts b/src/index.ts index e2f27cd..7c129b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,15 @@ -import { Plugin, showMessage, confirm, Dialog, Menu, isMobile, openTab, adaptHotkey } from "siyuan"; +import { + Plugin, + showMessage, + confirm, + Dialog, + Menu, + openTab, + adaptHotkey, + getFrontend, + getBackend, + IModel +} from "siyuan"; import "./index.scss"; import HelloExample from "./hello.svelte"; @@ -11,20 +22,53 @@ const DOCK_TYPE = "dock_tab"; export default class SamplePlugin extends Plugin { private customTab: () => any; + private isMobile: boolean; async onload() { - showMessage("Hello SiYuan Plugin"); - this.data[STORAGE_NAME] = {readonlyText: "Readonly"}; + this.data[STORAGE_NAME] = { readonlyText: "Readonly" }; + + const frontEnd = getFrontend(); + this.isMobile = frontEnd === "mobile" || frontEnd === "browser-mobile"; + // 图标的制作参见帮助文档 + this.addIcons(` + + + + +`); const topBarElement = this.addTopBar({ - icon: "iconEmoji", + icon: "iconFace", title: this.i18n.addTopBarIcon, - position: "left", + position: "right", callback: () => { - this.addMenu(topBarElement.getBoundingClientRect()); + let rect = topBarElement.getBoundingClientRect(); + // 如果被隐藏,则使用更多按钮 + if (rect.width === 0) { + rect = document.querySelector("#barMore").getBoundingClientRect(); + } + this.addMenu(rect); } }); + const statusIconTemp = document.createElement("template"); + statusIconTemp.innerHTML = `
+ + + +
`; + statusIconTemp.content.firstElementChild.addEventListener("click", () => { + confirm("⚠️", this.i18n.confirmRemove.replace("${name}", this.name), () => { + this.removeData(STORAGE_NAME).then(() => { + this.data[STORAGE_NAME] = { readonlyText: "Readonly" }; + showMessage(`[${this.name}]: ${this.i18n.removedData}`); + }); + }); + }); + this.addStatusBar({ + element: statusIconTemp.content.firstElementChild as HTMLElement, + }); + let div = document.createElement("div"); new HelloExample({ target: div, @@ -44,10 +88,18 @@ export default class SamplePlugin extends Plugin { } }); + this.addCommand({ + langKey: "showDialog", + hotkey: "⇧⌘M", + callback: () => { + this.showDialog(); + } + }); + this.addDock({ config: { position: "LeftBottom", - size: {width: 200, height: 0}, + size: { width: 200, height: 0 }, icon: "iconEmoji", title: "Custom Dock", }, @@ -79,6 +131,7 @@ export default class SamplePlugin extends Plugin { onLayoutReady() { this.loadData(STORAGE_NAME); + console.log(`frontend: ${getFrontend()}; backend: ${getBackend()}`); } onunload() { @@ -87,11 +140,27 @@ export default class SamplePlugin extends Plugin { console.log("onunload"); } - private wsEvent({ detail }: any) { + openSetting(): void { + let dialog = new Dialog({ + title: "SettingPannel", + content: `
`, + width: "600px", + destroyCallback: (options) => { + console.log("destroyCallback", options); + //You must destroy the component when the dialog is closed + pannel.$destroy(); + } + }); + let pannel = new SettingPannel({ + target: dialog.element.querySelector("#SettingPanel"), + }); + } + + private eventBusLog({ detail }: any) { console.log(detail); } - private blockIconEvent({detail}: any) { + private blockIconEvent({ detail }: any) { console.log(detail); detail.menu.addSeparator(0); const ids: string[] = []; @@ -106,70 +175,108 @@ export default class SamplePlugin extends Plugin { }); } - private async addMenu(rect: DOMRect) { + private showDialog() { + new Dialog({ + title: "Info", + content: '
This is a dialog
', + width: this.isMobile ? "92vw" : "520px", + }); + } + + private addMenu(rect: DOMRect) { const menu = new Menu("topBarSample", () => { console.log(this.i18n.byeMenu); }); - menu.addItem({ - icon: "iconHelp", - label: "Confirm", - click() { - confirm("Confirm", "Is this a confirm?", () => { - showMessage("confirm"); - }, () => { - showMessage("cancel"); - }); - } - }); - menu.addItem({ - icon: "iconFeedback", - label: "Message", - click: () => { - showMessage(this.i18n.helloPlugin); - } - }); menu.addItem({ icon: "iconInfo", label: "Dialog", - click: () => this.openHelloInDialog() + accelerator: this.commands[0].customHotkey, + click: this.showDialog }); - menu.addItem({ - icon: "iconLayoutBottom", - label: "Open Tab", - click: () => { - openTab({ - custom: { - icon: "iconEmoji", - title: "Custom Tab", - data: { - text: "This is my custom tab", + if (!this.isMobile) { + menu.addItem({ + icon: "iconLayoutBottom", + label: "Open Custom Tab", + click: () => { + const tab = openTab({ + app: this.app, + custom: { + icon: "iconFace", + title: "Custom Tab", + data: { + text: "This is my custom tab", + }, + fn: this.customTab }, - fn: this.customTab - }, - }); - } - }); - menu.addItem({ - icon: "iconLayout", - label: "Open Float Layer(open help)", - click: () => { - this.addFloatLayer({ - ids: ["20230523173319-xj1l3qu", "20230523173321-55o0w2n"], - defIds: ["20230523173323-imgm9tp", "20230523173324-cxu98t3"], - x: window.innerWidth - 768 - 120, - y: 32 - }); - } - }); - menu.addItem({ - icon: "iconTrashcan", - label: "Remove Data", - click: () => { - this.removeData(STORAGE_NAME).then(() => { - this.data[STORAGE_NAME] = {readonlyText: "Readonly"}; - }); - } - }); + }); + console.log(tab) + } + }); + menu.addItem({ + icon: "iconLayoutBottom", + label: "Open Asset Tab(open help first)", + click: () => { + const tab = openTab({ + app: this.app, + asset: { + path: "assets/paragraph-20210512165953-ag1nib4.svg" + } + }); + console.log(tab) + } + }); + menu.addItem({ + icon: "iconLayoutBottom", + label: "Open Doc Tab(open help first)", + click: async () => { + const tab = await openTab({ + app: this.app, + doc: { + id: "20200812220555-lj3enxa", + } + }); + console.log(tab) + } + }); + menu.addItem({ + icon: "iconLayoutBottom", + label: "Open Search Tab", + click: () => { + const tab = openTab({ + app: this.app, + search: { + k: "SiYuan" + } + }); + console.log(tab) + } + }); + menu.addItem({ + icon: "iconLayoutBottom", + label: "Open Card Tab", + click: () => { + const tab = openTab({ + app: this.app, + card: { + type: "all" + } + }); + console.log(tab) + } + }); + menu.addItem({ + icon: "iconLayout", + label: "Open Float Layer(open help first)", + click: () => { + this.addFloatLayer({ + ids: ["20210428212840-8rqwn5o", "20201225220955-l154bn4"], + defIds: ["20230415111858-vgohvf3", "20200813131152-0wk5akh"], + x: window.innerWidth - 768 - 120, + y: 32 + }); + } + }); + } menu.addItem({ icon: "iconScrollHoriz", label: "Event Bus", @@ -178,13 +285,13 @@ export default class SamplePlugin extends Plugin { icon: "iconSelect", label: "On ws-main", click: () => { - this.eventBus.on("ws-main", this.wsEvent); + this.eventBus.on("ws-main", this.eventBusLog); } }, { icon: "iconClose", label: "Off ws-main", click: () => { - this.eventBus.off("ws-main", this.wsEvent); + this.eventBus.off("ws-main", this.eventBusLog); } }, { icon: "iconSelect", @@ -202,35 +309,59 @@ export default class SamplePlugin extends Plugin { icon: "iconSelect", label: "On click-pdf", click: () => { - this.eventBus.on("click-pdf", this.wsEvent); + this.eventBus.on("click-pdf", this.eventBusLog); } }, { icon: "iconClose", label: "Off click-pdf", click: () => { - this.eventBus.off("click-pdf", this.wsEvent); + this.eventBus.off("click-pdf", this.eventBusLog); } }, { icon: "iconSelect", label: "On click-editorcontent", click: () => { - this.eventBus.on("click-editorcontent", this.wsEvent); + this.eventBus.on("click-editorcontent", this.eventBusLog); } }, { icon: "iconClose", label: "Off click-editorcontent", click: () => { - this.eventBus.off("click-editorcontent", this.wsEvent); + this.eventBus.off("click-editorcontent", this.eventBusLog); + } + }, { + icon: "iconSelect", + label: "On click-editortitleicon", + click: () => { + this.eventBus.on("click-editortitleicon", this.eventBusLog); + } + }, { + icon: "iconClose", + label: "Off click-editortitleicon", + click: () => { + this.eventBus.off("click-editortitleicon", this.eventBusLog); + } + }, { + icon: "iconSelect", + label: "On open-noneditableblock", + click: () => { + this.eventBus.on("open-noneditableblock", this.eventBusLog); + } + }, { + icon: "iconClose", + label: "Off open-noneditableblock", + click: () => { + this.eventBus.off("open-noneditableblock", this.eventBusLog); } }] }); menu.addSeparator(); menu.addItem({ icon: "iconSparkles", - label: this.data[STORAGE_NAME] || "Readonly", + label: this.data[STORAGE_NAME].readonlyText || "Readonly", type: "readonly", }); - if (isMobile()) { + if (this.isMobile) { menu.fullscreen(); } else { menu.open({ @@ -241,22 +372,6 @@ export default class SamplePlugin extends Plugin { } } - openSetting(): void { - let dialog = new Dialog({ - title: "SettingPannel", - content: `
`, - width: "600px", - destroyCallback: (options) => { - console.log("destroyCallback", options); - //You must destroy the component when the dialog is closed - pannel.$destroy(); - } - }); - let pannel = new SettingPannel({ - target: dialog.element.querySelector("#SettingPanel"), - }); - } - private openHelloInDialog() { let dialog = new Dialog({ title: "Hello World", diff --git a/src/siyuan.d.ts b/src/siyuan.d.ts new file mode 100644 index 0000000..4c74dc5 --- /dev/null +++ b/src/siyuan.d.ts @@ -0,0 +1,424 @@ +type TEventBus = "ws-main" | "click-blockicon" | "click-editorcontent" | "click-pdf" | + "click-editortitleicon" | "open-noneditableblock" + +type TCardType = "doc" | "notebook" | "all" + +declare global { + interface Window { + Lute: Lute + } +} + +interface ITab { + id: string; + headElement: HTMLElement; + panelElement: HTMLElement; + model: IModel; + title: string; + icon: string; + docIcon: string; + updateTitle: (title: string) => void; + pin: () => void; + unpin: () => void; + setDocIcon: (icon: string) => void; + close: () => void; +} + +interface IModel { + element: Element; + tab: ITab; + data: any; + type: string; +} + +interface IObject { + [key: string]: string; +} + +interface ILuteNode { + TokensStr: () => string; + __internal_object__: { + Parent: { + Type: number, + }, + HeadingLevel: string, + }; +} + +interface ISearchOption { + page?: number + group?: number, // 0:不分组,1:按文档分组 + hasReplace?: boolean, + method?: number // 0:文本,1:查询语法,2:SQL,3:正则表达式 + hPath?: string + idPath?: string[] + k: string + r?: string + types?: { + mathBlock: boolean + table: boolean + blockquote: boolean + superBlock: boolean + paragraph: boolean + document: boolean + heading: boolean + list: boolean + listItem: boolean + codeBlock: boolean + htmlBlock: boolean + embedBlock: boolean + } +} + +interface IWebSocketData { + cmd: string + callback?: string + data: any + msg: string + code: number + sid: string +} + +declare interface IPluginDockTab { + position: "LeftTop" | "LeftBottom" | "RightTop" | "RightBottom" | "BottomLeft" | "BottomRight", + size: { width: number, height: number }, + icon: string, + hotkey?: string, + title: string, + index?: number, + show?: boolean +} + +interface IMenuItemOption { + label?: string, + click?: (element: HTMLElement) => void, + type?: "separator" | "submenu" | "readonly", + accelerator?: string, + action?: string, + id?: string, + submenu?: IMenuItemOption[] + disabled?: boolean + icon?: string + iconHTML?: string + current?: boolean + bind?: (element: HTMLElement) => void + index?: number + element?: HTMLElement +} + +interface ICommandOption { + langKey: string, // 多语言 key + /** + * 目前需使用 MacOS 符号标识,顺序按照 ⌥⇧⌘,入 ⌥⇧⌘A + * "Ctrl": "⌘", + * "Shift": "⇧", + * "Alt": "⌥", + * "Tab": "⇥", + * "Backspace": "⌫", + * "Delete": "⌦", + * "Enter": "↩", + */ + hotkey: string, + customHotkey?: string, + callback?: () => void + fileTreeCallback?: (file: any) => void + editorCallback?: (protyle: any) => void + dockCallback?: (element: HTMLElement) => void +} + +export function fetchPost(url: string, data?: any, callback?: (response: IWebSocketData) => void, headers?: IObject): void; + +export function fetchSyncPost(url: string, data?: any): Promise; + +export function fetchGet(url: string, callback: (response: IWebSocketData) => void): void; + +export function openTab(options: { + app: App, + doc?: { + id: string, // 块 id + action?: string [] // cb-get-all:获取所有内容;cb-get-focus:打开后光标定位在 id 所在的块;cb-get-hl: 打开后 id 块高亮 + zoomIn?: boolean // 是否缩放 + }, + pdf?: { + path: string, + page?: number, // pdf 页码 + id?: string, // File Annotation id + }, + asset?: { + path: string, + }, + search?: ISearchOption + card?: { + type: TCardType, + id?: string, // cardType 为 all 时不传,否则传文档或笔记本 id + title?: string // cardType 为 all 时不传,否则传文档或笔记本名称 + }, + custom?: { + title: string, + icon: string, + data?: any + fn?: () => IModel, + } + position?: "right" | "bottom", + keepCursor?: boolean // 是否跳转到新 tab 上 + removeCurrentTab?: boolean // 在当前页签打开时需移除原有页签 + afterOpen?: () => void // 打开后回调 +}): ITab + +export function getFrontend(): "desktop" | "desktop-window" | "mobile" | "browser-desktop" | "browser-mobile"; + +export function getBackend(): "windows" | "linux" | "darwin" | "docker" | "android" | "ios" + +export function adaptHotkey(hotkey: string): string; + +export function confirm(title: string, text: string, confirmCallback?: () => void, cancelCallback?: () => void): void; + +/** + * @param timeout - ms. 0: manual close;-1: always show; 6000: default + * @param {string} [type=info] + */ +export function showMessage(text: string, timeout?: number, type?: "info" | "error", id?: string): void; + +export class App { + plugins: Plugin[]; +} + +export abstract class Plugin { + eventBus: EventBus; + i18n: IObject; + data: any; + name: string; + app: App; + commands: ICommandOption[]; + + constructor(options: { + app: App, + name: string, + i18n: IObject + }) + + onload(): void; + + onunload(): void; + + onLayoutReady(): void; + + /** + * Must be executed before the synchronous function. + * @param {string} [options.position=right] + */ + addTopBar(options: { + icon: string, + title: string, + callback: (event: MouseEvent) => void + position?: "right" | "left" + }): HTMLElement; + + /** + * Must be executed before the synchronous function. + * @param {string} [options.position=right] + */ + addStatusBar(options: { + element: HTMLElement, + position?: "right" | "left" + }): HTMLElement + + openSetting(): void + + loadData(storageName: string): Promise; + + saveData(storageName: string, content: any): Promise; + + removeData(storageName: string): Promise; + + addIcons(svg: string): void; + + /** + * Must be executed before the synchronous function. + */ + addTab(options: { + type: string, + destroy?: () => void, + resize?: () => void, + update?: () => void, + init: () => void + }): () => IModel + + /** + * Must be executed before the synchronous function. + */ + addDock(options: { + config: IPluginDockTab, + data: any, + type: string, + destroy?: () => void, + resize?: () => void, + update?: () => void, + init: () => void + }): { config: IPluginDockTab, model: IModel } + + addCommand(options: ICommandOption): void + + addFloatLayer(options: { + ids: string[], + defIds?: string[], + x?: number, + y?: number, + targetElement?: HTMLElement + }): void +} + +export class EventBus { + on(type: TEventBus, listener: (event: CustomEvent) => void): void; + + once(type: TEventBus, listener: (event: CustomEvent) => void): void; + + off(type: TEventBus, listener: (event: CustomEvent) => void): void; + + emit(type: TEventBus, detail?: any): boolean; +} + +export class Dialog { + + element: HTMLElement; + + constructor(options: { + title?: string, + transparent?: boolean, + content: string, + width?: string + height?: string, + destroyCallback?: (options?: IObject) => void + disableClose?: boolean + disableAnimation?: boolean + }); + + destroy(options?: IObject): void; + + bindInput(inputElement: HTMLInputElement | HTMLTextAreaElement, enterEvent?: () => void): void; +} + +export class Menu { + constructor(id?: string, closeCallback?: () => void); + + showSubMenu(subMenuElement: HTMLElement): void; + + addItem(options: IMenuItemOption): HTMLElement; + + addSeparator(index?: number): void; + + open(options: { x: number, y: number, h?: number, w?: number, isLeft?: boolean }): void; + + /** + * @param {string} [position=all] + */ + fullscreen(position?: "bottom" | "all"): void; + + close(): void; +} + +declare class Lute { + public static WalkStop: number; + public static WalkSkipChildren: number; + public static WalkContinue: number; + public static Version: string; + public static Caret: string; + + public static New(): Lute; + + public static EChartsMindmapStr(text: string): string; + + public static NewNodeID(): string; + + public static Sanitize(html: string): string; + + public static EscapeHTMLStr(str: string): string; + + public static UnEscapeHTMLStr(str: string): string; + + public static GetHeadingID(node: ILuteNode): string; + + public static BlockDOM2Content(html: string): string; + + private constructor(); + + public BlockDOM2Content(text: string): string; + + public BlockDOM2EscapeMarkerContent(text: string): string; + + public SetTextMark(enable: boolean): void; + + public SetHeadingID(enable: boolean): void; + + public SetProtyleMarkNetImg(enable: boolean): void; + + public SetSpellcheck(enable: boolean): void; + + public SetFileAnnotationRef(enable: boolean): void; + + public SetSetext(enable: boolean): void; + + public SetYamlFrontMatter(enable: boolean): void; + + public SetChineseParagraphBeginningSpace(enable: boolean): void; + + public SetRenderListStyle(enable: boolean): void; + + public SetImgPathAllowSpace(enable: boolean): void; + + public SetKramdownIAL(enable: boolean): void; + + public BlockDOM2Md(html: string): string; + + public BlockDOM2StdMd(html: string): string; + + public SetGitConflict(enable: boolean): void; + + public SetSuperBlock(enable: boolean): void; + + public SetTag(enable: boolean): void; + + public SetMark(enable: boolean): void; + + public SetSub(enable: boolean): void; + + public SetSup(enable: boolean): void; + + public SetBlockRef(enable: boolean): void; + + public SetSanitize(enable: boolean): void; + + public SetHeadingAnchor(enable: boolean): void; + + public SetImageLazyLoading(imagePath: string): void; + + public SetInlineMathAllowDigitAfterOpenMarker(enable: boolean): void; + + public SetToC(enable: boolean): void; + + public SetIndentCodeBlock(enable: boolean): void; + + public SetParagraphBeginningSpace(enable: boolean): void; + + public SetFootnotes(enable: boolean): void; + + public SetLinkRef(enalbe: boolean): void; + + public SetEmojiSite(emojiSite: string): void; + + public PutEmojis(emojis: IObject): void; + + public SpinBlockDOM(html: string): string; + + public Md2BlockDOM(html: string): string; + + public SetProtyleWYSIWYG(wysiwyg: boolean): void; + + public MarkdownStr(name: string, md: string): string; + + public IsValidLinkDest(text: string): boolean; + + public BlockDOM2InlineBlockDOM(html: string): string; + + public BlockDOM2HTML(html: string): string; +} \ No newline at end of file