mirror of https://github.com/logseq/logseq
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 apispull/3154/head
parent
5605170cf9
commit
72c038e6fe
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 one or more lines are too long
|
@ -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) {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}))
|
||||
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)))
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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!)
|
||||
|
|
Loading…
Reference in New Issue