diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml index 2e4de98fc..1e6ee2a20 100644 --- a/qutebrowser/javascript/.eslintrc.yaml +++ b/qutebrowser/javascript/.eslintrc.yaml @@ -54,3 +54,5 @@ rules: function-paren-newline: "off" multiline-comment-style: "off" no-bitwise: "off" + no-ternary: "off" + max-lines: "off" diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 61b537504..d635de412 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -40,7 +40,54 @@ window._qutebrowser.webelem = (function() { const funcs = {}; const elements = []; - function serialize_elem(elem) { + function get_frame_offset(frame) { + if (frame === null) { + // Dummy object with zero offset + return { + "top": 0, + "right": 0, + "bottom": 0, + "left": 0, + "height": 0, + "width": 0, + }; + } + return frame.frameElement.getBoundingClientRect(); + } + + // Add an offset rect to a base rect, for use with frames + function add_offset_rect(base, offset) { + return { + "top": base.top + offset.top, + "left": base.left + offset.left, + "bottom": base.bottom + offset.top, + "right": base.right + offset.left, + "height": base.height, + "width": base.width, + }; + } + + function get_caret_position(elem, frame) { + // 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. + try { + return elem.selectionStart; + } catch (err) { + if (err instanceof (frame + ? frame.DOMException + : DOMException) && + err.name === "InvalidStateError") { + // nothing to do, caret_position is already null + } else { + // not the droid we're looking for + throw err; + } + } + return null; + } + + function serialize_elem(elem, frame = null) { if (!elem) { return null; } @@ -48,21 +95,7 @@ window._qutebrowser.webelem = (function() { 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 caret_position = get_caret_position(elem, frame); const out = { "id": id, @@ -115,16 +148,13 @@ window._qutebrowser.webelem = (function() { out.attributes = attributes; const client_rects = elem.getClientRects(); + const frame_offset_rect = get_frame_offset(frame); + 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, - }); + out.rects.push( + add_offset_rect(rect, frame_offset_rect) + ); } // console.log(JSON.stringify(out)); @@ -132,9 +162,7 @@ window._qutebrowser.webelem = (function() { return out; } - function is_visible(elem) { - // FIXME:qtwebengine Handle frames and iframes - + function is_visible(elem, frame = null) { // 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 @@ -142,7 +170,8 @@ window._qutebrowser.webelem = (function() { // 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(); + const offset_rect = get_frame_offset(frame); + let rect = add_offset_rect(elem.getBoundingClientRect(), offset_rect); if (!rect || rect.top > window.innerHeight || @@ -174,8 +203,20 @@ window._qutebrowser.webelem = (function() { return true; } + // Returns true if the iframe is accessible without + // cross domain errors, else false. + function iframe_same_domain(frame) { + try { + frame.document; // eslint-disable-line no-unused-expressions + return true; + } catch (err) { + return false; + } + } + funcs.find_css = (selector, only_visible) => { const elems = document.querySelectorAll(selector); + const subelem_frames = window.frames; const out = []; for (let i = 0; i < elems.length; ++i) { @@ -184,14 +225,70 @@ window._qutebrowser.webelem = (function() { } } + // Recurse into frames and add them + for (let i = 0; i < subelem_frames.length; i++) { + if (iframe_same_domain(subelem_frames[i])) { + const frame = subelem_frames[i]; + const subelems = frame.document. + querySelectorAll(selector); + for (let elem_num = 0; elem_num < subelems.length; ++elem_num) { + if (!only_visible || + is_visible(subelems[elem_num], frame)) { + out.push(serialize_elem(subelems[elem_num], frame)); + } + } + } + } + return out; }; + // Runs a function in a frame until the result is not null, then return + function run_frames(func) { + for (let i = 0; i < window.frames.length; ++i) { + const frame = window.frames[i]; + if (iframe_same_domain(frame)) { + const result = func(frame); + if (result) { + return result; + } + } + } + return null; + } + funcs.find_id = (id) => { const elem = document.getElementById(id); - return serialize_elem(elem); + if (elem) { + return serialize_elem(elem); + } + + const serialized_elem = run_frames((frame) => { + const element = frame.window.document.getElementById(id); + return serialize_elem(element, frame); + }); + + if (serialized_elem) { + return serialized_elem; + } + + return null; }; + // Check if elem is an iframe, and if so, return the result of func on it. + // If no iframes match, return null + function call_if_frame(elem, func) { + // Check if elem is a frame, and if so, call func on the window + if ("contentWindow" in elem) { + const frame = elem.contentWindow; + if (iframe_same_domain(frame) && + "frameElement" in elem.contentWindow) { + return func(frame); + } + } + return null; + } + funcs.find_focused = () => { const elem = document.activeElement; @@ -201,26 +298,52 @@ window._qutebrowser.webelem = (function() { return null; } + // Check if we got an iframe, and if so, recurse inside of it + const frame_elem = call_if_frame(elem, + (frame) => serialize_elem(frame.document.activeElement, frame)); + + if (frame_elem !== null) { + return frame_elem; + } 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); + + + // Check if we got an iframe, and if so, recurse inside of it + const frame_elem = call_if_frame(elem, + (frame) => { + // Subtract offsets due to being in an iframe + const frame_offset_rect = + frame.frameElement.getBoundingClientRect(); + return serialize_elem(frame.document. + elementFromPoint(x - frame_offset_rect.left, + y - frame_offset_rect.top), frame); + }); + + if (frame_elem !== null) { + return frame_elem; + } 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; + if (elem) { + return serialize_elem(elem.parentNode); } - return serialize_elem(elem.parentNode); + + const serialized_frame_elem = run_frames((frame) => { + const node = frame.window.getSelection().anchorNode; + if (node) { + return serialize_elem(node.parentNode, frame); + } + return null; + }); + return serialized_frame_elem; }; funcs.set_value = (id, value) => { diff --git a/tests/end2end/data/hints/html/wrapped.html b/tests/end2end/data/hints/html/wrapped.html index 2ebde1a24..dcc05c8c7 100644 --- a/tests/end2end/data/hints/html/wrapped.html +++ b/tests/end2end/data/hints/html/wrapped.html @@ -1,9 +1,6 @@ - +
diff --git a/tests/end2end/data/hints/html/wrapped_button.html b/tests/end2end/data/hints/html/wrapped_button.html new file mode 100644 index 000000000..4d5fd81bb --- /dev/null +++ b/tests/end2end/data/hints/html/wrapped_button.html @@ -0,0 +1,21 @@ + + + + + + + +A link to be opened in the iframe above. + A second link for the iframe.
diff --git a/tests/end2end/data/iframe_search.html b/tests/end2end/data/iframe_search.html new file mode 100644 index 000000000..78f388eeb --- /dev/null +++ b/tests/end2end/data/iframe_search.html @@ -0,0 +1,11 @@ + + + + + +