Skip to content

Instantly share code, notes, and snippets.

@barneycarroll
Last active September 13, 2020 04:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save barneycarroll/70107eaa98378aab0da1d99998782854 to your computer and use it in GitHub Desktop.
Save barneycarroll/70107eaa98378aab0da1d99998782854 to your computer and use it in GitHub Desktop.
A browser plugin script to identify previous / next pagination links in forums and expose them to generic keyboard navigation
const options = {
duration: 600,
easing: 'cubic-bezier(0, 0.55, 0.45, 1)',
fill: 'both',
}
function feedback(message = 'error'){
const $circle = render(circle)
$circle.animate({
transform: ['scale(0%)', 'scale(120%)'],
}, options)
.onfinish = () => {
$circle.animate({
opacity: [1, 0],
}, options)
}
const $symbol = render(
message === 'error' ? x : arrow
)
if(message === 'next')
$symbol.firstElementChild.style.transform = 'rotate(.5turns)'
$symbol.animate({
transform: ['scale(200%)', 'scale(90%)'],
opacity: [0, 1],
}, {
...options,
duration: 200,
})
.onfinish = () => {
$symbol.animate({
opacity: [1, 0]
}, options)
}
}
function render(source){
const $ = document.createElement('div')
$.innerHTML = `
<div style="
transform-origin: 50% 50%;
position: fixed;
height: 100vh;
width: 100vw;
left: 0;
top: 0;
">
<object
type=image/svg+xml
data="data:image/svg+xml;base64,${ btoa(source) }"
style="object-fit: contain">
</object>
</div>
`
return document.body.appendChild(
$.firstElementChild
)
}
const arrow = `\
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 640">
<defs/>
<defs>
<path id="a" d="M311 0L0 320l311 320V437h329V202H311V0z"/>
</defs>
<use fill="#492d67" xlink:href="#a"/>
</svg>
`
const circle = `\
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 640">
<defs/>
<defs>
<path id="a" d="M640 320a320 320 0 11-640 0 320 320 0 01640 0z"/>
</defs>
<use fill="#f22ce6" xlink:href="#a"/>
</svg>
`
const x = `\
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 640">
<defs/>
<defs>
<path id="a" d="M435 320l205 205-115 115-205-205-205 205L0 525l205-205L0 115 115 0l205 205L525 0l115 115-205 205z"/>
</defs>
<use fill="#492d67" xlink:href="#a"/>
</svg>
`
{
"manifest_version": 2,
"name": "Traverse",
"version": "1.0",
"description": "Forum navigation assistance script. Finds common previous / next page link patterns and binds their actions to Ctrl + ⟵ / Ctrl + ⟶ keyboard commands.",
"icons": {
"64": "icons/traverse.svg"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["DOM.js"],
"run_at": "document_end"
}
],
"commands": {
"previous": {
"suggested_key": {
"default": "Ctrl+Left",
"mac": "MacCtrl+Left"
},
"description": "Navigate to previous page in thread"
},
"next": {
"suggested_key": {
"default": "Ctrl+Right",
"mac": "MacCtrl+Right"
},
"description": "Navigate to next page in thread"
}
}
}
// Commands interface
browser.commands.onCommand.addListener(direction =>
act(controls[direction])
)
// Controls are searched for on request...
const controls = new Proxy({}, {
get: (_, command) => _[command] || findControls()[command]
})
// ...up until the document is ready, at which point they're fixed
document.addEventListener('DOMContentLoaded', () => {
Object.assign(controls, findControls())
})
// Text patterns used by pagination controls,
// in order of most to least likely / ambiguous
const patterns = [
['previous', 'next'],
['<', '>' ],
['prev', 'next'],
['‹', '›' ],
['«', '»' ],
['<<', '>>' ],
].map(pair =>
pair.map(string =>
new RegExp('\\s*' + string + '\\s*', 'i')
)
)
// Element properties to test those patterns on
const properties = ['textContent', 'rel', 'title']
function findControls() {
const controls = {}
const $all = Array.from(document.all)
// The 2 commands, whose index maps to that of the pattern pair (0 , 1)
void ['previous', 'next'].forEach((command, index) =>
// Iterate through the patterns
patterns.find(pair =>
// And for every element on the page
$all.find($ =>
// If any of the relevant element properties match the pattern...
properties.some(property =>
pair[index].test($[property])
)
&&
// The containing actionable element is the pertinent control
(controls[command] = $.closest('a,button,input'))
)
)
)
return controls
}
// Activate pagination controls
function act($) {
if ($.onclick)
$.onclick()
if ($.matches('a'))
window.location.assign($.href)
else if ($.matches('input[type=submit]'))
$.form.submit()
}
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment