Rename repo
This commit is contained in:
commit
9e00cb84e7
14 changed files with 451 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.xpi
|
26
README.md
Normal file
26
README.md
Normal 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?
|
25
content_scripts/checkPage.js
Normal file
25
content_scripts/checkPage.js
Normal 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);
|
||||
}
|
||||
})();
|
140
content_scripts/pinpointer.js
Normal file
140
content_scripts/pinpointer.js
Normal 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
3
icons/LICENSE
Normal 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
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
BIN
icons/icon-32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 812 B |
BIN
icons/icon-48.png
Normal file
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
BIN
icons/icon-96.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
icons/icon.png
Normal file
BIN
icons/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
35
manifest.json
Normal file
35
manifest.json
Normal 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
38
popups/choose_element.css
Normal 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;
|
||||
}
|
36
popups/choose_element.html
Normal file
36
popups/choose_element.html
Normal 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
147
popups/choose_element.js
Normal 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);
|
Reference in a new issue