1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
|
/*
* Copyright © 2016-2020 Michael Gratton <mike@vee.net>
*
* 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;
this._selectionChanged = MessageSender("selection_changed");
this._contentLoaded = MessageSender("content_loaded");
this._preferredHeightChanged = MessageSender("preferred_height_changed");
this._commandStackChanged = MessageSender("command_stack_changed");
this._documentModified = MessageSender("document_modified");
// Set up an observer to keep track of modifications made to
// the document when editing.
let modifiedId = null;
this.bodyObserver = new MutationObserver((records) => {
if (modifiedId == null) {
modifiedId = window.setTimeout(() => {
this.documentModified();
this.checkCommandStack();
modifiedId = null;
}, 1000);
}
});
this.heightObserver = new ResizeObserver((entries) => {
this.updatePreferredHeight();
});
document.addEventListener("DOMContentLoaded", (e) => {
this.heightObserver.observe(window.document.documentElement);
this.loaded();
});
document.addEventListener("selectionchange", (e) => {
this.selectionChanged();
});
this.testResult = null;
},
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();
this._contentLoaded();
},
loadRemoteResources: function() {
const TYPES = "*[src], *[srcset]";
for (const element of document.body.querySelectorAll(TYPES)) {
let src = "";
try {
src = element.src;
} catch (e) {
// fine
}
if (src != "") {
element.src = "";
element.src = src;
}
let srcset = "";
try {
srcset = element.srcset;
} catch (e) {
// fine
}
if (srcset != "") {
element.srcset = "";
element.srcset = srcset;
}
}
},
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();
},
/**
* 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;
this._preferredHeightChanged(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;
this._commandStackChanged(this.undoEnabled, this.redoEnabled);
}
},
documentModified: function(element) {
this._documentModified();
},
selectionChanged: function() {
let hasSelection = !window.getSelection().isCollapsed;
if (this.hasSelection != hasSelection) {
this.hasSelection = hasSelection;
this._selectionChanged(hasSelection);
}
},
// Methods below are for unit tests.
testVoid: function() {
this.testResult = "void";
},
testReturn: function(value) {
this.testResult = value;
return value;
},
testThrow: function(value) {
this.testResult = value;
throw this.testResult;
}
};
let MessageSender = function(name) {
return function() {
// Since typeof(arguments) == 'object', convert to an array so
// that Components.WebView.MessageCallback callbacks get
// arrays or tuples rather than dicts as arguments
_GearyWebExtension.send(name, Array.from(arguments));
};
};
|