123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- /**
- * @typedef {{
- * global: boolean,
- * prevent: boolean,
- * stop: boolean,
- * exact: boolean,
- * rect: boolean,
- * key: string,
- * ctrl: boolean,
- * alt: boolean,
- * shift: boolean
- * }} EventOptions
- */
- /**
- * @typedef {{
- * finishLoad: () => void,
- * emitEvent: (args: { handler:string, payload:Object }) => void,
- * UpdateRootFontSize: (args: { size:number }) => void,
- * InjectCSS: (args: { uuid:string, path:string }) => void,
- * InjectJS: (args: { uuid:string, path:string }) => void,
- * InjectTTF: (args: { uuid:string, path:string, family:string, weight:string, style:string }) => void,
- * CallMethod: (args: { id:string, method:string, args:any[] }) => void,
- * PatchActualDOM: (args: { data:string }) => void
- * }} Bridge
- */
- /**
- * @typedef {{
- * ApplyStyle: (id:string, key:string, value:string) => void,
- * EraseStyle: (id:string, key:string) => void,
- * SetAttr: (id:string, name:string, value:string) => void,
- * RemoveAttr: (id:string, name:string) => void,
- * AttachEvent: (id:string, name:string, handler: string) => void,
- * DetachEvent: (id:string, name:string) => void,
- * SetText: (id:string, content:string) => void,
- * AppendNode: (parent:string, id:string, tag:string) => void
- * RemoveNode: (parent:string, id:string) => void,
- * UpdateNode: (old_id:string, new_id:string) => void,
- * ReplaceNode: (target:string, id:string, tag:string) => void,
- * SwapNode: (parent:string, a:string, b:string) => void,
- * MoveNode: (parent:string, id:string, pivot:string) => void
- * }} PatchOperations
- */
- const SvgXmlNamespace = 'http://www.w3.org/2000/svg'
- const RootElementSelector = 'html'
- /** @type {{ [id:string]: HTMLElement | SVGElement }} */
- let elementRegistry = {}
- /** @type {{ [id:string]: { [name:string]: { listener: function, handler: string } } }} */
- let eventsRegistry = {}
- /** @returns {void} */
- function registerBodyElement() {
- elementRegistry[0] = document.body
- }
- /** @type {Array<() => void>} */
- let updateHooks = []
- function WebUiRegisterUpdateHook(f) {
- updateHooks.push(f)
- f()
- return () => {
- for (let i = 0; i < updateHooks.length; i += 1) {
- if (updateHooks[i] === f) {
- updateHooks.splice(i, 1)
- break
- }
- }
- }
- }
- function runAllUpdateHooks() {
- for (let f of updateHooks) {
- f()
- }
- }
- window.addEventListener('load', _ => {
- let bridge = getBridgeObject()
- connectGeneralSignals(bridge)
- registerBodyElement()
- initializeInteraction(bridge)
- notifyInitialized(bridge)
- // disable page zoom by mouse wheel
- document.addEventListener('wheel',
- ev => { if(ev.ctrlKey) { ev.preventDefault() } },
- { passive: false })
- })
- /** @type {Bridge} */
- let WebBridge = {
- finishLoad: () => {
- alert(`IPC:${JSON.stringify({method:"finishLoad"})}`)
- },
- emitEvent: args => {
- alert(`IPC:${JSON.stringify({method:"emitEvent",args})}`)
- },
- UpdateRootFontSize: null,
- InjectCSS: null,
- InjectJS: null,
- InjectTTF: null,
- CallMethod: null,
- PatchActualDOM: null
- }
- // TODO: Refactor this file
- // NOTE: The following code assumes the WebBridge object is injected from
- // outside, which is not a valid assumption anymore
- // because the implementation changed from QtWebkit to QtWebEngine.
- // It still works but the code is a bit confusing.
- /** @returns {Bridge} */
- function getBridgeObject() {
- // @ts-ignore
- if (typeof WebBridge == 'undefined') {
- console.error('[QtBinding] Bridge Object Not Found')
- throw new Error('[QtBinding] Initialization Failed - Bridge Object Not Found')
- }
- /** @type {Bridge} */
- // @ts-ignore
- let bridge = WebBridge
- console.log('[QtBinding] Bridge Object Found', bridge)
- return bridge
- }
- /** @param {Bridge} bridge */
- function notifyInitialized(bridge) {
- bridge.finishLoad()
- console.log('[QtBinding] Bridge JS-Side Initialized')
- }
- /** @param {Bridge} bridge */
- function connectGeneralSignals(bridge) {
- bridge.UpdateRootFontSize = ({size}) => {
- let root = document.querySelector(RootElementSelector)
- root.style.fontSize = `${size}px`
- }
- bridge.InjectCSS = ({path}) => {
- let link_tag = document.createElement('link')
- link_tag.rel = 'stylesheet'
- link_tag.href = `asset:///${path}`
- document.head.appendChild(link_tag)
- }
- bridge.InjectJS = ({path}) => {
- let script_tag = document.createElement('script')
- script_tag.type = 'text/javascript'
- script_tag.src = `asset:///${path}`
- document.head.appendChild(script_tag)
- }
- bridge.InjectTTF = ({path, family, weight, style}) => {
- let style_tag = document.createElement('style')
- style_tag.textContent = `@font-face {
- font-family: '${family}';
- src: url(asset:///${path}) format('truetype');
- font-weight: ${weight};
- font-style: ${style};
- }`
- document.head.appendChild(style_tag)
- }
- bridge.CallMethod = _ => {
- // TODO: implementation
- throw new Error('bridge.CallMethod(): not implemented')
- }
- }
- /** @param {Bridge} bridge */
- function initializeInteraction(bridge) {
- /** @type {(parent:string, tag:string) => HTMLElement | SVGElement} */
- let createElement = (parent, tag) => {
- let parent_el = elementRegistry[parent]
- if (tag == 'svg' || parent_el instanceof SVGElement) {
- return document.createElementNS(SvgXmlNamespace, tag)
- } else {
- return document.createElement(tag)
- }
- }
- /** @type {(handler:string, opts: EventOptions) => (ev:any) => void} */
- let createListener = (handler, opts) => ev => {
- if (opts.prevent) { ev.preventDefault() }
- if (opts.stop) { ev.stopPropagation() }
- /** @type {Element} */
- let target = ev.target
- /** @type {Element} */
- let currentTarget = ev.currentTarget
- if (opts.exact && target !== currentTarget) {
- return
- }
- ev['webuiIsExactTarget'] = (target === currentTarget)
- if (ev instanceof KeyboardEvent) {
- let key = ev.key || ev['keyIdentifier']
- if (key == 'Esc') { key = 'Escape' }
- if (opts.key != '' && key != opts.key) { return }
- if (opts.ctrl && !(ev.ctrlKey)) { return }
- if (opts.alt && !(ev.altKey)) { return }
- if (opts.shift && !(ev.shiftKey)) { return }
- }
- if (opts.rect) {
- let rect = currentTarget.getBoundingClientRect()
- ev['webuiRectLeft'] = Math.round(rect.left)
- ev['webuiRectTop'] = Math.round(rect.top)
- ev['webuiRectWidth'] = Math.round(rect.width)
- ev['webuiRectHeight'] = Math.round(rect.height)
- if (ev instanceof MouseEvent) {
- ev['webuiCurrentTargetX'] = Math.round(ev.clientX - rect.left)
- ev['webuiCurrentTargetY'] = Math.round(ev.clientY - rect.top)
- }
- }
- if (target instanceof HTMLInputElement
- || target instanceof HTMLSelectElement) {
- ev['webuiValue'] = ev.target.value
- ev['webuiChecked'] = ev.target.checked
- }
- if (ev instanceof FocusEvent) {
- /** @type {Element} */
- // @ts-ignore
- let el = ev.relatedTarget
- let out = true
- while (el != null) {
- if (el === currentTarget) {
- out = false
- break
- }
- el = el.parentElement
- }
- ev['webuiFocusWentOutside'] = out
- }
- bridge.emitEvent({ handler, payload: ev })
- }
- /** @param {Element} el */
- let keepFocus = el => {
- let focus = document.activeElement
- if (focus && el.contains(focus)) {
- // @ts-ignore
- return () => { focus.focus && focus.focus() }
- } else {
- return () => {}
- }
- }
- /** @type {(name: string) => [string,EventOptions]} */
- let parseEventName = (name) => {
- let t = name.split('.')
- let kind = t[0] || ''
- t.shift()
- let raw_opts = t
- let opts = {
- global: false,
- prevent: false,
- stop: false,
- exact: false,
- rect: false,
- key: '',
- ctrl: false,
- alt: false,
- shift: false
- }
- for (let raw_opt of raw_opts) {
- if (opts[raw_opt] === false) {
- opts[raw_opt] = true
- } else if (raw_opt.startsWith('key=')) {
- opts.key = raw_opt.replace('key=', '')
- }
- }
- return [kind, opts]
- }
- /** @type {(el: Element, {global:bool}) => EventTarget} */
- let decideEventTarget = (el, {global}) => {
- if (global) {
- return window
- } else {
- return el
- }
- }
- /** @type {PatchOperations} */
- let patchOperations = {
- ApplyStyle: (id, key, val) => {
- try {
- elementRegistry[id].style[key] = val
- } catch (err) {
- console.log('ApplyStyle', { id, key, val }, err)
- }
- },
- EraseStyle: (id, key) => {
- try {
- elementRegistry[id].style[key] = ''
- } catch (err) {
- console.log('EraseStyle', { id, key }, err)
- }
- },
- SetAttr: (id, name, val) => {
- try {
- var el = elementRegistry[id]
- if (name == 'value') {
- el['value'] = val
- } else if (name == 'checked' || name == 'disabled') {
- el[name] = true
- } else if (name == 'webuiAutofocus') {
- el.focus()
- if (el instanceof HTMLInputElement && el.type == 'text') {
- el.select()
- }
- } else {
- el.setAttribute(name, val)
- }
- } catch (err) {
- console.log('SetAttr', { id, name, val }, err)
- }
- },
- RemoveAttr: (id, name) => {
- try {
- if (name == 'value') {
- elementRegistry[id]['value'] = ''
- } else if (name == 'checked' || name == 'disabled') {
- elementRegistry[id][name] = false
- } else if (name == 'webuiAutofocus') {
- // do nothing
- } else {
- elementRegistry[id].removeAttribute(name)
- }
- } catch (err) {
- console.log('RemoveAttr', { id, name }, err)
- }
- },
- AttachEvent: (id, name, handler) => {
- try {
- let [kind, opts] = parseEventName(name)
- let listener = createListener(handler, opts)
- let target = decideEventTarget(elementRegistry[id], opts)
- target.addEventListener(kind, listener)
- if (!(eventsRegistry[id])) { eventsRegistry[id] = {} }
- eventsRegistry[id][name] = { listener, handler }
- } catch (err) {
- console.log('AttachEvent', { id, name, handler }, err)
- }
- },
- DetachEvent: (id, name) => {
- try {
- let [kind, opts] = parseEventName(name)
- let target = decideEventTarget(elementRegistry[id], opts)
- let event = eventsRegistry[id][name]
- // @ts-ignore
- target.removeEventListener(kind, event.listener)
- delete eventsRegistry[id][name]
- } catch (err) {
- console.log('DetachEvent', { id, name }, err)
- }
- },
- SetText: (id, text) => {
- try {
- elementRegistry[id].textContent = text
- } catch (err) {
- console.log('SetText', { id, text }, err)
- }
- },
- AppendNode: (parent, id, tag) => {
- try {
- let el = createElement(parent, tag)
- if (el instanceof HTMLButtonElement) {
- // buttons should not get focus by mouse
- el.addEventListener('click', _ => { el.blur() })
- }
- elementRegistry[id] = el
- elementRegistry[parent].appendChild(el)
- } catch (err) {
- console.log('AppendNode', { parent, id, tag }, err)
- }
- },
- RemoveNode: (parent, id) => {
- try {
- elementRegistry[parent].removeChild(elementRegistry[id])
- delete elementRegistry[id]
- if (eventsRegistry[id]) {
- delete eventsRegistry[id]
- }
- } catch (err) {
- console.log('RemoveNode', { parent, id }, err)
- }
- },
- UpdateNode: (old_id, new_id) => {
- try {
- let el = elementRegistry[old_id]
- delete elementRegistry[old_id]
- elementRegistry[new_id] = el
- if (eventsRegistry[old_id]) {
- eventsRegistry[new_id] = eventsRegistry[old_id]
- delete eventsRegistry[old_id]
- }
- } catch (err) {
- console.log('UpdateNode', { old_id, new_id }, err)
- }
- },
- ReplaceNode: (parent, old_id, new_id, tag) => {
- try {
- let parent_el = elementRegistry[parent]
- let old_el = elementRegistry[old_id]
- let new_el = createElement(parent, tag)
- parent_el.insertBefore(new_el, old_el)
- parent_el.removeChild(old_el)
- delete elementRegistry[old_id]
- elementRegistry[new_id] = new_el
- if (eventsRegistry[old_id]) {
- delete eventsRegistry[old_id]
- }
- } catch (err) {
- console.log('ReplaceNode', { parent, old_id, new_id, tag }, err)
- }
- },
- SwapNode: (parent, a, b) => {
- try {
- let parent_el = elementRegistry[parent]
- let a_el = elementRegistry[a]
- let b_el = elementRegistry[b]
- if (a_el.nextElementSibling === b_el) {
- let restore = keepFocus(b_el)
- parent_el.insertBefore(b_el, a_el)
- restore()
- } else if (b_el.nextElementSibling === a_el) {
- let restore = keepFocus(a_el)
- parent_el.insertBefore(a_el, b_el)
- restore()
- } else {
- let placeholder = createElement(parent, 'div')
- parent_el.insertBefore(placeholder, b_el)
- let restore = keepFocus(b_el)
- parent_el.insertBefore(b_el, a_el)
- restore()
- restore = keepFocus(a_el)
- parent_el.insertBefore(a_el, placeholder)
- restore()
- parent_el.removeChild(placeholder)
- }
- } catch (err) {
- console.log('SwapNode', { parent, a, b }, err)
- }
- },
- MoveNode: (parent, id, pivot) => {
- try {
- let parent_el = elementRegistry[parent]
- let el = elementRegistry[id]
- let pivot_el = elementRegistry[pivot]
- let restore = keepFocus(el)
- parent_el.insertBefore(el, pivot_el)
- restore()
- } catch (err) {
- console.log('MoveNode', { parent, id, pivot }, err)
- }
- }
- }
- bridge.PatchActualDOM = ({data}) => {
- try {
- /** @type {unknown[]} */
- let items = JSON.parse(data)
- let i = 0
- let L = items.length
- while (i < L) {
- let name = String(items[i])
- if (patchOperations[name] instanceof Function) {
- /** @type {Function} */
- let op = patchOperations[name]
- let args = []
- for (let j = 0; j < op.length; j += 1) {
- i += 1
- args.push(items[i])
- }
- op.apply(null, args)
- } else {
- throw new Error(`unknown patch operation: ${name}`)
- }
- i += 1
- }
- runAllUpdateHooks()
- } catch (err) {
- console.log(`error patching DOM: ${data}`, err)
- }
- }
- }
|