| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- import { deepMerge } from './helpers'
- import { LSPluginCaller } from './LSPlugin.caller'
- import {
- IAppProxy, IDBProxy,
- IEditorProxy,
- ILSPluginUser,
- LSPluginBaseInfo,
- LSPluginUserEvents,
- SlashCommandAction,
- BlockCommandCallback,
- StyleString,
- ThemeOptions,
- UIOptions, IHookEvent
- } 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
- }
- }
- const debug = Debug('LSPlugin:user')
- /**
- * @param type
- * @param opts
- * @param action
- */
- function registerSimpleCommand (
- this: LSPluginUser,
- type: string,
- opts: {
- key: string,
- label: string
- },
- action: BlockCommandCallback
- ) {
- if (typeof action !== 'function') {
- return false
- }
- const { key, label } = opts
- const eventKey = `SimpleCommandHook${key}${++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]]]
- })
- }
- const app: Partial<IAppProxy> = {
- registerUIItem (
- type: 'toolbar' | 'pagebar',
- opts: { key: string, template: string }
- ) {
- const pid = this.baseInfo.id
- // opts.key = `${pid}_${opts.key}`
- this.caller?.call(`api:call`, {
- method: 'register-plugin-ui-item',
- args: [pid, type, opts]
- })
- return false
- },
- registerPagebarMenuItem (
- this: LSPluginUser,
- tag: string,
- action: (e: IHookEvent & { page: string }) => void
- ): unknown {
- if (typeof action !== 'function') {
- return false
- }
- const key = tag + '_' + this.baseInfo.id
- const label = tag
- const type = 'pagebar-menu-item'
- registerSimpleCommand.call(this,
- type, {
- key, label
- }, action)
- return false
- }
- }
- 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
- },
- registerBlockContextMenuItem (
- this: LSPluginUser,
- tag: string,
- action: BlockCommandCallback
- ): unknown {
- if (typeof action !== 'function') {
- return false
- }
- const key = + '_' + this.baseInfo.id
- const label = tag
- const type = 'block-context-menu-item'
- registerSimpleCommand.call(this,
- type, {
- key, label
- }, action)
- return false
- }
- }
- const db: Partial<IDBProxy> = {}
- type uiState = {
- key?: number,
- visible: boolean
- }
- const KEY_MAIN_UI = 0
- /**
- * User plugin instance
- * @public
- */
- 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
- 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 (opts?: { restoreEditingCursor: boolean }): void {
- const payload = { key: KEY_MAIN_UI, visible: false, cursor: opts?.restoreEditingCursor }
- 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
- }
- /**
- * @internal
- */
- _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].toLowerCase()
- const s = hookMatcher.input!
- const e = s.slice(f.length)
- const type = `hook:${tag}:${snakeCase(e)}`
- const handler = args[0]
- caller[f](type, handler)
- return f !== 'off' ? () => (caller.off(type, handler)) : void 0
- }
- }
- // Call host
- return caller.callAsync(`api:call`, {
- method: propKey,
- args: args
- })
- }
- }
- })
- }
- /**
- * The interface methods of {@link IAppProxy}
- */
- get App (): IAppProxy {
- return this._makeUserProxy(app, 'app')
- }
- get Editor (): IEditorProxy {
- return this._makeUserProxy(editor, 'editor')
- }
- get DB (): IDBProxy {
- return this._makeUserProxy(db)
- }
- }
- export * from './LSPlugin'
- /**
- * @internal
- */
- 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)
- // @ts-ignore
- window.logseq = setupPluginUserInstance({} as any, caller)
- }
|