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
pull/10693/head
Charlie 2023-12-13 15:42:21 +08:00 committed by GitHub
parent eb22435280
commit 036df25a17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 222 additions and 160 deletions

View File

@ -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>`

View File

@ -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",

View File

@ -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 }
)
}

View File

@ -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 {

View File

@ -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,

View File

@ -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()
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
/*! @license DOMPurify 2.3.8 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.8/LICENSE */

View File

@ -54,7 +54,7 @@
(try
(let [graph-path (state/get-graph-path)
_ (when (string/blank? graph-path)
(utils/send-to-renderer "getCurrentGraph" {})
(utils/send-to-renderer :setCurrentGraph {})
(throw (js/Error. "Empty graph path")))
p (.join node-path graph-path ".git")]
(when (and (fs/existsSync p)

View File

@ -83,10 +83,10 @@
(fn []
(state/pub-event! [:modal/set-git-username-and-email])))
(safe-api-call "getCurrentGraph"
(safe-api-call "setCurrentGraph"
(fn []
(when-let [graph (state/get-current-repo)]
(ipc/ipc "setCurrentGraph" graph))))
(ipc/ipc :setCurrentGraph graph))))
(safe-api-call "redirect"
(fn [data]

View File

@ -373,7 +373,7 @@
(some-> (js/document.querySelector ".cp__plugins-page") (.focus))
(reset! *search-key nil))))
:on-change #(let [^js target (.-target %)]
(reset! *search-key (util/trim-safe (.-value target))))
(reset! *search-key (some-> (.-value target) (string/triml))))
:value (or search-key "")}]])
(rum/defc panel-tab-developer

View File

@ -35,7 +35,10 @@
(get @state/state :command-palette/commands)))
(defn history
([] (or (storage/get "commands-history") []))
([] (or (try (storage/get "commands-history")
(catch js/Error e
(log/error :commands-history e)))
[]))
([vals] (storage/set "commands-history" vals)))
(defn- assoc-invokes [cmds]

View File

@ -25,12 +25,13 @@
(defn- normalize-keyword-for-json
[input]
(when input
(walk/postwalk
(fn [a]
(cond
(keyword? a) (csk/->camelCase (name a))
(uuid? a) (str a)
:else a)) input)))
(let [f (fn [[k v]] (if (keyword? k) [(csk/->camelCase (name k)) v] [k v]))]
(walk/postwalk
(fn [x]
(cond
(map? x) (into {} (map f x))
(uuid? x) (str x)
:else x)) input))))
(defn invoke-exported-api
[type & args]
@ -297,7 +298,8 @@
[pid key keybinding]
(let [id (keyword (str "plugin." pid "/" key))
binding (:binding keybinding)
binding (some->> (if (string? binding) [binding] (seq binding))
binding (some->> (if (string? binding) [binding] (vec binding))
(remove string/blank?)
(map shortcut-utils/undecorate-binding))
binding (if util/mac?
(or (:mac keybinding) binding) binding)
@ -495,7 +497,7 @@
payload)
(if (keyword? plugin-id) (name plugin-id) plugin-id))
(catch :default e
(js/console.error "[Hook Plugin Err]" e)))))
(log/error :invoke-hook-exception e)))))
(defn hook-plugin-app
([type payload] (hook-plugin-app type payload nil))

View File

@ -925,15 +925,17 @@
(def *shortcut-cmds (atom {}))
(defn add-shortcut!
[handler-id id shortcut-map]
(swap! *config assoc-in [handler-id id] shortcut-map)
(swap! *shortcut-cmds assoc id (:cmd shortcut-map))
(let [plugin? (str/starts-with? (str id) ":plugin.")
category (or (:category shortcut-map)
(if plugin?
:shortcut.category/plugins
:shortcut.category/others))]
(swap! *category update category #(conj % id))))
([handler-id id shortcut-map] (add-shortcut! handler-id id shortcut-map false))
([handler-id id shortcut-map config-only?]
(swap! *config assoc-in [handler-id id] shortcut-map)
(when-not config-only?
(swap! *shortcut-cmds assoc id (:cmd shortcut-map))
(let [plugin? (str/starts-with? (str id) ":plugin.")
category (or (:category shortcut-map)
(if plugin?
:shortcut.category/plugins
:shortcut.category/others))]
(swap! *category update category #(conj % id))))))
(defn remove-shortcut!
[handler-id id]

View File

@ -93,7 +93,8 @@
(when-let [handler (get-handler-by-id handler-id)]
(when-let [ks (dh/shortcut-binding shortcut-id)]
(doseq [k ks]
(.unregisterShortcut ^js handler (shortcut-utils/undecorate-binding k))))
(.unregisterShortcut ^js handler (shortcut-utils/undecorate-binding k)))))
(when shortcut-id
(shortcut-config/remove-shortcut! handler-id shortcut-id)))
(defn uninstall-shortcut-handler!

View File

@ -4,7 +4,6 @@
[logseq.sdk.core]
[logseq.sdk.utils :as sdk-utils]
[logseq.sdk.ui :as sdk-ui]
[logseq.sdk.git :as sdk-git]
[logseq.sdk.assets :as sdk-assets]
[clojure.string :as string]
[datascript.core :as d]
@ -31,6 +30,7 @@
[frontend.modules.outliner.tree :as outliner-tree]
[frontend.handler.command-palette :as palette-handler]
[frontend.modules.shortcut.core :as st]
[frontend.modules.shortcut.config :as shortcut-config]
[electron.listener :as el]
[frontend.state :as state]
[frontend.util :as util]
@ -352,7 +352,7 @@
cmd (assoc cmd :key (string/replace (:key cmd) ":" "-"))
key (:key cmd)
keybinding (:keybinding cmd)
palette-cmd (and palette? (plugin-handler/simple-cmd->palette-cmd pid cmd action))
palette-cmd (plugin-handler/simple-cmd->palette-cmd pid cmd action)
action' #(state/pub-event! [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}])]
;; handle simple commands
@ -369,8 +369,16 @@
(palette-handler/invoke-command palette-cmd)
(action')))
[mode-id id shortcut-map] (update shortcut-args 2 merge cmd {:fn dispatch-cmd :cmd palette-cmd})]
(println :shortcut/register-shortcut [mode-id id shortcut-map])
(st/register-shortcut! mode-id id shortcut-map)))))))
(cond
;; FIX ME: move to register logic
(= mode-id :shortcut.handler/block-editing-only)
(shortcut-config/add-shortcut! mode-id id shortcut-map)
:else
(do
(println :shortcut/register-shortcut [mode-id id shortcut-map])
(st/register-shortcut! mode-id id shortcut-map)))))))))
(defn ^:export unregister_plugin_simple_command
[pid]
@ -378,10 +386,10 @@
(plugin-handler/unregister-plugin-simple-command pid)
;; remove palette commands
(let [palette-matched (->> (palette-handler/get-commands)
(filter #(string/includes? (str (:id %)) (str "plugin." pid))))]
(when (seq palette-matched)
(doseq [cmd palette-matched]
(let [cmds-matched (->> (vals @shortcut-config/*shortcut-cmds)
(filter #(string/includes? (str (:id %)) (str "plugin." pid))))]
(when (seq cmds-matched)
(doseq [cmd cmds-matched]
(palette-handler/unregister (:id cmd))
;; remove keybinding commands
(when (seq (:shortcut cmd))
@ -897,19 +905,10 @@
(when-let [args (and args (seq (bean/->clj args)))]
(shell/run-git-command! args)))
;; git
(def ^:export git_exec_command sdk-git/exec_command)
(def ^:export git_load_ignore_file sdk-git/load_ignore_file)
(def ^:export git_save_ignore_file sdk-git/save_ignore_file)
;; ui
(def ^:export show_msg sdk-ui/-show_msg)
(def ^:export ui_show_msg sdk-ui/show_msg)
(def ^:export ui_close_msg sdk-ui/close_msg)
;; assets
(def ^:export assets_list_files_of_current_graph sdk-assets/list_files_of_current_graph)
(def ^:export assets_make_url sdk-assets/make_url)
(def ^:export make_asset_url sdk-assets/make_url)
;; experiments
@ -998,16 +997,6 @@
(p/then #(bean/->js %))))
;; helpers
(defn ^:export query_element_by_id
[id]
(when-let [^js el (gdom/getElement id)]
(if el (str (.-tagName el) "#" id) false)))
(defn ^:export query_element_rect
[selector]
(when-let [^js el (js/document.querySelector selector)]
(bean/->js (.toJSON (.getBoundingClientRect el)))))
(defn ^:export set_focused_settings
[pid]
(when-let [plugin (state/get-plugin-by-id pid)]

View File

@ -2,7 +2,10 @@
(:require [electron.ipc :as ipc]
[cljs-bean.core :as bean]
[promesa.core :as p]
[frontend.handler.editor :as editor-handler]))
[frontend.handler.editor :as editor-handler]
[frontend.extensions.pdf.assets :as pdf-assets]
[frontend.state :as state]
[frontend.util :as util]))
(def ^:export make_url editor-handler/make-asset-url)
@ -10,3 +13,12 @@
[^js exts]
(p/let [files (ipc/ipc :getAssetsFiles {:exts exts})]
(bean/->js files)))
(defn ^:export built_in_open
[asset-file]
(when-let [ext (util/trim-safe (util/get-file-ext asset-file))]
(cond
(contains? #{"pdf"} ext)
(state/set-current-pdf! (pdf-assets/inflate-asset asset-file))
:else false)))

View File

@ -1,7 +1,9 @@
(ns logseq.sdk.ui
(:require [frontend.handler.notification :as notification]
[cljs-bean.core :as bean]
[goog.dom :as gdom]
[sci.core :as sci]
[frontend.util :as util]
[clojure.string :as string]))
(defn- parse-hiccup-ui
@ -19,9 +21,9 @@
(let [{:keys [key timeout]} (bean/->clj opts)
hiccup? (and (string? content) (string/starts-with? (string/triml content) "[:"))
content (if hiccup? (parse-hiccup-ui content) content)
uid (when (string? key) (keyword key))
clear? (not= timeout 0)
key' (notification/show! content (keyword status) clear? uid timeout nil)]
uid (when (string? key) (keyword key))
clear? (not= timeout 0)
key' (notification/show! content (keyword status) clear? uid timeout nil)]
(name key'))))
(defn ^:export show_msg
@ -31,4 +33,30 @@
(defn ^:export close_msg
[key]
(when (string? key)
(notification/clear! (keyword key)) nil))
(notification/clear! (keyword key)) nil))
(defn ^:export query_element_rect
[selector]
(when-let [^js el (js/document.querySelector selector)]
(bean/->js (.toJSON (.getBoundingClientRect el)))))
(defn ^:export query_element_by_id
[id]
(when-let [^js el (gdom/getElement id)]
(if el (str (.-tagName el) "#" id) false)))
(defn ^:export check_slot_valid
[slot]
(when (string? slot)
(boolean (query_element_by_id slot))))
(defn ^:export resolve_theme_css_props_vals
[props]
(when-let [props (if (string? props) [props] (bean/->clj props))]
(let [^js s (js/window.getComputedStyle js/document.body)]
(some->> (for [prop props]
(when (string? prop)
[prop (util/trim-safe (.getPropertyValue s prop))]))
(remove empty?)
(into {})
(bean/->js)))))