From d5aee686e5e188f66fd42017ab5f35e71b185c91 Mon Sep 17 00:00:00 2001 From: Jelmer van Arnhem Date: Sun, 17 Feb 2019 11:28:54 +0100 Subject: [PATCH 1/2] fix select and onclick in follow mode --- README.md | 2 +- app/help.html | 9 ++-- app/js/follow.js | 8 +-- app/js/preload.js | 131 +++++++++++++++++++++++++++------------------- package-lock.json | 106 ++++++++++++++++++------------------- package.json | 4 +- 6 files changed, 139 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 1284484a..9c0dd245 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ The selectors are divided in the following colors: - Blue for regular links, these will be opened normally or in a new tab with `F` - Green for text-like input fields, choosing any of these will go to insert mode with the field focused - Red for clickable buttons and boxes, these will be clicked automatically without entering insert mode -- Orange for inline onclick handlers, these will be clicked to trigger the onclick +- Orange for JavaScript event handlers, these will be clicked to trigger the event # Starting Vieb diff --git a/app/help.html b/app/help.html index 142a1de0..2a731c4e 100644 --- a/app/help.html +++ b/app/help.html @@ -111,7 +111,7 @@

modes

  • - Elements with javascript onclick + Elements with javascript event handlers will be outlined in yellow.
  • @@ -427,10 +427,9 @@

    follow by click

    This allows you to start typing in the input field after choosing it with follow mode.
  • - Onclick handlers (all elements with an onclick attribute) outlined in yellow - - Elements with onclick attributes present will be made clickable, - using this last type of follow mode link. - Currently this won't detect elements with eventHandlers added by JavaScript. + Onclick handlers outlined in yellow - + Elements with JavaScript event handlers will be made clickable. + Both the onclick attribute and elements with event listeners are supported.
  • Clicking links with the mouse when using insert mode will mostly do the same thing. diff --git a/app/js/follow.js b/app/js/follow.js index 7adb1e5a..a976d128 100644 --- a/app/js/follow.js +++ b/app/js/follow.js @@ -32,7 +32,8 @@ const startFollowNewTab = () => { const startFollow = () => { document.getElementById("follow").innerHTML = "" - if (TABS.currentPage().src === "" || TABS.currentPage().isLoading()) { + if (TABS.currentPage().src === "" || + TABS.currentPage().isLoadingMainFrame()) { UTIL.notify( "Follow mode will be available when the page is done loading") } else { @@ -160,11 +161,6 @@ const enterKey = identifier => { "button": "left", "clickCount": 1 }) - TABS.currentPage().sendInputEvent({ - "type": "mouseLeave", - "x": link.x * factor, - "y": link.y * factor - }) if (link.type !== "inputs-insert") { cancelFollow() } diff --git a/app/js/preload.js b/app/js/preload.js index 08bf8d42..fb4f548e 100644 --- a/app/js/preload.js +++ b/app/js/preload.js @@ -19,20 +19,61 @@ const { ipcRenderer } = require("electron") +const urls = ["a"] +const clickableInputs = ["button", "input[type=\"button\"]", + "input[type=\"radio\"]", "input[type=\"checkbox\"]", + "input[type=\"submit\"]", "summary"] +const textlikeInputs = ["input:not([type=\"radio\"]):not([type=\"checkbox\"])" + + ":not([type=\"submit\"]):not([type=\"button\"])", "textarea", "select"] +const onclickElements = "*:not(button):not(input)[onclick]" + ipcRenderer.on("follow-mode-request", e => { const allLinks = [] //a tags with href as the link, can be opened in new tab or current tab - allLinks.push(...gatherAnchorTags()) + allLinks.push(...allElementsBySelectors("url", urls)) //input tags such as checkboxes, can be clicked but have no text input - allLinks.push(...gatherClickableInputs()) + allLinks.push(...allElementsBySelectors("inputs-click", clickableInputs)) //input tags such as email and text, can have text inserted - allLinks.push(...gatherTextLikeInputs()) + allLinks.push(...allElementsBySelectors("inputs-insert", textlikeInputs)) //All other elements with onclick listeners - allLinks.push(...gatherOnclickElements()) + const clickableElements = [...document.querySelectorAll(onclickElements)] + clickableElements.push(...elementsWithClickListener) + clickableElements.forEach(element => { + const clickable = parseElement(element, "onclick") + if (clickable !== null) { + //Only show onclick elements for which there is no existing link + const similarExistingLinks = allLinks.filter(link => { + return checkForDuplicateLink(clickable, link) + }) + if (similarExistingLinks.length === 0) { + allLinks.push(clickable) + } + } + }) //Send response back to webview, which will forward it to follow.js e.sender.sendToHost("follow-response", allLinks) }) +const checkForDuplicateLink = (element, existing) => { + //check for exactly the same dimensions + if (element.height === existing.height) { + if (element.x === existing.x && element.y === existing.y) { + if (element.width === existing.width) { + return true + } + } + } + //check if the new element is overlapping an existing link + if (element.height + element.y >= existing.height + existing.y) { + if (element.width + element.x >= existing.width + existing.x) { + if (element.x <= existing.x && element.y <= existing.y) { + return true + } + } + } + return false +} + const parseElement = (element, type) => { const rects = [...element.getClientRects()] let dimensions = element.getBoundingClientRect() @@ -117,46 +158,21 @@ const isVisible = (element, doSizeCheck=true) => { return true } -const gatherAnchorTags = () => { - const elements = [...document.getElementsByTagName("a")] - const tags = [] - elements.forEach(element => { - const clickableElement = parseElement(element, "url") - if (clickableElement !== null) { - tags.push(clickableElement) - } - }) - return tags -} - -const gatherClickableInputs = () => { - //Only easily clickable inputs will be matched with this function: - //buttons, checkboxes, submit and radiobuttons +const allElementsBySelectors = (type, selectors) => { const elements = [] - elements.push(...document.getElementsByTagName("button")) - elements.push(...document.querySelectorAll("input[type=\"button\"]")) - elements.push(...document.querySelectorAll("input[type=\"radio\"]")) - elements.push(...document.querySelectorAll("input[type=\"checkbox\"]")) - elements.push(...document.querySelectorAll("input[type=\"submit\"]")) - const tags = [] - elements.forEach(element => { - const clickableElement = parseElement(element, "inputs-click") - if (clickableElement !== null) { - tags.push(clickableElement) - } + const iframes = [...document.getElementsByTagName("iframe")] + selectors.forEach(selector => { + elements.push(...document.querySelectorAll(selector)) + iframes.forEach(frame => { + if (frame.contentDocument) { + elements.push( + ...frame.contentDocument.querySelectorAll(selector)) + } + }) }) - return tags -} - -const gatherTextLikeInputs = () => { - //Input fields with text input or similar - const elements = [...document.querySelectorAll( - "input:not([type=\"radio\"]):not([type=\"checkbox\"])" - + ":not([type=\"submit\"]):not([type=\"button\"])")] - elements.push(...document.getElementsByTagName("textarea")) const tags = [] elements.forEach(element => { - const clickableElement = parseElement(element, "inputs-insert") + const clickableElement = parseElement(element, type) if (clickableElement !== null) { tags.push(clickableElement) } @@ -164,18 +180,27 @@ const gatherTextLikeInputs = () => { return tags } -const gatherOnclickElements = () => { - const elements = [...document.querySelectorAll( - "*:not(button):not(input)[onclick]")] - //This won't access onclick added by javascript, - //but there is a separate issue for that (#9) - const tags = [] - elements.forEach(element => { - const clickableElement = parseElement(element, "onclick") - if (clickableElement !== null) { - tags.push(clickableElement) +const elementsWithClickListener = [] + +Node.prototype.realAddEventListener = Node.prototype.addEventListener +Node.prototype.addEventListener = function(type, listener, options) { + this.realAddEventListener(type, listener, options) + if (type === "click" && this !== document) { + elementsWithClickListener.push(this) + } +} +Node.prototype.realRemoveEventListener = Node.prototype.removeEventListener +Node.prototype.removeEventListener = function(type, listener, options) { + try { + this.realRemoveEventListener(type, listener, options) + } catch (e) { + //This is a bug in the underlying website + } + if (type === "click" && this !== document) { + try { + elementsWithClickListener.remove(this) + } catch (e) { + //The element was already removed from the list before } - }) - return tags + } } - diff --git a/package-lock.json b/package-lock.json index a35704e3..6f43bc69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,9 +31,9 @@ } }, "@types/node": { - "version": "10.12.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.24.tgz", - "integrity": "sha512-GWWbvt+z9G5otRBW8rssOFgRY87J9N/qbhqfjMZ+gUuL6zoL+Hm6gP/8qQBG4jjimqdaNLCehcVapZ/Fs2WjCQ==", + "version": "10.12.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.26.tgz", + "integrity": "sha512-nMRqS+mL1TOnIJrL6LKJcNZPB8V3eTfRo9FQA2b5gDvrHurC8XbSA86KNe0dShlEL7ReWJv/OU9NL7Z0dnqWTg==", "dev": true }, "acorn": { @@ -489,12 +489,6 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, "cli-boxes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", @@ -760,9 +754,9 @@ } }, "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { "esutils": "^2.0.2" @@ -812,9 +806,9 @@ "dev": true }, "electron": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/electron/-/electron-4.0.4.tgz", - "integrity": "sha512-zG5VtLrmPfmw1fXY/3BEtRZk7OZ7djQhweZ6rW+R5NeF6s8RTz/AwTGtLoBo4z8wmJ5QTy0Y941FZw4pe5YlpA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/electron/-/electron-4.0.5.tgz", + "integrity": "sha512-UWFH6SrzNtzfvusGUFYxXDrgsUEbtBXkH/66hpDWxjA2Ckt7ozcYIujZpshbr7LPy8kV3ZRxIvoyCMdaS5DkVQ==", "dev": true, "requires": { "@types/node": "^10.12.18", @@ -952,35 +946,35 @@ "dev": true }, "eslint": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.13.0.tgz", - "integrity": "sha512-nqD5WQMisciZC5EHZowejLKQjWGuFS5c70fxqSKlnDME+oz9zmE8KTlX+lHSg+/5wsC/kf9Q9eMkC8qS3oM2fg==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.14.0.tgz", + "integrity": "sha512-jrOhiYyENRrRnWlMYANlGZTqb89r2FuRT+615AabBoajhNjeh9ywDNlh2LU9vTqf0WYN+L3xdXuIi7xuj/tK9w==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "ajv": "^6.5.3", + "ajv": "^6.9.1", "chalk": "^2.1.0", "cross-spawn": "^6.0.5", "debug": "^4.0.1", - "doctrine": "^2.1.0", + "doctrine": "^3.0.0", "eslint-scope": "^4.0.0", "eslint-utils": "^1.3.1", "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.0", + "espree": "^5.0.1", "esquery": "^1.0.1", "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", + "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob": "^7.1.2", "globals": "^11.7.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.1.0", + "inquirer": "^6.2.2", "js-yaml": "^3.12.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.5", + "lodash": "^4.17.11", "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", @@ -991,7 +985,7 @@ "semver": "^5.5.1", "strip-ansi": "^4.0.0", "strip-json-comments": "^2.0.1", - "table": "^5.0.2", + "table": "^5.2.3", "text-table": "^0.2.0" }, "dependencies": { @@ -1057,12 +1051,12 @@ "dev": true }, "espree": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.0.tgz", - "integrity": "sha512-1MpUfwsdS9MMoN7ZXqAr9e9UKdVHDcvrJpyx7mm1WuQlx/ygErEQBzgi5Nh5qBHIoYweprhtMkTCb9GhcAIcsA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", "dev": true, "requires": { - "acorn": "^6.0.2", + "acorn": "^6.0.7", "acorn-jsx": "^5.0.0", "eslint-visitor-keys": "^1.0.0" } @@ -1207,13 +1201,12 @@ } }, "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", "dev": true, "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" + "flat-cache": "^2.0.1" } }, "find-up": { @@ -1238,17 +1231,22 @@ } }, "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", "dev": true, "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" } }, + "flatted": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", + "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "dev": true + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1363,9 +1361,9 @@ } }, "globals": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.10.0.tgz", - "integrity": "sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", + "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", "dev": true }, "got": { @@ -1945,18 +1943,18 @@ "dev": true }, "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==", "dev": true }, "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", "dev": true, "requires": { - "mime-db": "~1.37.0" + "mime-db": "~1.38.0" } }, "mimic-fn": { @@ -3277,9 +3275,9 @@ "dev": true }, "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", "dev": true, "requires": { "mkdirp": "^0.5.1" diff --git a/package.json b/package.json index ae62a031..1603d77a 100644 --- a/package.json +++ b/package.json @@ -130,8 +130,8 @@ "email": "Jelmerro@users.noreply.github.com", "license": "GPL-3.0+", "devDependencies": { - "electron": "^4.0.4", + "electron": "^4.0.5", "electron-builder": "^20.38.5", - "eslint": "^5.13.0" + "eslint": "^5.14.0" } } From 857052dd0bdcc1c5e48098eb81044f819cec7398 Mon Sep 17 00:00:00 2001 From: Jelmer van Arnhem Date: Sun, 17 Feb 2019 11:35:04 +0100 Subject: [PATCH 2/2] fix color naming --- app/help.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/help.html b/app/help.html index 2a731c4e..5ada1bf6 100644 --- a/app/help.html +++ b/app/help.html @@ -113,7 +113,7 @@

    modes

    Elements with javascript event handlers - will be outlined in yellow. + will be outlined in orange. Press f to see the difference for the examples above. @@ -427,7 +427,7 @@

    follow by click

    This allows you to start typing in the input field after choosing it with follow mode.
  • - Onclick handlers outlined in yellow - + Onclick handlers outlined in orange - Elements with JavaScript event handlers will be made clickable. Both the onclick attribute and elements with event listeners are supported.