| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- import { SettingSchemaDesc, StyleString, UIOptions } from './LSPlugin'
- import { PluginLocal } from './LSPlugin.core'
- import * as nodePath from 'path'
- import DOMPurify from 'dompurify'
- import { merge } from 'lodash-es'
- import { snakeCase } from 'snake-case'
- import * as callables from './callable.apis'
- declare global {
- interface Window {
- api: any
- apis: any
- }
- }
- 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://'
- export const URL_LSP = PROTOCOL_LSP + 'logseq.io/'
- let _appPathRoot
- // 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'
- ))
- }
- export async function getSDKPathRoot(): Promise<string> {
- if (IS_DEV) {
- // TODO: cache in preference file
- return localStorage.getItem('LSP_DEV_SDK_ROOT') || 'http://localhost:8080'
- }
- const appPathRoot = await getAppPathRoot()
- return safetyPathJoin(appPathRoot, 'js')
- }
- export function isObject(item: any) {
- return item === Object(item) && !Array.isArray(item)
- }
- export const deepMerge = merge
- 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) {
- return str.charAt(0).toUpperCase() + str.slice(1)
- }
- export function withFileProtocol(path: string) {
- if (!path) return ''
- const reg = /^(http|file|lsp)/
- if (!reg.test(path)) {
- path = PROTOCOL_FILE + path
- }
- return path
- }
- export function safetyPathJoin(basePath: string, ...parts: Array<string>) {
- try {
- const url = new URL(basePath)
- if (!url.origin) throw new Error(null)
- const fullPath = path.join(basePath.substr(url.origin.length), ...parts)
- return url.origin + fullPath
- } catch (e) {
- return path.join(basePath, ...parts)
- }
- }
- export function safetyPathNormalize(basePath: string) {
- if (!basePath?.match(/^(http?|lsp|assets):/)) {
- basePath = path.normalize(basePath)
- }
- return basePath
- }
- /**
- * @param timeout milliseconds
- * @param tag string
- */
- export function deferred<T = any>(timeout?: number, tag?: string) {
- let resolve: any, reject: any
- let settled = false
- const timeFn = (r: Function) => {
- return (v: T) => {
- timeout && clearTimeout(timeout)
- r(v)
- settled = true
- }
- }
- const promise = new Promise<T>((resolve1, reject1) => {
- resolve = timeFn(resolve1)
- reject = timeFn(reject1)
- if (timeout) {
- // @ts-ignore
- timeout = setTimeout(
- () => reject(new Error(`[deferred timeout] ${tag}`)),
- timeout
- )
- }
- })
- return {
- created: Date.now(),
- 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 = safeSnakeCase(method)
- const logseqHostExportedApi = Object.assign(
- // @ts-ignore
- window.logseq?.api || {},
- callables
- )
- 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(this, args)
- }
- export function setupIframeSandbox(
- props: Record<string, any>,
- target: HTMLElement
- ) {
- const iframe = document.createElement('iframe')
- iframe.classList.add('lsp-iframe-sandbox')
- Object.entries(props).forEach(([k, v]) => {
- iframe.setAttribute(k, v)
- })
- target.appendChild(iframe)
- return async () => {
- target.removeChild(iframe)
- }
- }
- export function setupInjectedStyle(
- style: StyleString,
- attrs: Record<string, any>
- ) {
- const key = attrs['data-injected-style']
- let el = key && document.querySelector(`[data-injected-style=${key}]`)
- if (el) {
- el.textContent = style
- return
- }
- el = document.createElement('style')
- el.textContent = style
- attrs &&
- Object.entries(attrs).forEach(([k, v]) => {
- el.setAttribute(k, v)
- })
- document.head.append(el)
- return () => {
- document.head.removeChild(el)
- }
- }
- const injectedUIEffects = new Map<string, () => void>()
- export function setupInjectedUI(
- this: PluginLocal,
- ui: UIOptions,
- attrs: Record<string, string>,
- initialCallback?: (e: { el: HTMLElement; float: boolean }) => void
- ) {
- let slot: string = ''
- let selector: string
- let float: boolean
- const pl = this
- if ('slot' in ui) {
- slot = ui.slot
- selector = `#${slot}`
- } else if ('path' in ui) {
- selector = ui.path
- } else {
- float = true
- }
- const id = `${pl.id}--${ui.key}`
- const key = id
- const target = float
- ? document.body
- : selector && document.querySelector(selector)
- if (!target) {
- 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
- injectedUIEffects.get(id)?.call(null)
- return
- }
- 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
- }
- el = document.createElement('div')
- el.id = id
- el.dataset.injectedUi = key || ''
- 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,
- }))
- }
- if (!!slot && ui.reset) {
- 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)
- // 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
- const msgType = trigger.dataset[`on${ucFirst(type)}`]
- msgType &&
- pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
- },
- false
- )
- })
- // callback
- initialCallback?.({ el, float })
- teardownUI = () => {
- disposeFloat?.()
- injectedUIEffects.delete(id)
- target!.removeChild(el)
- }
- injectedUIEffects.set(id, teardownUI)
- return teardownUI
- }
- 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) => {
- let v: any
- switch (k) {
- case FLAG_RECT:
- if (!ds.hasOwnProperty(FLAG_RECT)) return
- v = target.getBoundingClientRect().toJSON()
- break
- default:
- v = target[k]
- }
- if (typeof v === 'object') {
- v = { ...v }
- }
- obj[k] = v
- })
- }
- return obj
- }
- export function injectTheme(url: string) {
- const link = document.createElement('link')
- link.rel = 'stylesheet'
- link.href = url
- document.head.appendChild(link)
- const ejectTheme = () => {
- try {
- document.head.removeChild(link)
- } catch (e) {
- console.error(e)
- }
- }
- return ejectTheme
- }
- export function mergeSettingsWithSchema(
- settings: Record<string, any>,
- schema: Array<SettingSchemaDesc>
- ) {
- const defaults = (schema || []).reduce((a, b) => {
- if ('default' in b) {
- a[b.key] = b.default
- }
- return a
- }, {})
- // shadow copy
- return Object.assign(defaults, settings)
- }
|