mirror of
https://github.com/logseq/logseq.git
synced 2026-05-01 09:26:28 +00:00
Enhance/more ns plugin api (#4828)
* improve(plugin): WIP add settings schema * improve(plugin): add identity for settings modal * improve(plugin): WIP add settings input * fix(ui): scrollbar overlay of modal panel content * improve(plugin): WIP add more render types of setting item * improve(plugin): WIP polish settings items * improve(plugin): WIP settings list of plugins * improve(plugin): more settings types & polish releated ui * fix(plugin): sometimes disable plugin not work * improve(plugin): polish ui of plugin settings * fix(dev): warning of lint * improve(plugin): add api of settings changed * chore: build libs core * fix(ui): width of settings panel wrap * improve(plugin): separate layouts data from settings aio file * imporve(plugin): container size of single plugin settings * fix: add missing state * improve(plugin): add Git ns * improve(plugin): git related api * improve(api): type of git result * chore: build libs core * fix(dev): kondo lint * fix(plugin): use cdn sdk when js entry * chore: build libs core * fix(plugin): env condition * improve(plugin): add UI ns * fix(api): arguments of datascript query * enhance(api): manageable message instance of UI tools * enhance(api): WIP add experiments api * enhance(api): WIP add resources state of plugin * improve(plugin): add status of loading script resources * improve(plugin): more opts for script loader * improve(plugin): WIP add fenced code renderer hook * improve(plugin): fenced code renderer hook * fix(plugin): resource root path of plugin fs location * imporve(plugin): support local files for loading scripts * improve(plugin): types of expirements api * fix: typo of class * enhance(api): add namespace related apis * enhance(api): add linked refrences related apis * enhance(plugin): add sample links to related api comments * improve(plugin): add db changed hook & optimize strategy of caller for hooks * improve(plugin): compatible commands registration for old sdk * improve(plugin): collect user sdk version for plugin local * improve(plugin): add internal callable apis for user sdk * chore(plugin): missing files & bump libs version * improve(plugin): compatiable for old sdk about hook messaging optimization * improve(plugin): db hook optimization for old sdk * enhance(ux): auto focus searchbar when open plugins list * improve(plugin): api of a hook from specific block changed event * improve(plugin): api of db block change hook * improve(plugin): add show bracket user config of api * improve(plugin): api of db block change hook * fix(api): toggle collapsed of block * improve(api): try to init grpah with git before exec git commands * improve(plugin): attributes of sandbox container * improve(dev): support register command with keybinding * improve(plugin): add api of register shortcut command * fix(plugin): reubild slash commands when new command registration * fix(dev): lint * improve(dev): lint script of libs codebase * chore(dev): remove useless codes * improve(plugin):sanitize path string of plugin repo value * fix(plugin): rebuild commands list when unregister a plugin * fix(ui): overflow width of query result table * chore: rebuild libs core * improve(plugin): add assets related apis * chore: rebuild libs core * improve(plugin): support replace state of into block in page api * improve(plugin): prepend/append child block in page * improve(plugin): polished exceptions message of plugin update/install * fix(plugin): update settings within gui * improve(ux): debounce change event of input for plugin settings gui * chore: rebuild libs core * enhance(plugin): catch exception of hook plugin
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
import { SettingSchemaDesc, StyleString, UIOptions } from './LSPlugin'
|
||||
import { PluginLocal } from './LSPlugin.core'
|
||||
import { snakeCase } from 'snake-case'
|
||||
import * as nodePath from 'path'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { merge } from 'lodash-es'
|
||||
|
||||
interface IObject {
|
||||
[key: string]: any;
|
||||
}
|
||||
import { snakeCase } from 'snake-case'
|
||||
import * as callables from './callable.apis'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -16,7 +13,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export const path = navigator.platform.toLowerCase() === 'win32' ? nodePath.win32 : nodePath.posix
|
||||
export const path =
|
||||
navigator.platform.toLowerCase() === 'win32' ? nodePath.win32 : nodePath.posix
|
||||
export const IS_DEV = process.env.NODE_ENV === 'development'
|
||||
export const PROTOCOL_FILE = 'file://'
|
||||
export const PROTOCOL_LSP = 'lsp://'
|
||||
@@ -24,17 +22,21 @@ export const URL_LSP = PROTOCOL_LSP + 'logseq.io/'
|
||||
|
||||
let _appPathRoot
|
||||
|
||||
export async function getAppPathRoot (): Promise<string> {
|
||||
// TODO: snakeCase of lodash is incompatible with `snake-case`
|
||||
export const safeSnakeCase = snakeCase
|
||||
|
||||
export async function getAppPathRoot(): Promise<string> {
|
||||
if (_appPathRoot) {
|
||||
return _appPathRoot
|
||||
}
|
||||
|
||||
return (_appPathRoot =
|
||||
await invokeHostExportedApi('_callApplication', 'getAppPath')
|
||||
)
|
||||
return (_appPathRoot = await invokeHostExportedApi(
|
||||
'_callApplication',
|
||||
'getAppPath'
|
||||
))
|
||||
}
|
||||
|
||||
export async function getSDKPathRoot (): Promise<string> {
|
||||
export async function getSDKPathRoot(): Promise<string> {
|
||||
if (IS_DEV) {
|
||||
// TODO: cache in preference file
|
||||
return localStorage.getItem('LSP_DEV_SDK_ROOT') || 'http://localhost:8080'
|
||||
@@ -45,24 +47,24 @@ export async function getSDKPathRoot (): Promise<string> {
|
||||
return safetyPathJoin(appPathRoot, 'js')
|
||||
}
|
||||
|
||||
export function isObject (item: any) {
|
||||
return (item === Object(item) && !Array.isArray(item))
|
||||
export function isObject(item: any) {
|
||||
return item === Object(item) && !Array.isArray(item)
|
||||
}
|
||||
|
||||
export const deepMerge = merge
|
||||
|
||||
export function genID () {
|
||||
export function genID() {
|
||||
// Math.random should be unique because of its seeding algorithm.
|
||||
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
|
||||
// after the decimal.
|
||||
return '_' + Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
export function ucFirst (str: string) {
|
||||
export function ucFirst(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
export function withFileProtocol (path: string) {
|
||||
export function withFileProtocol(path: string) {
|
||||
if (!path) return ''
|
||||
const reg = /^(http|file|lsp)/
|
||||
|
||||
@@ -73,7 +75,7 @@ export function withFileProtocol (path: string) {
|
||||
return path
|
||||
}
|
||||
|
||||
export function safetyPathJoin (basePath: string, ...parts: Array<string>) {
|
||||
export function safetyPathJoin(basePath: string, ...parts: Array<string>) {
|
||||
try {
|
||||
const url = new URL(basePath)
|
||||
if (!url.origin) throw new Error(null)
|
||||
@@ -84,7 +86,7 @@ export function safetyPathJoin (basePath: string, ...parts: Array<string>) {
|
||||
}
|
||||
}
|
||||
|
||||
export function safetyPathNormalize (basePath: string) {
|
||||
export function safetyPathNormalize(basePath: string) {
|
||||
if (!basePath?.match(/^(http?|lsp|assets):/)) {
|
||||
basePath = path.normalize(basePath)
|
||||
}
|
||||
@@ -95,7 +97,7 @@ export function safetyPathNormalize (basePath: string) {
|
||||
* @param timeout milliseconds
|
||||
* @param tag string
|
||||
*/
|
||||
export function deferred<T = any> (timeout?: number, tag?: string) {
|
||||
export function deferred<T = any>(timeout?: number, tag?: string) {
|
||||
let resolve: any, reject: any
|
||||
let settled = false
|
||||
const timeFn = (r: Function) => {
|
||||
@@ -112,41 +114,48 @@ export function deferred<T = any> (timeout?: number, tag?: string) {
|
||||
|
||||
if (timeout) {
|
||||
// @ts-ignore
|
||||
timeout = setTimeout(() => reject(new Error(`[deferred timeout] ${tag}`)), timeout)
|
||||
timeout = setTimeout(
|
||||
() => reject(new Error(`[deferred timeout] ${tag}`)),
|
||||
timeout
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
created: Date.now(),
|
||||
setTag: (t: string) => tag = t,
|
||||
resolve, reject, promise,
|
||||
get settled () {
|
||||
setTag: (t: string) => (tag = t),
|
||||
resolve,
|
||||
reject,
|
||||
promise,
|
||||
get settled() {
|
||||
return settled
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function invokeHostExportedApi (
|
||||
method: string,
|
||||
...args: Array<any>
|
||||
) {
|
||||
method = method?.startsWith('_call') ? method :
|
||||
method?.replace(/^[_$]+/, '')
|
||||
const method1 = snakeCase(method)
|
||||
export function invokeHostExportedApi(method: string, ...args: Array<any>) {
|
||||
method = method?.startsWith('_call') ? method : method?.replace(/^[_$]+/, '')
|
||||
const method1 = safeSnakeCase(method)
|
||||
|
||||
// @ts-ignore
|
||||
const logseqHostExportedApi = window.logseq?.api || {}
|
||||
const logseqHostExportedApi = Object.assign(
|
||||
// @ts-ignore
|
||||
window.logseq?.api || {},
|
||||
callables
|
||||
)
|
||||
|
||||
const fn = logseqHostExportedApi[method1] || window.apis[method1] ||
|
||||
logseqHostExportedApi[method] || window.apis[method]
|
||||
const fn =
|
||||
logseqHostExportedApi[method1] ||
|
||||
window.apis[method1] ||
|
||||
logseqHostExportedApi[method] ||
|
||||
window.apis[method]
|
||||
|
||||
if (!fn) {
|
||||
throw new Error(`Not existed method #${method}`)
|
||||
}
|
||||
return typeof fn !== 'function' ? fn : fn.apply(null, args)
|
||||
return typeof fn !== 'function' ? fn : fn.apply(this, args)
|
||||
}
|
||||
|
||||
export function setupIframeSandbox (
|
||||
export function setupIframeSandbox(
|
||||
props: Record<string, any>,
|
||||
target: HTMLElement
|
||||
) {
|
||||
@@ -165,7 +174,7 @@ export function setupIframeSandbox (
|
||||
}
|
||||
}
|
||||
|
||||
export function setupInjectedStyle (
|
||||
export function setupInjectedStyle(
|
||||
style: StyleString,
|
||||
attrs: Record<string, any>
|
||||
) {
|
||||
@@ -180,9 +189,10 @@ export function setupInjectedStyle (
|
||||
el = document.createElement('style')
|
||||
el.textContent = style
|
||||
|
||||
attrs && Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
attrs &&
|
||||
Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
|
||||
document.head.append(el)
|
||||
|
||||
@@ -193,11 +203,11 @@ export function setupInjectedStyle (
|
||||
|
||||
const injectedUIEffects = new Map<string, () => void>()
|
||||
|
||||
export function setupInjectedUI (
|
||||
export function setupInjectedUI(
|
||||
this: PluginLocal,
|
||||
ui: UIOptions,
|
||||
attrs: Record<string, string>,
|
||||
initialCallback?: (e: { el: HTMLElement, float: boolean }) => void
|
||||
initialCallback?: (e: { el: HTMLElement; float: boolean }) => void
|
||||
) {
|
||||
let slot: string = ''
|
||||
let selector: string
|
||||
@@ -217,21 +227,32 @@ export function setupInjectedUI (
|
||||
const id = `${ui.key}-${slot}-${pl.id}`
|
||||
const key = `${ui.key}--${pl.id}`
|
||||
|
||||
const target = float ? document.body : (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}`)
|
||||
console.error(
|
||||
`${this.debugTag} can not resolve selector target ${selector}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (ui.template) {
|
||||
// safe template
|
||||
ui.template = DOMPurify.sanitize(
|
||||
ui.template, {
|
||||
ADD_TAGS: ['iframe'],
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
ADD_ATTR: ['allow', 'src', 'allowfullscreen', 'frameborder', 'scrolling', 'target']
|
||||
})
|
||||
} else { // remove ui
|
||||
ui.template = DOMPurify.sanitize(ui.template, {
|
||||
ADD_TAGS: ['iframe'],
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
ADD_ATTR: [
|
||||
'allow',
|
||||
'src',
|
||||
'allowfullscreen',
|
||||
'frameborder',
|
||||
'scrolling',
|
||||
'target',
|
||||
],
|
||||
})
|
||||
} else {
|
||||
// remove ui
|
||||
injectedUIEffects.get(id)?.call(null)
|
||||
return
|
||||
}
|
||||
@@ -243,20 +264,23 @@ export function setupInjectedUI (
|
||||
content.innerHTML = ui.template
|
||||
|
||||
// update attributes
|
||||
attrs && Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
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
|
||||
}
|
||||
ui.style &&
|
||||
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
|
||||
}
|
||||
|
||||
@@ -275,13 +299,15 @@ export function setupInjectedUI (
|
||||
// TODO: enhance template
|
||||
content.innerHTML = ui.template
|
||||
|
||||
attrs && Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
attrs &&
|
||||
Object.entries(attrs).forEach(([k, v]) => {
|
||||
el.setAttribute(k, v)
|
||||
})
|
||||
|
||||
ui.style && Object.entries(ui.style).forEach(([k, v]) => {
|
||||
el.style[k] = v
|
||||
})
|
||||
ui.style &&
|
||||
Object.entries(ui.style).forEach(([k, v]) => {
|
||||
el.style[k] = v
|
||||
})
|
||||
|
||||
let teardownUI: () => void
|
||||
let disposeFloat: () => void
|
||||
@@ -291,33 +317,54 @@ export function setupInjectedUI (
|
||||
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 }))
|
||||
disposeFloat =
|
||||
(pl._setupResizableContainer(el, key),
|
||||
pl._setupDraggableContainer(el, {
|
||||
key,
|
||||
close: () => teardownUI(),
|
||||
title: attrs?.title,
|
||||
}))
|
||||
}
|
||||
|
||||
if (!!slot && ui.reset) {
|
||||
const exists = Array.from(target.querySelectorAll('[data-injected-ui]'))
|
||||
.map((it: HTMLElement) => it.id)
|
||||
const exists = Array.from(
|
||||
target.querySelectorAll('[data-injected-ui]')
|
||||
).map((it: HTMLElement) => it.id)
|
||||
|
||||
exists?.forEach((exist: string) => {
|
||||
injectedUIEffects.get(exist)?.call(null)
|
||||
})
|
||||
}
|
||||
|
||||
target.appendChild(el);
|
||||
target.appendChild(el)
|
||||
|
||||
// TODO: How handle events
|
||||
['click', 'focus', 'focusin', 'focusout', 'blur', 'dblclick',
|
||||
'keyup', 'keypress', 'keydown', 'change', 'input'].forEach((type) => {
|
||||
el.addEventListener(type, (e) => {
|
||||
const target = e.target! as HTMLElement
|
||||
const trigger = target.closest(`[data-on-${type}]`) as HTMLElement
|
||||
if (!trigger) return
|
||||
;[
|
||||
'click',
|
||||
'focus',
|
||||
'focusin',
|
||||
'focusout',
|
||||
'blur',
|
||||
'dblclick',
|
||||
'keyup',
|
||||
'keypress',
|
||||
'keydown',
|
||||
'change',
|
||||
'input',
|
||||
].forEach((type) => {
|
||||
el.addEventListener(
|
||||
type,
|
||||
(e) => {
|
||||
const target = e.target! as HTMLElement
|
||||
const trigger = target.closest(`[data-on-${type}]`) as HTMLElement
|
||||
if (!trigger) return
|
||||
|
||||
const msgType = trigger.dataset[`on${ucFirst(type)}`]
|
||||
msgType && pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
|
||||
}, false)
|
||||
const msgType = trigger.dataset[`on${ucFirst(type)}`]
|
||||
msgType &&
|
||||
pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
|
||||
},
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
// callback
|
||||
@@ -333,16 +380,20 @@ export function setupInjectedUI (
|
||||
return teardownUI
|
||||
}
|
||||
|
||||
export function transformableEvent (target: HTMLElement, e: Event) {
|
||||
export function cleanInjectedScripts(this: PluginLocal) {
|
||||
const scripts = document.head.querySelectorAll(`script[data-ref=${this.id}]`)
|
||||
|
||||
scripts?.forEach((it) => it.remove())
|
||||
}
|
||||
|
||||
export function transformableEvent(target: HTMLElement, e: Event) {
|
||||
const obj: any = {}
|
||||
|
||||
if (target) {
|
||||
const ds = target.dataset
|
||||
const FLAG_RECT = 'rect'
|
||||
|
||||
;['value', 'id', 'className',
|
||||
'dataset', FLAG_RECT
|
||||
].forEach((k) => {
|
||||
;['value', 'id', 'className', 'dataset', FLAG_RECT].forEach((k) => {
|
||||
let v: any
|
||||
|
||||
switch (k) {
|
||||
@@ -367,7 +418,7 @@ export function transformableEvent (target: HTMLElement, e: Event) {
|
||||
|
||||
let injectedThemeEffect: any = null
|
||||
|
||||
export function setupInjectedTheme (url?: string) {
|
||||
export function setupInjectedTheme(url?: string) {
|
||||
injectedThemeEffect?.call()
|
||||
|
||||
if (!url) return
|
||||
@@ -387,9 +438,10 @@ export function setupInjectedTheme (url?: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function mergeSettingsWithSchema (
|
||||
export function mergeSettingsWithSchema(
|
||||
settings: Record<string, any>,
|
||||
schema: Array<SettingSchemaDesc>) {
|
||||
schema: Array<SettingSchemaDesc>
|
||||
) {
|
||||
const defaults = (schema || []).reduce((a, b) => {
|
||||
if ('default' in b) {
|
||||
a[b.key] = b.default
|
||||
@@ -399,4 +451,4 @@ export function mergeSettingsWithSchema (
|
||||
|
||||
// shadow copy
|
||||
return Object.assign(defaults, settings)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user