LSPlugin.shadow.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import EventEmitter from 'eventemitter3'
  2. import { PluginLocal } from './LSPlugin.core'
  3. import { LSPluginUser } from './LSPlugin.user'
  4. // @ts-ignore
  5. const { importHTML, createSandboxContainer } = window.QSandbox || {}
  6. function userFetch(url, opts) {
  7. if (!url.startsWith('http')) {
  8. url = url.replace('file://', '')
  9. return new Promise(async (resolve, reject) => {
  10. try {
  11. const content = await window.apis.doAction(['readFile', url])
  12. resolve({
  13. text() {
  14. return content
  15. },
  16. })
  17. } catch (e) {
  18. console.error(e)
  19. reject(e)
  20. }
  21. })
  22. }
  23. return fetch(url, opts)
  24. }
  25. class LSPluginShadowFrame extends EventEmitter<'mounted' | 'unmounted'> {
  26. private _frame?: HTMLElement
  27. private _root?: ShadowRoot
  28. private _loaded = false
  29. private _unmountFns: Array<() => Promise<void>> = []
  30. constructor(private _pluginLocal: PluginLocal) {
  31. super()
  32. _pluginLocal._dispose(() => {
  33. this._unmount()
  34. })
  35. }
  36. async load() {
  37. const { name, entry } = this._pluginLocal.options
  38. if (this.loaded || !entry) return
  39. const { template, execScripts } = await importHTML(entry, {
  40. fetch: userFetch,
  41. })
  42. this._mount(template, document.body)
  43. const sandbox = createSandboxContainer(name, {
  44. elementGetter: () => this._root?.firstChild,
  45. })
  46. const global = sandbox.instance.proxy as any
  47. global.__shadow_mode__ = true
  48. global.LSPluginLocal = this._pluginLocal
  49. global.LSPluginShadow = this
  50. global.LSPluginUser = global.logseq = new LSPluginUser(
  51. this._pluginLocal.toJSON() as any,
  52. this._pluginLocal.caller!
  53. )
  54. // TODO: {mount, unmount}
  55. const execResult: any = await execScripts(global, true)
  56. this._unmountFns.push(execResult.unmount)
  57. this._loaded = true
  58. }
  59. _mount(content: string, container: HTMLElement) {
  60. const frame = (this._frame = document.createElement('div'))
  61. frame.classList.add('lsp-shadow-sandbox')
  62. frame.id = this._pluginLocal.id
  63. this._root = frame.attachShadow({ mode: 'open' })
  64. this._root.innerHTML = `<div>${content}</div>`
  65. container.appendChild(frame)
  66. this.emit('mounted')
  67. }
  68. _unmount() {
  69. for (const fn of this._unmountFns) {
  70. fn && fn.call(null)
  71. }
  72. }
  73. destroy() {
  74. this.frame?.parentNode?.removeChild(this.frame)
  75. }
  76. get loaded(): boolean {
  77. return this._loaded
  78. }
  79. get document() {
  80. return this._root?.firstChild as HTMLElement
  81. }
  82. get frame(): HTMLElement {
  83. return this._frame!
  84. }
  85. }
  86. export { LSPluginShadowFrame }