mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
Enhance: plugin APIs (#10399)
* enhance(plugin): call apis with the sdk ns * enhance(plugin): types * enhance(api): get value from the computed style * enhance(api): types * enhance(plugin): types * enhance(plugin): types * fix: lint * fix(apis): incorrect shortcut command registion for block editing mode #10392 * fix(api): types * enhance(apis): support register shortcuts with multi binding vals * fix(plugins): normalize command key to make the internal keyword legal * chore(plugin): build libs core * chore(plugin): bump version * enhance(apis): normalize apis cljs data * chore(plugin): update libs user sdk * chore(plugin): CHANGELOG.md * fix: typo * fix(ux): support querying plugins with right space chars
This commit is contained in:
@@ -4,8 +4,20 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.15]
|
||||
## [0.0.16]
|
||||
### Added
|
||||
- Support api of `logseq.UI.queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>`
|
||||
- Support api of `logseq.UI.queryElementById: (id: string) => Promise<string | boolean>`
|
||||
- Support api of `logseq.UI.checkSlotValid: (slot: UISlotIdentity['slot']) => Promise<boolean>`
|
||||
- Support api of `logseq.UI.resolveThemeCssPropsVals: (props: string | Array<string>) => Promise<any>`
|
||||
- Support api of `logseq.Assets.builtInOpen(path: string): Promise<boolean | undefined>`
|
||||
|
||||
### Fixed
|
||||
- fix Plugin can't register command shortcut with editing mode [#10392](https://github.com/logseq/logseq/issues/10392)
|
||||
- fix [Plugin API] [Keymap] Command without keybinding can't be present in Keymap [#10466](https://github.com/logseq/logseq/issues/10466)
|
||||
- fix [Possible DATA LOSS] [Plugin API] [Keymap] Any plugin could break the global config.edn [#10465](https://github.com/logseq/logseq/issues/10465)
|
||||
|
||||
## [0.0.15]
|
||||
### Added
|
||||
- Support a plug-in flag for the plugin slash commands item
|
||||
- Support api of `logseq.App.setCurrentGraphConfigs: (configs: {}) => Promise<void>`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@logseq/libs",
|
||||
"version": "0.0.15",
|
||||
"version": "0.0.16",
|
||||
"description": "Logseq SDK libraries",
|
||||
"main": "dist/lsplugin.user.js",
|
||||
"typings": "index.d.ts",
|
||||
|
||||
@@ -54,6 +54,7 @@ const DIR_PLUGINS = 'plugins'
|
||||
declare global {
|
||||
interface Window {
|
||||
LSPluginCore: LSPluginCore
|
||||
DOMPurify: typeof DOMPurify
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,23 +309,25 @@ function initProviderHandlers(pluginLocal: PluginLocal) {
|
||||
// provider:ui
|
||||
pluginLocal.on(_('ui'), (ui: UIOptions) => {
|
||||
pluginLocal._onHostMounted(() => {
|
||||
pluginLocal._dispose(
|
||||
setupInjectedUI.call(
|
||||
pluginLocal,
|
||||
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)
|
||||
}
|
||||
)
|
||||
const ret = setupInjectedUI.call(
|
||||
pluginLocal,
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
if (typeof ret === 'function') {
|
||||
pluginLocal._dispose(ret)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -346,8 +349,6 @@ function initApiProxyHandlers(pluginLocal: PluginLocal) {
|
||||
}
|
||||
}
|
||||
|
||||
const { _sync } = payload
|
||||
|
||||
if (pluginLocal.shadow) {
|
||||
if (payload.actor) {
|
||||
payload.actor.resolve(ret)
|
||||
@@ -355,6 +356,8 @@ function initApiProxyHandlers(pluginLocal: PluginLocal) {
|
||||
return
|
||||
}
|
||||
|
||||
const { _sync } = payload
|
||||
|
||||
if (_sync != null) {
|
||||
const reply = (result: any) => {
|
||||
pluginLocal.caller?.callUserModel(LSPMSG_SYNC, {
|
||||
@@ -430,7 +433,7 @@ class PluginLocal extends EventEmitter<
|
||||
|
||||
async _setupUserSettings(reload?: boolean) {
|
||||
const { _options } = this
|
||||
const logger = (this._logger = new PluginLogger('Loader'))
|
||||
const logger = (this._logger = new PluginLogger(`Loader:${this.debugTag}`))
|
||||
|
||||
if (_options.settings && !reload) {
|
||||
return
|
||||
@@ -532,7 +535,7 @@ class PluginLocal extends EventEmitter<
|
||||
const localRoot = (this._localRoot = safetyPathNormalize(url))
|
||||
const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
|
||||
|
||||
// Pick legal attrs
|
||||
// Pick legal attrs
|
||||
;[
|
||||
'name',
|
||||
'author',
|
||||
@@ -642,10 +645,10 @@ class PluginLocal extends EventEmitter<
|
||||
<meta charset="UTF-8">
|
||||
<title>logseq plugin entry</title>
|
||||
${
|
||||
IS_DEV
|
||||
? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
|
||||
: `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
|
||||
}
|
||||
IS_DEV
|
||||
? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
|
||||
: `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
|
||||
}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
@@ -866,7 +869,7 @@ class PluginLocal extends EventEmitter<
|
||||
|
||||
this._dispose(cleanInjectedScripts.bind(this))
|
||||
} catch (e) {
|
||||
this.logger?.error('[Load Plugin]', e, true)
|
||||
this.logger.error('load', e, true)
|
||||
|
||||
this.dispose().catch(null)
|
||||
this._status = PluginLocalLoadStatus.ERROR
|
||||
@@ -924,7 +927,7 @@ class PluginLocal extends EventEmitter<
|
||||
)
|
||||
this.emit('beforeunload', eventBeforeUnload)
|
||||
} catch (e) {
|
||||
this.logger.error('[beforeunload]', e)
|
||||
this.logger.error('beforeunload', e)
|
||||
}
|
||||
|
||||
await this.dispose()
|
||||
@@ -932,7 +935,7 @@ class PluginLocal extends EventEmitter<
|
||||
|
||||
this.emit('unloaded')
|
||||
} catch (e) {
|
||||
this.logger.error('[unload Error]', e)
|
||||
this.logger.error('unload', e)
|
||||
} finally {
|
||||
this._status = PluginLocalLoadStatus.UNLOADED
|
||||
}
|
||||
@@ -1038,7 +1041,7 @@ class PluginLocal extends EventEmitter<
|
||||
|
||||
get debugTag() {
|
||||
const name = this._options?.name
|
||||
return `#${this._id} ${name ?? ''}`
|
||||
return `#${this._id} - ${name ?? ''}`
|
||||
}
|
||||
|
||||
get localRoot(): string {
|
||||
@@ -1103,8 +1106,7 @@ class LSPluginCore
|
||||
| 'beforereload'
|
||||
| 'reloaded'
|
||||
>
|
||||
implements ILSPluginThemeManager
|
||||
{
|
||||
implements ILSPluginThemeManager {
|
||||
private _isRegistering = false
|
||||
private _readyIndicator?: DeferredActor
|
||||
private readonly _hostMountedActor: DeferredActor = deferred()
|
||||
@@ -1566,12 +1568,12 @@ class LSPluginCore
|
||||
await this.saveUserPreferences(
|
||||
theme.mode
|
||||
? {
|
||||
themes: {
|
||||
...this._userPreferences.themes,
|
||||
mode: theme.mode,
|
||||
[theme.mode]: theme,
|
||||
},
|
||||
}
|
||||
themes: {
|
||||
...this._userPreferences.themes,
|
||||
mode: theme.mode,
|
||||
[theme.mode]: theme,
|
||||
},
|
||||
}
|
||||
: { theme: theme }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ export interface BlockEntity {
|
||||
level?: number
|
||||
meta?: { timestamps: any; properties: any; startPos: number; endPos: number }
|
||||
title?: Array<any>
|
||||
marker?: string
|
||||
marker?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,9 +235,10 @@ export type BlockCursorPosition = {
|
||||
rect: DOMRect
|
||||
}
|
||||
|
||||
export type Keybinding = string | Array<string>
|
||||
export type SimpleCommandKeybinding = {
|
||||
mode?: 'global' | 'non-editing' | 'editing'
|
||||
binding: string
|
||||
binding: Keybinding
|
||||
mac?: string // special for Mac OS
|
||||
}
|
||||
|
||||
@@ -469,25 +470,6 @@ export interface IAppProxy {
|
||||
removeTemplate: (name: string) => Promise<any>
|
||||
insertTemplate: (target: BlockUUID, name: string) => Promise<any>
|
||||
|
||||
// ui
|
||||
queryElementById: (id: string) => Promise<string | boolean>
|
||||
|
||||
/**
|
||||
* @added 0.0.5
|
||||
* @param selector
|
||||
*/
|
||||
queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>
|
||||
|
||||
/**
|
||||
* @deprecated Use `logseq.UI.showMsg` instead
|
||||
* @param content
|
||||
* @param status
|
||||
*/
|
||||
showMsg: (
|
||||
content: string,
|
||||
status?: 'success' | 'warning' | 'error' | string
|
||||
) => void
|
||||
|
||||
setZoomFactor: (factor: number) => void
|
||||
setFullScreen: (flag: boolean | 'toggle') => void
|
||||
setLeftSidebarVisible: (flag: boolean | 'toggle') => void
|
||||
@@ -891,20 +873,16 @@ export type UIMsgOptions = {
|
||||
export type UIMsgKey = UIMsgOptions['key']
|
||||
|
||||
export interface IUIProxy {
|
||||
/**
|
||||
* @added 0.0.2
|
||||
*
|
||||
* @param content
|
||||
* @param status
|
||||
* @param opts
|
||||
*/
|
||||
showMsg: (
|
||||
content: string,
|
||||
status?: 'success' | 'warning' | 'error' | string,
|
||||
opts?: Partial<UIMsgOptions>
|
||||
) => Promise<UIMsgKey>
|
||||
|
||||
closeMsg: (key: UIMsgKey) => void
|
||||
queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>
|
||||
queryElementById: (id: string) => Promise<string | boolean>
|
||||
checkSlotValid: (slot: UISlotIdentity['slot']) => Promise<boolean>
|
||||
resolveThemeCssPropsVals: (props: string | Array<string>) => Promise<Record<string, string | undefined> | null>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -938,6 +916,13 @@ export interface IAssetsProxy {
|
||||
* @param path
|
||||
*/
|
||||
makeUrl(path: string): Promise<string>
|
||||
|
||||
/**
|
||||
* try to open asset type file in Logseq app
|
||||
* @added 0.0.16
|
||||
* @param path
|
||||
*/
|
||||
builtInOpen(path: string): Promise<boolean | undefined>
|
||||
}
|
||||
|
||||
export interface ILSPluginThemeManager {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
mergeSettingsWithSchema,
|
||||
PluginLogger,
|
||||
safeSnakeCase,
|
||||
safetyPathJoin,
|
||||
safetyPathJoin, normalizeKeyStr,
|
||||
} from './helpers'
|
||||
import { LSPluginCaller } from './LSPlugin.caller'
|
||||
import * as callableAPIs from './callable.apis'
|
||||
@@ -77,12 +77,21 @@ function registerSimpleCommand (
|
||||
},
|
||||
action: SimpleCommandCallback
|
||||
) {
|
||||
const { key, label, desc, palette, keybinding, extras } = opts
|
||||
|
||||
if (typeof action !== 'function') {
|
||||
this.logger.error(`${key || label}: command action should be function.`)
|
||||
return false
|
||||
}
|
||||
|
||||
const { key, label, desc, palette, keybinding, extras } = opts
|
||||
const eventKey = `SimpleCommandHook${key}${++registeredCmdUid}`
|
||||
const normalizedKey = normalizeKeyStr(key)
|
||||
|
||||
if (!normalizedKey) {
|
||||
this.logger.error(`${label}: command key is required.`)
|
||||
return false
|
||||
}
|
||||
|
||||
const eventKey = `SimpleCommandHook${normalizedKey}${++registeredCmdUid}`
|
||||
|
||||
this.Editor['on' + eventKey](action)
|
||||
|
||||
@@ -92,7 +101,7 @@ function registerSimpleCommand (
|
||||
this.baseInfo.id,
|
||||
// [cmd, action]
|
||||
[
|
||||
{ key, label, type, desc, keybinding, extras },
|
||||
{ key: normalizedKey, label, type, desc, keybinding, extras },
|
||||
['editor/hook', eventKey],
|
||||
],
|
||||
palette,
|
||||
@@ -171,7 +180,7 @@ const app: Partial<IAppProxy> = {
|
||||
|
||||
const { binding } = keybinding
|
||||
const group = '$shortcut$'
|
||||
const key = group + safeSnakeCase(binding)
|
||||
const key = opts.key || (group + safeSnakeCase(binding?.toString()))
|
||||
|
||||
return registerSimpleCommand.call(
|
||||
this,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SettingSchemaDesc, StyleString, UIOptions } from './LSPlugin'
|
||||
import { PluginLocal } from './LSPlugin.core'
|
||||
import * as nodePath from 'path'
|
||||
import DOMPurify from 'dompurify'
|
||||
import merge from 'deepmerge';
|
||||
import merge from 'deepmerge'
|
||||
import { snakeCase } from 'snake-case'
|
||||
import * as callables from './callable.apis'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
@@ -211,12 +211,23 @@ export function deferred<T = any>(timeout?: number, tag?: string) {
|
||||
|
||||
export function invokeHostExportedApi(method: string, ...args: Array<any>) {
|
||||
method = method?.startsWith('_call') ? method : method?.replace(/^[_$]+/, '')
|
||||
const method1 = safeSnakeCase(method)
|
||||
let method1 = safeSnakeCase(method)
|
||||
|
||||
// @ts-ignore
|
||||
const nsSDK = window.logseq?.sdk
|
||||
const supportedNS = nsSDK && Object.keys(nsSDK)
|
||||
let nsTarget = {}
|
||||
const ns0 = method1?.split('_')?.[0]
|
||||
|
||||
if (ns0 && supportedNS.includes(ns0)) {
|
||||
method1 = method1.replace(new RegExp(`^${ns0}_`), '')
|
||||
nsTarget = nsSDK?.[ns0]
|
||||
}
|
||||
|
||||
const logseqHostExportedApi = Object.assign(
|
||||
// @ts-ignore
|
||||
window.logseq?.api || {},
|
||||
callables
|
||||
{}, window.logseq?.api,
|
||||
nsTarget, callables
|
||||
)
|
||||
|
||||
const fn =
|
||||
@@ -266,9 +277,9 @@ export function setupInjectedStyle(
|
||||
el.textContent = style
|
||||
|
||||
attrs &&
|
||||
Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
|
||||
document.head.append(el)
|
||||
|
||||
@@ -313,7 +324,7 @@ export function setupInjectedUI(
|
||||
console.error(
|
||||
`${this.debugTag} can not resolve selector target ${selector}`
|
||||
)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if (ui.template) {
|
||||
@@ -344,22 +355,22 @@ export function setupInjectedUI(
|
||||
|
||||
// update attributes
|
||||
attrs &&
|
||||
Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
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
|
||||
}
|
||||
Object.entries(ui.style).forEach(([k, v]) => {
|
||||
if (
|
||||
positionDirty &&
|
||||
['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
el.style[k] = v
|
||||
})
|
||||
el.style[k] = v
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -379,14 +390,14 @@ export function setupInjectedUI(
|
||||
content.innerHTML = ui.template
|
||||
|
||||
attrs &&
|
||||
Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
|
||||
ui.style &&
|
||||
Object.entries(ui.style).forEach(([k, v]) => {
|
||||
el.style[k] = v
|
||||
})
|
||||
Object.entries(ui.style).forEach(([k, v]) => {
|
||||
el.style[k] = v
|
||||
})
|
||||
|
||||
let teardownUI: () => void
|
||||
let disposeFloat: () => void
|
||||
@@ -399,11 +410,11 @@ export function setupInjectedUI(
|
||||
el.classList.add('lsp-ui-float-container', 'visible')
|
||||
disposeFloat =
|
||||
(pl._setupResizableContainer(el, key),
|
||||
pl._setupDraggableContainer(el, {
|
||||
key,
|
||||
close: () => teardownUI(),
|
||||
title: attrs?.title,
|
||||
}))
|
||||
pl._setupDraggableContainer(el, {
|
||||
key,
|
||||
close: () => teardownUI(),
|
||||
title: attrs?.title,
|
||||
}))
|
||||
}
|
||||
|
||||
if (!!slot && ui.reset) {
|
||||
@@ -542,3 +553,8 @@ export function mergeSettingsWithSchema(
|
||||
// shadow copy
|
||||
return Object.assign(defaults, settings)
|
||||
}
|
||||
|
||||
export function normalizeKeyStr(s: string) {
|
||||
if (typeof s !== 'string') return
|
||||
return s.trim().replace(/\s/g, '_').toLowerCase()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user