diff --git a/core/browser.py b/core/browser.py index a9d977c..aa86941 100644 --- a/core/browser.py +++ b/core/browser.py @@ -77,6 +77,7 @@ class BrowserView(QWebEngineView): self.clear_focus_js = self.read_js_content("clear_focus.js") self.select_input_text_js = self.read_js_content("select_input_text.js") self.dark_mode_js = self.read_js_content("dark_mode.js") + self.caret_browsing_js = self.read_js_content("caret_browsing.js") self.get_selection_text_js = self.read_js_content("get_selection_text.js") self.focus_input_js = self.read_js_content("focus_input.js") @@ -602,6 +603,10 @@ class BrowserBuffer(Buffer): self.draw_progressbar = False self.eval_dark_js = False + self.eval_caret_js = False + self.caret_browsing_activated = False + self.caret_browsing_mark_activated = False + self.caret_browsing_search_text = "" self.progressbar_progress = 0 self.progressbar_color = QColor(233, 129, 35, 255) self.progressbar_height = 2 @@ -780,10 +785,13 @@ class BrowserBuffer(Buffer): ''' Update the Progress Bar.''' if progress < 100: # Update progres. + self.eval_caret_js = False self.progressbar_progress = progress self.update() elif progress == 100 and self.draw_progressbar: self.init_auto_fill() + self.buffer_widget.eval_js(self.buffer_widget.caret_browsing_js) + self.eval_caret_js = True if self.emacs_var_dict["eaf-browser-enable-adblocker"] == "true": self.buffer_widget.load_adblocker() if self.dark_mode_is_enable(): @@ -879,6 +887,10 @@ class BrowserBuffer(Buffer): self.buffer_widget._search_text(str(result_content)) elif result_tag == "search_text_backward": self.buffer_widget._search_text(str(result_content), True) + elif result_tag == "caret_search_text_forward": + self._caret_search_text(str(result_content)) + elif result_tag == "caret_search_text_backward": + self._caret_search_text(str(result_content), True) elif result_tag == "jump_link": self.buffer_widget.jump_to_link(str(result_content).strip()) elif result_tag == "jump_link_new_buffer": @@ -947,6 +959,117 @@ class BrowserBuffer(Buffer): subprocess.Popen(aria2_args, stdout=null_file) + def caret_browsing(self): + ''' Init caret browsing.''' + if self.eval_caret_js: + self.buffer_widget.eval_js("CaretBrowsing.setInitialCursor();") + self.message_to_emacs.emit("Caret browsing activated.") + self.caret_browsing_activated = True + self.caret_browsing_search_text = "" + + def caret_exit(self): + ''' Exit caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.shutdown();") + self.message_to_emacs.emit("Caret browsing deactivated.") + self.caret_browsing_activated = False + + @interactive(insert_or_do=True) + def caret_next_line(self): + ''' Switch to next line in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.move('forward', 'line');") + + @interactive(insert_or_do=True) + def caret_previous_line(self): + ''' Switch to previous line in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.move('backward', 'line');") + + @interactive(insert_or_do=True) + def caret_next_character(self): + ''' Switch to next character in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.move('forward', 'character');") + + @interactive(insert_or_do=True) + def caret_previous_character(self): + ''' Switch to previous character in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.move('backward', 'character');") + + @interactive(insert_or_do=True) + def caret_next_word(self): + ''' Switch to next word in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.move('forward', 'word');") + + @interactive(insert_or_do=True) + def caret_previous_word(self): + ''' Switch to previous word in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.move('backward', 'word');") + + @interactive(insert_or_do=True) + def caret_to_bottom(self): + ''' Switch to next word in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.move('forward', 'documentboundary');") + + @interactive(insert_or_do=True) + def caret_to_top(self): + ''' Switch to previous word in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.move('backward', 'documentboundary');") + + def caret_toggle_mark(self): + ''' Toggle mark in caret browsing.''' + if self.caret_browsing_activated: + self.buffer_widget.eval_js("CaretBrowsing.toggleMark();") + if self.buffer_widget.execute_js("CaretBrowsing.markEnabled"): + self.caret_browsing_mark_activated = True + self.message_to_emacs.emit("Mark is on.") + else: + self.caret_browsing_mark_activated = False + self.message_to_emacs.emit("Mark is off.") + + def caret_clear_search(self): + ''' Clear search text in caret browsing.''' + if self.caret_browsing_activated: + if self.caret_browsing_mark_activated: + self.caret_browsing_search_text = "" + self.message_to_emacs.emit("Cleared caret search text.") + + @interactive(insert_or_do=True) + def caret_search_forward(self): + ''' Search Text forward in caret browsing.''' + if self.caret_browsing_activated: + if self.caret_browsing_mark_activated: + if self.caret_browsing_search_text == "": + self.send_input_message("Forward Search Text and Select: ", "caret_search_text_forward") + else: + self._caret_search_text(self.caret_browsing_search_text) + + @interactive(insert_or_do=True) + def caret_search_backward(self): + ''' Search Text backward in caret browsing.''' + if self.caret_browsing_activated: + if self.caret_browsing_mark_activated: + if self.caret_browsing_search_text == "": + self.send_input_message("Backward Search Text and Select: ", "caret_search_text_backward") + else: + self._caret_search_text(self.caret_browsing_search_text,True) + + def _caret_search_text(self, text, is_backward = False): + if self.caret_browsing_search_text != text: + self.caret_browsing_search_text = text + if is_backward: + if not self.buffer_widget.execute_js("window.find('"+text+"',false,true)"): + self.message_to_emacs.emit("Unable to find more, please try forward search.") + else: + if not self.buffer_widget.execute_js("window.find('"+text+"')"): + self.message_to_emacs.emit("Unable to find more, please try backward search.") + @interactive(insert_or_do=True) def open_download_manage_page(self): ''' Open download manage page.''' diff --git a/core/js/caret_browsing.js b/core/js/caret_browsing.js new file mode 100644 index 0000000..6b7d0e8 --- /dev/null +++ b/core/js/caret_browsing.js @@ -0,0 +1,1130 @@ + +/** + * This file is derived from qutebrowser caret.js + * https://github.com/qutebrowser/qutebrowser/blob/b44e3ba657e622cf813d0c072d6fe538fca9bf2a/qutebrowser/javascript/caret.js + * + * Copyright 2018-2020 Florian Bruhin (The Compiler) + * + * Ported chrome-caretbrowsing extension. + * https://cs.chromium.org/chromium/src/ui/accessibility/extensions/caretbrowsing/ + * + */ + +// Copyright 2014 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"use strict"; + +const axs = {}; + +axs.dom = {}; + +axs.color = {}; + +axs.utils = {}; + +axs.dom.parentElement = function(node) { + if (!node) { + return null; + } + const composedNode = axs.dom.composedParentNode(node); + if (!composedNode) { + return null; + } + switch (composedNode.nodeType) { + case Node.ELEMENT_NODE: + return composedNode; + default: + return axs.dom.parentElement(composedNode); + } +}; + +axs.dom.shadowHost = function(node) { + if ("host" in node) { + return node.host; + } + return null; +}; + +axs.dom.composedParentNode = function(node) { + if (!node) { + return null; + } + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return axs.dom.shadowHost(node); + } + const parentNode = node.parentNode; + if (!parentNode) { + return null; + } + if (parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return axs.dom.shadowHost(parentNode); + } + if (!parentNode.shadowRoot) { + return parentNode; + } + const points = node.getDestinationInsertionPoints(); + if (points.length > 0) { + return axs.dom.composedParentNode(points[points.length - 1]); + } + return null; +}; + +axs.color.Color = function(red, green, blue, alpha) { // eslint-disable-line max-params,max-len + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; +}; + +axs.color.parseColor = function(colorText) { + if (colorText === "transparent") { + return new axs.color.Color(0, 0, 0, 0); + } + let match = colorText.match(/^rgb\((\d+), (\d+), (\d+)\)$/); + if (match) { + const blue = parseInt(match[3], 10); + const green = parseInt(match[2], 10); + const red = parseInt(match[1], 10); + return new axs.color.Color(red, green, blue, 1); + } + match = colorText.match(/^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/); + if (match) { + const red = parseInt(match[1], 10); + const green = parseInt(match[2], 10); + const blue = parseInt(match[3], 10); + const alpha = parseFloat(match[4]); + return new axs.color.Color(red, green, blue, alpha); + } + return null; +}; + +axs.color.flattenColors = function(color1, color2) { + const colorAlpha = color1.alpha; + return new axs.color.Color( + ((1 - colorAlpha) * color2.red) + (colorAlpha * color1.red), + ((1 - colorAlpha) * color2.green) + (colorAlpha * color1.green), + ((1 - colorAlpha) * color2.blue) + (colorAlpha * color2.blue), + color1.alpha + (color2.alpha * (1 - color1.alpha))); +}; + +axs.utils.getParentBgColor = function(_el) { + let el = _el; + let el2 = el; + let iter = null; + el = []; + for (iter = null; (el2 = axs.dom.parentElement(el2));) { + const style = window.getComputedStyle(el2, null); + if (style) { + const color = axs.color.parseColor(style.backgroundColor); + if (color && + (style.opacity < 1 && + (color.alpha *= style.opacity), + color.alpha !== 0 && + (el.push(color), color.alpha === 1))) { + iter = !0; + break; + } + } + } + if (!iter) { + el.push(new axs.color.Color(255, 255, 255, 1)); + } + for (el2 = el.pop(); el.length;) { + iter = el.pop(); + el2 = axs.color.flattenColors(iter, el2); + } + return el2; +}; + +axs.utils.getFgColor = function(el, el2, color) { + let color2 = axs.color.parseColor(el.color); + if (!color2) { + return null; + } + if (color2.alpha < 1) { + color2 = axs.color.flattenColors(color2, color); + } + if (el.opacity < 1) { + const el3 = axs.utils.getParentBgColor(el2); + color2.alpha *= el.opacity; + color2 = axs.color.flattenColors(color2, el3); + } + return color2; +}; + +axs.utils.getBgColor = function(el, elParent) { + let color = axs.color.parseColor(el.backgroundColor); + if (!color) { + return null; + } + if (el.opacity < 1) { + color.alpha *= el.opacity; + } + if (color.alpha < 1) { + const bgColor = axs.utils.getParentBgColor(elParent); + if (bgColor === null) { + return null; + } + color = axs.color.flattenColors(color, bgColor); + } + return color; +}; + +axs.color.colorChannelToString = function(_color) { + const color = Math.round(_color); + if (color < 15) { + return `0${color.toString(16)}`; + } + return color.toString(16); +}; + +axs.color.colorToString = function(color) { + if (color.alpha === 1) { + const red = axs.color.colorChannelToString(color.red); + const green = axs.color.colorChannelToString(color.green); + const blue = axs.color.colorChannelToString(color.blue); + return `#${red}${green}${blue}`; + } + const arr = [color.red, color.green, color.blue, color.alpha].join(); + return `rgba(${arr})`; +}; + +const Cursor = function(node, index, text) { // eslint-disable-line func-style,max-len + this.node = node; + this.index = index; + this.text = text; +}; + +Cursor.prototype.clone = function() { + return new Cursor(this.node, this.index, this.text); +}; + +Cursor.prototype.copyFrom = function(otherCursor) { + this.node = otherCursor.node; + this.index = otherCursor.index; + this.text = otherCursor.text; +}; + +const TraverseUtil = {}; + +TraverseUtil.getNodeText = function(node) { + if (node.constructor === Text) { + return node.data; + } + return ""; +}; + +TraverseUtil.treatAsLeafNode = function(node) { + return node.childNodes.length === 0 || + node.nodeName === "SELECT" || + node.nodeName === "OBJECT"; +}; + +TraverseUtil.isWhitespace = function(ch) { + return (ch === " " || ch === "\n" || ch === "\r" || ch === "\t"); +}; + +TraverseUtil.isVisible = function(node) { + if (!node.style) { + return true; + } + const style = window.getComputedStyle(node, null); + return (Boolean(style) && + style.display !== "none" && + style.visibility !== "hidden"); +}; + +TraverseUtil.isSkipped = function(_node) { + let node = _node; + if (node.constructor === Text) { + node = node.parentElement; + } + if (node.className === "CaretBrowsing_Caret" || + node.className === "CaretBrowsing_AnimateCaret") { + return true; + } + return false; +}; + +TraverseUtil.forwardsChar = function(cursor, nodesCrossed) { // eslint-disable-line max-statements,max-len + for (;;) { + let childNode = null; + if (!TraverseUtil.treatAsLeafNode(cursor.node)) { + for (let i = cursor.index; + i < cursor.node.childNodes.length; + i++) { + const node = cursor.node.childNodes[i]; + if (TraverseUtil.isSkipped(node)) { + nodesCrossed.push(node); + } else if (TraverseUtil.isVisible(node)) { + childNode = node; + break; + } + } + } + if (childNode) { + cursor.node = childNode; + cursor.index = 0; + cursor.text = TraverseUtil.getNodeText(cursor.node); + if (cursor.node.constructor !== Text) { + nodesCrossed.push(cursor.node); + } + } else { + if (cursor.index < cursor.text.length) { + return cursor.text[cursor.index++]; + } + + while (cursor.node !== null) { + let siblingNode = null; + for (let node = cursor.node.nextSibling; + node !== null; + node = node.nextSibling) { + if (TraverseUtil.isSkipped(node)) { + nodesCrossed.push(node); + } else if (TraverseUtil.isVisible(node)) { + siblingNode = node; + break; + } + } + if (siblingNode) { + cursor.node = siblingNode; + cursor.text = TraverseUtil.getNodeText(siblingNode); + cursor.index = 0; + + if (cursor.node.constructor !== Text) { + nodesCrossed.push(cursor.node); + } + + break; + } + + const parentNode = cursor.node.parentNode; + if (parentNode && + parentNode.constructor !== HTMLBodyElement) { + cursor.node = cursor.node.parentNode; + cursor.text = null; + cursor.index = 0; + } else { + return null; + } + } + } + } +}; + +TraverseUtil.getNextChar = function( // eslint-disable-line max-params + startCursor, endCursor, nodesCrossed, skipWhitespace) { + startCursor.copyFrom(endCursor); + let fChar = TraverseUtil.forwardsChar(endCursor, nodesCrossed); + if (fChar === null) { + return null; + } + + const initialWhitespace = TraverseUtil.isWhitespace(fChar); + + while ((TraverseUtil.isWhitespace(fChar)) || + (TraverseUtil.isSkipped(endCursor.node))) { + fChar = TraverseUtil.forwardsChar(endCursor, nodesCrossed); + if (fChar === null) { + return null; + } + } + if (skipWhitespace || !initialWhitespace) { + startCursor.copyFrom(endCursor); + startCursor.index--; + return fChar; + } + + for (let i = 0; i < nodesCrossed.length; i++) { + if (TraverseUtil.isSkipped(nodesCrossed[i])) { + endCursor.index--; + startCursor.copyFrom(endCursor); + startCursor.index--; + return " "; + } + } + endCursor.index--; + return " "; +}; + +TraverseUtil.backwardsChar = function(cursor, nodesCrossed) { + while (true) { + // Move down until we get to a leaf node. + var childNode = null; + if (!TraverseUtil.treatAsLeafNode(cursor.node)) { + for (var i = cursor.index - 1; i >= 0; i--) { + var node = cursor.node.childNodes[i]; + if (TraverseUtil.isSkipped(node)) { + nodesCrossed.push(node); + continue; + } + if (TraverseUtil.isVisible(node)) { + childNode = node; + break; + } + } + } + if (childNode) { + cursor.node = childNode; + cursor.text = TraverseUtil.getNodeText(cursor.node); + if (cursor.text.length) + cursor.index = cursor.text.length; + else + cursor.index = cursor.node.childNodes.length; + if (cursor.node.constructor != Text) + nodesCrossed.push(cursor.node); + continue; + } + + // Return the previous character from this leaf node. + if (cursor.text.length > 0 && cursor.index > 0) { + return cursor.text[--cursor.index]; + } + + // Move to the previous sibling, going up the tree as necessary. + while (true) { + // Try to move to the previous sibling. + var siblingNode = null; + for (var node = cursor.node.previousSibling; + node != null; + node = node.previousSibling) { + if (TraverseUtil.isSkipped(node)) { + nodesCrossed.push(node); + continue; + } + if (TraverseUtil.isVisible(node)) { + siblingNode = node; + break; + } + } + if (siblingNode) { + cursor.node = siblingNode; + cursor.text = TraverseUtil.getNodeText(siblingNode); + if (cursor.text.length) + cursor.index = cursor.text.length; + else + cursor.index = cursor.node.childNodes.length; + if (cursor.node.constructor != Text) + nodesCrossed.push(cursor.node); + break; + } + + // Otherwise, move to the parent. + if (cursor.node.parentNode && + cursor.node.parentNode.constructor != HTMLBodyElement) { + cursor.node = cursor.node.parentNode; + cursor.text = null; + cursor.index = 0; + } else { + return null; + } + } + } +}; + +TraverseUtil.getNextWord = function(startCursor, endCursor, + nodesCrossed) { + +// Find the first non-whitespace or non-skipped character. +var cursor = endCursor.clone(); +var c = TraverseUtil.forwardsChar(cursor, nodesCrossed); +if (c == null) + return null; +while ((TraverseUtil.isWhitespace(c)) || + (TraverseUtil.isSkipped(cursor.node))) { + c = TraverseUtil.forwardsChar(cursor, nodesCrossed); + if (c == null) + return null; +} + +// Set startCursor to the position immediately before the first +// character in our word. It's safe to decrement |index| because +// forwardsChar guarantees that the cursor will be immediately to the +// right of the returned character on exit. +startCursor.copyFrom(cursor); +startCursor.index--; + +// Keep building up our word until we reach a whitespace character or +// would cross a tag. Don't actually return any tags crossed, because this +// word goes up until the tag boundary but not past it. +endCursor.copyFrom(cursor); +var word = c; +var newNodesCrossed = []; +c = TraverseUtil.forwardsChar(cursor, newNodesCrossed); +if (c == null) { + return word; +} +while (!TraverseUtil.isWhitespace(c) && + newNodesCrossed.length == 0) { + word += c; + endCursor.copyFrom(cursor); + c = TraverseUtil.forwardsChar(cursor, newNodesCrossed); + if (c == null) { + return word; + } +} +return word; +}; + +TraverseUtil.getPreviousWord = function(startCursor, endCursor, + nodesCrossed) { +// Find the first non-whitespace or non-skipped character. +var cursor = startCursor.clone(); +var c = TraverseUtil.backwardsChar(cursor, nodesCrossed); +if (c == null) + return null; +while ((TraverseUtil.isWhitespace(c) || + (TraverseUtil.isSkipped(cursor.node)))) { + c = TraverseUtil.backwardsChar(cursor, nodesCrossed); + if (c == null) + return null; +} + +// Set endCursor to the position immediately after the first +// character we've found (the last character of the word, since we're +// searching backwards). +endCursor.copyFrom(cursor); +endCursor.index++; + +// Keep building up our word until we reach a whitespace character or +// would cross a tag. Don't actually return any tags crossed, because this +// word goes up until the tag boundary but not past it. +startCursor.copyFrom(cursor); +var word = c; +var newNodesCrossed = []; +c = TraverseUtil.backwardsChar(cursor, newNodesCrossed); +if (c == null) + return word; +while (!TraverseUtil.isWhitespace(c) && + newNodesCrossed.length == 0) { + word = c + word; + startCursor.copyFrom(cursor); + c = TraverseUtil.backwardsChar(cursor, newNodesCrossed); + if (c == null) + return word; +} + +return word; +}; + +const CaretBrowsing = {}; + +CaretBrowsing.isEnabled = false; + +CaretBrowsing.onEnable = "flash"; + +CaretBrowsing.onJump = "flash"; + +CaretBrowsing.isWindowFocused = false; + +CaretBrowsing.isCaretVisible = false; + +CaretBrowsing.caretElement = undefined; + +CaretBrowsing.caretX = 0; + +CaretBrowsing.caretY = 0; + +CaretBrowsing.caretWidth = 0; + +CaretBrowsing.caretHeight = 0; + +CaretBrowsing.caretForeground = "#000"; + +CaretBrowsing.caretBackground = "#fff"; + +CaretBrowsing.isSelectionCollapsed = false; + +CaretBrowsing.blinkFunctionId = null; + +CaretBrowsing.targetX = null; + +CaretBrowsing.blinkFlag = true; + +CaretBrowsing.markEnabled = false; + +CaretBrowsing.positionCaret = function() { + var start = new Cursor(document.body, 0, ''); + var end = new Cursor(document.body, 0, ''); + var nodesCrossed = []; + var result = TraverseUtil.getNextChar(start, end, nodesCrossed, true); + if (result == null) { + return; + } + CaretBrowsing.setAndValidateSelection(start, start); +} + +CaretBrowsing.isFocusable = function(targetNode) { + if (!targetNode || typeof (targetNode.tabIndex) !== "number") { + return false; + } + + if (targetNode.tabIndex >= 0) { + return true; + } + + if (targetNode.hasAttribute && + targetNode.hasAttribute("tabindex") && + targetNode.getAttribute("tabindex") === "-1") { + return true; + } + + return false; +} + +CaretBrowsing.isControlThatNeedsArrowKeys = function(node) { // eslint-disable-line complexity,max-len + if (!node) { + return false; + } + + if (node === document.body || node !== document.activeElement) { + return false; + } + + if (node.constructor === HTMLSelectElement) { + return true; + } + + if (node.constructor === HTMLInputElement) { + switch (node.type) { // eslint-disable-line default-case + case "email": + case "number": + case "password": + case "search": + case "text": + case "tel": + case "url": + case "": + return true; + case "datetime": + case "datetime-local": + case "date": + case "month": + case "radio": + case "range": + case "week": + return true; + } + } + + if (node.getAttribute && CaretBrowsing.isFocusable(node)) { + const role = node.getAttribute("role"); + switch (role) { // eslint-disable-line default-case + case "combobox": + case "grid": + case "gridcell": + case "listbox": + case "menu": + case "menubar": + case "menuitem": + case "menuitemcheckbox": + case "menuitemradio": + case "option": + case "radiogroup": + case "scrollbar": + case "slider": + case "spinbutton": + case "tab": + case "tablist": + case "textbox": + case "tree": + case "treegrid": + case "treeitem": + return true; + } + } + + return false; +}; + +CaretBrowsing.injectCaretStyles = function() { + const style = ".CaretBrowsing_Caret {" + + " position: absolute;" + + " z-index: 2147483647;" + + " min-height: 10px;" + + " background-color: #000;" + + "}" + + ".CaretBrowsing_AnimateCaret {" + + " position: absolute;" + + " z-index: 2147483647;" + + " min-height: 10px;" + + "}" + + ".CaretBrowsing_FlashVert {" + + " position: absolute;" + + " z-index: 2147483647;" + + " background: linear-gradient(" + + " 270deg," + + " rgba(128, 128, 255, 0) 0%," + + " rgba(128, 128, 255, 0.3) 45%," + + " rgba(128, 128, 255, 0.8) 50%," + + " rgba(128, 128, 255, 0.3) 65%," + + " rgba(128, 128, 255, 0) 100%);" + + "}"; + const node = document.createElement("style"); + node.innerHTML = style; + document.body.appendChild(node); +}; + +CaretBrowsing.setInitialCursor = function() { + if (CaretBrowsing.post_message_down("CaretBrowsing.setInitialCursor")) { + return; + } + const sel = window.getSelection(); + if (!CaretBrowsing.initiated) { + if (sel.rangeCount == 0) { + CaretBrowsing.positionCaret(); + } + + CaretBrowsing.injectCaretStyles(); + CaretBrowsing.initiated = true; + } + + CaretBrowsing.toggle(); + if (CaretBrowsing.isEnabled) { + // when doing an i-search, the selection is totally cleared (sigh) + if (sel.rangeCount == 0) { + CaretBrowsing.positionCaret(); + } + // do not handle previous selection for now, just removes it. + CaretBrowsing.markEnabled = sel.type == "Range"; + if (CaretBrowsing.markEnabled) { + CaretBrowsing.markEnabled = false; + sel.collapse(sel.anchorNode, sel.anchorOffset); + window.setTimeout(() => { + CaretBrowsing.updateCaretOrSelection(true); + }, 0); + } + } +}; + +CaretBrowsing.shutdown = function() { + if (CaretBrowsing.post_message_down("CaretBrowsing.shutdown")) { + return; + } + CaretBrowsing.toggle(false); +} + +CaretBrowsing.setAndValidateSelection = function(start, end) { + const sel = window.getSelection(); + sel.setBaseAndExtent(start.node, start.index, end.node, end.index); + + if (sel.rangeCount !== 1) { + return false; + } + + return (sel.anchorNode === start.node && + sel.anchorOffset === start.index && + sel.focusNode === end.node && + sel.focusOffset === end.index); +}; + + +CaretBrowsing.setCaretElementNormalStyle = function() { + const element = CaretBrowsing.caretElement; + element.className = "CaretBrowsing_Caret"; + if (CaretBrowsing.isSelectionCollapsed) { + element.style.opacity = "1.0"; + } else { + element.style.opacity = "0.0"; + } + element.style.left = `${CaretBrowsing.caretX}px`; + element.style.top = `${CaretBrowsing.caretY}px`; + element.style.width = `${CaretBrowsing.caretWidth}px`; + element.style.height = `${CaretBrowsing.caretHeight}px`; + element.style.color = CaretBrowsing.caretForeground; +}; + +CaretBrowsing.animateCaretElement = function() { + const element = CaretBrowsing.caretElement; + element.style.left = `${CaretBrowsing.caretX - 50}px`; + element.style.top = `${CaretBrowsing.caretY - 100}px`; + element.style.width = `${CaretBrowsing.caretWidth + 100}px`; + element.style.height = `${CaretBrowsing.caretHeight + 200}px`; + element.className = "CaretBrowsing_AnimateCaret"; + + window.setTimeout(() => { + if (!CaretBrowsing.caretElement) { + return; + } + CaretBrowsing.setCaretElementNormalStyle(); + element.style.transition = "all 0.8s ease-in"; + function listener() { + element.removeEventListener( + "transitionend", listener, false); + element.style.transition = "none"; + } + element.addEventListener( + "transitionend", listener, false); + }, 0); +}; + +CaretBrowsing.flashCaretElement = function() { + const x = CaretBrowsing.caretX; + const y = CaretBrowsing.caretY; + + const vert = document.createElement("div"); + vert.className = "CaretBrowsing_FlashVert"; + vert.style.left = `${x - 6}px`; + vert.style.top = `${y - 100}px`; + vert.style.width = "11px"; + vert.style.height = `${200}px`; + document.body.appendChild(vert); + + window.setTimeout(() => { + document.body.removeChild(vert); + if (CaretBrowsing.caretElement) { + CaretBrowsing.setCaretElementNormalStyle(); + } + }, 250); +}; + +CaretBrowsing.createCaretElement = function() { + const element = document.createElement("div"); + element.className = "CaretBrowsing_Caret"; + document.body.appendChild(element); + CaretBrowsing.caretElement = element; + + if (CaretBrowsing.onEnable === "anim") { + CaretBrowsing.animateCaretElement(); + } else if (CaretBrowsing.onEnable === "flash") { + CaretBrowsing.flashCaretElement(); + } else { + CaretBrowsing.setCaretElementNormalStyle(); + } +}; + +CaretBrowsing.getCursorRect = function(cursor) { // eslint-disable-line max-statements,max-len + let node = cursor.node; + const index = cursor.index; + const rect = { + "left": 0, + "top": 0, + "width": 1, + "height": 0, + }; + if (node.constructor === Text) { + let left = index; + let right = index; + const max = node.data.length; + const newRange = document.createRange(); + while (left > 0 || right < max) { + if (left > 0) { + left--; + newRange.setStart(node, left); + newRange.setEnd(node, index); + const rangeRect = newRange.getBoundingClientRect(); + if (rangeRect && rangeRect.width && rangeRect.height) { + rect.left = rangeRect.right; + rect.top = rangeRect.top; + rect.height = rangeRect.height; + break; + } + } + if (right < max) { + right++; + newRange.setStart(node, index); + newRange.setEnd(node, right); + const rangeRect = newRange.getBoundingClientRect(); + if (rangeRect && rangeRect.width && rangeRect.height) { + rect.left = rangeRect.left; + rect.top = rangeRect.top; + rect.height = rangeRect.height; + break; + } + } + } + } else { + rect.height = node.offsetHeight; + while (node !== null) { + rect.left += node.offsetLeft; + rect.top += node.offsetTop; + node = node.offsetParent; + } + } + rect.left += window.pageXOffset; + rect.top += window.pageYOffset; + return rect; +}; + +CaretBrowsing.updateCaretOrSelection = + function(scrollToSelection) { // eslint-disable-line max-statements + const previousX = CaretBrowsing.caretX; + const previousY = CaretBrowsing.caretY; + + const sel = window.getSelection(); + if (sel.rangeCount === 0) { + if (CaretBrowsing.caretElement) { + CaretBrowsing.isSelectionCollapsed = false; + CaretBrowsing.caretElement.style.opacity = "0.0"; + } + return; + } + + const range = sel.getRangeAt(0); + if (!range) { + if (CaretBrowsing.caretElement) { + CaretBrowsing.isSelectionCollapsed = false; + CaretBrowsing.caretElement.style.opacity = "0.0"; + } + return; + } + + if (CaretBrowsing.isControlThatNeedsArrowKeys( + document.activeElement)) { + let node = document.activeElement; + CaretBrowsing.caretWidth = node.offsetWidth; + CaretBrowsing.caretHeight = node.offsetHeight; + CaretBrowsing.caretX = 0; + CaretBrowsing.caretY = 0; + while (node.offsetParent) { + CaretBrowsing.caretX += node.offsetLeft; + CaretBrowsing.caretY += node.offsetTop; + node = node.offsetParent; + } + CaretBrowsing.isSelectionCollapsed = false; + } else if (range.startOffset !== range.endOffset || + range.startContainer !== range.endContainer) { + const rect = range.getBoundingClientRect(); + if (!rect) { + return; + } + CaretBrowsing.caretX = rect.left + window.pageXOffset; + CaretBrowsing.caretY = rect.top + window.pageYOffset; + CaretBrowsing.caretWidth = rect.width; + CaretBrowsing.caretHeight = rect.height; + CaretBrowsing.isSelectionCollapsed = false; + } else { + const rect = CaretBrowsing.getCursorRect( + new Cursor(range.startContainer, + range.startOffset, + TraverseUtil.getNodeText(range.startContainer))); + CaretBrowsing.caretX = rect.left; + CaretBrowsing.caretY = rect.top; + CaretBrowsing.caretWidth = rect.width; + CaretBrowsing.caretHeight = rect.height; + CaretBrowsing.isSelectionCollapsed = true; + } + + if (CaretBrowsing.caretElement) { + const element = CaretBrowsing.caretElement; + if (CaretBrowsing.isSelectionCollapsed) { + element.style.opacity = "1.0"; + element.style.left = `${CaretBrowsing.caretX}px`; + element.style.top = `${CaretBrowsing.caretY}px`; + element.style.width = `${CaretBrowsing.caretWidth}px`; + element.style.height = `${CaretBrowsing.caretHeight}px`; + } else { + element.style.opacity = "0.0"; + } + } else { + CaretBrowsing.createCaretElement(); + } + + let elem = range.startContainer; + if (elem.constructor === Text) { + elem = elem.parentElement; + } + const style = window.getComputedStyle(elem); + const bg = axs.utils.getBgColor(style, elem); + const fg = axs.utils.getFgColor(style, elem, bg); + CaretBrowsing.caretBackground = axs.color.colorToString(bg); + CaretBrowsing.caretForeground = axs.color.colorToString(fg); + + if (scrollToSelection) { + const rect = CaretBrowsing.getCursorRect( + new Cursor(sel.focusNode, sel.focusOffset, + TraverseUtil.getNodeText(sel.focusNode))); + + const yscroll = window.pageYOffset; + const pageHeight = window.innerHeight; + const caretY = rect.top; + const caretHeight = Math.min(rect.height, 30); + if (yscroll + pageHeight < caretY + caretHeight) { + window.scroll(0, (caretY + caretHeight - pageHeight + 100)); + } else if (caretY < yscroll) { + window.scroll(0, (caretY - 100)); + } + } + + if (Math.abs(previousX - CaretBrowsing.caretX) > 500 || + Math.abs(previousY - CaretBrowsing.caretY) > 100) { + if (CaretBrowsing.onJump === "anim") { + CaretBrowsing.animateCaretElement(); + } else if (CaretBrowsing.onJump === "flash") { + CaretBrowsing.flashCaretElement(); + } + } + }; + +CaretBrowsing.toggle = function(enabled) { + if (enabled == undefined) { + enabled = !CaretBrowsing.isEnabled; + } + + CaretBrowsing.isEnabled = enabled; + const obj = {}; + obj.enabled = CaretBrowsing.isEnabled; + CaretBrowsing.updateIsCaretVisible(); + if (!obj.enabled) { + var sel = window.getSelection(); + sel.collapse(sel.focusNode, sel.focusOffset); + } +}; + +CaretBrowsing.onClick = function() { + if (!CaretBrowsing.isEnabled) { + return true; + } + window.setTimeout(() => { + CaretBrowsing.targetX = null; + CaretBrowsing.updateCaretOrSelection(false); + }, 0); + return true; +}; + +CaretBrowsing.caretBlinkFunction = function() { + if (CaretBrowsing.caretElement) { + if (CaretBrowsing.blinkFlag) { + CaretBrowsing.caretElement.style.backgroundColor = + CaretBrowsing.caretForeground; + CaretBrowsing.blinkFlag = false; + } else { + CaretBrowsing.caretElement.style.backgroundColor = + CaretBrowsing.caretBackground; + CaretBrowsing.blinkFlag = true; + } + } +}; + +CaretBrowsing.updateIsCaretVisible = function() { + CaretBrowsing.isCaretVisible = + (CaretBrowsing.isEnabled && CaretBrowsing.isWindowFocused); + if (CaretBrowsing.isCaretVisible && !CaretBrowsing.caretElement) { + // CaretBrowsing.setInitialCursor(); + CaretBrowsing.updateCaretOrSelection(true); + if (CaretBrowsing.caretElement) { + CaretBrowsing.blinkFunctionId = window.setInterval( + CaretBrowsing.caretBlinkFunction, 500); + } + } else if (!CaretBrowsing.isCaretVisible && + CaretBrowsing.caretElement) { + window.clearInterval(CaretBrowsing.blinkFunctionId); + if (CaretBrowsing.caretElement) { + CaretBrowsing.isSelectionCollapsed = false; + CaretBrowsing.caretElement.parentElement.removeChild( + CaretBrowsing.caretElement); + CaretBrowsing.caretElement = null; + } + } +}; + +CaretBrowsing.onWindowFocus = function() { + CaretBrowsing.isWindowFocused = true; + CaretBrowsing.updateIsCaretVisible(); +}; + +CaretBrowsing.onWindowBlur = function() { + CaretBrowsing.isWindowFocused = false; + CaretBrowsing.updateIsCaretVisible(); +}; + +CaretBrowsing.init = function() { + CaretBrowsing.isWindowFocused = document.hasFocus(); + + document.addEventListener("click", CaretBrowsing.onClick, false); + window.addEventListener("focus", CaretBrowsing.onWindowFocus, false); + window.addEventListener("blur", CaretBrowsing.onWindowBlur, false); +}; + +window.setTimeout(() => { + if (!window.caretBrowsingLoaded) { + window.caretBrowsingLoaded = true; + CaretBrowsing.init(); + } +}, 0); + + +CaretBrowsing.move = function(direction, granularity) { + if (CaretBrowsing.post_message_down("CaretBrowsing.move", + [direction, granularity])) { + return; + } + var sel = window.getSelection(); + var action = CaretBrowsing.markEnabled ? "extend" : "move"; + sel.modify(action, direction, granularity); + window.setTimeout(() => { + CaretBrowsing.updateCaretOrSelection(true); + }, 0); +}; + + +CaretBrowsing.toggleMark = function() { + if (CaretBrowsing.post_message_down("CaretBrowsing.toggleMark")) { + return; + } + CaretBrowsing.markEnabled = !CaretBrowsing.markEnabled; + if (!CaretBrowsing.markEnabled) { + var sel = window.getSelection(); + sel.collapse(sel.focusNode, sel.focusOffset); + window.setTimeout(() => { + CaretBrowsing.updateCaretOrSelection(true); + }, 0); + } +}; + +CaretBrowsing.cutSelection = function() { + if (CaretBrowsing.post_message_down("CaretBrowsing.cutSelection")) { + return; + } + // clear the current selection + if (CaretBrowsing.markEnabled) { CaretBrowsing.toggleMark(); } +}; + +CaretBrowsing.post_message_down = function(message_name, arg) { + if (document.activeElement.tagName === "IFRAME") { + post_message(document.activeElement.contentWindow, message_name, arg); + return true; + } + return false; +} + +if (self !== top) { + register_message_handler("CaretBrowsing.setInitialCursor", + CaretBrowsing.setInitialCursor); + register_message_handler("CaretBrowsing.shutdown", + CaretBrowsing.shutdown); + register_message_handler("CaretBrowsing.move", function (args) { + CaretBrowsing.move(args[0], args[1]); + }); + register_message_handler("CaretBrowsing.toggleMark", + CaretBrowsing.toggleMark); + register_message_handler("CaretBrowsing.cutSelection", + CaretBrowsing.cutSelection); +} \ No newline at end of file diff --git a/eaf.el b/eaf.el index 6948f16..5ebf372 100644 --- a/eaf.el +++ b/eaf.el @@ -294,6 +294,7 @@ Try not to modify this alist directly. Use `eaf-setq' to modify instead." ("C-y" . "yank_text") ("C-w" . "kill_text") ("M-e" . "edit_focus_text") + ("M-c" . "caret_browsing") ("M-s" . "open_link") ("M-S" . "open_link_new_buffer") ("M-d" . "open_link_background_buffer") @@ -312,6 +313,19 @@ Try not to modify this alist directly. Use `eaf-setq' to modify instead." ("M->" . "scroll_to_bottom") ("M-t" . "new_blank_page") ("SPC" . "insert_or_scroll_up_page") + ("C-q" . "caret_exit") + ("z" . "insert_or_caret_next_line") + ("Z" . "insert_or_caret_previous_line") + ("a" . "insert_or_caret_next_character") + ("A" . "insert_or_caret_previous_character") + ("w" . "insert_or_caret_next_word") + ("W" . "insert_or_caret_previous_word") + ("b" . "insert_or_caret_to_bottom") + ("B" . "insert_or_caret_to_top") + ("/" . "insert_or_caret_search_forward") + ("?" . "insert_or_caret_search_backward") + ("C-i" . "caret_toggle_mark") + ("C-." . "caret_clear_search") ("J" . "insert_or_select_left_tab") ("K" . "insert_or_select_right_tab") ("j" . "insert_or_scroll_up")