mirror of
https://github.com/logseq/logseq.git
synced 2026-05-01 01:16:27 +00:00
347 lines
7.6 KiB
TypeScript
347 lines
7.6 KiB
TypeScript
import { deepMerge } from './helpers'
|
|
import { LSPluginCaller } from './LSPlugin.caller'
|
|
import {
|
|
IAppProxy, IDBProxy,
|
|
IEditorProxy,
|
|
ILSPluginUser,
|
|
LSPluginBaseInfo,
|
|
LSPluginUserEvents,
|
|
SlashCommandAction,
|
|
BlockCommandCallback,
|
|
StyleString,
|
|
ThemeOptions,
|
|
UIOptions
|
|
} from './LSPlugin'
|
|
import Debug from 'debug'
|
|
import * as CSS from 'csstype'
|
|
import { snakeCase } from 'snake-case'
|
|
import EventEmitter from 'eventemitter3'
|
|
|
|
declare global {
|
|
interface Window {
|
|
__LSP__HOST__: boolean
|
|
logseq: ILSPluginUser
|
|
}
|
|
}
|
|
|
|
const debug = Debug('LSPlugin:user')
|
|
|
|
const app: Partial<IAppProxy> = {}
|
|
|
|
let registeredCmdUid = 0
|
|
|
|
const editor: Partial<IEditorProxy> = {
|
|
registerSlashCommand (
|
|
this: LSPluginUser,
|
|
tag: string,
|
|
actions: BlockCommandCallback | Array<SlashCommandAction>
|
|
) {
|
|
debug('Register slash command #', this.baseInfo.id, tag, actions)
|
|
|
|
if (typeof actions === 'function') {
|
|
actions = [
|
|
['editor/clear-current-slash', false],
|
|
['editor/restore-saved-cursor'],
|
|
['editor/hook', actions]
|
|
]
|
|
}
|
|
|
|
actions = actions.map((it) => {
|
|
const [tag, ...args] = it
|
|
|
|
switch (tag) {
|
|
case 'editor/hook':
|
|
let key = args[0]
|
|
let fn = () => {
|
|
this.caller?.callUserModel(key)
|
|
}
|
|
|
|
if (typeof key === 'function') {
|
|
fn = key
|
|
}
|
|
|
|
const eventKey = `SlashCommandHook${tag}${++registeredCmdUid}`
|
|
|
|
it[1] = eventKey
|
|
|
|
// register command listener
|
|
this.Editor['on' + eventKey](fn)
|
|
break
|
|
default:
|
|
}
|
|
|
|
return it
|
|
})
|
|
|
|
this.caller?.call(`api:call`, {
|
|
method: 'register-plugin-slash-command',
|
|
args: [this.baseInfo.id, [tag, actions]]
|
|
})
|
|
|
|
return false
|
|
},
|
|
|
|
registerBlockContextMenu (
|
|
this: LSPluginUser,
|
|
tag: string,
|
|
action: BlockCommandCallback
|
|
): boolean {
|
|
if (typeof action !== 'function') {
|
|
return false
|
|
}
|
|
|
|
const key = tag
|
|
const label = tag
|
|
const type = 'block-context-menu'
|
|
const eventKey = `SimpleCommandHook${tag}${++registeredCmdUid}`
|
|
|
|
this.Editor['on' + eventKey](action)
|
|
|
|
this.caller?.call(`api:call`, {
|
|
method: 'register-plugin-simple-command',
|
|
args: [this.baseInfo.id, [{ key, label, type }, ['editor/hook', eventKey]]]
|
|
})
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
const db: Partial<IDBProxy> = {}
|
|
|
|
type uiState = {
|
|
key?: number,
|
|
visible: boolean
|
|
}
|
|
|
|
const KEY_MAIN_UI = 0
|
|
|
|
/**
|
|
* User plugin instance
|
|
*/
|
|
export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements ILSPluginUser {
|
|
/**
|
|
* @private
|
|
*/
|
|
private _connected: boolean = false
|
|
|
|
/**
|
|
* ui frame identities
|
|
* @private
|
|
*/
|
|
private _ui = new Map<number, uiState>()
|
|
|
|
/**
|
|
* handler of before unload plugin
|
|
* @private
|
|
*/
|
|
private _beforeunloadCallback?: (e: any) => Promise<void>
|
|
|
|
/**
|
|
* @param _baseInfo
|
|
* @param _caller
|
|
*/
|
|
constructor (
|
|
private _baseInfo: LSPluginBaseInfo,
|
|
private _caller: LSPluginCaller
|
|
) {
|
|
super()
|
|
|
|
_caller.on('settings:changed', (payload) => {
|
|
const b = Object.assign({}, this.settings)
|
|
const a = Object.assign(this._baseInfo.settings, payload)
|
|
this.emit('settings:changed', { ...a }, b)
|
|
})
|
|
|
|
_caller.on('beforeunload', async (payload) => {
|
|
const { actor, ...rest } = payload
|
|
const cb = this._beforeunloadCallback
|
|
if (!cb || !actor) return
|
|
|
|
try {
|
|
cb && await cb(rest)
|
|
actor.resolve(null)
|
|
} catch (e) {
|
|
console.debug(`${_caller.debugTag} [beforeunload] `, e)
|
|
actor.reject(e)
|
|
}
|
|
})
|
|
}
|
|
|
|
async ready (
|
|
model?: any,
|
|
callback?: any
|
|
) {
|
|
if (this._connected) return
|
|
|
|
try {
|
|
|
|
if (typeof model === 'function') {
|
|
callback = model
|
|
model = {}
|
|
}
|
|
|
|
let baseInfo = await this._caller.connectToParent(model)
|
|
|
|
baseInfo = deepMerge(this._baseInfo, baseInfo)
|
|
|
|
this._connected = true
|
|
|
|
if (baseInfo?.id) {
|
|
this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
|
|
}
|
|
|
|
callback && callback.call(this, baseInfo)
|
|
} catch (e) {
|
|
console.error('[LSPlugin Ready Error]', e)
|
|
}
|
|
}
|
|
|
|
beforeunload (callback: (e: any) => Promise<void>): void {
|
|
if (typeof callback !== 'function') return
|
|
this._beforeunloadCallback = callback
|
|
}
|
|
|
|
provideModel (model: Record<string, any>) {
|
|
this.caller._extendUserModel(model)
|
|
return this
|
|
}
|
|
|
|
provideTheme (theme: ThemeOptions) {
|
|
this.caller.call('provider:theme', theme)
|
|
return this
|
|
}
|
|
|
|
provideStyle (style: StyleString) {
|
|
this.caller.call('provider:style', style)
|
|
return this
|
|
}
|
|
|
|
provideUI (ui: UIOptions) {
|
|
this.caller.call('provider:ui', ui)
|
|
return this
|
|
}
|
|
|
|
updateSettings (attrs: Record<string, any>) {
|
|
this.caller.call('settings:update', attrs)
|
|
// TODO: update associated baseInfo settings
|
|
}
|
|
|
|
setMainUIAttrs (attrs: Record<string, any>): void {
|
|
this.caller.call('main-ui:attrs', attrs)
|
|
}
|
|
|
|
setMainUIInlineStyle (style: CSS.Properties): void {
|
|
this.caller.call('main-ui:style', style)
|
|
}
|
|
|
|
hideMainUI (): void {
|
|
const payload = { key: KEY_MAIN_UI, visible: false }
|
|
this.caller.call('main-ui:visible', payload)
|
|
this.emit('ui:visible:changed', payload)
|
|
this._ui.set(payload.key, payload)
|
|
}
|
|
|
|
showMainUI (): void {
|
|
const payload = { key: KEY_MAIN_UI, visible: true }
|
|
this.caller.call('main-ui:visible', payload)
|
|
this.emit('ui:visible:changed', payload)
|
|
this._ui.set(payload.key, payload)
|
|
}
|
|
|
|
toggleMainUI (): void {
|
|
const payload = { key: KEY_MAIN_UI, toggle: true }
|
|
const state = this._ui.get(payload.key)
|
|
if (state && state.visible) {
|
|
this.hideMainUI()
|
|
} else {
|
|
this.showMainUI()
|
|
}
|
|
}
|
|
|
|
get isMainUIVisible (): boolean {
|
|
const state = this._ui.get(0)
|
|
return Boolean(state && state.visible)
|
|
}
|
|
|
|
get connected (): boolean {
|
|
return this._connected
|
|
}
|
|
|
|
get baseInfo (): LSPluginBaseInfo {
|
|
return this._baseInfo
|
|
}
|
|
|
|
get settings () {
|
|
return this.baseInfo?.settings
|
|
}
|
|
|
|
get caller (): LSPluginCaller {
|
|
return this._caller
|
|
}
|
|
|
|
_makeUserProxy (
|
|
target: any,
|
|
tag?: 'app' | 'editor' | 'db'
|
|
) {
|
|
const that = this
|
|
const caller = this.caller
|
|
|
|
return new Proxy(target, {
|
|
get (target: any, propKey, receiver) {
|
|
const origMethod = target[propKey]
|
|
|
|
return function (this: any, ...args: any) {
|
|
if (origMethod) {
|
|
const ret = origMethod.apply(that, args)
|
|
if (ret === false) return
|
|
}
|
|
|
|
// Handle hook
|
|
if (tag) {
|
|
const hookMatcher = propKey.toString().match(/^(once|off|on)/i)
|
|
|
|
if (hookMatcher != null) {
|
|
const f = hookMatcher[0]
|
|
const s = hookMatcher.input!
|
|
const e = s.slice(f.length)
|
|
|
|
caller[f.toLowerCase()](`hook:${tag}:${snakeCase(e)}`, args[0])
|
|
return
|
|
}
|
|
}
|
|
|
|
// Call host
|
|
return caller.callAsync(`api:call`, {
|
|
method: propKey,
|
|
args: args
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
get App (): IAppProxy {
|
|
return this._makeUserProxy(app, 'app')
|
|
}
|
|
|
|
get Editor () {
|
|
return this._makeUserProxy(editor, 'editor')
|
|
}
|
|
|
|
get DB (): IDBProxy {
|
|
return this._makeUserProxy(db)
|
|
}
|
|
}
|
|
|
|
export function setupPluginUserInstance (
|
|
pluginBaseInfo: LSPluginBaseInfo,
|
|
pluginCaller: LSPluginCaller
|
|
) {
|
|
return new LSPluginUser(pluginBaseInfo, pluginCaller)
|
|
}
|
|
|
|
if (window.__LSP__HOST__ == null) { // Entry of iframe mode
|
|
const caller = new LSPluginCaller(null)
|
|
window.logseq = setupPluginUserInstance({} as any, caller)
|
|
}
|