| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- import { StyleString, UIOptions } from './LSPlugin'
- import { PluginLocal } from './LSPlugin.core'
- import { snakeCase } from 'snake-case'
- import * as path from 'path'
- interface IObject {
- [key: string]: any;
- }
- declare global {
- interface Window {
- api: any
- apis: any
- }
- }
- 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
- 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 path.join(appPathRoot, 'js')
- }
- export function isObject (item: any) {
- return (item === Object(item) && !Array.isArray(item))
- }
- export function deepMerge (
- target: IObject,
- ...sources: Array<IObject>
- ) {
- // return the target if no sources passed
- if (!sources.length) {
- return target
- }
- const result: IObject = target
- if (isObject(result)) {
- const len: number = sources.length
- for (let i = 0; i < len; i += 1) {
- const elm: any = sources[i]
- if (isObject(elm)) {
- for (const key in elm) {
- if (elm.hasOwnProperty(key)) {
- if (isObject(elm[key])) {
- if (!result[key] || !isObject(result[key])) {
- result[key] = {}
- }
- deepMerge(result[key], elm[key])
- } else {
- if (Array.isArray(result[key]) && Array.isArray(elm[key])) {
- // concatenate the two arrays and remove any duplicate primitive values
- result[key] = Array.from(new Set(result[key].concat(elm[key])))
- } else {
- result[key] = elm[key]
- }
- }
- }
- }
- }
- }
- }
- return result
- }
- 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 = 'file://' + path
- }
- return path
- }
- /**
- * @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>
- ) {
- const method1 = snakeCase(method)
- // @ts-ignore
- const logseqHostExportedApi = window.logseq?.api || {}
- 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)
- }
- 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)
- }
- }
- export function setupInjectedUI (
- this: PluginLocal,
- ui: UIOptions,
- attrs: Record<string, any>
- ) {
- const pl = this
- let slot = ''
- let selector = ''
- if ('slot' in ui) {
- slot = ui.slot
- selector = `#${slot}`
- } else {
- selector = ui.path
- }
- const target = 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 (el) {
- el.innerHTML = ui.template
- return
- }
- el = document.createElement('div')
- el.id = id
- el.dataset.injectedUi = key || ''
- // TODO: Support more
- el.innerHTML = ui.template
- attrs && Object.entries(attrs).forEach(([k, v]) => {
- el.setAttribute(k, v)
- })
- 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)
- })
- return () => {
- target!.removeChild(el)
- }
- }
- 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
- }
- let injectedThemeEffect: any = null
- export function setupInjectedTheme (url?: string) {
- injectedThemeEffect?.call()
- if (!url) return
- const link = document.createElement('link')
- link.rel = 'stylesheet'
- link.href = url
- document.head.appendChild(link)
- return (injectedThemeEffect = () => {
- document.head.removeChild(link)
- injectedThemeEffect = null
- })
- }
|