Rename repo

This commit is contained in:
Rumperuu 2018-07-18 10:43:03 +01:00
commit 9e00cb84e7
14 changed files with 451 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.xpi

26
README.md Normal file
View file

@ -0,0 +1,26 @@
# Pinpointer
Pinpointer is a browser extension that allows you to create and share links to arbitrary content on a webpage.
You can read more about the extension on my website [here](https://bengoldsworthy.net/program/pinpointer/).
## Use
After installing the extension, an icon will appear in your browser toolbar. Select an element on a web page and click the icon, then click the first button to select that element. Fine-tune your choices, and then click the second button to generate the link.
With the extension installed, any received Pinpoint links will automatically highlight the element selected. The links are backwards compatible, so you don't have to worry about the recipient using the extension or not.
## Future Improvements
### Major
* Add an options menu so a user can specify (for example) how they want elements in Pinpoint links that they receive to be highlighted.
* Look at ways of modifying `getPath()` to produce shorter selectors.
* Make it look less ugly.
* Find an efficient method of making every element _except_ the highlighted one low-opacity? Previous attempts to do so would make the highlighted element's parent low-opacity, and you can't make a child more opaque than its parent.
* Move repo off of GitHub.
### Minor
* Add a button to cancel element highlighting when recieving a Pinpointer link.
* Should the first button on the form be removed, and just have clicking the Pinpointer icon go straight to the selector specification stage?

View file

@ -0,0 +1,25 @@
/**
Title: Pinpointer
Version: 1.0
Author: Ben Goldsworthy <me@bengoldsworthy.net>
This file is a part of Pinpointer, a browser extension that allows you to
create and share links to arbitrary content on a webpage.
This script highlights elements when provided with a Pinpointer link.
*/
(function() {
/**
* Check if an extension-compatible fragment identfier is present (and that
* it's not just a regular identifier). If it is, then scroll the page to
* the correct element and style it.
*/
var currentElement = window.location.href.split('#')[1];
if ((typeof currentElement != 'undefined') && (document.querySelector(":target") == null)) {
currentElement = atob(decodeURIComponent(currentElement));
elem = document.querySelector(currentElement);
elem.scrollIntoView(true);
elem.style.cssText = "background-color: lightgreen; border: 2px dashed green;";
window.scrollBy(0, -50);
}
})();

View file

@ -0,0 +1,140 @@
/**
Title: Pinpointer
Version: 1.0
Author: Ben Goldsworthy <me@bengoldsworthy.net>
This file is a part of Pinpointer, a browser extension that allows you to
create and share links to arbitrary content on a webpage.
This script runs in the browser windo and selects elements to pass on to the
extension popup menu.
*/
(function() {
/**
* Protects against multiple injections of the same content script.
*/
if (window.hasRun)
return;
window.hasRun = true;
var firstRun = true;
// Creates a stylesheet used for highlighting selected elements.
// Source: https://davidwalsh.name/add-rules-stylesheets
var sheet = (function() {
// Create the <style> tag
var style = document.createElement("style");
// WebKit hack :(
style.appendChild(document.createTextNode(""));
// Add the <style> element to the page
document.head.appendChild(style);
return style.sheet;
})();
/**
* This function has been _heavily_ adapted from the `jquery-getpath.js`
* plugin, released under the MIT License by Dave Cardwell.
*
* The original plugin can be found at:
* http://davecardwell.co.uk/javascript/jquery/plugins/jquery-getpath/
*/
function getPath(path, elem) {
// If this elem is an `<html>` node, we've reached the end of the path.
if (elem.tagName == 'HTML')
return 'html' + path;
// Get the elem's tag name and (if applicable, and only the former if
// possible), ID and class names.
var cur = elem.tagName.toLowerCase();
if (elem.id != "") {
cur += '#' + elem.id;
} else {
var classes = elem.className;
if (classes != "")
cur += '.' + classes.trim().split(/[\s\n]+/).join('.');}
// Recurse up the DOM.
return getPath(' > ' + cur + path, elem.parentNode);
}
/**
* Listen for messages from the popup script.
* Upon receiving them, either send the path for the currently-selected page
* element or highlight the new element.
*/
browser.runtime.onMessage.addListener(
function(message, sender, sendResponse) {
switch(message.type) {
// In this case, send back the path that uniquely identifies the
// currently-selected element.
case "grabElement":
// Check if anything is actually selected first.
var selection = window.getSelection();
if (selection.anchorOffset != selection.focusOffset) {
// If it is, get the path to the selected element.
var elem = selection.anchorNode.parentNode;
var path = getPath("", elem);
// If the given path selector does not uniquely specify a single
// element, replace the class selectors for the final element
// with an `:nth-of-type()` psuedo-selector.
if (document.querySelectorAll(path).length > 1) {
// Get the last element's type, shorn of class selectors.
var tmpPath = path.split(" > ");
var lastElem = tmpPath.pop().split(".")[0];
path = tmpPath.join(" > ");
path += " > " + lastElem;
// Find all its same-type siblings, enumerate them and figure
// out which number our element is.
var sibs = document.querySelectorAll(path);
sibs.forEach(function(currentValue, currentIndex, listObj) {
if (currentValue.isEqualNode(elem)) {
path += ":nth-of-type(" + (currentIndex + 1) + ")";
return;
}
});
}
sendResponse(path);
highlight(path);
} else {
sendResponse(null);
}
break;
case "highlightElement":
highlight(message.selector);
break;
default:
console.error("Unrecognised message: ", message);
}
}
);
/**
* Highlight an element, given a selector.
*/
function highlight(selector) {
if (!firstRun) sheet.deleteRule(0);
else firstRun = false;
sheet.insertRule(selector + " { opacity: 0.7; border: 2px dashed green; }");
}
/**
* When focus returns to the tab (i.e. the user clicks off of the pop-up),
* clear any element highlighting.
*/
window.addEventListener("focus", function(event) {
if (!firstRun) {
sheet.deleteRule(0);
firstRun = true;
}
}, false);
})();

3
icons/LICENSE Normal file
View file

@ -0,0 +1,3 @@
The link part of the icon is taken from SimpleIcon on Wikimedia Commons. https://commons.wikimedia.org/wiki/File:Simpleicons_Interface_link-symbol-of-two-chains-links-linked.svg
The magnifying glass part is taken from Derferman on Wikimedia Commons. https://commons.wikimedia.org/wiki/File:Magnifying_glass_icon.svg

BIN
icons/icon-32-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

BIN
icons/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

BIN
icons/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/icon-96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

35
manifest.json Normal file
View file

@ -0,0 +1,35 @@
{
"description": "Create and share links to arbitrary bits of webpages.",
"manifest_version": 2,
"name": "Pinpointer",
"version": "1.0",
"homepage_url": "https://github.com/Rumperuu/Pinpointer",
"icons": {
"48": "icons/icon-48.png",
"96": "icons/icon-96.png"
},
"permissions": [
"activeTab"
],
"browser_action": {
"default_icon": "icons/icon-32.png",
"theme_icons": [{
"light": "icons/icon-32-light.png",
"dark": "icons/icon-32.png",
"size": 32
}],
"default_title": "Pinpointer",
"default_popup": "popups/choose_element.html"
},
"content_scripts": [
{
"matches": ["http://*/*","https://*/*"],
"js": ["content_scripts/checkPage.js"]
}
]
}

38
popups/choose_element.css Normal file
View file

@ -0,0 +1,38 @@
/**
Title: Pinpointer
Version: 1.0
Author: Ben Goldsworthy <me@bengoldsworthy.net>
This file is a part of Pinpointer, a browser extension that allows you to
create and share links to arbitrary content on a webpage.
This stylesheet styles the layout of the extension popup menu.
*/
html, body {
width: auto;
}
.hidden {
display: none;
}
.button {
color: white;
margin: 3% auto;
padding: 4px;
text-align: center;
font-size: 1.5em;
cursor: pointer;
background-color: green;
}
.button:hover {
background-color: darkgreen;
}
span {
border-radius: 25px;
padding: 4px;
font-weight: bold;
}

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<!--
Title: Pinpointer
Version: 1.0
Author: Ben Goldsworthy <me@bengoldsworthy.net>
This file is a part of Pinpointer, a browser extension that allows you to
create and share links to arbitrary content on a webpage.
This markup represents the extension popup menu.
-->
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="choose_element.css"/>
</head>
<body>
<h1>Pinpointer</h1>
<div id="popup-content">
<p id="step1">Highlight the part of the page you want to create a link to, and then click the button below:</p>
<div class="button" id="grabElement">Click to select element.</div>
<p id="step2" class="hidden">Modify the exact selection below, and then click the button.</p>
<p id="selector" class="hidden"></p>
<div class="button hidden" id="generateLink">Click to generate link.</div>
<p id="link" class="hidden"></p>
</div>
<div id="error-content" class="hidden">
<p>Error.</p>
</div>
<script src="choose_element.js"></script>
</body>
</html>

147
popups/choose_element.js Normal file
View file

@ -0,0 +1,147 @@
/**
Title: Pinpointer
Version: 1.0
Author: Ben Goldsworthy <me@bengoldsworthy.net>
This file is a part of Pinpointer, a browser extension that allows you to
create and share links to arbitrary content on a webpage.
This script provides the logic of the extension popup menu.
*/
/**
* Listens for clicks within the pop-up. These will be to either grab a selected
* element's CSS selector, to change the selected element or to generate the
* URL.
*/
function listenForClicks() {
var maxIndex = 0;
var splitPath = new Array();
var currentURL;
document.addEventListener("click", (e) => {
switch (e.target.id) {
// Get the active tab, then request the path to the currently-selected
// element and display it.
case "grabElement":
browser.tabs.query({active: true, currentWindow: true}, function(tabs) {
// Strips the fragment identifier off of the current URL, if
// applicable.
currentURL = tabs[0].url.split('#')[0];
// Requests the unique path of the currently-selected element.
browser.tabs.sendMessage(tabs[0].id, {type: "grabElement"}, function(path) {
if (path != null) {
// Splits the path into constituent elements.
splitPath = path.split(' > ');
maxIndex = splitPath.length - 1;
// Builds and displays the precision selection form.
document.getElementById("selector").classList.remove("hidden");
var back = document.createElement("button");
back.id = "back";
back.setAttribute('type', 'button');
if (maxIndex <= 0) back.disabled = true;
back.appendChild(document.createTextNode("<"));
document.getElementById("selector").appendChild(back);
for (var i = 0; i <= maxIndex; i++) {
var segment = document.createElement("span");
segment.classList.add("segment");
var selector = document.createTextNode(splitPath[i]);
segment.appendChild(selector);
document.getElementById("selector").appendChild(segment);
if (i + 1 <= maxIndex) {
var sef = document.createElement("span");
var segSep = document.createElement("span");
segSep.classList.add("segSep");
segSep.appendChild(document.createTextNode(" > "));
document.getElementById("selector").appendChild(segSep);
}
}
var fwd = document.createElement("button");
fwd.id = "fwd";
fwd.setAttribute('type', 'button');
if (maxIndex == splitPath.length - 1) fwd.disabled = true;
fwd.appendChild(document.createTextNode(">"));
document.getElementById("selector").appendChild(fwd);
// Shows the next steps for the user.
document.getElementById("step2").classList.remove("hidden");
document.getElementById("generateLink").classList.remove("hidden");
} else {
// Otherwise, if nothing is currently highlighted, display
// a message.
document.getElementById("selector").innerHTML = 'Please select some text first.';
}
});
});
break;
// In this case, generate and display the URL.
case "generateLink":
// Re-form the selector into a string.
splitPath = splitPath.slice(0, maxIndex + 1);
var finalPath = splitPath.join(" > ");
// Encode it and append it to the current URL.
var encodedPath = encodeURIComponent(btoa(finalPath));
var linkURL = currentURL + '#' + encodedPath;
var linkElem = document.createElement('a');
linkElem.href = linkURL;
linkElem.target = "_blank";
linkElem.rel = "noopener noreferrer";
linkElem.id = "linkElement";
linkElem.appendChild(document.createTextNode(linkElem));
document.getElementById("link").appendChild(linkElem);
document.getElementById("link").classList.remove("hidden");
break;
// Selects the previous element in the selector.
case "back":
if (maxIndex > 0) {
document.getElementsByClassName("segment")[maxIndex--].classList.add("hidden");
document.getElementsByClassName("segSep")[maxIndex].classList.add("hidden");
browser.tabs.query({active: true, currentWindow: true}, function(tabs) {
browser.tabs.sendMessage(tabs[0].id, {type: "highlightElement", selector: splitPath.slice(0, maxIndex + 1).join(" > ") });
});
if (maxIndex <= 0)
e.target.disabled = true;
if (document.getElementById("fwd").disabled)
document.getElementById("fwd").disabled = false;
}
break;
// Selects the next element in the selector.
case "fwd":
if (maxIndex < splitPath.length - 1) {
document.getElementsByClassName("segSep")[maxIndex++].classList.remove("hidden");
document.getElementsByClassName("segment")[maxIndex].classList.remove("hidden");
browser.tabs.query({active: true, currentWindow: true}, function(tabs) {
browser.tabs.sendMessage(tabs[0].id, {type: "highlightElement", selector: splitPath.slice(0, maxIndex + 1).join(" > ") });
});
if (maxIndex == splitPath.length - 1)
e.target.disabled = true;
if (document.getElementById("back").disabled)
document.getElementById("back").disabled = false;
}
break;
default:
}
});
}
/**
* Displays the popup's error message if there is an error executing the script.
*/
function reportExecuteScriptError(error) {
document.getElementById("popup-content").classList.add("hidden");
document.getElementById("error-content").classList.remove("hidden");
console.error(`Failed to execute beastify content script: ${error.message}`);
}
/**
* Injects the content script into the active tab and adds a click handler.
* Handles errors if need be.
*/
browser.tabs.executeScript({file: "/content_scripts/pinpointer.js"})
.then(listenForClicks)
.catch(reportExecuteScriptError);