Browse Source

Feat/moveable plugin UI container (#3045)

* improve(plugin): add container for main ui frame

* feat(plugin): support draggable & resizable UI container for main ui

* feat: support fork sub layout container

* improve(plugin): add editor selection api

* improve(plugin): click outside configure for float container

* improve(plugin): api of journal for create-page

* improve(plugin): api of open-in-right-sidebar

* improve(plugin): add full screen api

* improve(plugin): api of register-palette-command

* improve(plugin): add apis
Charlie 3 years ago
parent
commit
72c038e6fe

+ 1 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.0.1-alpha.29",
+  "version": "0.0.1-alpha.30",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",

+ 46 - 6
libs/src/LSPlugin.caller.ts

@@ -60,6 +60,7 @@ class LSPluginCaller extends EventEmitter {
     }
   }
 
+  // run in sandbox
   async connectToParent (userModel = {}) {
     if (this._connected) return
 
@@ -72,7 +73,14 @@ class LSPluginCaller extends EventEmitter {
     const readyDeferred = deferred(1000 * 5)
 
     const model: any = this._extendUserModel({
-      [LSPMSG_READY]: async () => {
+      [LSPMSG_READY]: async (baseInfo) => {
+        // dynamically setup common msg handler
+        model[LSPMSGFn(baseInfo?.pid)] = ({ type, payload }: { type: string, payload: any }) => {
+          debug(`[call from host (_call)] ${this._debugTag}`, type, payload)
+          // host._call without async
+          caller.emit(type, payload)
+        }
+
         await readyDeferred.resolve()
       },
 
@@ -87,7 +95,7 @@ class LSPluginCaller extends EventEmitter {
       },
 
       [LSPMSG]: async ({ ns, type, payload }: any) => {
-        debug(`[call from host] ${this._debugTag}`, ns, type, payload)
+        debug(`[call from host (async)] ${this._debugTag}`, ns, type, payload)
 
         if (ns && ns.startsWith('hook')) {
           caller.emit(`${ns}:${type}`, payload)
@@ -187,8 +195,8 @@ class LSPluginCaller extends EventEmitter {
     return this._callUserModel?.call(this, type, payload)
   }
 
+  // run in host
   async _setupIframeSandbox () {
-    const cnt = document.body
     const pl = this._pluginLocal!
     const id = pl.id
     const url = new URL(pl.options.entry!)
@@ -197,11 +205,30 @@ class LSPluginCaller extends EventEmitter {
       .set(`__v__`, IS_DEV ? Date.now().toString() : pl.options.version)
 
     // clear zombie sandbox
-    const zb = cnt.querySelector(`#${id}`)
+    const zb = document.querySelector(`#${id}`)
     if (zb) zb.parentElement.removeChild(zb)
 
+    const cnt = document.createElement('div')
+    cnt.classList.add('lsp-iframe-sandbox-container')
+    cnt.id = id
+
+    // TODO: apply any container layout data
+    {
+      const mainLayoutInfo = this._pluginLocal.settings.get('layout')?.[0]
+      if (mainLayoutInfo) {
+        cnt.dataset.inited_layout = 'true'
+        const { width, height, left, top } = mainLayoutInfo
+        Object.assign(cnt.style, {
+          width: width + 'px', height: height + 'px',
+          left: left + 'px', top: top + 'px'
+        })
+      }
+    }
+
+    document.body.appendChild(cnt)
+
     const pt = new Postmate({
-      id, container: cnt, url: url.href,
+      id: id + '_iframe', container: cnt, url: url.href,
       classListArray: ['lsp-iframe-sandbox'],
       model: { baseInfo: JSON.parse(JSON.stringify(pl.toJSON())) }
     })
@@ -310,10 +337,18 @@ class LSPluginCaller extends EventEmitter {
   }
 
   _getSandboxIframeContainer () {
-    return this._parent?.frame
+    return this._parent?.frame.parentNode as HTMLDivElement
   }
 
   _getSandboxShadowContainer () {
+    return this._shadow?.frame.parentNode as HTMLDivElement
+  }
+
+  _getSandboxIframeRoot () {
+    return this._parent?.frame
+  }
+
+  _getSandboxShadowRoot () {
     return this._shadow?.frame
   }
 
@@ -322,13 +357,18 @@ class LSPluginCaller extends EventEmitter {
   }
 
   async destroy () {
+    let root: HTMLElement = null
     if (this._parent) {
+      root = this._getSandboxIframeContainer()
       await this._parent.destroy()
     }
 
     if (this._shadow) {
+      root = this._getSandboxShadowContainer()
       this._shadow.destroy()
     }
+
+    root?.parentNode.removeChild(root)
   }
 }
 

+ 158 - 23
libs/src/LSPlugin.core.ts

@@ -26,11 +26,10 @@ import {
   LSPluginPkgConfig,
   StyleOptions,
   StyleString,
-  ThemeOptions, UIFrameAttrs,
+  ThemeOptions, UIContainerAttrs,
   UIOptions
 } from './LSPlugin'
 import { snakeCase } from 'snake-case'
-import DOMPurify from 'dompurify'
 
 const debug = Debug('LSPlugin:core')
 const DIR_PLUGINS = 'plugins'
@@ -175,13 +174,13 @@ function initMainUIHandlers (pluginLocal: PluginLocal) {
   const _ = (label: string): any => `main-ui:${label}`
 
   pluginLocal.on(_('visible'), ({ visible, toggle, cursor }) => {
-    const el = pluginLocal.getMainUI()
+    const el = pluginLocal.getMainUIContainer()
     el?.classList[toggle ? 'toggle' : (visible ? 'add' : 'remove')]('visible')
     // pluginLocal.caller!.callUserModel(LSPMSG, { type: _('visible'), payload: visible })
     // auto focus frame
     if (visible) {
       if (!pluginLocal.shadow && el) {
-        (el as HTMLIFrameElement).contentWindow?.focus()
+        (el.querySelector('iframe') as HTMLIFrameElement)?.contentWindow?.focus()
       }
     }
 
@@ -190,16 +189,38 @@ function initMainUIHandlers (pluginLocal: PluginLocal) {
     }
   })
 
-  pluginLocal.on(_('attrs'), (attrs: Partial<UIFrameAttrs>) => {
-    const el = pluginLocal.getMainUI()
+  pluginLocal.on(_('attrs'), (attrs: Partial<UIContainerAttrs>) => {
+    const el = pluginLocal.getMainUIContainer()
     Object.entries(attrs).forEach(([k, v]) => {
       el?.setAttribute(k, v)
+      if (k === 'draggable' && v) {
+        pluginLocal._dispose(
+          pluginLocal._setupDraggableContainer(el, {
+            title: pluginLocal.options.name,
+            close: () => {
+              pluginLocal.caller.call('sys:ui:visible', { toggle: true })
+            }
+          }))
+      }
+
+      if (k === 'resizable' && v) {
+        pluginLocal._dispose(
+          pluginLocal._setupResizableContainer(el))
+      }
     })
   })
 
   pluginLocal.on(_('style'), (style: Record<string, any>) => {
-    const el = pluginLocal.getMainUI()
+    const el = pluginLocal.getMainUIContainer()
+    const isInitedLayout = !!el.dataset.inited_layout
+
     Object.entries(style).forEach(([k, v]) => {
+      if (isInitedLayout && [
+        'left', 'top', 'bottom', 'right', 'width', 'height'
+      ].includes(k)) {
+        return
+      }
+
       el!.style[k] = v
     })
   })
@@ -247,10 +268,14 @@ function initProviderHandlers (pluginLocal: PluginLocal) {
 
       pluginLocal._dispose(
         setupInjectedUI.call(pluginLocal,
-          ui, {
+          ui, Object.assign({
             'data-ref': pluginLocal.id
-          })
-      )
+          }, ui.attrs || {}),
+          ({ el, float }) => {
+            if (!float) return
+            const identity = el.dataset.identity
+            pluginLocal.layoutCore.move_container_to_top(identity)
+          }))
     })
   })
 }
@@ -383,7 +408,7 @@ class PluginLocal
     }
   }
 
-  getMainUI (): HTMLElement | undefined {
+  getMainUIContainer (): HTMLElement | undefined {
     if (this.shadow) {
       return this.caller?._getSandboxShadowContainer()
     }
@@ -423,20 +448,23 @@ class PluginLocal
       throw new IllegalPluginPackageError(e.message)
     }
 
-    // Pick legal attrs
-    ['name', 'author', 'repository', 'version',
-      'description', 'repo', 'title', 'effect'
-    ].forEach(k => {
+    const localRoot = this._localRoot = safetyPathNormalize(url)
+    const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
+
+      // Pick legal attrs
+    ;['name', 'author', 'repository', 'version',
+      'description', 'repo', 'title', 'effect',
+    ].concat(!this.isInstalledInDotRoot ? ['devEntry'] : []).forEach(k => {
       this._options[k] = pkg[k]
     })
 
-    const localRoot = this._localRoot = safetyPathNormalize(url)
-    const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
-    const validateMain = (main) => main && /\.(js|html)$/.test(main)
+    const validateEntry = (main) => main && /\.(js|html)$/.test(main)
 
     // Entry from main
-    if (validateMain(pkg.main)) { // Theme has no main
-      this._options.entry = this._resolveResourceFullUrl(pkg.main, localRoot)
+    const entry = logseq.entry || logseq.main || pkg.main
+    if (validateEntry(entry)) { // Theme has no main
+      this._options.entry = this._resolveResourceFullUrl(entry, localRoot)
+      this._options.devEntry = logseq.devEntry
 
       if (logseq.mode) {
         this._options.mode = logseq.mode
@@ -489,8 +517,8 @@ class PluginLocal
   }
 
   async _tryToNormalizeEntry () {
-    let { entry, settings } = this.options
-    let devEntry = settings?.get('_devEntry')
+    let { entry, settings, devEntry } = this.options
+    devEntry = devEntry || settings?.get('_devEntry')
 
     if (devEntry) {
       this._options.entry = devEntry
@@ -548,6 +576,108 @@ class PluginLocal
     })
   }
 
+  _persistMainUILayoutData (e: { width: number, height: number, left: number, top: number }) {
+    const layouts = this.settings.get('layouts') || []
+    layouts[0] = e
+    this.settings.set('layout', layouts)
+  }
+
+  _setupDraggableContainer (
+    el: HTMLElement,
+    opts: Partial<{ key: string, title: string, close: () => void }> = {}): () => void {
+    const ds = el.dataset
+    if (ds.inited_draggable) return
+    if (!ds.identity) {
+      ds.identity = 'dd-' + genID()
+    }
+    const isInjectedUI = !!opts.key
+    const handle = document.createElement('div')
+    handle.classList.add('draggable-handle')
+
+    handle.innerHTML = `
+      <div class="th">
+        <div class="l"><h3>${opts.title || ''}</h3></div>
+        <div class="r">
+          <a class="button x"><i class="ti ti-x"></i></a>
+        </div>
+      </div>
+    `
+
+    handle.querySelector('.x')
+      .addEventListener('click', (e) => {
+        opts?.close?.()
+        e.stopPropagation()
+      }, false)
+
+    handle.addEventListener('mousedown', (e) => {
+      const target = e.target as HTMLElement
+      if (target?.closest('.r')) {
+        e.stopPropagation()
+        e.preventDefault()
+        return
+      }
+    }, false)
+
+    el.prepend(handle)
+
+    // move to top
+    el.addEventListener('mousedown', (e) => {
+      this.layoutCore.move_container_to_top(ds.identity)
+    }, true)
+
+    const setTitle = (title) => {
+      handle.querySelector('h3').textContent = title
+    }
+    const dispose = this.layoutCore.setup_draggable_container_BANG_(el,
+      !isInjectedUI ? this._persistMainUILayoutData.bind(this) : () => {})
+
+    ds.inited_draggable = 'true'
+
+    if (opts.title) {
+      setTitle(opts.title)
+    }
+
+    // click outside
+    let removeOutsideListener = null
+    if (ds.close === 'outside') {
+      const handler = (e) => {
+        const target = e.target
+        if (!el.contains(target)) {
+          opts.close()
+        }
+      }
+
+      document.addEventListener('click', handler, false)
+      removeOutsideListener = () => {
+        document.removeEventListener('click', handler)
+      }
+    }
+
+    return () => {
+      dispose()
+      removeOutsideListener?.()
+    }
+  }
+
+  _setupResizableContainer (el: HTMLElement, key?: string): () => void {
+    const ds = el.dataset
+    if (ds.inited_resizable) return
+    if (!ds.identity) {
+      ds.identity = 'dd-' + genID()
+    }
+    const handle = document.createElement('div')
+    handle.classList.add('resizable-handle')
+    el.prepend(handle)
+
+    // @ts-ignore
+    const layoutCore = window.frontend.modules.layout.core
+    const dispose = layoutCore.setup_resizable_container_BANG_(el,
+      !key ? this._persistMainUILayoutData.bind(this) : () => {})
+
+    ds.inited_resizable = 'true'
+    return dispose
+  }
+
   async load (readyIndicator?: DeferredActor) {
     if (this.pending) {
       return
@@ -580,7 +710,7 @@ class PluginLocal
       await this._caller.connectToChild()
 
       const readyFn = () => {
-        this._caller?.callUserModel(LSPMSG_READY)
+        this._caller?.callUserModel(LSPMSG_READY, { pid: this.id })
       }
 
       if (readyIndicator) {
@@ -690,6 +820,11 @@ class PluginLocal
     }
   }
 
+  get layoutCore (): any {
+    // @ts-ignore
+    return window.frontend.modules.layout.core
+  }
+
   get isInstalledInDotRoot () {
     const dotRoot = this.dotConfigRoot
     const plgRoot = this.localRoot

+ 60 - 6
libs/src/LSPlugin.ts

@@ -20,7 +20,7 @@ export type StyleOptions = {
   style: StyleString
 }
 
-export type UIFrameAttrs = {
+export type UIContainerAttrs = {
   draggable: boolean
   resizable: boolean
 
@@ -30,7 +30,10 @@ export type UIFrameAttrs = {
 export type UIBaseOptions = {
   key?: string
   replace?: boolean
-  template: string
+  template: string | null
+  style?: CSS.Properties
+  attrs?: Record<string, string>
+  close?: 'outside' | string
 }
 
 export type UIPathIdentity = {
@@ -51,14 +54,18 @@ export type UISlotOptions = UIBaseOptions & UISlotIdentity
 
 export type UIPathOptions = UIBaseOptions & UIPathIdentity
 
-export type UIOptions = UIPathOptions | UISlotOptions
+export type UIOptions = UIBaseOptions | UIPathOptions | UISlotOptions
 
 export interface LSPluginPkgConfig {
   id: PluginLocalIdentity
+  main: string
+  entry: string // alias of main
   title: string
   mode: 'shadow' | 'iframe'
   themes: Array<ThemeOptions>
   icon: string
+
+  [key: string]: any
 }
 
 export interface LSPluginBaseInfo {
@@ -167,6 +174,7 @@ export type SlashCommandActionCmd =
   | 'editor/clear-current-slash'
   | 'editor/restore-saved-cursor'
 export type SlashCommandAction = [cmd: SlashCommandActionCmd, ...args: any]
+export type SimpleCommandCallback = (e: IHookEvent) => void
 export type BlockCommandCallback = (e: IHookEvent & { uuid: BlockUUID }) => Promise<void>
 export type BlockCursorPosition = { left: number, top: number, height: number, pos: number, rect: DOMRect }
 
@@ -174,10 +182,28 @@ export type BlockCursorPosition = { left: number, top: number, height: number, p
  * App level APIs
  */
 export interface IAppProxy {
+  // base
   getUserInfo: () => Promise<AppUserInfo | null>
-
   getUserConfigs: () => Promise<AppUserConfigs>
 
+  // commands
+  registerCommand: (
+    type: string,
+    opts: {
+      key: string,
+      label: string,
+      desc?: string,
+      palette?: boolean
+    },
+    action: SimpleCommandCallback) => void
+
+  registerCommandPalette: (
+    opts: {
+      key: string,
+      label: string,
+    },
+    action: SimpleCommandCallback) => void
+
   // native
   relaunch: () => Promise<void>
   quit: () => Promise<void>
@@ -191,8 +217,10 @@ export interface IAppProxy {
   replaceState: (k: string, params?: Record<string, any>, query?: Record<string, any>) => void
 
   // ui
-  showMsg: (content: string, status?: 'success' | 'warning' | string) => void
+  queryElementById: (id: string) => string | boolean
+  showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string) => void
   setZoomFactor: (factor: number) => void
+  setFullScreen: (flag: boolean | 'toggle') => void
 
   registerUIItem: (
     type: 'toolbar' | 'pagebar',
@@ -208,7 +236,28 @@ export interface IAppProxy {
   onCurrentGraphChanged: IUserHook
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
   onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
+
+  /**
+   * provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`
+   *
+   * @example
+   * ```ts
+   * // e.g. {{renderer :h1, hello world, green}}
+   *
+   * logseq.App.onMacroRendererSlotted(({ slot, payload: { arguments } }) => {
+   *   let [type, text, color] = arguments
+   *   if (type !== ':h1') return
+   *    logseq.provideUI({
+   *      key: 'h1-playground',
+   *      slot, template: `
+   *       <h2 style="color: ${color || 'red'}">${text}</h2>
+   *      `,
+   *   })
+   * })
+   * ```
+   */
   onMacroRendererSlotted: IUserSlotHook<{ payload: { arguments: Array<string>, uuid: string, [key: string]: any } }>
+
   onPageHeadActionsSlotted: IUserSlotHook
   onRouteChanged: IUserHook<{ path: string, template: string }>
   onSidebarVisibleChanged: IUserHook<{ visible: boolean }>
@@ -328,7 +377,7 @@ export interface IEditorProxy extends Record<string, any> {
   createPage: (
     pageName: BlockPageName,
     properties?: {},
-    opts?: Partial<{ redirect: boolean, createFirstBlock: boolean, format: BlockEntity['format'] }>
+    opts?: Partial<{ redirect: boolean, createFirstBlock: boolean, format: BlockEntity['format'], journal: boolean }>
   ) => Promise<PageEntity | null>
 
   deletePage: (
@@ -369,6 +418,11 @@ export interface IEditorProxy extends Record<string, any> {
     pageName: BlockPageName,
     blockId: BlockIdentity
   ) => void
+
+  openInRightSidebar: (uuid: BlockUUID) => void
+
+  // events
+  onInputSelectionEnd: IUserHook<{ caret: any, point: { x: number, y: number }, start: number, end: number, text: string }>
 }
 
 /**

+ 54 - 10
libs/src/LSPlugin.user.ts

@@ -12,7 +12,7 @@ import {
   ThemeOptions,
   UIOptions, IHookEvent, BlockIdentity,
   BlockPageName,
-  UIFrameAttrs
+  UIContainerAttrs, SimpleCommandCallback
 } from './LSPlugin'
 import Debug from 'debug'
 import * as CSS from 'csstype'
@@ -30,7 +30,7 @@ const PROXY_CONTINUE = Symbol.for('proxy-continue')
 const debug = Debug('LSPlugin:user')
 
 /**
- * @param type
+ * @param type (key of group commands)
  * @param opts
  * @param action
  */
@@ -39,26 +39,43 @@ function registerSimpleCommand (
   type: string,
   opts: {
     key: string,
-    label: string
+    label: string,
+    desc?: string,
+    palette?: boolean
   },
-  action: BlockCommandCallback
+  action: SimpleCommandCallback
 ) {
   if (typeof action !== 'function') {
     return false
   }
 
-  const { key, label } = opts
+  const { key, label, desc, palette } = 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]]]
+    args: [this.baseInfo.id, [{ key, label, type, desc }, ['editor/hook', eventKey]], palette]
   })
 }
 
 const app: Partial<IAppProxy> = {
+  registerCommand: registerSimpleCommand,
+
+  registerCommandPalette (
+    opts: { key: string; label: string },
+    action: SimpleCommandCallback) {
+
+    const { key, label } = opts
+    const group = 'global-palette-command'
+
+    return registerSimpleCommand.call(
+      this, group,
+      { key, label, palette: true },
+      action)
+  },
+
   registerUIItem (
     type: 'toolbar' | 'pagebar',
     opts: { key: string, template: string }
@@ -89,6 +106,18 @@ const app: Partial<IAppProxy> = {
       type, {
         key, label
       }, action)
+  },
+
+  setFullScreen (flag) {
+    const sf = (...args) => this._callWin('setFullScreen', ...args)
+
+    if (flag === 'toggle') {
+      this._callWin('isFullScreen').then(r => {
+        r ? sf() : sf(true)
+      })
+    } else {
+      flag ? sf(true) : sf()
+    }
   }
 }
 
@@ -219,6 +248,12 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
   ) {
     super()
 
+    _caller.on('sys:ui:visible', (payload) => {
+      if (payload?.toggle) {
+        this.toggleMainUI()
+      }
+    })
+
     _caller.on('settings:changed', (payload) => {
       const b = Object.assign({}, this.settings)
       const a = Object.assign(this._baseInfo.settings, payload)
@@ -307,7 +342,7 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
     // TODO: update associated baseInfo settings
   }
 
-  setMainUIAttrs (attrs: Partial<UIFrameAttrs>): void {
+  setMainUIAttrs (attrs: Partial<UIContainerAttrs>): void {
     this.caller.call('main-ui:attrs', attrs)
   }
 
@@ -340,7 +375,7 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
   }
 
   get isMainUIVisible (): boolean {
-    const state = this._ui.get(0)
+    const state = this._ui.get(KEY_MAIN_UI)
     return Boolean(state && state.visible)
   }
 
@@ -405,14 +440,23 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
 
           // Call host
           return caller.callAsync(`api:call`, {
-            method: propKey,
-            args: args
+            tag, method: propKey, args: args
           })
         }
       }
     })
   }
 
+  /**
+   * @param args
+   */
+  _callWin (...args) {
+    return this._caller.callAsync(`api:call`, {
+      method: '_callMainWin',
+      args: args
+    })
+  }
+
   /**
    * The interface methods of {@link IAppProxy}
    */

+ 76 - 16
libs/src/helpers.ts

@@ -169,7 +169,8 @@ export function invokeHostExportedApi (
   method: string,
   ...args: Array<any>
 ) {
-  method = method?.replace(/^[_$]+/, '')
+  method = method?.startsWith('_call') ? method :
+    method?.replace(/^[_$]+/, '')
   const method1 = snakeCase(method)
 
   // @ts-ignore
@@ -229,33 +230,37 @@ export function setupInjectedStyle (
   }
 }
 
+const injectedUIEffects = new Map<string, () => void>()
+
 export function setupInjectedUI (
   this: PluginLocal,
   ui: UIOptions,
-  attrs: Record<string, any>
+  attrs: Record<string, string>,
+  initialCallback?: (e: { el: HTMLElement, float: boolean }) => void
 ) {
+  let slot: string = ''
+  let selector: string
+  let float: boolean
+
   const pl = this
-  let slot = ''
-  let selector = ''
+  const id = `${ui.key}-${slot}-${pl.id}`
+  const key = `${ui.key}-${pl.id}`
 
   if ('slot' in ui) {
     slot = ui.slot
     selector = `#${slot}`
-  } else {
+  } else if ('path' in ui) {
     selector = ui.path
+  } else {
+    float = true
   }
 
-  const target = selector && document.querySelector(selector)
+  const target = float ? document.body : (selector && document.querySelector(selector))
   if (!target) {
     console.error(`${this.debugTag} can not resolve selector target ${selector}`)
     return
   }
 
-  const id = `${ui.key}-${slot}-${pl.id}`
-  const key = `${ui.key}-${pl.id}`
-
-  let el = document.querySelector(`#${id}`) as HTMLElement
-
   if (ui.template) {
     // safe template
     ui.template = DOMPurify.sanitize(
@@ -264,10 +269,32 @@ export function setupInjectedUI (
         ALLOW_UNKNOWN_PROTOCOLS: true,
         ADD_ATTR: ['allow', 'src', 'allowfullscreen', 'frameborder', 'scrolling']
       })
+  } else { // remove ui
+    injectedUIEffects.get(key)?.call(null)
+    return
   }
 
-  if (el) {
-    el.innerHTML = ui.template
+  let el = document.querySelector(`#${id}`) as HTMLElement
+  let content = float ? el?.querySelector('.ls-ui-float-content') : el
+
+  if (content) {
+    content.innerHTML = ui.template
+
+    // update attributes
+    attrs && Object.entries(attrs).forEach(([k, v]) => {
+      el.setAttribute(k, v)
+    })
+
+    let positionDirty = el.dataset.dx != null
+    ui.style && Object.entries(ui.style).forEach(([k, v]) => {
+      if (positionDirty && [
+        'left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
+      ) {
+        return
+      }
+
+      el.style[k] = v
+    })
     return
   }
 
@@ -275,13 +302,38 @@ export function setupInjectedUI (
   el.id = id
   el.dataset.injectedUi = key || ''
 
-  // TODO: Support more
-  el.innerHTML = ui.template
+  if (float) {
+    content = document.createElement('div')
+    content.classList.add('ls-ui-float-content')
+    el.appendChild(content)
+  } else {
+    content = el
+  }
+
+  // TODO: enhance template
+  content.innerHTML = ui.template
 
   attrs && Object.entries(attrs).forEach(([k, v]) => {
     el.setAttribute(k, v)
   })
 
+  ui.style && Object.entries(ui.style).forEach(([k, v]) => {
+    el.style[k] = v
+  })
+
+  let teardownUI: () => void
+  let disposeFloat: () => void
+
+  if (float) {
+    el.setAttribute('draggable', 'true')
+    el.setAttribute('resizable', 'true')
+    ui.close && (el.dataset.close = ui.close)
+    el.classList.add('lsp-ui-float-container', 'visible')
+    disposeFloat = (
+      pl._setupResizableContainer(el, key),
+        pl._setupDraggableContainer(el, { key, close: () => teardownUI(), title: attrs?.title }))
+  }
+
   target.appendChild(el);
 
   // TODO: How handle events
@@ -297,9 +349,17 @@ export function setupInjectedUI (
     }, false)
   })
 
-  return () => {
+  // callback
+  initialCallback?.({ el, float })
+
+  teardownUI = () => {
+    disposeFloat?.()
+    injectedUIEffects.delete(key)
     target!.removeChild(el)
   }
+
+  injectedUIEffects.set(key, teardownUI)
+  return teardownUI
 }
 
 export function transformableEvent (target: HTMLElement, e: Event) {

+ 3 - 4
libs/src/postmate/index.ts

@@ -224,8 +224,7 @@ export class ChildAPI {
       // Reply to Parent
       resolveValue(this.model, property)
         .then(value => {
-          // @ts-ignore
-          e.source.postMessage({
+          (e.source as WindowProxy).postMessage({
             property,
             postmate: 'reply',
             type: messageType,
@@ -383,7 +382,7 @@ export class Model {
    */
   sendHandshakeReply () {
     return new Promise((resolve, reject) => {
-      const shake = (e) => {
+      const shake = (e: MessageEvent<any>) => {
         if (!e.data.postmate) {
           return
         }
@@ -395,7 +394,7 @@ export class Model {
           if (process.env.NODE_ENV !== 'production') {
             log('Child: Sending handshake reply to Parent')
           }
-          e.source.postMessage({
+          (e.source as WindowProxy).postMessage({
             postmate: 'handshake-reply',
             type: messageType,
           }, e.origin)

File diff suppressed because it is too large
+ 0 - 1
resources/js/lsplugin.core.js


+ 10 - 0
resources/js/preload.js

@@ -150,6 +150,16 @@ contextBridge.exposeInMainWorld('apis', {
     return await ipcRenderer.invoke('call-application', type, ...args)
   },
 
+  /**
+   * internal
+   * @param type
+   * @param args
+   * @private
+   */
+  async _callMainWin (type, ...args) {
+    return await ipcRenderer.invoke('call-main-win', type, ...args)
+  },
+
   getFilePathFromClipboard,
 
   setZoomFactor (factor) {

+ 11 - 1
src/electron/electron/core.cljs

@@ -51,6 +51,7 @@
                                           {:plugins                 true ; pdf
                                            :nodeIntegration         false
                                            :nodeIntegrationInWorker false
+                                           :webSecurity             (not dev?)
                                            :contextIsolation        true
                                            :spellcheck              ((fnil identity true) (cfgs/get-item :spell-check))
                                            ;; Remove OverlayScrollbars and transition `.scrollbar-spacing`
@@ -171,6 +172,7 @@
   [^js win]
   (let [toggle-win-channel "toggle-max-or-min-active-win"
         call-app-channel "call-application"
+        call-win-channel "call-main-win"
         export-publish-assets "export-publish-assets"
         quit-dirty-state "set-quit-dirty-state"
         web-contents (. win -webContents)]
@@ -196,6 +198,13 @@
                (fn [_ type & args]
                  (try
                    (js-invoke app type args)
+                   (catch js/Error e
+                     (js/console.error e)))))
+
+      (.handle call-win-channel
+               (fn [_ type & args]
+                 (try
+                   (js-invoke @*win type args)
                    (catch js/Error e
                      (js/console.error e))))))
 
@@ -243,7 +252,8 @@
     #(do (.removeHandler ipcMain toggle-win-channel)
          (.removeHandler ipcMain export-publish-assets)
          (.removeHandler ipcMain quit-dirty-state)
-         (.removeHandler ipcMain call-app-channel))))
+         (.removeHandler ipcMain call-app-channel)
+         (.removeHandler ipcMain call-win-channel))))
 
 (defn- destroy-window!
   [^js win]

+ 6 - 4
src/main/frontend/components/command_palette.cljs

@@ -8,7 +8,8 @@
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [clojure.string :as string]))
 
 (defn translate [t {:keys [id desc]}]
   (when id
@@ -24,14 +25,15 @@
   [{:keys [id shortcut] :as cmd} chosen?]
   (let [first-shortcut (first (str/split shortcut #" \| "))]
     (rum/with-context [[t] i18n/*tongue-context*]
-     (let [desc (translate t cmd)]
-       [:div.inline-grid.grid-cols-4.gap-x-4.w-full
+                      (let [desc (translate t cmd)]
+                        [:div.inline-grid.grid-cols-4.gap-x-4.w-full
         {:class (when chosen? "chosen")}
         [:span.col-span-3 desc]
         [:div.col-span-1.justify-end.tip.flex
          (when (and (keyword? id) (namespace id))
            [:code.opacity-20.bg-transparent (namespace id)])
-         [:code.ml-1 first-shortcut]]]))))
+         (when-not (string/blank? first-shortcut)
+           [:code.ml-1 first-shortcut])]]))))
 
 (rum/defcs command-palette <
   (shortcut/disable-all-shortcuts)

+ 138 - 10
src/main/frontend/components/plugins.css

@@ -1,3 +1,7 @@
+:root {
+  --ls-draggable-handle-height: 30px;
+}
+
 .cp__plugins {
   &-page {
     > h1 {
@@ -286,22 +290,146 @@
   }
 }
 
-.lsp-iframe-sandbox, .lsp-shadow-sandbox {
+.lsp-iframe-sandbox, .lsp-shadow-sandbox, .lsp-ui-float {
+  height: 100%;
+  width: 100%;
   position: absolute;
   top: 0;
   left: 0;
-  z-index: -1;
-  visibility: hidden;
-  height: 0;
-  width: 0;
-  padding: 0;
+  right: 0;
+  bottom: 0;
   margin: 0;
+  padding: 0;
+
+  &-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: -1;
+    visibility: hidden;
+    display: none;
+    height: 0;
+    width: 0;
+    padding: 0;
+    margin: 0;
+    border:2px solid var(--ls-border-color);
+
+    &.visible {
+      z-index: var(--ls-z-index-level-2);
+      width: 100%;
+      height: 100%;
+      visibility: visible;
+      display: block;
+    }
+
+    &[draggable=true] {
+      -webkit-user-drag: none;
+
+      > .draggable-handle {
+        display: block;
+        height: var(--ls-draggable-handle-height);
+        cursor: move;
+        user-select: none;
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        overflow: hidden;
+
+        > .th {
+          display: flex;
+          align-items: center;
+          height: var(--ls-draggable-handle-height);
+          user-select: none;
+          position: relative;
+          background-color: var(--ls-secondary-background-color);
+          color: var(--ls-primary-text-color);
+
+          > .l {
+            flex-basis: 80%;
+          }
+
+          > .r {
+            position: absolute;
+            right: 0;
+            top: 0;
+          }
+
+          h3 {
+            padding: 0 5px;
+            white-space: nowrap;
+            max-width: 60%;
+            overflow: hidden;
+            -webkit-line-clamp: 1;
+            text-overflow: ellipsis;
+          }
+
+          a.button {
+            &:hover {
+              background-color: transparent;
+            }
+          }
+        }
+      }
+
+      .lsp-iframe-sandbox,
+      .lsp-shadow-sandbox,
+      .ls-ui-float-content {
+        height: calc(100% - var(--ls-draggable-handle-height));
+        width: 100%;
+        margin-top: var(--ls-draggable-handle-height);
+        overflow: auto;
+      }
+
+      .ls-ui-float-content {
+        user-select: text;
+      }
+
+      &.is-dragging {
+        /*height: var(--ls-draggable-handle-height) !important;*/
+        overflow: hidden;
+        opacity: .7;
+
+        > .draggable-handle {
+          background-color: rgba(0, 0, 0, .1);
+          height: 100%;
+          z-index: 3;
+        }
+      }
+    }
+
+    &[resizable=true] {
+      > .resizable-handle {
+        position: absolute;
+        bottom: -1px;
+        right: -1px;
+        height: 15px;
+        width: 15px;
+        z-index: 2;
+        opacity: 0;
+        cursor: nwse-resize;
+        user-select: none;
+      }
+
+      &.is-resizing {
+        > .resizable-handle {
+          width: 90%;
+          height: 80%;
+        }
+      }
+    }
+  }
+}
+
+.lsp-ui-float-container {
+  top: 40%;
+  left: 30%;
+
+  .draggable-handle {
+  }
 
   &.visible {
-    z-index: 1;
-    width: 100%;
-    height: 100%;
-    visibility: visible;
+    height: unset;
   }
 }
 

+ 5 - 1
src/main/frontend/core.cljs

@@ -15,7 +15,11 @@
   []
   (rfe/start!
    (rf/router routes/routes nil)
-   route/set-route-match!
+   (fn [route]
+     (route/set-route-match! route)
+     (plugin-handler/hook-plugin-app
+      :route-changed (select-keys route [:template :path :parameters])))
+
    ;; set to false to enable HistoryAPI
    {:use-fragment true}))
 

+ 18 - 3
src/main/frontend/handler/command_palette.cljs

@@ -12,10 +12,11 @@
                             ;; action fn expects zero number of arities
                             (fn [action] (zero? (.-length action)))))
 (s/def :command/shortcut string?)
+(s/def :command/tag vector?)
 
 (s/def :command/command
   (s/keys :req-un [:command/id :command/desc :command/action]
-          :opt-un [:command/shortcut]))
+          :opt-un [:command/shortcut :command/tag]))
 
 (defn global-shortcut-commands []
   (->> [:shortcut.handler/editor-global
@@ -31,8 +32,13 @@
   (->> (get @state/state :command-palette/commands)
        (sort-by :id)))
 
-(defn history []
-  (or (storage/get "commands-history") []))
+(defn get-commands-unique []
+  (reduce #(assoc %1 (:id %2) %2) {}
+          (get @state/state :command-palette/commands)))
+
+(defn history
+  ([] (or (storage/get "commands-history") []))
+  ([vals] (storage/set "commands-history" vals)))
 
 (defn- assoc-invokes [cmds]
   (let [invokes (->> (history)
@@ -82,6 +88,15 @@
                                     :id  id})
       (state/set-state! :command-palette/commands (conj cmds command)))))
 
+(defn unregister
+  [id]
+  (let [id (keyword id)
+        cmds (get-commands-unique)]
+    (when (contains? cmds id)
+      (state/set-state! :command-palette/commands (vals (dissoc cmds id)))
+      ;; clear history
+      (history (filter #(not= (:id %) id) (history))))))
+
 (defn register-global-shortcut-commands []
   (let [cmds (global-shortcut-commands)]
     (doseq [cmd cmds] (register cmd))))

+ 6 - 5
src/main/frontend/handler/editor.cljs

@@ -151,11 +151,12 @@
   [block-id]
   (when block-id
     (when-let [block (db/pull [:block/uuid block-id])]
-      (state/sidebar-add-block!
-       (state/get-current-repo)
-       (:db/id block)
-       :block
-       block))))
+      (let [page? (nil? (:block/page block))]
+        (state/sidebar-add-block!
+          (state/get-current-repo)
+          (:db/id block)
+          (if page? :page :block)
+          block)))))
 
 (defn reset-cursor-range!
   [node]

+ 4 - 0
src/main/frontend/handler/events.cljs

@@ -20,6 +20,7 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.commands :as commands]
             [frontend.spec :as spec]
             [frontend.state :as state]
             [frontend.ui :as ui]
@@ -207,6 +208,9 @@
 (defmethod handle :instrument [[_ {:keys [type payload]}]]
   (posthog/capture type payload))
 
+(defmethod handle :exec-plugin-cmd [[_ {:keys [type key pid cmd action]}]]
+  (commands/exec-plugin-simple-command! pid cmd action))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 10 - 1
src/main/frontend/handler/plugin.cljs

@@ -188,6 +188,15 @@
   [pid]
   (swap! state/state md/dissoc-in [:plugin/installed-commands (keyword pid)]))
 
+(defn simple-cmd->palette-cmd
+  [pid {:keys [key label type desc] :as cmd} action]
+  (let [palette-cmd {:id     (keyword (str "plugin." pid "/" key))
+                     :desc   (or desc label)
+                     :action (fn []
+                               (state/pub-event!
+                                 [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
+    palette-cmd))
+
 (defn register-plugin-simple-command
   ;; action => [:action-key :event-key]
   [pid {:keys [key label type] :as cmd} action]
@@ -340,7 +349,7 @@
             clear-commands! (fn [pid]
                               ;; commands
                               (unregister-plugin-slash-command pid)
-                              (unregister-plugin-simple-command pid)
+                              (invoke-exported-api "unregister_plugin_simple_command" pid)
                               (unregister-plugin-ui-items pid))
 
             _ (doto js/LSPluginCore

+ 1 - 3
src/main/frontend/handler/route.cljs

@@ -2,7 +2,6 @@
   (:require [clojure.string :as string]
             [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.search :as search-handler]
             [frontend.state :as state]
@@ -112,8 +111,7 @@
       (jump-to-anchor! anchor)
       (util/scroll-to (util/app-scroll-container-node)
                       (state/get-saved-scroll-position)
-                      false))
-    (plugin-handler/hook-plugin-app :route-changed (select-keys route [:template :path :parameters]))))
+                      false))))
 
 (defn go-to-search!
   [search-mode]

+ 114 - 0
src/main/frontend/modules/layout/core.cljs

@@ -0,0 +1,114 @@
+(ns frontend.modules.layout.core
+  (:require [cljs-bean.core :as bean]
+            [frontend.util :as frontend-utils]))
+
+(defonce *movable-containers (atom {}))
+
+(defn- calc-layout-data
+  [^js cnt ^js evt]
+  (.toJSON (.getBoundingClientRect cnt)))
+
+(defn ^:export move-container-to-top
+  [identity]
+  (when-let [^js/HTMLElement container (and (> (count @*movable-containers) 1)
+                                            (get @*movable-containers identity))]
+    (let [zdx (->> @*movable-containers
+                   (map (fn [[_ ^js el]]
+                          (let [^js c (js/getComputedStyle el)
+                                v1 (.-visibility c)
+                                v2 (.-display c)]
+                            (when-let [z (and (= "visible" v1)
+                                              (not= "none" v2)
+                                              (.-zIndex c))]
+                              z))))
+                   (remove nil?))
+          zdx (bean/->js zdx)
+          zdx (and zdx (js/Math.max.apply nil zdx))
+          zdx' (frontend-utils/safe-parse-int (.. container -style -zIndex))]
+
+      (when (or (nil? zdx') (not= zdx zdx'))
+        (set! (.. container -style -zIndex) (inc zdx))))))
+
+(defn ^:export setup-draggable-container!
+  [^js/HTMLElement el callback]
+  (when-let [^js/HTMLElement handle (.querySelector el ".draggable-handle")]
+    (let [^js cls (.-classList el)
+          ^js ds (.-dataset el)
+          identity (.-identity ds)
+          ing? "is-dragging"]
+
+      ;; draggable
+      (-> (js/interact handle)
+          (.draggable
+            (bean/->js
+              {:listeners
+               {:move (fn [^js/MouseEvent e]
+                        (let [^js dset (.-dataset el)
+                              dx (.-dx e)
+                              dy (.-dy e)
+                              dx' (frontend-utils/safe-parse-float (.-dx dset))
+                              dy' (frontend-utils/safe-parse-float (.-dy dset))
+                              x (+ dx (if dx' dx' 0))
+                              y (+ dy (if dy' dy' 0))]
+
+                          ;; update container position
+                          (set! (.. el -style -transform) (str "translate(" x "px, " y "px)"))
+
+                          ;; cache dx dy
+                          (set! (.. el -dataset -dx) x)
+                          (set! (.. el -dataset -dy) y)))}}))
+          (.on "dragstart" (fn [] (.add cls ing?)))
+          (.on "dragend" (fn [e]
+                           (.remove cls ing?)
+                           (callback (bean/->js (calc-layout-data el e))))))
+      ;; manager
+      (swap! *movable-containers assoc identity el)
+
+      #(swap! *movable-containers dissoc identity el))))
+
+(defn ^:export setup-resizable-container!
+  [^js/HTMLElement el callback]
+  (let [^js cls (.-classList el)
+        ^js ds (.-dataset el)
+        identity (.-identity ds)
+        ing? "is-resizing"]
+
+    ;; resizable
+    (-> (js/interact el)
+        (.resizable
+          (bean/->js
+            {:edges
+             {:left true :top true :bottom true :right true}
+
+             :listeners
+             {:start (fn [] (.add cls ing?))
+              :end   (fn [e] (.remove cls ing?) (callback (bean/->js (calc-layout-data el e))))
+              :move  (fn [^js/MouseEvent e]
+                       (let [^js dset (.-dataset el)
+                             w (.. e -rect -width)
+                             h (.. e -rect -height)
+
+                             ;; update position from top/left
+                             dx (.. e -deltaRect -left)
+                             dy (.. e -deltaRect -top)
+
+                             dx' (frontend-utils/safe-parse-float (.-dx dset))
+                             dy' (frontend-utils/safe-parse-float (.-dy dset))
+
+                             x (+ dx (if dx' dx' 0))
+                             y (+ dy (if dy' dy' 0))]
+
+                         ;; update container position
+                         (set! (.. el -style -transform) (str "translate(" x "px, " y "px)"))
+
+                         ;; update container size
+                         (set! (.. el -style -width) (str w "px"))
+                         (set! (.. el -style -height) (str h "px"))
+
+                         (set! (. dset -dx) x)
+                         (set! (. dset -dy) y)))}})))
+
+    ;; manager
+    (swap! *movable-containers assoc identity el)
+
+    #(swap! *movable-containers dissoc identity el)))

+ 19 - 2
src/main/frontend/ui.cljs

@@ -9,6 +9,9 @@
             [frontend.state :as state]
             [frontend.ui.date-picker]
             [frontend.util :as util]
+            [frontend.util.cursor :as cursor]
+            [frontend.handler.plugin :as plugin-handler]
+            [cljs-bean.core :as bean]
             [goog.dom :as gdom]
             [promesa.core :as p]
             [goog.object :as gobj]
@@ -32,7 +35,21 @@
 (def Tippy (r/adapt-class (gobj/get react-tippy "Tooltip")))
 (def ReactTweetEmbed (r/adapt-class react-tweet-embed))
 
-(rum/defc ls-textarea < rum/reactive
+(rum/defc ls-textarea
+  < rum/reactive
+  {:did-mount (fn [state]
+                (let [^js el (rum/dom-node state)]
+                  (. el addEventListener "mouseup"
+                     #(let [start (.-selectionStart el)
+                            end (.-selectionEnd el)]
+                        (when-let [e (and (not= start end)
+                                          {:caret (cursor/get-caret-pos el)
+                                           :start start :end end
+                                           :text  (. (.-value el) substring start end)
+                                           :point {:x (.-x %) :y (.-y %)}})]
+
+                          (plugin-handler/hook-plugin-editor :input-selection-end (bean/->js e))))))
+                state)}
   [{:keys [on-change] :as props}]
   (let [skip-composition? (or
                            (state/sub :editor/show-page-search?)
@@ -506,7 +523,7 @@
                    (state/close-settings!))
         modal-panel-content (or modal-panel-content (fn [close] [:div]))]
     [:div.ui__modal
-     {:style {:z-index (if show? 100 -1)}}
+     {:style {:z-index (if show? 9999 -1)}}
      (css-transition
       {:in show? :timeout 0}
       (fn [state]

+ 1 - 1
src/main/frontend/ui.css

@@ -45,7 +45,7 @@
 
 .ui__notifications {
   position: fixed;
-  z-index: 99;
+  z-index: var(--ls-z-index-level-4);
   width: 100%;
   top: 3.2em;
   pointer-events: none;

+ 28 - 5
src/main/logseq/api.cljs

@@ -23,6 +23,7 @@
             [frontend.handler.plugin :as plugin-handler]
             [frontend.modules.outliner.core :as outliner]
             [frontend.modules.outliner.tree :as outliner-tree]
+            [frontend.handler.command-palette :as palette-handler]
             [electron.listener :as el]
             [frontend.state :as state]
             [frontend.util :as util]
@@ -32,7 +33,8 @@
             [medley.core :as medley]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
-            [sci.core :as sci]))
+            [sci.core :as sci]
+            [frontend.modules.layout.core]))
 
 ;; helpers
 (defn- normalize-keyword-for-json
@@ -227,10 +229,21 @@
                               (rest %)) actions)]))))
 
 (def ^:export register_plugin_simple_command
-  (fn [pid ^js cmd-action]
+  (fn [pid ^js cmd-action palette?]
     (when-let [[cmd action] (bean/->clj cmd-action)]
-      (plugin-handler/register-plugin-simple-command
-        pid cmd (assoc action 0 (keyword (first action)))))))
+      (let [action (assoc action 0 (keyword (first action)))]
+        (plugin-handler/register-plugin-simple-command pid cmd action)
+        (when-let [palette-cmd (and palette? (plugin-handler/simple-cmd->palette-cmd pid cmd action))]
+          (palette-handler/register palette-cmd))))))
+
+(defn ^:export unregister_plugin_simple_command
+  [pid]
+  (plugin-handler/unregister-plugin-simple-command pid)
+  (let [palette-matched (->> (palette-handler/get-commands)
+                             (filter #(string/includes? (str (:id %)) (str "plugin." pid))))]
+    (when (seq palette-matched)
+      (doseq [cmd palette-matched]
+        (palette-handler/unregister (:id cmd))))))
 
 (def ^:export register_plugin_ui_item
   (fn [pid type ^js opts]
@@ -328,10 +341,11 @@
     (some-> (if-let [page (db-model/get-page name)]
               page
               (let [properties (bean/->clj properties)
-                    {:keys [redirect createFirstBlock format]} (bean/->clj opts)
+                    {:keys [redirect createFirstBlock format journal]} (bean/->clj opts)
                     name (page-handler/create!
                            name
                            {:redirect?           (if (boolean? redirect) redirect true)
+                            :journal?            journal
                             :create-first-block? (if (boolean? createFirstBlock) createFirstBlock true)
                             :format              format
                             :properties          properties})]
@@ -348,6 +362,10 @@
 (def ^:export rename_page
   page-handler/rename!)
 
+(defn ^:export open_in_right_sidebar
+  [block-uuid]
+  (editor-handler/open-block-in-sidebar! (medley/uuid block-uuid)))
+
 (def ^:export edit_block
   (fn [block-uuid {:keys [pos] :or {pos :max} :as opts}]
     (when-let [block-uuid (and block-uuid (medley/uuid block-uuid))]
@@ -523,6 +541,11 @@
                           content (if hiccup? (parse-hiccup-ui content) content)]
                       (notification/show! content (keyword status)))))
 
+(defn ^:export query_element_by_id
+  [id]
+  (let [^js el (gdom/getElement id)]
+    (if el (str (.-tagName el) "#" id) false)))
+
 (defn ^:export force_save_graph
   []
   (p/let [_ (el/persist-dbs!)

Some files were not shown because too many files changed in this diff