/* * Copyright 2016 Michael Gratton * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. */ /** * Application logic for Components.WebView and subclasses. */ let PageState = function() { this.init.apply(this, arguments); }; PageState.prototype = { init: function() { this.isLoaded = false; this.undoEnabled = false; this.redoEnabled = false; this.hasSelection = false; this.lastPreferredHeight = 0; let state = this; // Set up an observer to keep track of modifications made to // the document when editing. let modifiedId = null; this.bodyObserver = new MutationObserver(function(records) { if (modifiedId == null) { modifiedId = window.setTimeout(function() { state.documentModified(); state.checkCommandStack(); modifiedId = null; }, 1000); } }); document.addEventListener("DOMContentLoaded", function(e) { state.loaded(); }); // Coalesce multiple calls to updatePreferredHeight using a // timeout to avoid the overhead of multiple JS messages sent // to the app and hence view multiple resizes being queued. let queueTimeout = null; let queuePreferredHeightUpdate = function() { if (queueTimeout != null) { clearTimeout(queueTimeout); } queueTimeout = setTimeout( function() { state.updatePreferredHeight(); }, 100 ); }; // Queues an update when the complete document is loaded. // // Note also that the delay introduced here by this last call // to queuePreferredHeightUpdate when the complete document is // loaded seems to be important to get an accurate idea of the // final document size. window.addEventListener("load", function(e) { queuePreferredHeightUpdate(); }, true); // load does not bubble // Queues updates for any STYLE, IMG and other loaded // elements, hence handles resizing when the user later // requests remote images loading. document.addEventListener("load", function(e) { queuePreferredHeightUpdate(); }, true); // load does not bubble // Queues an update if the window changes size, e.g. if the // user resized the window. Only trigger when the width has // changed however since the height should only change as the // body is being loaded. let width = window.innerWidth; window.addEventListener("resize", function(e) { let currentWidth = window.innerWidth; if (width != currentWidth) { width = currentWidth; queuePreferredHeightUpdate(); } }, false); // load does not bubble // Queues an update when a transition has completed, e.g. if the // user resized the window window.addEventListener("transitionend", function(e) { queuePreferredHeightUpdate(); }, false); // load does not bubble }, getPreferredHeight: function() { // Return the scroll height of the HTML element since the BODY // may have margin/border/padding and we want to know // precisely how high the widget needs to be to avoid // scrolling. return window.document.documentElement.scrollHeight; }, getHtml: function() { return document.body.innerHTML; }, loaded: function() { this.isLoaded = true; // Always fire a preferred height update first so that it will // be vaguegly correct when notifying of the HTML load // completing. this.updatePreferredHeight(); window.webkit.messageHandlers.contentLoaded.postMessage(null); }, loadRemoteImages: function() { window._gearyAllowRemoteResourceLoads = true; let images = document.getElementsByTagName("IMG"); for (let i = 0; i < images.length; i++) { let img = images.item(i); let src = img.src; img.src = ""; img.src = src; } }, setEditable: function(enabled) { if (!enabled) { this.stopBodyObserver(); } document.body.contentEditable = enabled; if (enabled) { // Enable modification observation only after the document // has been set editable as WebKit will alter some attrs this.startBodyObserver(); } }, startBodyObserver: function() { let config = { attributes: true, childList: true, characterData: true, subtree: true }; this.bodyObserver.observe(document.body, config); }, stopBodyObserver: function() { this.bodyObserver.disconnect(); }, remoteImageLoadBlocked: function() { window.webkit.messageHandlers.remoteImageLoadBlocked.postMessage(null); }, /** * Sends "preferredHeightChanged" message if it has changed. */ updatePreferredHeight: function(height) { if (height === undefined) { height = this.getPreferredHeight(); } // Don't send the message until after the DOM has been fully // loaded and processed by any derived classes. Since // ConversationPageState may collapse any quotes, sending the // current preferred height before then may send a value that // is too large, causing the message body view to grow then // shrink again, leading to visual flicker. if (this.isLoaded && height > 0 && height != this.lastPreferredHeight) { this.lastPreferredHeight = height; window.webkit.messageHandlers.preferredHeightChanged.postMessage( height ); } }, checkCommandStack: function() { let canUndo = document.queryCommandEnabled("undo"); let canRedo = document.queryCommandEnabled("redo"); if (canUndo != this.undoEnabled || canRedo != this.redoEnabled) { this.undoEnabled = canUndo; this.redoEnabled = canRedo; window.webkit.messageHandlers.commandStackChanged.postMessage( this.undoEnabled + "," + this.redoEnabled ); } }, documentModified: function(element) { window.webkit.messageHandlers.documentModified.postMessage(null); }, selectionChanged: function() { let hasSelection = !window.getSelection().isCollapsed; if (this.hasSelection != hasSelection) { this.hasSelection = hasSelection; window.webkit.messageHandlers.selectionChanged.postMessage(hasSelection); } } };