bridge.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. /**
  2. * @typedef {{
  3. * global: boolean,
  4. * prevent: boolean,
  5. * stop: boolean,
  6. * exact: boolean,
  7. * rect: boolean,
  8. * key: string,
  9. * ctrl: boolean,
  10. * alt: boolean,
  11. * shift: boolean
  12. * }} EventOptions
  13. */
  14. /**
  15. * @typedef {{
  16. * finishLoad: () => void,
  17. * emitEvent: (args: { handler:string, payload:Object }) => void,
  18. * UpdateRootFontSize: (args: { size:number }) => void,
  19. * InjectCSS: (args: { uuid:string, path:string }) => void,
  20. * InjectJS: (args: { uuid:string, path:string }) => void,
  21. * InjectTTF: (args: { uuid:string, path:string, family:string, weight:string, style:string }) => void,
  22. * CallMethod: (args: { id:string, method:string, args:any[] }) => void,
  23. * PatchActualDOM: (args: { data:string }) => void
  24. * }} Bridge
  25. */
  26. /**
  27. * @typedef {{
  28. * ApplyStyle: (id:string, key:string, value:string) => void,
  29. * EraseStyle: (id:string, key:string) => void,
  30. * SetAttr: (id:string, name:string, value:string) => void,
  31. * RemoveAttr: (id:string, name:string) => void,
  32. * AttachEvent: (id:string, name:string, handler: string) => void,
  33. * DetachEvent: (id:string, name:string) => void,
  34. * SetText: (id:string, content:string) => void,
  35. * AppendNode: (parent:string, id:string, tag:string) => void
  36. * RemoveNode: (parent:string, id:string) => void,
  37. * UpdateNode: (old_id:string, new_id:string) => void,
  38. * ReplaceNode: (target:string, id:string, tag:string) => void,
  39. * SwapNode: (parent:string, a:string, b:string) => void,
  40. * MoveNode: (parent:string, id:string, pivot:string) => void
  41. * }} PatchOperations
  42. */
  43. const SvgXmlNamespace = 'http://www.w3.org/2000/svg'
  44. const RootElementSelector = 'html'
  45. /** @type {{ [id:string]: HTMLElement | SVGElement }} */
  46. let elementRegistry = {}
  47. /** @type {{ [id:string]: { [name:string]: { listener: function, handler: string } } }} */
  48. let eventsRegistry = {}
  49. /** @returns {void} */
  50. function registerBodyElement() {
  51. elementRegistry[0] = document.body
  52. }
  53. /** @type {Array<() => void>} */
  54. let updateHooks = []
  55. function WebUiRegisterUpdateHook(f) {
  56. updateHooks.push(f)
  57. f()
  58. return () => {
  59. for (let i = 0; i < updateHooks.length; i += 1) {
  60. if (updateHooks[i] === f) {
  61. updateHooks.splice(i, 1)
  62. break
  63. }
  64. }
  65. }
  66. }
  67. function runAllUpdateHooks() {
  68. for (let f of updateHooks) {
  69. f()
  70. }
  71. }
  72. window.addEventListener('load', _ => {
  73. let bridge = getBridgeObject()
  74. connectGeneralSignals(bridge)
  75. registerBodyElement()
  76. initializeInteraction(bridge)
  77. notifyInitialized(bridge)
  78. // disable page zoom by mouse wheel
  79. document.addEventListener('wheel',
  80. ev => { if(ev.ctrlKey) { ev.preventDefault() } },
  81. { passive: false })
  82. })
  83. /** @type {Bridge} */
  84. let WebBridge = {
  85. finishLoad: () => {
  86. alert(`IPC:${JSON.stringify({method:"finishLoad"})}`)
  87. },
  88. emitEvent: args => {
  89. alert(`IPC:${JSON.stringify({method:"emitEvent",args})}`)
  90. },
  91. UpdateRootFontSize: null,
  92. InjectCSS: null,
  93. InjectJS: null,
  94. InjectTTF: null,
  95. CallMethod: null,
  96. PatchActualDOM: null
  97. }
  98. // TODO: Refactor this file
  99. // NOTE: The following code assumes the WebBridge object is injected from
  100. // outside, which is not a valid assumption anymore
  101. // because the implementation changed from QtWebkit to QtWebEngine.
  102. // It still works but the code is a bit confusing.
  103. /** @returns {Bridge} */
  104. function getBridgeObject() {
  105. // @ts-ignore
  106. if (typeof WebBridge == 'undefined') {
  107. console.error('[QtBinding] Bridge Object Not Found')
  108. throw new Error('[QtBinding] Initialization Failed - Bridge Object Not Found')
  109. }
  110. /** @type {Bridge} */
  111. // @ts-ignore
  112. let bridge = WebBridge
  113. console.log('[QtBinding] Bridge Object Found', bridge)
  114. return bridge
  115. }
  116. /** @param {Bridge} bridge */
  117. function notifyInitialized(bridge) {
  118. bridge.finishLoad()
  119. console.log('[QtBinding] Bridge JS-Side Initialized')
  120. }
  121. /** @param {Bridge} bridge */
  122. function connectGeneralSignals(bridge) {
  123. bridge.UpdateRootFontSize = ({size}) => {
  124. let root = document.querySelector(RootElementSelector)
  125. root.style.fontSize = `${size}px`
  126. }
  127. bridge.InjectCSS = ({path}) => {
  128. let link_tag = document.createElement('link')
  129. link_tag.rel = 'stylesheet'
  130. link_tag.href = `asset:///${path}`
  131. document.head.appendChild(link_tag)
  132. }
  133. bridge.InjectJS = ({path}) => {
  134. let script_tag = document.createElement('script')
  135. script_tag.type = 'text/javascript'
  136. script_tag.src = `asset:///${path}`
  137. document.head.appendChild(script_tag)
  138. }
  139. bridge.InjectTTF = ({path, family, weight, style}) => {
  140. let style_tag = document.createElement('style')
  141. style_tag.textContent = `@font-face {
  142. font-family: '${family}';
  143. src: url(asset:///${path}) format('truetype');
  144. font-weight: ${weight};
  145. font-style: ${style};
  146. }`
  147. document.head.appendChild(style_tag)
  148. }
  149. bridge.CallMethod = _ => {
  150. // TODO: implementation
  151. throw new Error('bridge.CallMethod(): not implemented')
  152. }
  153. }
  154. /** @param {Bridge} bridge */
  155. function initializeInteraction(bridge) {
  156. /** @type {(parent:string, tag:string) => HTMLElement | SVGElement} */
  157. let createElement = (parent, tag) => {
  158. let parent_el = elementRegistry[parent]
  159. if (tag == 'svg' || parent_el instanceof SVGElement) {
  160. return document.createElementNS(SvgXmlNamespace, tag)
  161. } else {
  162. return document.createElement(tag)
  163. }
  164. }
  165. /** @type {(handler:string, opts: EventOptions) => (ev:any) => void} */
  166. let createListener = (handler, opts) => ev => {
  167. if (opts.prevent) { ev.preventDefault() }
  168. if (opts.stop) { ev.stopPropagation() }
  169. /** @type {Element} */
  170. let target = ev.target
  171. /** @type {Element} */
  172. let currentTarget = ev.currentTarget
  173. if (opts.exact && target !== currentTarget) {
  174. return
  175. }
  176. ev['webuiIsExactTarget'] = (target === currentTarget)
  177. if (ev instanceof KeyboardEvent) {
  178. let key = ev.key || ev['keyIdentifier']
  179. if (key == 'Esc') { key = 'Escape' }
  180. if (opts.key != '' && key != opts.key) { return }
  181. if (opts.ctrl && !(ev.ctrlKey)) { return }
  182. if (opts.alt && !(ev.altKey)) { return }
  183. if (opts.shift && !(ev.shiftKey)) { return }
  184. }
  185. if (opts.rect) {
  186. let rect = currentTarget.getBoundingClientRect()
  187. ev['webuiRectLeft'] = Math.round(rect.left)
  188. ev['webuiRectTop'] = Math.round(rect.top)
  189. ev['webuiRectWidth'] = Math.round(rect.width)
  190. ev['webuiRectHeight'] = Math.round(rect.height)
  191. if (ev instanceof MouseEvent) {
  192. ev['webuiCurrentTargetX'] = Math.round(ev.clientX - rect.left)
  193. ev['webuiCurrentTargetY'] = Math.round(ev.clientY - rect.top)
  194. }
  195. }
  196. if (target instanceof HTMLInputElement
  197. || target instanceof HTMLSelectElement) {
  198. ev['webuiValue'] = ev.target.value
  199. ev['webuiChecked'] = ev.target.checked
  200. }
  201. if (ev instanceof FocusEvent) {
  202. /** @type {Element} */
  203. // @ts-ignore
  204. let el = ev.relatedTarget
  205. let out = true
  206. while (el != null) {
  207. if (el === currentTarget) {
  208. out = false
  209. break
  210. }
  211. el = el.parentElement
  212. }
  213. ev['webuiFocusWentOutside'] = out
  214. }
  215. bridge.emitEvent({ handler, payload: ev })
  216. }
  217. /** @param {Element} el */
  218. let keepFocus = el => {
  219. let focus = document.activeElement
  220. if (focus && el.contains(focus)) {
  221. // @ts-ignore
  222. return () => { focus.focus && focus.focus() }
  223. } else {
  224. return () => {}
  225. }
  226. }
  227. /** @type {(name: string) => [string,EventOptions]} */
  228. let parseEventName = (name) => {
  229. let t = name.split('.')
  230. let kind = t[0] || ''
  231. t.shift()
  232. let raw_opts = t
  233. let opts = {
  234. global: false,
  235. prevent: false,
  236. stop: false,
  237. exact: false,
  238. rect: false,
  239. key: '',
  240. ctrl: false,
  241. alt: false,
  242. shift: false
  243. }
  244. for (let raw_opt of raw_opts) {
  245. if (opts[raw_opt] === false) {
  246. opts[raw_opt] = true
  247. } else if (raw_opt.startsWith('key=')) {
  248. opts.key = raw_opt.replace('key=', '')
  249. }
  250. }
  251. return [kind, opts]
  252. }
  253. /** @type {(el: Element, {global:bool}) => EventTarget} */
  254. let decideEventTarget = (el, {global}) => {
  255. if (global) {
  256. return window
  257. } else {
  258. return el
  259. }
  260. }
  261. /** @type {PatchOperations} */
  262. let patchOperations = {
  263. ApplyStyle: (id, key, val) => {
  264. try {
  265. elementRegistry[id].style[key] = val
  266. } catch (err) {
  267. console.log('ApplyStyle', { id, key, val }, err)
  268. }
  269. },
  270. EraseStyle: (id, key) => {
  271. try {
  272. elementRegistry[id].style[key] = ''
  273. } catch (err) {
  274. console.log('EraseStyle', { id, key }, err)
  275. }
  276. },
  277. SetAttr: (id, name, val) => {
  278. try {
  279. var el = elementRegistry[id]
  280. if (name == 'value') {
  281. el['value'] = val
  282. } else if (name == 'checked' || name == 'disabled') {
  283. el[name] = true
  284. } else if (name == 'webuiAutofocus') {
  285. el.focus()
  286. if (el instanceof HTMLInputElement && el.type == 'text') {
  287. el.select()
  288. }
  289. } else {
  290. el.setAttribute(name, val)
  291. }
  292. } catch (err) {
  293. console.log('SetAttr', { id, name, val }, err)
  294. }
  295. },
  296. RemoveAttr: (id, name) => {
  297. try {
  298. if (name == 'value') {
  299. elementRegistry[id]['value'] = ''
  300. } else if (name == 'checked' || name == 'disabled') {
  301. elementRegistry[id][name] = false
  302. } else if (name == 'webuiAutofocus') {
  303. // do nothing
  304. } else {
  305. elementRegistry[id].removeAttribute(name)
  306. }
  307. } catch (err) {
  308. console.log('RemoveAttr', { id, name }, err)
  309. }
  310. },
  311. AttachEvent: (id, name, handler) => {
  312. try {
  313. let [kind, opts] = parseEventName(name)
  314. let listener = createListener(handler, opts)
  315. let target = decideEventTarget(elementRegistry[id], opts)
  316. target.addEventListener(kind, listener)
  317. if (!(eventsRegistry[id])) { eventsRegistry[id] = {} }
  318. eventsRegistry[id][name] = { listener, handler }
  319. } catch (err) {
  320. console.log('AttachEvent', { id, name, handler }, err)
  321. }
  322. },
  323. DetachEvent: (id, name) => {
  324. try {
  325. let [kind, opts] = parseEventName(name)
  326. let target = decideEventTarget(elementRegistry[id], opts)
  327. let event = eventsRegistry[id][name]
  328. // @ts-ignore
  329. target.removeEventListener(kind, event.listener)
  330. delete eventsRegistry[id][name]
  331. } catch (err) {
  332. console.log('DetachEvent', { id, name }, err)
  333. }
  334. },
  335. SetText: (id, text) => {
  336. try {
  337. elementRegistry[id].textContent = text
  338. } catch (err) {
  339. console.log('SetText', { id, text }, err)
  340. }
  341. },
  342. AppendNode: (parent, id, tag) => {
  343. try {
  344. let el = createElement(parent, tag)
  345. if (el instanceof HTMLButtonElement) {
  346. // buttons should not get focus by mouse
  347. el.addEventListener('click', _ => { el.blur() })
  348. }
  349. elementRegistry[id] = el
  350. elementRegistry[parent].appendChild(el)
  351. } catch (err) {
  352. console.log('AppendNode', { parent, id, tag }, err)
  353. }
  354. },
  355. RemoveNode: (parent, id) => {
  356. try {
  357. elementRegistry[parent].removeChild(elementRegistry[id])
  358. delete elementRegistry[id]
  359. if (eventsRegistry[id]) {
  360. delete eventsRegistry[id]
  361. }
  362. } catch (err) {
  363. console.log('RemoveNode', { parent, id }, err)
  364. }
  365. },
  366. UpdateNode: (old_id, new_id) => {
  367. try {
  368. let el = elementRegistry[old_id]
  369. delete elementRegistry[old_id]
  370. elementRegistry[new_id] = el
  371. if (eventsRegistry[old_id]) {
  372. eventsRegistry[new_id] = eventsRegistry[old_id]
  373. delete eventsRegistry[old_id]
  374. }
  375. } catch (err) {
  376. console.log('UpdateNode', { old_id, new_id }, err)
  377. }
  378. },
  379. ReplaceNode: (parent, old_id, new_id, tag) => {
  380. try {
  381. let parent_el = elementRegistry[parent]
  382. let old_el = elementRegistry[old_id]
  383. let new_el = createElement(parent, tag)
  384. parent_el.insertBefore(new_el, old_el)
  385. parent_el.removeChild(old_el)
  386. delete elementRegistry[old_id]
  387. elementRegistry[new_id] = new_el
  388. if (eventsRegistry[old_id]) {
  389. delete eventsRegistry[old_id]
  390. }
  391. } catch (err) {
  392. console.log('ReplaceNode', { parent, old_id, new_id, tag }, err)
  393. }
  394. },
  395. SwapNode: (parent, a, b) => {
  396. try {
  397. let parent_el = elementRegistry[parent]
  398. let a_el = elementRegistry[a]
  399. let b_el = elementRegistry[b]
  400. if (a_el.nextElementSibling === b_el) {
  401. let restore = keepFocus(b_el)
  402. parent_el.insertBefore(b_el, a_el)
  403. restore()
  404. } else if (b_el.nextElementSibling === a_el) {
  405. let restore = keepFocus(a_el)
  406. parent_el.insertBefore(a_el, b_el)
  407. restore()
  408. } else {
  409. let placeholder = createElement(parent, 'div')
  410. parent_el.insertBefore(placeholder, b_el)
  411. let restore = keepFocus(b_el)
  412. parent_el.insertBefore(b_el, a_el)
  413. restore()
  414. restore = keepFocus(a_el)
  415. parent_el.insertBefore(a_el, placeholder)
  416. restore()
  417. parent_el.removeChild(placeholder)
  418. }
  419. } catch (err) {
  420. console.log('SwapNode', { parent, a, b }, err)
  421. }
  422. },
  423. MoveNode: (parent, id, pivot) => {
  424. try {
  425. let parent_el = elementRegistry[parent]
  426. let el = elementRegistry[id]
  427. let pivot_el = elementRegistry[pivot]
  428. let restore = keepFocus(el)
  429. parent_el.insertBefore(el, pivot_el)
  430. restore()
  431. } catch (err) {
  432. console.log('MoveNode', { parent, id, pivot }, err)
  433. }
  434. }
  435. }
  436. bridge.PatchActualDOM = ({data}) => {
  437. try {
  438. /** @type {unknown[]} */
  439. let items = JSON.parse(data)
  440. let i = 0
  441. let L = items.length
  442. while (i < L) {
  443. let name = String(items[i])
  444. if (patchOperations[name] instanceof Function) {
  445. /** @type {Function} */
  446. let op = patchOperations[name]
  447. let args = []
  448. for (let j = 0; j < op.length; j += 1) {
  449. i += 1
  450. args.push(items[i])
  451. }
  452. op.apply(null, args)
  453. } else {
  454. throw new Error(`unknown patch operation: ${name}`)
  455. }
  456. i += 1
  457. }
  458. runAllUpdateHooks()
  459. } catch (err) {
  460. console.log(`error patching DOM: ${data}`, err)
  461. }
  462. }
  463. }