helpers.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import { StyleString, UIOptions } from './LSPlugin'
  2. import { PluginLocal } from './LSPlugin.core'
  3. import { snakeCase } from 'snake-case'
  4. import * as path from 'path'
  5. interface IObject {
  6. [key: string]: any;
  7. }
  8. declare global {
  9. interface Window {
  10. api: any
  11. apis: any
  12. }
  13. }
  14. export const IS_DEV = process.env.NODE_ENV === 'development'
  15. export const PROTOCOL_FILE = 'file://'
  16. export const PROTOCOL_LSP = 'lsp://'
  17. export const URL_LSP = PROTOCOL_LSP + 'logseq.io/'
  18. let _appPathRoot
  19. export async function getAppPathRoot (): Promise<string> {
  20. if (_appPathRoot) {
  21. return _appPathRoot
  22. }
  23. return (_appPathRoot =
  24. await invokeHostExportedApi('_callApplication', 'getAppPath')
  25. )
  26. }
  27. export async function getSDKPathRoot (): Promise<string> {
  28. if (IS_DEV) {
  29. // TODO: cache in preference file
  30. return localStorage.getItem('LSP_DEV_SDK_ROOT') || 'http://localhost:8080'
  31. }
  32. const appPathRoot = await getAppPathRoot()
  33. return path.join(appPathRoot, 'js')
  34. }
  35. export function isObject (item: any) {
  36. return (item === Object(item) && !Array.isArray(item))
  37. }
  38. export function deepMerge (
  39. target: IObject,
  40. ...sources: Array<IObject>
  41. ) {
  42. // return the target if no sources passed
  43. if (!sources.length) {
  44. return target
  45. }
  46. const result: IObject = target
  47. if (isObject(result)) {
  48. const len: number = sources.length
  49. for (let i = 0; i < len; i += 1) {
  50. const elm: any = sources[i]
  51. if (isObject(elm)) {
  52. for (const key in elm) {
  53. if (elm.hasOwnProperty(key)) {
  54. if (isObject(elm[key])) {
  55. if (!result[key] || !isObject(result[key])) {
  56. result[key] = {}
  57. }
  58. deepMerge(result[key], elm[key])
  59. } else {
  60. if (Array.isArray(result[key]) && Array.isArray(elm[key])) {
  61. // concatenate the two arrays and remove any duplicate primitive values
  62. result[key] = Array.from(new Set(result[key].concat(elm[key])))
  63. } else {
  64. result[key] = elm[key]
  65. }
  66. }
  67. }
  68. }
  69. }
  70. }
  71. }
  72. return result
  73. }
  74. export function genID () {
  75. // Math.random should be unique because of its seeding algorithm.
  76. // Convert it to base 36 (numbers + letters), and grab the first 9 characters
  77. // after the decimal.
  78. return '_' + Math.random().toString(36).substr(2, 9)
  79. }
  80. export function ucFirst (str: string) {
  81. return str.charAt(0).toUpperCase() + str.slice(1)
  82. }
  83. export function withFileProtocol (path: string) {
  84. if (!path) return ''
  85. const reg = /^(http|file|lsp)/
  86. if (!reg.test(path)) {
  87. path = 'file://' + path
  88. }
  89. return path
  90. }
  91. /**
  92. * @param timeout milliseconds
  93. * @param tag string
  94. */
  95. export function deferred<T = any> (timeout?: number, tag?: string) {
  96. let resolve: any, reject: any
  97. let settled = false
  98. const timeFn = (r: Function) => {
  99. return (v: T) => {
  100. timeout && clearTimeout(timeout)
  101. r(v)
  102. settled = true
  103. }
  104. }
  105. const promise = new Promise<T>((resolve1, reject1) => {
  106. resolve = timeFn(resolve1)
  107. reject = timeFn(reject1)
  108. if (timeout) {
  109. // @ts-ignore
  110. timeout = setTimeout(() => reject(new Error(`[deferred timeout] ${tag}`)), timeout)
  111. }
  112. })
  113. return {
  114. created: Date.now(),
  115. setTag: (t: string) => tag = t,
  116. resolve, reject, promise,
  117. get settled () {
  118. return settled
  119. }
  120. }
  121. }
  122. export function invokeHostExportedApi (
  123. method: string,
  124. ...args: Array<any>
  125. ) {
  126. const method1 = snakeCase(method)
  127. // @ts-ignore
  128. const logseqHostExportedApi = window.logseq?.api || {}
  129. const fn = logseqHostExportedApi[method1] || window.apis[method1] ||
  130. logseqHostExportedApi[method] || window.apis[method]
  131. if (!fn) {
  132. throw new Error(`Not existed method #${method}`)
  133. }
  134. return typeof fn !== 'function' ? fn : fn.apply(null, args)
  135. }
  136. export function setupIframeSandbox (
  137. props: Record<string, any>,
  138. target: HTMLElement
  139. ) {
  140. const iframe = document.createElement('iframe')
  141. iframe.classList.add('lsp-iframe-sandbox')
  142. Object.entries(props).forEach(([k, v]) => {
  143. iframe.setAttribute(k, v)
  144. })
  145. target.appendChild(iframe)
  146. return async () => {
  147. target.removeChild(iframe)
  148. }
  149. }
  150. export function setupInjectedStyle (
  151. style: StyleString,
  152. attrs: Record<string, any>
  153. ) {
  154. const key = attrs['data-injected-style']
  155. let el = key && document.querySelector(`[data-injected-style=${key}]`)
  156. if (el) {
  157. el.textContent = style
  158. return
  159. }
  160. el = document.createElement('style')
  161. el.textContent = style
  162. attrs && Object.entries(attrs).forEach(([k, v]) => {
  163. el.setAttribute(k, v)
  164. })
  165. document.head.append(el)
  166. return () => {
  167. document.head.removeChild(el)
  168. }
  169. }
  170. export function setupInjectedUI (
  171. this: PluginLocal,
  172. ui: UIOptions,
  173. attrs: Record<string, any>
  174. ) {
  175. const pl = this
  176. let slot = ''
  177. let selector = ''
  178. if ('slot' in ui) {
  179. slot = ui.slot
  180. selector = `#${slot}`
  181. } else {
  182. selector = ui.path
  183. }
  184. const target = selector && document.querySelector(selector)
  185. if (!target) {
  186. console.error(`${this.debugTag} can not resolve selector target ${selector}`)
  187. return
  188. }
  189. const id = `${ui.key}-${slot}-${pl.id}`
  190. const key = `${ui.key}-${pl.id}`
  191. let el = document.querySelector(`#${id}`) as HTMLElement
  192. if (el) {
  193. el.innerHTML = ui.template
  194. return
  195. }
  196. el = document.createElement('div')
  197. el.id = id
  198. el.dataset.injectedUi = key || ''
  199. // TODO: Support more
  200. el.innerHTML = ui.template
  201. attrs && Object.entries(attrs).forEach(([k, v]) => {
  202. el.setAttribute(k, v)
  203. })
  204. target.appendChild(el);
  205. // TODO: How handle events
  206. ['click', 'focus', 'focusin', 'focusout', 'blur', 'dblclick',
  207. 'keyup', 'keypress', 'keydown', 'change', 'input'].forEach((type) => {
  208. el.addEventListener(type, (e) => {
  209. const target = e.target! as HTMLElement
  210. const trigger = target.closest(`[data-on-${type}]`) as HTMLElement
  211. if (!trigger) return
  212. const msgType = trigger.dataset[`on${ucFirst(type)}`]
  213. msgType && pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
  214. }, false)
  215. })
  216. return () => {
  217. target!.removeChild(el)
  218. }
  219. }
  220. export function transformableEvent (target: HTMLElement, e: Event) {
  221. const obj: any = {}
  222. if (target) {
  223. const ds = target.dataset
  224. const FLAG_RECT = 'rect'
  225. ;['value', 'id', 'className',
  226. 'dataset', FLAG_RECT
  227. ].forEach((k) => {
  228. let v: any
  229. switch (k) {
  230. case FLAG_RECT:
  231. if (!ds.hasOwnProperty(FLAG_RECT)) return
  232. v = target.getBoundingClientRect().toJSON()
  233. break
  234. default:
  235. v = target[k]
  236. }
  237. if (typeof v === 'object') {
  238. v = { ...v }
  239. }
  240. obj[k] = v
  241. })
  242. }
  243. return obj
  244. }
  245. let injectedThemeEffect: any = null
  246. export function setupInjectedTheme (url?: string) {
  247. injectedThemeEffect?.call()
  248. if (!url) return
  249. const link = document.createElement('link')
  250. link.rel = 'stylesheet'
  251. link.href = url
  252. document.head.appendChild(link)
  253. return (injectedThemeEffect = () => {
  254. document.head.removeChild(link)
  255. injectedThemeEffect = null
  256. })
  257. }