qutebrowser/qutebrowser/javascript/webelem.js
Florian Bruhin 180fb0d65a Fix handling of caret position with Qt 5.10
With Chromium 61 in Qt 5.10, we get null when getting .selectionStart on a
non-text element, like changed in the WhatWG HTML standard:
https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-selectionstart

See https://www.chromestatus.com/feature/5740194741354496

Older QtWebEngines and QtWebKit raise InvalidStateError instead.

This also changes the surrounding code and API so None is used to say "there's
no caret position available", which seems like a nicer API.
2017-11-08 16:27:26 +01:00

259 lines
8.1 KiB
JavaScript

/**
* Copyright 2016-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
*
* This file is part of qutebrowser.
*
* qutebrowser is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* qutebrowser is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* The connection for web elements between Python and Javascript works like
* this:
*
* - Python calls into Javascript and invokes a function to find elements (one
* of the find_* functions).
* - Javascript gets the requested element, and calls serialize_elem on it.
* - serialize_elem saves the javascript element object in "elements", gets some
* attributes from the element, and assigns an ID (index into 'elements') to
* it.
* - Python gets this information and constructs a Python wrapper object with
* the information it got right away, and the ID.
* - When Python wants to modify an element, it calls javascript again with the
* element ID.
* - Javascript gets the element from the elements array, and modifies it.
*/
"use strict";
window._qutebrowser.webelem = (function() {
const funcs = {};
const elements = [];
function serialize_elem(elem) {
if (!elem) {
return null;
}
const id = elements.length;
elements[id] = elem;
// With older Chromium versions (and QtWebKit), InvalidStateError will
// be thrown if elem doesn't have selectionStart.
// With newer Chromium versions (>= Qt 5.10), we get null.
let caret_position = null;
try {
caret_position = elem.selectionStart;
} catch (err) {
if (err instanceof DOMException &&
err.name === "InvalidStateError") {
// nothing to do, caret_position is already null
} else {
// not the droid we're looking for
throw err;
}
}
const out = {
"id": id,
"value": elem.value,
"outer_xml": elem.outerHTML,
"rects": [], // Gets filled up later
"caret_position": caret_position,
};
// https://github.com/qutebrowser/qutebrowser/issues/2569
if (typeof elem.tagName === "string") {
out.tag_name = elem.tagName;
} else if (typeof elem.nodeName === "string") {
out.tag_name = elem.nodeName;
} else {
out.tag_name = "";
}
if (typeof elem.className === "string") {
out.class_name = elem.className;
} else {
// e.g. SVG elements
out.class_name = "";
}
if (typeof elem.textContent === "string") {
out.text = elem.textContent;
} else if (typeof elem.text === "string") {
out.text = elem.text;
} // else: don't add the text at all
const attributes = {};
for (let i = 0; i < elem.attributes.length; ++i) {
const attr = elem.attributes[i];
attributes[attr.name] = attr.value;
}
out.attributes = attributes;
const client_rects = elem.getClientRects();
for (let k = 0; k < client_rects.length; ++k) {
const rect = client_rects[k];
out.rects.push({
"top": rect.top,
"right": rect.right,
"bottom": rect.bottom,
"left": rect.left,
"height": rect.height,
"width": rect.width,
});
}
// console.log(JSON.stringify(out));
return out;
}
function is_visible(elem) {
// FIXME:qtwebengine Handle frames and iframes
// Adopted from vimperator:
// https://github.com/vimperator/vimperator-labs/blob/vimperator-3.14.0/common/content/hints.js#L259-L285
// FIXME:qtwebengine we might need something more sophisticated like
// the cVim implementation here?
// https://github.com/1995eaton/chromium-vim/blob/1.2.85/content_scripts/dom.js#L74-L134
const win = elem.ownerDocument.defaultView;
let rect = elem.getBoundingClientRect();
if (!rect ||
rect.top > window.innerHeight ||
rect.bottom < 0 ||
rect.left > window.innerWidth ||
rect.right < 0) {
return false;
}
rect = elem.getClientRects()[0];
if (!rect) {
return false;
}
const style = win.getComputedStyle(elem, null);
if (style.getPropertyValue("visibility") !== "visible" ||
style.getPropertyValue("display") === "none" ||
style.getPropertyValue("opacity") === "0") {
// FIXME:qtwebengine do we need this <area> handling?
// visibility and display style are misleading for area tags and
// they get "display: none" by default.
// See https://github.com/vimperator/vimperator-labs/issues/236
if (elem.nodeName.toLowerCase() !== "area" &&
!elem.classList.contains("ace_text-input")) {
return false;
}
}
return true;
}
funcs.find_css = (selector, only_visible) => {
const elems = document.querySelectorAll(selector);
const out = [];
for (let i = 0; i < elems.length; ++i) {
if (!only_visible || is_visible(elems[i])) {
out.push(serialize_elem(elems[i]));
}
}
return out;
};
funcs.find_id = (id) => {
const elem = document.getElementById(id);
return serialize_elem(elem);
};
funcs.find_focused = () => {
const elem = document.activeElement;
if (!elem || elem === document.body) {
// "When there is no selection, the active element is the page's
// <body> or null."
return null;
}
return serialize_elem(elem);
};
funcs.find_at_pos = (x, y) => {
// FIXME:qtwebengine
// If the element at the specified point belongs to another document
// (for example, an iframe's subdocument), the subdocument's parent
// element is returned (the iframe itself).
const elem = document.elementFromPoint(x, y);
return serialize_elem(elem);
};
// Function for returning a selection to python (so we can click it)
funcs.find_selected_link = () => {
const elem = window.getSelection().anchorNode;
if (!elem) {
return null;
}
return serialize_elem(elem.parentNode);
};
funcs.set_value = (id, value) => {
elements[id].value = value;
};
funcs.insert_text = (id, text) => {
const elem = elements[id];
elem.focus();
document.execCommand("insertText", false, text);
};
funcs.set_attribute = (id, name, value) => {
elements[id].setAttribute(name, value);
};
funcs.remove_blank_target = (id) => {
let elem = elements[id];
while (elem !== null) {
const tag = elem.tagName.toLowerCase();
if (tag === "a" || tag === "area") {
if (elem.getAttribute("target") === "_blank") {
elem.setAttribute("target", "_top");
}
break;
}
elem = elem.parentElement;
}
};
funcs.click = (id) => {
const elem = elements[id];
elem.click();
};
funcs.focus = (id) => {
const elem = elements[id];
elem.focus();
};
funcs.move_cursor_to_end = (id) => {
const elem = elements[id];
elem.selectionStart = elem.value.length;
elem.selectionEnd = elem.value.length;
};
return funcs;
})();