UNPKG

remoteview

Version:

Remote isolated browser platform by Dosyago

1,036 lines (916 loc) 34.1 kB
import {handleSelectMessage} from './handlers/selectInput.js'; import {fetchTabs} from './handlers/targetInfo.js'; import {demoZombie, fetchDemoTabs} from './handlers/demo.js'; import {handleKeysCanInputMessage} from './handlers/keysCanInput.js'; import {handleElementInfo} from './handlers/elementInfo.js'; import {handleScrollNotification} from './handlers/scrollNotify.js'; import {resetLoadingIndicator,showLoadingIndicator} from './handlers/loadingIndicator.js'; import {resetFavicon, handleFaviconMessage} from './handlers/favicon.js'; import EventQueue from './eventQueue.js'; import {default as transformEvent, getKeyId, controlChars} from './transformEvent.js'; import {logit, sleep, debounce, DEBUG, BLANK, isFirefox, isSafari, deviceIsMobile} from './common.js'; import {component, subviews} from './view.js'; import installDemoPlugin from '../../plugins/demo/installPlugin.js'; import installAppminifierPlugin from '../../plugins/appminifier/installPlugin.js'; import installProjectorPlugin from '../../plugins/projector/installPlugin.js'; const ThrottledEvents = new Set([ "mousemove", "pointermove", "touchmove" ]); const CancelWhenSyncValue = new Set([ "keydown", "keyup", "keypress", "compositionstart", "compositionend", "compositionupdate", ]); const EnsureCancelWhenSyncValue = e => { if ( !e.type.startsWith("key") ) { return true; } else { const id = getKeyId(e); return !controlChars.has(id); } }; const SessionlessEvents = new Set([ "window-bounds", "window-bounds-preImplementation", "user-agent", "hide-scrollbars" ]); const IMMEDIATE = 0; const SHORT_DELAY = 20; const LONG_DELAY = 300; const VERY_LONG_DELAY = 60000; const EVENT_THROTTLE_MS = 40; /* 20, 40, 80 */ // view frame debug let latestRequestId = 0; export default async function voodoo(selector, position, { postInstallTasks: postInstallTasks = [], preInstallTasks: preInstallTasks = [], canvasBondTasks: canvasBondTasks = [], bondTasks: bondTasks = [], useViewFrame: useViewFrame = false, demoMode: demoMode = false, } = {}) { const sessionToken = location.hash && location.hash.slice(1); location.hash = ''; const closed = new Set(); const listeners = new Map(); const lastTarget = '[lastTarget]'; const {tabs,activeTarget,requestId} = await (demoMode ? fetchDemoTabs() : fetchTabs({sessionToken})); latestRequestId = requestId; const state = { H, // bandwidth messageDelay: 0, // time it takes to receive an average, non-frame message showBandwidthRate: true, myBandwidth: 0, serverBandwidth: 0, totalBytes: 0, totalServerBytesThisSecond: 0, totalBytesThisSecond: 0, totalBandwidth: 0, frameBandwidth: [], // demo mode demoMode, // if we are using a view frame (instead of canvas) useViewFrame, // for chrome vs firefox and mobile vs desktop to handle // different ways of doing IME input and keypress openKey: '', lastKeypressKey: '', // for firefox because it's IME does not fire inputType // so we have no simple way to handle deleting content backward // this should be FF on MOBILE only probably so that's why it's false convertTypingEventsToSyncValueEvents: deviceIsMobile(), //convertTypingEventsToSyncValueEvents: false, // for safari to detect if pointerevents work DoesNotSupportPointerEvents: true, // safari to keep track of composition isComposing: false, // useful for input events that don't support data // invalidated by the first data prop set DataIsNotSupported: true, sizeBrowserToBounds, asyncSizeBrowserToBounds, emulateNavigator, hideScrollbars, bondTasks, canvasBondTasks, // tabs updateTabsTasks: [], lastTarget, activeTarget, tabs, attached: new Set(), activateTab, closeTab, createTab, activeTab, favicons: new Map(), // timing constants IMMEDIATE, SHORT_DELAY, LONG_DELAY, VERY_LONG_DELAY, EVENT_THROTTLE_MS, viewState: {}, clearViewport, addListener(name, func) { let funcList = listeners.get(name); if ( ! funcList ) { funcList = []; listeners.set(name, funcList); } funcList.push(func); } }; const updateTabs = debounce(rawUpdateTabs, LONG_DELAY); if ( state.demoMode ) { state.demoEventConsumer = demoZombie; } if ( DEBUG.dev ) { Object.assign(self, {state}); } if ( location.search.includes(`url=`) ) { let taskUrl; try { taskUrl = decodeURIComponent(location.search.split('&').filter(x => x.includes('url='))[0].split('=')[1]); } catch(e) { alert(e); console.warn(e); taskUrl = location.search.split('&').filter(x => x.includes('url='))[0].split('=')[1]; } postInstallTasks.push(async ({queue}) => { let completed = false; queue.addMetaListener('changed', async meta => { if ( completed ) return; if ( meta.changed.type == 'page' && meta.changed.url.startsWith("https://isolation.site/redirect") ) { await sleep(500); await activateTab(null, meta.changed) await sleep(2000); H({ synthetic: true, type: "url-address", event: null, url: taskUrl }); completed = true; await sleep(5000); history.pushState("", "", "/") } }); state.createTab(null, "https://isolation.site/redirect.html"); }); } const queue = new EventQueue(state, sessionToken); // plugins const plugins = new Map; if ( state.useViewFrame ) { installAppminifierPlugin(state,queue); if ( location.pathname == "/factory.html" ) { installProjectorPlugin(state,queue); } if ( state.demoMode ) { installDemoPlugin(state,queue); } } if ( isSafari() ) { queue.send({type:"isSafari"}); } else if ( isFirefox() ) { queue.send({type:"isFirefox"}); } if ( deviceIsMobile() ) { state.hideScrollbars(); queue.send({type:"isMobile"}); } // event handlers // input queue.addMetaListener('selectInput', meta => handleSelectMessage(meta, state)); queue.addMetaListener('keyInput', meta => handleKeysCanInputMessage(meta, state)); queue.addMetaListener('favicon', meta => handleFaviconMessage(meta, state)); queue.addMetaListener('navigated', () => canKeysInput()); queue.addMetaListener('navigated', ({navigated:{targetId}}) => resetFavicon({targetId}, state)); //queue.addMetaListener('navigated', meta => takeShot(meta, state)); // element info queue.addMetaListener('elementInfo', meta => handleElementInfo(meta, state)); // scroll queue.addMetaListener('scroll', meta => handleScrollNotification(meta, state)); // loading queue.addMetaListener('resource', meta => showLoadingIndicator(meta, state)); queue.addMetaListener('failed', meta => { if ( meta.failed.params.type == "Document" ) { // we also need to make sure the failure happens at the top level document // rather than writing the top level document for any failure in a sub frame if ( meta.failed.params.errorText && meta.failed.params.errorText.includes("ABORTED") ) { // do nothing. Aborts are normal, and just mean existing document stays there. If we // overwrite that document with an aborted message, the normal function of the page // will fail // so we could notify with a writeCanvas instead // writeCanvas(`Request failed: ${meta.failed.params.errorText}`, meta.failed.frameId, meta.failed.sessionId); // but there's not really any point since these aborted errors are normally // quickly followed by something that works // plus people are not used to seeing aborted } else { writeDocument(`Request failed: ${meta.failed.params.errorText}`, meta.failed.frameId, meta.failed.sessionId); } } }); queue.addMetaListener('navigated', meta => resetLoadingIndicator(meta, state)); if ( DEBUG.val >= DEBUG.med ) { queue.addMetaListener('navigated', meta => console.log(meta)); queue.addMetaListener('changed', meta => console.log(meta)); queue.addMetaListener('created', meta => console.log(meta)); queue.addMetaListener('attached', meta => console.log(meta)); queue.addMetaListener('detached', meta => console.log(meta)); queue.addMetaListener('destroyed', meta => console.log(meta)); queue.addMetaListener('crashed', meta => console.log(meta)); queue.addMetaListener('consoleMessage', meta => console.log(meta)); } // patch tabs array with changes as they come through queue.addMetaListener('changed', ({changed}) => { const tab = findTab(changed.targetId); if ( tab ) { Object.assign(tab, changed); subviews.TabList(state); } updateTabs({changed}); }); // tabs queue.addMetaListener('created', meta => { if ( meta.created.type == 'page') { setTimeout(() => activateTab(null, meta.created), LONG_DELAY); //state.updateTabsTasks.push(() => setTimeout(() => activateTab(null, meta.created), LONG_DELAY)); //updateTabs(); //sizeBrowserToBounds(state.viewState.canvasEl, meta.created.targetId); } }); queue.addMetaListener('attached', meta => { const attached = meta.attached.targetInfo; if ( attached.type == 'page' ) { state.attached.add(attached.targetId); if ( state.useViewFrame ) { sizeBrowserToBounds(state.viewState.viewFrameEl, attached.targetId); } else { sizeBrowserToBounds(state.viewState.canvasEl, attached.targetId); emulateNavigator(); } updateTabs(); } }); queue.addMetaListener('navigated', updateTabs); queue.addMetaListener('detached', updateTabs); queue.addMetaListener('destroyed', ({destroyed}) => { closed.delete(destroyed.targetId); updateTabs(); }); queue.addMetaListener('crashed', updateTabs); //modals queue.addMetaListener('modal', modalMessage => subviews.openModal(modalMessage, state)); // remote secure downloads queue.addMetaListener('download', ({download}) => { const {sessionId, filename} = download; const modal = { sessionId, type: 'notice', message: `The file "${filename}" is downloading to a secure location and will be displayed securely momentarily if it is a supported format.`, otherButton: { title: 'Get License', onclick: () => window.open('mailto:cris@dosycorp.com?Subject=ViewFinder+License+Support+Inquiry&body=Hi%20Cris', "_blank") }, title: "SecureView\u2122 Enabled", }; subviews.openModal({modal}, state); }); queue.addMetaListener('secureview', ({secureview}) => { const {url} = secureview; if ( url ) { createTab(null, url); } }); // HTTP auth queue.addMetaListener('authRequired', ({authRequired}) => { const {requestId} = authRequired; const modal = { requestId, type: 'auth', message: `Provide credentials to continue`, title: `HTTP Auth`, }; subviews.openModal({modal}, state); }); // File chooser queue.addMetaListener('fileChooser', ({fileChooser}) => { const {sessionId, mode, accept, csrfToken} = fileChooser; const modal = { sessionId, mode, accept, csrfToken, type: 'filechooser', message: `Securely send files to the remote page.`, title: `File Chooser`, }; console.log({fileChooserModal:modal}); subviews.openModal({modal}, state); }); // make this so we can call it on resize window._voodoo_asyncSizeTab = () => setTimeout(sizeTab, 0); // bond tasks canvasBondTasks.push(indicateNoOpenTabs); canvasBondTasks.push(installZoomListener); canvasBondTasks.push(asyncSizeBrowserToBounds); canvasBondTasks.push(rawUpdateTabs); if ( isSafari() ) { canvasBondTasks.push(installSafariLongTapListener); } bondTasks.push(canKeysInput); bondTasks.push(getFavicon); bondTasks.push(installTopLevelKeyListeners); const preInstallView = {queue}; for( const task of preInstallTasks ) { try { task(preInstallView); } catch(e) { console.error(`Task ${task} failed with ${e}`); } } component(state).to(selector, position); const api = { back: () => 1, forward: () => 1, reload: () => 1, stop: () => 1, mouse: () => 1, touch: () => 1, scroll: () => 1, key: () => 1, type: () => 1, omni: () => 1, newTab: () => 1, closeTab: () => 1, switchTab: () => 1, }; const pluginView = {addToQueue, subscribeToQueue, requestRender, api}; const poppetView = {loadPlugin, api}; const postInstallView = {queue}; await sleep(0); for( const task of postInstallTasks ) { try { task(postInstallView); } catch(e) { console.error(`Task ${task} failed with ${e}`); } } if ( activeTarget ) { setTimeout(() => activateTab(null, {targetId:activeTarget}), LONG_DELAY); //state.updateTabsTasks.push(() => setTimeout(() => activateTab(null, {targetId:activeTarget}), LONG_DELAY)); //updateTabs(); //sizeBrowserToBounds(state.viewState.canvasEl, activeTarget); } return poppetView; // closures /*function doShot() { setTimeout(() => { queue.send({ type: "doShot", synthetic: true }); }, SHORT_DELAY); }*/ function runListeners(name, data) { const funcList = listeners.get(name); if ( ! funcList || funcList.length == 0 ) return false; let score = false; for (const func of funcList) { try { func(data); score = score || true; } catch(e) { console.log(`Listeners func for ${name} fails: ${func}\nError: ${e + e.stack}`); } } return score; } function findTab(id) { return state.tabs.find(({targetId}) => id == targetId); } function activeTab() { return state.tabs.length == 1 ? state.tabs[0] : findTab(state.activeTarget) || {}; } function indicateNoOpenTabs() { if ( state.tabs.length == 0 ) { clearViewport(); if ( state.useViewFrame ) { try { state.viewState.viewFrameEl.contentDocument.body.innerHTML = ` <em>${state.factoryMode ? 'Factory Mode' : 'Custom Mode'}. No tabs open.</em> `; } catch(e) { console.warn(e); } } else { writeCanvas("No tabs open."); } } } function writeCanvas(text) { const canv = state.viewState.canvasEl; const ctx = state.viewState.ctx; ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canv.width, canv.height); ctx.fillStyle = 'silver'; ctx.font = 'italic 3vmax sans-serif'; ctx.textAlign = "center"; ctx.fillText(text, innerWidth/2, innerHeight/2-6*Math.max(innerWidth/100, innerHeight/100)); } function writeDocument(html, frameId, sessionId) { queue.send({ type: 'setDocument', html, frameId, sessionId, synthetic: true }); } function clearViewport() { if ( state.useViewFrame ) { try { state.viewState.viewFrameEl.contentDocument.body.innerHTML = ``; } catch(e) { console.warn(e); } } else { const canv = state.viewState.canvasEl; const ctx = state.viewState.ctx; ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canv.width, canv.height); } } function sendKey(keyEvent) { const {viewState} = state; if ( ! ( viewState.shouldHaveFocus || document.activeElement == viewState.omniBoxInput ) ) { let ev = keyEvent; if ( ev.key == "Tab" || ev.key == "Enter" ) { // do nothing } else{ H(ev); } } } function installTopLevelKeyListeners() { if ( ! deviceIsMobile() ) { self.addEventListener('keydown', sendKey); self.addEventListener('keypress', sendKey); self.addEventListener('keyup', sendKey); } } function installSafariLongTapListener(el) { const FLAGS = {passive:true, capture:true}; const MIN_DURATION = 200; const MAX_MOVEMENT = 20; let lastStart; el.addEventListener('touchstart', ts => lastStart = ts, FLAGS); el.addEventListener('touchend', triggerContextMenuIfLongEnough, FLAGS); el.addEventListener('touchcancel', triggerContextMenuIfLongEnough, FLAGS); function triggerContextMenuIfLongEnough(tf) { // space const touch1 = lastStart.changedTouches[0]; const touch2 = tf.changedTouches[0]; const movement = Math.hypot( touch2.pageX - touch1.pageX, touch2.pageY - touch1.pageY ); // time const duration = tf.timeStamp - lastStart.timeStamp; if ( duration > MIN_DURATION && movement < MAX_MOVEMENT ) { lastStart.preventDefault(); tf.preventDefault(); const {pageX,pageY,clientX,clientY} = touch1; el.dispatchEvent(new CustomEvent('contextmenu', {detail:{pageX,pageY,clientX,clientY}})); } } } function installZoomListener(el) { const FLAGS = {passive:true}; let lastScale = 1.0; let scaling = false; let startDist = 0; let lastDist = 0; let touch; el.addEventListener('touchstart', begin, FLAGS); el.addEventListener('touchmove', move, FLAGS); el.addEventListener('touchend', end, FLAGS); el.addEventListener('touchcancel', end, FLAGS); el.addEventListener('wheel', sendZoom, {passive:true, capture: true}); function sendZoom(event) { if ( event.ctrlKey || event.deltaZ != 0 ) { const delta = event.deltaZ || event.deltaY; const direction = Math.sign(delta); let multiplier; if ( direction > 0 ) { multiplier = 1/1.25; } else { multiplier = 1.25; } const scale = lastScale * multiplier; lastScale = scale; DEBUG.val > DEBUG.low && console.log('sending zoom ' + scale); H({ synthetic:true, type:'zoom', scale, event }); } } function begin(event) { if ( event.touches.length == 2 ) { startDist = Math.hypot( event.touches[0].pageX - event.touches[1].pageX, event.touches[0].pageY - event.touches[1].pageY ); if ( startDist > 8 ) { scaling = true; touch = event.touches[0]; } else { scaling = false; } } } function move(event) { if ( scaling ) { const dist = Math.hypot( event.touches[0].pageX - event.touches[1].pageX, event.touches[0].pageY - event.touches[1].pageY ); lastDist = dist; } } function end() { if ( scaling ) { if ( lastDist < 8 ) { // do nothing, } else { const scale = lastScale * Math.abs(lastDist / startDist); lastScale = scale; DEBUG.val > DEBUG.low && console.log('sending zoom ' + scale); H({ synthetic:true, type:'zoom', scale, event: touch }); } scaling = false; startDist = 0; lastDist = 0; touch = false; } } } function H(event) { // block if no tabs if (state.tabs.length == 0) { if ( SessionlessEvents.has(event.type) ) { DEBUG.val > DEBUG.low && console.log(`passing through sessionless event of type ${event.type}`); } else return; } if ( event.defaultPrevented ) return; const mouseEventOnPointerDevice = event.type.startsWith("mouse") && event.type !== "wheel" && !state.DoesNotSupportPointerEvents; const tabKeyPressForBrowserUI = event.key == "Tab" && !event.vRetargeted; const unnecessaryIfSyncValue = ( state.convertTypingEventsToSyncValueEvents && CancelWhenSyncValue.has(event.type) && EnsureCancelWhenSyncValue(event) ); const eventCanBeIgnored = mouseEventOnPointerDevice || tabKeyPressForBrowserUI || unnecessaryIfSyncValue; if ( eventCanBeIgnored ) return; const pointerEvent = event.type.startsWith("pointer"); const mouseWheel = event.type == "wheel"; const syntheticNonTypingEventWrapper = event.synthetic && event.type != "typing" && event.event; if ( mouseWheel ) { // do nothing } else if ( pointerEvent ) { state.DoesNotSupportPointerEvents = false; } else if ( syntheticNonTypingEventWrapper ) { event.event.preventDefault && event.event.preventDefault(); } const simulated = event.event && event.event.simulated; const hasTarget = event.event && event.event.target; if ( event.type == "typing" && hasTarget && ! simulated && state.convertTypingEventsToSyncValueEvents ) { event.type = 'typing-syncValue'; event.value = event.event.target.value; event.contextId = state.contextIdOfFocusedInput; event.data = ""; } const isThrottled = ThrottledEvents.has(event.type); const transformedEvent = transformEvent(event); if ( mouseWheel ) { transformedEvent.contextId = state.viewState.latestScrollContext; } if ( isThrottled ) { queue.send(transformedEvent); } else { if ( event.type == "keydown" && event.key == "Enter" ) { // Note // We do this to make sure we send composed input data when enter is pressed // in an input field, if we do not do this, the current composition is not printed // but only if we are not using sync mode // (otherwise we will add an unnecessary bit on the end!) if ( ! state.convertTypingEventsToSyncValueEvents ) { if ( !! state.latestData && !! event.target.matches('input') && state.latestData.length > 1 ) { queue.send(transformEvent({ synthetic: true, type: 'typing', data: state.latestData, event: {enterKey:true,simulated:true} })); state.latestCommitData = state.latestData; state.latestData = ""; } } } else if ( event.type == "keydown" && event.key == "Backspace" ) { state.backspaceFiring = true; if ( state.viewState.shouldHaveFocus && ! state.convertTypingEventsToSyncValueEvents ) { state.viewState.shouldHaveFocus.value = ""; } } else if ( event.type == "keyup" && event.key == "Backspace" ) { state.backspaceFiring = false; } else if ( event.type == "pointerdown" || event.type == "mousedown" ) { if ( ! state.convertTypingEventsToSyncValueEvents ) { //const {timeStamp,type} = event; const {latestData} = state; if ( !! state.viewState.shouldHaveFocus && !! latestData && latestData.length > 1 && latestData != state.latestCommitData) { state.isComposing = false; const data = latestData; queue.send(transformEvent({ synthetic: true, type: 'typing', data: data, event: {pointerDown:true,simulated:true} })); state.latestCommitData = data; state.latestData = ""; } } } else if ( event.type == "pointerup" || event.type == "mouseup" ) { if ( state.viewState.killNextMouseReleased ) { state.viewState.killNextMouseReleased = false; return; } } queue.send(transformedEvent); } } function sizeBrowserToBounds(el, targetId) { let {width, height} = el.getBoundingClientRect(); width = Math.round(width); height = Math.round(height); if ( el.width != width || el.height != height ) { el.width = width; el.height = height; } const mobile = deviceIsMobile(); H({ synthetic: true, type: "window-bounds", width, height, targetId: targetId || state.activeTarget }); H({ synthetic: true, type: "window-bounds-preImplementation", width, height, mobile, targetId: targetId || state.activeTarget }); self.ViewportWidth = width; self.ViewportHeight = height; } function sizeTab() { return sizeBrowserToBounds(state.viewState.canvasEl); } function asyncSizeBrowserToBounds(el) { setTimeout(() => (sizeBrowserToBounds(el), indicateNoOpenTabs()), 0); } function emulateNavigator() { const {platform,userAgent,language:acceptLanguage} = navigator; H({ synthetic: true, type: "user-agent", userAgent, platform , acceptLanguage }); } function hideScrollbars() { H({ synthetic: true, type: "hide-scrollbars", }); } async function activateTab(click, tab) { // don't activate if the click was a close click from our tab if ( click && click.currentTarget.querySelector('button.close') == click.target ) return; click && click.preventDefault(); // sometimes we delay the call to activate tab and // in the meantime the list of tabs can empty // so we exit if there is no tab to activate if ( ! tab ) return; if ( click ) { setTimeout( () => click.target.closest('li') .scrollIntoView({inline:'center', behavior:'smooth'}), LONG_DELAY ); } if ( state.activeTarget == tab.targetId ) { if ( state.viewState.omniBoxInput == state.viewState.lastActive ) { state.viewState.omniBoxInput.focus(); } return; } const {targetId} = tab; queue.send({ command: { name: "Target.activateTarget", params: {targetId}, requiresShot: true, } }); sizeTab(); canKeysInput(); state.lastTarget = state.activeTarget; state.activeTarget = targetId; // we assume that a listener will call clearviewport // this returns false if there are no listeners if ( ! runListeners('activateTab') ) { clearViewport(); } state.active = activeTab(); subviews.TabList(state); subviews.OmniBox(state); subviews.LoadingIndicator(state); sizeBrowserToBounds(state.viewState.canvasEl); setTimeout(() => { if ( state.active && state.active.url != BLANK ) { canKeysInput(); } else { /** writeDocument(` <!DOCTYPE html> <style> :root { height: 100%; background: #${Math.floor(Math.random() * 0x1000000).toString(16).padStart(6, 0)}; color: navy; font-family: system-ui; } h2 { } strong { padding: 0.5rem; } </style> <h2> Secure ViewFinder </h2> <strong> Current time: ${(new Date).toString()} </strong> </html> `); **/ writeCanvas("Secure ViewFinder"); state.viewState.omniBoxInput.focus(); } }, SHORT_DELAY); } async function closeTab(click, tab, index) { const {targetId} = tab; closed.add(targetId); resetLoadingIndicator({navigated:targetId},state); setTimeout(() => closed.delete(targetId), VERY_LONG_DELAY); const events = [ { command: { name: "Target.closeTarget", params: {targetId}, } } ]; await queue.send(events); state.tabs.splice(index,1); if ( state.activeTarget == targetId ) { if ( state.tabs.length == 0 ) { state.activeTarget = null; } else { if ( index >= state.tabs.length ) { index = state.tabs.length-1; } const newActive = state.tabs[index]; activateTab(click, newActive); } } else { updateTabs(); } subviews.TabList(state); subviews.LoadingIndicator(state); } async function rawUpdateTabs() { let {tabs,activeTarget,requestId} = await (demoMode ? fetchDemoTabs() : fetchTabs({sessionToken})); tabs = tabs.filter(({targetId}) => !closed.has(targetId)); if ( requestId <= latestRequestId ) { return; } else { latestRequestId = requestId; } state.tabs = tabs; if ( demoMode ) { state.activeTarget = activeTarget; } state.active = activeTab(); // this ensures we activate the tab if ( state.tabs.length == 1 ) { setTimeout(() => activateTab(null, state.tabs[0]), LONG_DELAY); //state.updateTabsTasks.push(() => setTimeout(() => activateTab(null, state.tabs[0]), LONG_DELAY)); //updateTabs(); //sizeBrowserToBounds(state.viewState.canvasEl, state.tabs[0].targetId); } else if( !state.activeTarget || !state.active) { if ( state.tabs.length ) { setTimeout(() => activateTab(null, state.tabs[0]), LONG_DELAY); //state.updateTabsTasks.push(() => setTimeout(() => activateTab(null, state.tabs[0]), LONG_DELAY)); //updateTabs(); //sizeBrowserToBounds(state.viewState.canvasEl, state.tabs[0].targetId); } } subviews.Controls(state); subviews.TabList(state); if ( state.tabs.length == 0 ) { indicateNoOpenTabs(); } while(state.updateTabsTasks.length) { const task = state.updateTabsTasks.shift(); try { task(); } catch(e) { console.warn("State update tabs task failed", e, task); } } } async function createTab(click, url = BLANK) { queue.send({ command: { name: "Target.createTarget", params: { url, enableBeginFrameControl: DEBUG.frameControl }, } }); if ( click ) { click.target.blur(); click.currentTarget.blur(); } } function canKeysInput() { if ( state.viewState.viewFrameEl ) return; setTimeout(() => { queue.send({ type: "canKeysInput", synthetic: true }); }, SHORT_DELAY); } function getFavicon() { setTimeout(() => { queue.send({ type: "getFavicon", synthetic: true }); }, IMMEDIATE); } function loadPlugin(plugin) { plugins.set(plugin.name, plugin); plugin.load(pluginView); } function addToQueue(/*...events*/) { console.warn("Unimplemented"); } function requestRender(/*pluginRenderedView*/) { console.warn("Unimplemented"); } function subscribeToQueue(/*name, listener*/) { console.warn("Unimplemented"); } } export function cloneKeyEvent(event, vRetargeted) { return { type: event.type, keyCode: event.keyCode, key: event.key, code: event.code, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, shiftKey: event.shiftKey, vRetargeted }; }