From fbfc7467b5a7796cfa01da09553ff7f4927eff30 Mon Sep 17 00:00:00 2001 From: frostime <frostime@foxmail.com> Date: Sat, 3 Jun 2023 15:50:28 +0800 Subject: [PATCH] copy from `plugin-sample:index.ts` --- .gitignore | 1 + plugin.json | 2 +- src/index.ts | 293 +++++++++++++++++++++++---------- src/siyuan.d.ts | 424 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 630 insertions(+), 90 deletions(-) create mode 100644 src/siyuan.d.ts 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(`<symbol id="iconFace" viewBox="0 0 32 32"> +<path d="M13.667 17.333c0 0.92-0.747 1.667-1.667 1.667s-1.667-0.747-1.667-1.667 0.747-1.667 1.667-1.667 1.667 0.747 1.667 1.667zM20 15.667c-0.92 0-1.667 0.747-1.667 1.667s0.747 1.667 1.667 1.667 1.667-0.747 1.667-1.667-0.747-1.667-1.667-1.667zM29.333 16c0 7.36-5.973 13.333-13.333 13.333s-13.333-5.973-13.333-13.333 5.973-13.333 13.333-13.333 13.333 5.973 13.333 13.333zM14.213 5.493c1.867 3.093 5.253 5.173 9.12 5.173 0.613 0 1.213-0.067 1.787-0.16-1.867-3.093-5.253-5.173-9.12-5.173-0.613 0-1.213 0.067-1.787 0.16zM5.893 12.627c2.28-1.293 4.040-3.4 4.88-5.92-2.28 1.293-4.040 3.4-4.88 5.92zM26.667 16c0-1.040-0.16-2.040-0.44-2.987-0.933 0.2-1.893 0.32-2.893 0.32-4.173 0-7.893-1.92-10.347-4.92-1.4 3.413-4.187 6.093-7.653 7.4 0.013 0.053 0 0.12 0 0.187 0 5.88 4.787 10.667 10.667 10.667s10.667-4.787 10.667-10.667z"></path> +</symbol> +<symbol id="iconSaving" viewBox="0 0 32 32"> +<path d="M20 13.333c0-0.733 0.6-1.333 1.333-1.333s1.333 0.6 1.333 1.333c0 0.733-0.6 1.333-1.333 1.333s-1.333-0.6-1.333-1.333zM10.667 12h6.667v-2.667h-6.667v2.667zM29.333 10v9.293l-3.76 1.253-2.24 7.453h-7.333v-2.667h-2.667v2.667h-7.333c0 0-3.333-11.28-3.333-15.333s3.28-7.333 7.333-7.333h6.667c1.213-1.613 3.147-2.667 5.333-2.667 1.107 0 2 0.893 2 2 0 0.28-0.053 0.533-0.16 0.773-0.187 0.453-0.347 0.973-0.427 1.533l3.027 3.027h2.893zM26.667 12.667h-1.333l-4.667-4.667c0-0.867 0.12-1.72 0.347-2.547-1.293 0.333-2.347 1.293-2.787 2.547h-8.227c-2.573 0-4.667 2.093-4.667 4.667 0 2.507 1.627 8.867 2.68 12.667h2.653v-2.667h8v2.667h2.68l2.067-6.867 3.253-1.093v-4.707z"></path> +</symbol>`); 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 = `<div class="toolbar__item b3-tooltips b3-tooltips__w" aria-label="Remove plugin-sample Data"> + <svg> + <use xlink:href="#iconTrashcan"></use> + </svg> +</div>`; + 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: `<div id="SettingPanel"></div>`, + 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: '<div class="b3-dialog__content">This is a dialog</div>', + 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: `<div id="SettingPanel"></div>`, - 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<IWebSocketData>; + +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<any>; + + saveData(storageName: string, content: any): Promise<void>; + + removeData(storageName: string): Promise<any>; + + 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<any>) => void): void; + + once(type: TEventBus, listener: (event: CustomEvent<any>) => void): void; + + off(type: TEventBus, listener: (event: CustomEvent<any>) => 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