Skip to content

Commit

Permalink
Merge pull request #16 from Jelmerro/bugfix/follow-mode-improvements
Browse files Browse the repository at this point in the history
fix select and onclick in follow mode
  • Loading branch information
Jelmerro authored Feb 17, 2019
2 parents 103c02b + 857052d commit 0f96a01
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 122 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 5 additions & 6 deletions app/help.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ <h2 id="basics-modes">modes</h2>
</li>
<li>
<span onclick="changeColor(this);">
Elements with javascript onclick
Elements with javascript event handlers
</span>
will be outlined in yellow.
will be outlined in orange.
</li>
</ul>
Press <kbd>f</kbd> to see the difference for the examples above.
Expand Down Expand Up @@ -427,10 +427,9 @@ <h2 id="follow-click">follow by click</h2>
This allows you to start typing in the input field after choosing it with follow mode.
</li>
<li>
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 orange -
Elements with JavaScript event handlers will be made clickable.
Both the onclick attribute and elements with event listeners are supported.
</li>
</ul>
Clicking links with the mouse when using insert mode will mostly do the same thing.
Expand Down
8 changes: 2 additions & 6 deletions app/js/follow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
Expand Down
131 changes: 78 additions & 53 deletions app/js/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -117,65 +158,49 @@ 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)
}
})
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
}
}

Loading

0 comments on commit 0f96a01

Please sign in to comment.