LSPlugin.user.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import { deepMerge, invokeHostExportedApi } from './helpers'
  2. import { LSPluginCaller } from './LSPlugin.caller'
  3. import {
  4. IAppProxy, IDBProxy,
  5. IEditorProxy,
  6. ILSPluginUser,
  7. LSPluginBaseInfo, LSPluginUserEvents, SlashCommandAction,
  8. StyleString,
  9. ThemeOptions,
  10. UIOptions
  11. } from './LSPlugin'
  12. import Debug from 'debug'
  13. import { snakeCase } from 'snake-case'
  14. import EventEmitter from 'eventemitter3'
  15. declare global {
  16. interface Window {
  17. __LSP__HOST__: boolean
  18. logseq: ILSPluginUser
  19. }
  20. }
  21. const debug = Debug('LSPlugin:user')
  22. const app: Partial<IAppProxy> = {}
  23. let registeredCmdUid = 0
  24. const editor: Partial<IEditorProxy> = {
  25. registerSlashCommand (
  26. this: LSPluginUser,
  27. tag: string,
  28. actions: Array<SlashCommandAction>
  29. ) {
  30. debug('Register slash command #', this.baseInfo.id, tag, actions)
  31. actions = actions.map((it) => {
  32. const [tag, ...args] = it
  33. switch (tag) {
  34. case 'editor/hook':
  35. let key = args[0]
  36. let fn = () => {
  37. this.caller?.callUserModel(key)
  38. }
  39. if (typeof key === 'function') {
  40. fn = key
  41. }
  42. const eventKey = `SlashCommandHook${tag}${++registeredCmdUid}`
  43. it[1] = eventKey
  44. // register command listener
  45. this.Editor['on' + eventKey](fn)
  46. break
  47. default:
  48. }
  49. return it
  50. })
  51. this.caller?.call(`api:call`, {
  52. method: 'register-plugin-slash-command',
  53. args: [this.baseInfo.id, [tag, actions]]
  54. })
  55. return false
  56. },
  57. registerBlockContextMenu (
  58. this: LSPluginUser,
  59. tag: string,
  60. action: () => void
  61. ): boolean {
  62. if (typeof action !== 'function') {
  63. return false
  64. }
  65. const key = tag
  66. const label = tag
  67. const type = 'block-context-menu'
  68. const eventKey = `SimpleCommandHook${tag}${++registeredCmdUid}`
  69. this.Editor['on' + eventKey](action)
  70. this.caller?.call(`api:call`, {
  71. method: 'register-plugin-simple-command',
  72. args: [this.baseInfo.id, [{ key, label, type }, ['editor/hook', eventKey]]]
  73. })
  74. return false
  75. }
  76. }
  77. const db: Partial<IDBProxy> = {}
  78. type uiState = {
  79. key?: number,
  80. visible: boolean
  81. }
  82. const KEY_MAIN_UI = 0
  83. /**
  84. * User plugin instance
  85. */
  86. export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements ILSPluginUser {
  87. /**
  88. * Indicate connected with host
  89. * @private
  90. */
  91. private _connected: boolean = false
  92. private _ui = new Map<number, uiState>()
  93. /**
  94. * @param _baseInfo
  95. * @param _caller
  96. */
  97. constructor (
  98. private _baseInfo: LSPluginBaseInfo,
  99. private _caller: LSPluginCaller
  100. ) {
  101. super()
  102. _caller.on('settings:changed', (payload) => {
  103. const b = Object.assign({}, this.settings)
  104. const a = Object.assign(this._baseInfo.settings, payload)
  105. this.emit('settings:changed', { ...a }, b)
  106. })
  107. }
  108. async ready (
  109. model?: any,
  110. callback?: any
  111. ) {
  112. if (this._connected) return
  113. try {
  114. if (typeof model === 'function') {
  115. callback = model
  116. model = {}
  117. }
  118. let baseInfo = await this._caller.connectToParent(model)
  119. baseInfo = deepMerge(this._baseInfo, baseInfo)
  120. this._connected = true
  121. callback && callback.call(this, baseInfo)
  122. } catch (e) {
  123. console.error('[LSPlugin Ready Error]', e)
  124. }
  125. }
  126. provideModel (model: Record<string, any>) {
  127. this.caller._extendUserModel(model)
  128. return this
  129. }
  130. provideTheme (theme: ThemeOptions) {
  131. this.caller.call('provider:theme', theme)
  132. return this
  133. }
  134. provideStyle (style: StyleString) {
  135. this.caller.call('provider:style', style)
  136. return this
  137. }
  138. provideUI (ui: UIOptions) {
  139. this.caller.call('provider:ui', ui)
  140. return this
  141. }
  142. updateSettings (attrs: Record<string, any>) {
  143. this.caller.call('settings:update', attrs)
  144. // TODO: update associated baseInfo settings
  145. }
  146. setMainUIAttrs (attrs: Record<string, any>): void {
  147. this.caller.call('main-ui:attrs', attrs)
  148. }
  149. setMainUIInlineStyle (style: CSSStyleDeclaration): void {
  150. this.caller.call('main-ui:style', style)
  151. }
  152. hideMainUI (): void {
  153. const payload = { key: KEY_MAIN_UI, visible: false }
  154. this.caller.call('main-ui:visible', payload)
  155. this.emit('ui:visible:changed', payload)
  156. this._ui.set(payload.key, payload)
  157. }
  158. showMainUI (): void {
  159. const payload = { key: KEY_MAIN_UI, visible: true }
  160. this.caller.call('main-ui:visible', payload)
  161. this.emit('ui:visible:changed', payload)
  162. this._ui.set(payload.key, payload)
  163. }
  164. toggleMainUI (): void {
  165. const payload = { key: KEY_MAIN_UI, toggle: true }
  166. const state = this._ui.get(payload.key)
  167. if (state && state.visible) {
  168. this.hideMainUI()
  169. } else {
  170. this.showMainUI()
  171. }
  172. }
  173. get isMainUIVisible (): boolean {
  174. const state = this._ui.get(0)
  175. return Boolean(state && state.visible)
  176. }
  177. get connected (): boolean {
  178. return this._connected
  179. }
  180. get baseInfo (): LSPluginBaseInfo {
  181. return this._baseInfo
  182. }
  183. get settings () {
  184. return this.baseInfo?.settings
  185. }
  186. get caller (): LSPluginCaller {
  187. return this._caller
  188. }
  189. _makeUserProxy (
  190. target: any,
  191. tag?: 'app' | 'editor' | 'db'
  192. ) {
  193. const that = this
  194. const caller = this.caller
  195. return new Proxy(target, {
  196. get (target: any, propKey, receiver) {
  197. const origMethod = target[propKey]
  198. return function (this: any, ...args: any) {
  199. if (origMethod) {
  200. const ret = origMethod.apply(that, args)
  201. if (ret === false) return
  202. }
  203. // Handle hook
  204. if (tag) {
  205. const hookMatcher = propKey.toString().match(/^(once|off|on)/i)
  206. if (hookMatcher != null) {
  207. const f = hookMatcher[0]
  208. const s = hookMatcher.input!
  209. const e = s.slice(f.length)
  210. caller[f.toLowerCase()](`hook:${tag}:${snakeCase(e)}`, args[0])
  211. return
  212. }
  213. }
  214. // Call host
  215. return caller.callAsync(`api:call`, {
  216. method: propKey,
  217. args: args
  218. })
  219. }
  220. }
  221. })
  222. }
  223. get App (): IAppProxy {
  224. return this._makeUserProxy(app, 'app')
  225. }
  226. get Editor () {
  227. return this._makeUserProxy(editor, 'editor')
  228. }
  229. get DB (): IDBProxy {
  230. return this._makeUserProxy(db)
  231. }
  232. }
  233. export function setupPluginUserInstance (
  234. pluginBaseInfo: LSPluginBaseInfo,
  235. pluginCaller: LSPluginCaller
  236. ) {
  237. return new LSPluginUser(pluginBaseInfo, pluginCaller)
  238. }
  239. if (window.__LSP__HOST__ == null) { // Entry of iframe mode
  240. debug('Entry of iframe mode.')
  241. const caller = new LSPluginCaller(null)
  242. window.logseq = setupPluginUserInstance({} as any, caller)
  243. }