import _ from "lodash"; import utils from "utils"; import CommonMixin from "dom/common_mixin"; import CommandsMixin from "dom/commands_mixin"; import Dimensions from "dom/dimensions"; import GraphicsBuilder from "dom/graphics_builder"; import TextBuilder from "dom/text_builder"; // Entrypoint for managing a single tab export default class extends utils.mixins(CommonMixin, CommandsMixin) { constructor() { super(); this.dimensions = new Dimensions(); // Whether the DOM has loaded this.is_dom_loaded = false; // Whether the page has finished "spinning" this.is_page_finished_loading = false; // For Browsh used via the interactive CLI ap this._is_interactive_mode = false; // For Browsh used via the HTTP server this._is_raw_mode = false; this._setupInit(); } _postSetupConstructor() { this._injectCustomCSS(); this.dimensions.channel = this.channel; this.graphics_builder = new GraphicsBuilder( this.channel, this.dimensions, this.config ); this.text_builder = new TextBuilder( this.channel, this.dimensions, this.graphics_builder, this.config ); } _willHideText() { if (this.is_dom_loaded && this.graphics_builder) { this.graphics_builder.hideText(); } else { setTimeout(this._willHideText.bind(this), 1); } } sendFrame() { this.dimensions.update(); if (this.dimensions.dom.is_new) { this.sendAllBigFrames(); } this.sendSmallPixelFrame(); this._sendTabInfo(); if (!this._is_first_frame_finished) { this.sendMessage("/status,parsing_complete"); } this._is_first_frame_finished = true; } sendAllBigFrames() { if (!this._is_interactive_mode) { return; } if (!this.dimensions.tty.width) { this.log("Not sending big frames without TTY data"); return; } else { this.log("Sending big frames..."); } this.dimensions.update(); this.dimensions.setSubFrameDimensions("big"); this.text_builder.sendFrame(); this.graphics_builder.sendFrame(); this.dimensions.frame.x_last_big_frame = this.dimensions.frame.x_scroll; this.dimensions.frame.y_last_big_frame = this.dimensions.frame.y_scroll; } sendRawText() { if (this.is_page_finished_loading) { this.dimensions.update(); this.dimensions.setSubFrameDimensions("raw_text"); this.text_builder.sendRawText(this._raw_mode_type); } else { setTimeout(this.sendRawText.bind(this), 1); } } sendSmallPixelFrame() { if (!this._is_interactive_mode) { return; } if (!this.dimensions.tty.width) { this.log("Not sending small frames without TTY data"); return; } this.dimensions.update(); this.dimensions.setSubFrameDimensions("small"); this.graphics_builder.sendFrame(); } sendSmallTextFrame() { if (!this._is_interactive_mode) { return; } if (!this.dimensions.tty.width) { this.log("Not sending small frames without TTY data"); return; } this.dimensions.update(); this.dimensions.setSubFrameDimensions("small"); this.text_builder.sendFrame(); } _postCommsInit() { this.log("Webextension postCommsInit()"); this._sendTabInfo(); this.sendMessage("/status,page_init"); this._listenForBackgroundMessages(); this._startWindowEventListeners(); } // Fire up the TTY interactive mode. It doesn't need to wait for any particular // DOM stage as it's good to just get something in front of the user as soon // as possible. _setupInteractiveMode() { this._setupDebouncedFunctions(); this._startMutationObserver(); this.sendAllBigFrames(); // TODO: // Disabling CSS transitions is not easy, many pages won't even render // if they're disabled. Eg; Google's login process. // What if we could get a post-transition hook? setTimeout(() => { this.sendAllBigFrames(); }, 500); } _setupDebouncedFunctions() { this._debouncedSmallTextFrame = _.debounce(this.sendSmallTextFrame, 100, { leading: true, }); } _setupInit() { if (this._isWindowAlreadyLoaded()) { this._init(100); } else { this._init(); } } _isWindowAlreadyLoaded() { if (document.body === undefined) { return false; } return !!this.dimensions.findMeasuringBox(); } _init(delay = 0) { // When the webext devtools auto reloads this code, the background process // can sometimes still be loading, in which case we need to wait. setTimeout(() => this._registerWithBackground(), delay); } _registerWithBackground() { let sending = browser.runtime.sendMessage("/register"); sending.then( (r) => this._registrationSuccess(r), (e) => this._registrationError(e) ); } _registrationSuccess(registered) { this.channel = browser.runtime.connect({ // We need to give ourselves a unique channel name, so the background // process can identify us amongst other tabs. name: registered.id.toString(), }); this._postCommsInit(); } _registrationError(error) { this.log(error); } _startWindowEventListeners() { window.addEventListener("DOMContentLoaded", () => { this.is_dom_loaded = true; this.log("DOM LOADED"); this._fixStickyElements(); this._willHideText(); }); window.addEventListener("load", () => { this.is_page_finished_loading = true; this.config.page_load_duration = Date.now() - this.config.start_time; this.log("PAGE LOADED"); }); window.addEventListener("unload", () => { this.sendMessage("/status,window_unload"); }); window.addEventListener("error", (error) => { this.logError(error); }); } _startMutationObserver() { let target = document.querySelector("body"); let observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { this.log("!!MUTATION!!", mutation); this._debouncedSmallTextFrame(); }); }); observer.observe(target, { subtree: true, characterData: true, childList: true, }); } _listenForBackgroundMessages() { this.channel.onMessage.addListener((message) => { try { this._handleBackgroundMessage(message); } catch (error) { this.logError(error); } }); } // Sticky elements are, for example, those headers that follow you down the page as you // scroll. They are annoying even in desktop browsers, however because of the lower frame // rate of Browsh, sticky elements stutter down the page, so it's even more annoying. Not // to mention the screen real estate that sticky elements take up, which is even more // noticeable on a small TTY screen like Browsh's. // // Note that this uses `getComputedStyle()`, which can be expensive, there should only // be 1 that parses that entire tree during page load. So if there's reason to use more // CSS tricks like this, then the call should be refactored. _fixStickyElements() { let position; let i, elements = document.querySelectorAll("body *"); for (i = 0; i < elements.length; i++) { position = getComputedStyle(elements[i]).position; if (position === "fixed" || position === "sticky") { elements[i].style.setProperty("position", "absolute", "important"); } } } _injectCustomCSS() { var node = document.createElement("style"); node.innerHTML = this.config.browsh.custom_css; if (document.body) { document.body.appendChild(node); } } }