2017-11-08 23:20:41 +00:00
|
|
|
/** Hint links.
|
|
|
|
|
|
|
|
TODO:
|
|
|
|
|
|
|
|
important
|
|
|
|
Connect to input system
|
|
|
|
Gluing into tridactyl
|
|
|
|
unimportant
|
|
|
|
Frames
|
|
|
|
Redraw on reflow
|
|
|
|
*/
|
|
|
|
|
2017-11-28 23:20:42 +00:00
|
|
|
import * as DOM from './dom'
|
2017-11-08 23:20:41 +00:00
|
|
|
import {log} from './math'
|
|
|
|
import {permutationsWithReplacement, islice, izip, map} from './itertools'
|
|
|
|
import {hasModifiers} from './keyseq'
|
2017-11-09 00:41:07 +00:00
|
|
|
import state from './state'
|
2017-11-27 14:43:01 +00:00
|
|
|
import {messageActiveTab, message} from './messaging'
|
2017-11-29 20:13:40 +00:00
|
|
|
import * as config from './config'
|
2017-11-30 04:11:49 +00:00
|
|
|
import * as TTS from './text_to_speech'
|
2017-11-27 14:43:01 +00:00
|
|
|
import {HintSaveType} from './hinting_background'
|
2017-12-30 00:46:26 +00:00
|
|
|
import Logger from './logging'
|
|
|
|
const logger = new Logger('hinting')
|
2017-11-08 23:20:41 +00:00
|
|
|
|
|
|
|
/** Simple container for the state of a single frame's hints. */
|
|
|
|
class HintState {
|
|
|
|
public focusedHint: Hint
|
2018-02-13 23:57:29 +00:00
|
|
|
readonly hintHost = html`<div class="TridactylHintHost">`
|
2017-11-08 23:20:41 +00:00
|
|
|
readonly hints: Hint[] = []
|
|
|
|
public filter = ''
|
2017-11-09 00:41:07 +00:00
|
|
|
public hintchars = ''
|
2017-11-08 23:20:41 +00:00
|
|
|
|
2018-02-13 23:57:29 +00:00
|
|
|
constructor(public filterFunc: HintFilter) {}
|
2017-12-26 22:16:46 -05:00
|
|
|
|
2017-11-08 23:20:41 +00:00
|
|
|
destructor() {
|
|
|
|
// Undo any alterations of the hinted elements
|
|
|
|
for (const hint of this.hints) {
|
|
|
|
hint.hidden = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove all hints from the DOM.
|
|
|
|
this.hintHost.remove()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let modeState: HintState = undefined
|
|
|
|
|
|
|
|
/** For each hintable element, add a hint */
|
2017-11-22 03:43:31 +00:00
|
|
|
export function hintPage(
|
|
|
|
hintableElements: Element[],
|
|
|
|
onSelect: HintSelectedCallback,
|
2017-12-28 18:35:52 -05:00
|
|
|
buildHints: HintBuilder = defaultHintBuilder(),
|
|
|
|
filterHints: HintFilter = defaultHintFilter(),
|
2017-11-22 03:43:31 +00:00
|
|
|
) {
|
2017-11-19 07:57:30 +00:00
|
|
|
state.mode = 'hint'
|
2017-12-26 22:16:46 -05:00
|
|
|
modeState = new HintState(filterHints)
|
|
|
|
buildHints(hintableElements, onSelect)
|
2017-11-23 01:10:45 +00:00
|
|
|
|
|
|
|
if (modeState.hints.length) {
|
2017-12-30 00:46:26 +00:00
|
|
|
logger.debug("hints", modeState.hints)
|
2017-11-23 01:10:45 +00:00
|
|
|
modeState.focusedHint = modeState.hints[0]
|
|
|
|
modeState.focusedHint.focused = true
|
|
|
|
document.body.appendChild(modeState.hintHost)
|
|
|
|
} else {
|
|
|
|
reset()
|
|
|
|
}
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
|
2017-12-28 18:35:52 -05:00
|
|
|
function defaultHintBuilder() {
|
2018-01-01 14:38:52 -05:00
|
|
|
switch (config.get('hintfiltermode')) {
|
|
|
|
case 'simple':
|
|
|
|
return buildHintsSimple
|
|
|
|
case 'vimperator':
|
|
|
|
return buildHintsVimperator
|
|
|
|
case 'vimperator-reflow':
|
|
|
|
return buildHintsVimperator
|
2017-12-28 18:35:52 -05:00
|
|
|
}
|
2017-12-26 22:16:46 -05:00
|
|
|
}
|
|
|
|
|
2017-12-28 18:35:52 -05:00
|
|
|
function defaultHintFilter() {
|
2018-01-01 14:38:52 -05:00
|
|
|
switch (config.get('hintfiltermode')) {
|
|
|
|
case 'simple':
|
|
|
|
return filterHintsSimple
|
|
|
|
case 'vimperator':
|
|
|
|
return filterHintsVimperator
|
|
|
|
case 'vimperator-reflow':
|
|
|
|
return (fstr) => filterHintsVimperator(fstr, true)
|
2017-12-28 18:35:52 -05:00
|
|
|
}
|
2017-12-26 22:16:46 -05:00
|
|
|
}
|
|
|
|
|
2018-02-13 23:14:20 +00:00
|
|
|
/** An infinite stream of hints
|
|
|
|
|
|
|
|
Earlier hints prefix later hints
|
|
|
|
*/
|
|
|
|
function* hintnames_simple(hintchars = config.get("hintchars")): IterableIterator<string> {
|
|
|
|
for (let taglen = 1; true; taglen++) {
|
|
|
|
yield* map(
|
|
|
|
permutationsWithReplacement(hintchars, taglen),
|
|
|
|
e => e.join('')
|
|
|
|
)
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-13 23:14:20 +00:00
|
|
|
/** Shorter hints
|
|
|
|
|
|
|
|
Hints that are prefixes of other hints are a bit annoying because you have to select them with Enter or Space.
|
|
|
|
|
|
|
|
This function removes hints that prefix other hints by observing that:
|
|
|
|
let h = hintchars.length
|
|
|
|
if n < h ** 2
|
|
|
|
then n / h = number of single character hintnames that would prefix later hints
|
|
|
|
|
|
|
|
So it removes them. This function is not yet clever enough to realise that if n > h ** 2 it should remove
|
|
|
|
h + (n - h**2 - h) / h ** 2
|
|
|
|
and so on, but we hardly ever see that many hints, so whatever.
|
|
|
|
|
|
|
|
*/
|
|
|
|
function* hintnames(n: number, hintchars = config.get("hintchars")): IterableIterator<string> {
|
|
|
|
let source = hintnames_simple(hintchars)
|
|
|
|
const num2skip = Math.floor(n / hintchars.length)
|
|
|
|
yield* islice(source, num2skip, n + num2skip)
|
|
|
|
}
|
|
|
|
|
2017-11-22 03:43:31 +00:00
|
|
|
/** Uniform length hintnames */
|
2017-11-29 20:13:40 +00:00
|
|
|
function* hintnames_uniform(n: number, hintchars = config.get("hintchars")): IterableIterator<string> {
|
2017-11-22 03:43:31 +00:00
|
|
|
if (n <= hintchars.length)
|
|
|
|
yield* islice(hintchars[Symbol.iterator](), n)
|
|
|
|
else {
|
|
|
|
// else calculate required length of each tag
|
|
|
|
const taglen = Math.ceil(log(n, hintchars.length))
|
|
|
|
// And return first n permutations
|
|
|
|
yield* map(islice(permutationsWithReplacement(hintchars, taglen), n),
|
2017-11-29 20:13:40 +00:00
|
|
|
perm => {
|
|
|
|
return perm.join('')
|
|
|
|
})
|
2017-11-22 03:43:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-08 23:20:41 +00:00
|
|
|
type HintSelectedCallback = (Hint) => any
|
|
|
|
|
|
|
|
/** Place a flag by each hintworthy element */
|
|
|
|
class Hint {
|
2018-02-13 23:56:37 +00:00
|
|
|
public readonly flag = document.createElement('span')
|
2017-11-08 23:20:41 +00:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
private readonly target: Element,
|
|
|
|
public readonly name: string,
|
2017-12-26 22:16:46 -05:00
|
|
|
public readonly filterData: any,
|
2017-11-08 23:20:41 +00:00
|
|
|
private readonly onSelect: HintSelectedCallback
|
|
|
|
) {
|
|
|
|
const rect = target.getClientRects()[0]
|
|
|
|
this.flag.textContent = name
|
2017-11-09 00:41:07 +00:00
|
|
|
this.flag.className = 'TridactylHint'
|
|
|
|
/* this.flag.style.cssText = ` */
|
|
|
|
/* top: ${rect.top}px; */
|
|
|
|
/* left: ${rect.left}px; */
|
|
|
|
/* ` */
|
2017-11-08 23:20:41 +00:00
|
|
|
this.flag.style.cssText = `
|
2017-11-09 00:41:07 +00:00
|
|
|
top: ${window.scrollY + rect.top}px;
|
|
|
|
left: ${window.scrollX + rect.left}px;
|
2017-11-08 23:20:41 +00:00
|
|
|
`
|
|
|
|
modeState.hintHost.appendChild(this.flag)
|
2017-11-09 00:41:07 +00:00
|
|
|
target.classList.add('TridactylHintElem')
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// These styles would be better with pseudo selectors. Can we do custom ones?
|
|
|
|
// If not, do a state machine.
|
|
|
|
set hidden(hide: boolean) {
|
|
|
|
this.flag.hidden = hide
|
|
|
|
if (hide) {
|
|
|
|
this.focused = false
|
2017-11-09 00:41:07 +00:00
|
|
|
this.target.classList.remove('TridactylHintElem')
|
2017-11-08 23:20:41 +00:00
|
|
|
} else
|
2017-11-09 00:41:07 +00:00
|
|
|
this.target.classList.add('TridactylHintElem')
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
set focused(focus: boolean) {
|
|
|
|
if (focus) {
|
2017-11-09 00:41:07 +00:00
|
|
|
this.target.classList.add('TridactylHintActive')
|
|
|
|
this.target.classList.remove('TridactylHintElem')
|
2017-11-08 23:20:41 +00:00
|
|
|
} else {
|
2017-11-09 00:41:07 +00:00
|
|
|
this.target.classList.add('TridactylHintElem')
|
|
|
|
this.target.classList.remove('TridactylHintActive')
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
select() {
|
|
|
|
this.onSelect(this)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-26 22:16:46 -05:00
|
|
|
type HintBuilder = (els: Element[], onSelect: HintSelectedCallback) => void
|
|
|
|
|
|
|
|
function buildHintsSimple(els: Element[], onSelect: HintSelectedCallback) {
|
|
|
|
let names = hintnames(els.length)
|
|
|
|
for (let [el, name] of izip(els, names)) {
|
|
|
|
logger.debug({el, name})
|
|
|
|
modeState.hintchars += name
|
|
|
|
modeState.hints.push(new Hint(el, name, null, onSelect))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function buildHintsVimperator(els: Element[], onSelect: HintSelectedCallback) {
|
|
|
|
let names = hintnames(els.length)
|
2018-01-01 16:34:26 -05:00
|
|
|
// escape the hintchars string so that strange things don't happen
|
|
|
|
// when special characters are used as hintchars (for example, ']')
|
|
|
|
const escapedHintChars = config.get('hintchars').replace(/^\^|[-\\\]]/g, "\\$&")
|
|
|
|
const filterableTextFilter = new RegExp('[' + escapedHintChars + ']', 'gi')
|
2017-12-26 22:16:46 -05:00
|
|
|
for (let [el, name] of izip(els, names)) {
|
|
|
|
let ft = elementFilterableText(el)
|
2018-02-13 23:56:37 +00:00
|
|
|
// strip out hintchars
|
2017-12-28 14:47:39 -05:00
|
|
|
ft = ft.replace(filterableTextFilter, '')
|
2017-12-26 22:16:46 -05:00
|
|
|
logger.debug({el, name, ft})
|
|
|
|
modeState.hintchars += name + ft
|
|
|
|
modeState.hints.push(new Hint(el, name, ft, onSelect))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function elementFilterableText(el: Element): string {
|
2017-12-28 14:47:39 -05:00
|
|
|
const nodename = el.nodeName.toLowerCase()
|
2018-02-13 23:28:04 +00:00
|
|
|
let text: string
|
2017-12-26 22:16:46 -05:00
|
|
|
if (nodename == 'input') {
|
2018-02-13 23:28:04 +00:00
|
|
|
text = (<HTMLInputElement>el).value
|
2017-12-26 22:16:46 -05:00
|
|
|
} else if (0 < el.textContent.length) {
|
2018-02-13 23:28:04 +00:00
|
|
|
text = el.textContent
|
2017-12-26 22:16:46 -05:00
|
|
|
} else if (el.hasAttribute('title')) {
|
2018-02-13 23:28:04 +00:00
|
|
|
text = el.getAttribute('title')
|
2017-12-26 22:16:46 -05:00
|
|
|
} else {
|
2018-02-13 23:28:04 +00:00
|
|
|
text = el.innerHTML
|
2017-12-26 22:16:46 -05:00
|
|
|
}
|
2018-02-13 23:28:04 +00:00
|
|
|
// Truncate very long text values
|
|
|
|
return text.slice(0,2048).toLowerCase() || ''
|
2017-12-26 22:16:46 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type HintFilter = (string) => void
|
|
|
|
|
|
|
|
/** Show only hints prefixed by fstr. Focus first match */
|
|
|
|
function filterHintsSimple(fstr) {
|
2017-11-08 23:20:41 +00:00
|
|
|
const active: Hint[] = []
|
|
|
|
let foundMatch
|
|
|
|
for (let h of modeState.hints) {
|
|
|
|
if (!h.name.startsWith(fstr)) h.hidden = true
|
|
|
|
else {
|
|
|
|
if (! foundMatch) {
|
|
|
|
h.focused = true
|
|
|
|
modeState.focusedHint = h
|
|
|
|
foundMatch = true
|
|
|
|
}
|
2017-11-09 00:41:07 +00:00
|
|
|
h.hidden = false
|
2017-11-08 23:20:41 +00:00
|
|
|
active.push(h)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2017-11-09 00:41:07 +00:00
|
|
|
if (active.length == 1) {
|
|
|
|
selectFocusedHint()
|
|
|
|
}
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
|
2018-02-13 23:56:37 +00:00
|
|
|
/** Partition the filter string into hintchars and content filter strings.
|
|
|
|
Apply each part in sequence, reducing the list of active hints.
|
|
|
|
|
|
|
|
Update display after all filtering, adjusting labels if appropriate.
|
|
|
|
|
|
|
|
Consider: This is a poster child for separating data and display. If they
|
|
|
|
weren't so tied here we could do a neat dynamic programming thing and just
|
|
|
|
throw the data at a reactalike.
|
|
|
|
*/
|
2017-12-28 17:02:01 -05:00
|
|
|
function filterHintsVimperator(fstr, reflow=false) {
|
2018-02-13 23:56:37 +00:00
|
|
|
|
|
|
|
/** Partition a fstr into a tagged array of substrings */
|
|
|
|
function partitionFstr(fstr) {
|
|
|
|
const peek = (a) => a[a.length - 1]
|
|
|
|
const hintChars = config.get('hintchars')
|
|
|
|
|
|
|
|
// For each char, either add it to the existing run if there is one and
|
|
|
|
// it's a matching type or start a new run
|
|
|
|
const runs = []
|
|
|
|
for (const char of fstr) {
|
|
|
|
const isHintChar = hintChars.includes(char)
|
|
|
|
if (! peek(runs) || peek(runs).isHintChar !== isHintChar) {
|
|
|
|
runs.push({str: char, isHintChar})
|
|
|
|
} else {
|
|
|
|
peek(runs).str += char
|
2017-12-28 14:47:39 -05:00
|
|
|
}
|
|
|
|
}
|
2018-02-13 23:56:37 +00:00
|
|
|
return runs
|
2017-12-28 14:47:39 -05:00
|
|
|
}
|
|
|
|
|
2018-02-13 23:56:37 +00:00
|
|
|
function rename(hints) {
|
|
|
|
const names = hintnames(hints.length)
|
|
|
|
for (const [hint, name] of izip(hints, names)) {
|
|
|
|
hint.name = name
|
|
|
|
}
|
2017-12-28 14:47:39 -05:00
|
|
|
}
|
|
|
|
|
2018-02-13 23:56:37 +00:00
|
|
|
// Start with all hints
|
|
|
|
let active = modeState.hints
|
|
|
|
|
|
|
|
// If we're reflowing, the names may be wrong at this point, so apply the original names.
|
|
|
|
if (reflow) rename(active)
|
|
|
|
|
|
|
|
// Filter down (renaming as required)
|
|
|
|
for (const run of partitionFstr(fstr)) {
|
|
|
|
if (run.isHintChar) {
|
|
|
|
// Filter by label
|
|
|
|
active = active.filter(hint => hint.name.startsWith(run.str))
|
|
|
|
} else {
|
|
|
|
// By text
|
|
|
|
active = active.filter(hint => hint.filterData.includes(run.str))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reflow && ! run.isHintChars) {
|
|
|
|
rename(active)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update display
|
|
|
|
// Hide all hints
|
|
|
|
for (const hint of modeState.hints) {
|
|
|
|
// Warning: this could cause flickering.
|
|
|
|
hint.hidden = true
|
|
|
|
}
|
|
|
|
// Show and update labels of active
|
|
|
|
for (const hint of active) {
|
|
|
|
hint.hidden = false
|
|
|
|
hint.flag.textContent = hint.name
|
|
|
|
}
|
|
|
|
// Focus first hint
|
|
|
|
if (active.length) {
|
2017-12-28 14:47:39 -05:00
|
|
|
if (modeState.focusedHint) {
|
|
|
|
modeState.focusedHint.focused = false
|
|
|
|
}
|
2018-02-13 23:56:37 +00:00
|
|
|
active[0].focused = true
|
|
|
|
modeState.focusedHint = active[0]
|
2017-12-28 14:47:39 -05:00
|
|
|
}
|
|
|
|
|
2018-02-13 23:56:37 +00:00
|
|
|
// Select focused hint if it's the only match
|
2017-12-28 14:47:39 -05:00
|
|
|
if (active.length == 1) {
|
|
|
|
selectFocusedHint()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-08 23:20:41 +00:00
|
|
|
/** Remove all hints, reset STATE. */
|
|
|
|
function reset() {
|
|
|
|
modeState.destructor()
|
|
|
|
modeState = undefined
|
2017-11-19 07:57:30 +00:00
|
|
|
state.mode = 'normal'
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** If key is in hintchars, add it to filtstr and filter */
|
|
|
|
function pushKey(ke) {
|
|
|
|
if (hasModifiers(ke)) {
|
|
|
|
return
|
|
|
|
} else if (ke.key === 'Backspace') {
|
|
|
|
modeState.filter = modeState.filter.slice(0,-1)
|
2018-02-13 23:56:37 +00:00
|
|
|
modeState.filterFunc(modeState.filter)
|
2017-11-08 23:20:41 +00:00
|
|
|
} else if (ke.key.length > 1) {
|
|
|
|
return
|
2017-11-09 00:41:07 +00:00
|
|
|
} else if (modeState.hintchars.includes(ke.key)) {
|
2017-11-08 23:20:41 +00:00
|
|
|
modeState.filter += ke.key
|
2018-02-13 23:56:37 +00:00
|
|
|
modeState.filterFunc(modeState.filter)
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Array of hintable elements in viewport
|
|
|
|
|
|
|
|
Elements are hintable if
|
|
|
|
1. they can be meaningfully selected, clicked, etc
|
|
|
|
2. they're visible
|
|
|
|
1. Within viewport
|
|
|
|
2. Not hidden by another element
|
|
|
|
*/
|
2017-11-29 13:23:30 +00:00
|
|
|
function hintables(selectors=HINTTAGS_selectors) {
|
|
|
|
return DOM.getElemsBySelector(selectors, [DOM.isVisible])
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
|
2017-11-22 20:47:35 +00:00
|
|
|
function elementswithtext() {
|
2017-11-28 23:20:42 +00:00
|
|
|
|
2017-11-29 12:09:37 +00:00
|
|
|
return DOM.getElemsBySelector("*",
|
2017-11-28 23:20:42 +00:00
|
|
|
[DOM.isVisible, hint => {
|
|
|
|
return hint.textContent != ""
|
|
|
|
}]
|
|
|
|
)
|
2017-11-22 20:47:35 +00:00
|
|
|
}
|
|
|
|
|
2017-11-27 14:43:01 +00:00
|
|
|
/** Returns elements that point to a saveable resource
|
|
|
|
*/
|
|
|
|
function saveableElements() {
|
|
|
|
return DOM.getElemsBySelector(HINTTAGS_saveable, [DOM.isVisible])
|
|
|
|
}
|
|
|
|
|
2017-11-22 20:38:02 +00:00
|
|
|
/** Get array of images in the viewport
|
|
|
|
*/
|
|
|
|
function hintableImages() {
|
2017-11-28 23:20:42 +00:00
|
|
|
return DOM.getElemsBySelector(HINTTAGS_img_selectors, [DOM.isVisible])
|
2017-11-22 20:38:02 +00:00
|
|
|
}
|
|
|
|
|
2017-11-28 22:51:53 +00:00
|
|
|
/** Get arrat of "anchors": elements which have id or name and can be addressed
|
|
|
|
* with the hash/fragment in the URL
|
|
|
|
*/
|
|
|
|
function anchors() {
|
2017-11-28 23:20:42 +00:00
|
|
|
return DOM.getElemsBySelector(HINTTAGS_anchor_selectors, [DOM.isVisible])
|
2017-11-28 22:51:53 +00:00
|
|
|
}
|
|
|
|
|
2017-11-28 22:51:53 +00:00
|
|
|
/** Array of items that can be killed with hint kill
|
|
|
|
*/
|
|
|
|
function killables() {
|
|
|
|
return DOM.getElemsBySelector(HINTTAGS_killable_selectors, [DOM.isVisible])
|
|
|
|
}
|
|
|
|
|
2017-11-12 01:14:45 +00:00
|
|
|
// CSS selectors. More readable for web developers. Not dead. Leaves browser to care about XML.
|
|
|
|
const HINTTAGS_selectors = `
|
|
|
|
input:not([type=hidden]):not([disabled]),
|
|
|
|
a,
|
|
|
|
area,
|
|
|
|
iframe,
|
|
|
|
textarea,
|
|
|
|
button,
|
|
|
|
select,
|
2017-12-04 02:20:13 +00:00
|
|
|
summary,
|
2017-11-12 01:14:45 +00:00
|
|
|
[onclick],
|
|
|
|
[onmouseover],
|
|
|
|
[onmousedown],
|
|
|
|
[onmouseup],
|
|
|
|
[oncommand],
|
|
|
|
[role='link'],
|
|
|
|
[role='button'],
|
|
|
|
[role='checkbox'],
|
|
|
|
[role='combobox'],
|
|
|
|
[role='listbox'],
|
|
|
|
[role='listitem'],
|
|
|
|
[role='menuitem'],
|
|
|
|
[role='menuitemcheckbox'],
|
|
|
|
[role='menuitemradio'],
|
|
|
|
[role='option'],
|
|
|
|
[role='radio'],
|
|
|
|
[role='scrollbar'],
|
|
|
|
[role='slider'],
|
|
|
|
[role='spinbutton'],
|
|
|
|
[role='tab'],
|
|
|
|
[role='textbox'],
|
|
|
|
[role='treeitem'],
|
2017-11-22 21:14:43 +00:00
|
|
|
[class*='button'],
|
2017-11-12 01:14:45 +00:00
|
|
|
[tabindex]
|
|
|
|
`
|
|
|
|
|
2017-11-22 20:38:02 +00:00
|
|
|
const HINTTAGS_img_selectors = `
|
|
|
|
img,
|
|
|
|
[src]
|
|
|
|
`
|
|
|
|
|
2017-11-28 22:51:53 +00:00
|
|
|
const HINTTAGS_anchor_selectors = `
|
|
|
|
[id],
|
|
|
|
[name]
|
|
|
|
`
|
|
|
|
|
2017-11-28 22:51:53 +00:00
|
|
|
const HINTTAGS_killable_selectors = `
|
|
|
|
span,
|
|
|
|
div,
|
|
|
|
iframe,
|
|
|
|
img,
|
|
|
|
button,
|
|
|
|
article,
|
|
|
|
summary
|
|
|
|
`
|
|
|
|
|
2017-11-27 14:43:01 +00:00
|
|
|
/** CSS selector for elements which point to a saveable resource
|
|
|
|
*/
|
|
|
|
const HINTTAGS_saveable = `
|
|
|
|
[href]:not([href='#'])
|
|
|
|
`
|
|
|
|
|
2017-11-19 06:44:55 +00:00
|
|
|
import {activeTab, browserBg, l, firefoxVersionAtLeast} from './lib/webext'
|
2017-11-18 01:51:46 +00:00
|
|
|
|
2017-11-19 01:57:50 +00:00
|
|
|
async function openInBackground(url: string) {
|
|
|
|
const thisTab = await activeTab()
|
|
|
|
const options: any = {
|
2017-11-18 02:52:33 +00:00
|
|
|
active: false,
|
|
|
|
url,
|
2017-11-19 01:57:50 +00:00
|
|
|
index: thisTab.index + 1,
|
|
|
|
}
|
|
|
|
if (await l(firefoxVersionAtLeast(57))) options.openerTabId = thisTab.id
|
|
|
|
return browserBg.tabs.create(options)
|
2017-11-18 02:52:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** if `target === _blank` clicking the link is treated as opening a popup and is blocked. Use webext API to avoid that. */
|
|
|
|
function simulateClick(target: HTMLElement) {
|
|
|
|
// target can be set to other stuff, and we'll fail in annoying ways.
|
|
|
|
// There's no easy way around that while this code executes outside of the
|
|
|
|
// magic 'short lived event handler' context.
|
|
|
|
//
|
|
|
|
// OTOH, hardly anyone uses that functionality any more.
|
|
|
|
if ((target as HTMLAnchorElement).target === '_blank' ||
|
|
|
|
(target as HTMLAnchorElement).target === '_new'
|
|
|
|
) {
|
|
|
|
browserBg.tabs.create({url: (target as HTMLAnchorElement).href})
|
|
|
|
} else {
|
2017-11-28 23:20:42 +00:00
|
|
|
DOM.mouseEvent(target, "click")
|
2017-11-18 02:52:33 +00:00
|
|
|
// Sometimes clicking the element doesn't focus it sufficiently.
|
|
|
|
target.focus()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-18 01:51:46 +00:00
|
|
|
function hintPageOpenInBackground() {
|
|
|
|
hintPage(hintables(), hint=>{
|
|
|
|
hint.target.focus()
|
2017-11-18 02:52:33 +00:00
|
|
|
if (hint.target.href) {
|
|
|
|
// Try to open with the webext API. If that fails, simulate a click on this page anyway.
|
|
|
|
openInBackground(hint.target.href).catch(()=>simulateClick(hint.target))
|
2017-11-18 01:51:46 +00:00
|
|
|
} else {
|
|
|
|
// This is to mirror vimperator behaviour.
|
2017-11-18 02:52:33 +00:00
|
|
|
simulateClick(hint.target)
|
2017-11-18 01:51:46 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2017-11-08 23:20:41 +00:00
|
|
|
|
2017-11-29 13:23:30 +00:00
|
|
|
function hintPageSimple(selectors=HINTTAGS_selectors) {
|
|
|
|
hintPage(hintables(selectors), hint=>{
|
2017-11-18 02:52:33 +00:00
|
|
|
simulateClick(hint.target)
|
2017-11-09 21:30:28 +00:00
|
|
|
})
|
2017-11-09 00:41:07 +00:00
|
|
|
}
|
|
|
|
|
2017-11-22 20:47:35 +00:00
|
|
|
function hintPageTextYank() {
|
|
|
|
hintPage(elementswithtext(), hint=>{
|
|
|
|
messageActiveTab("commandline_frame", "setClipboard", [hint.target.textContent])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-22 19:09:52 +00:00
|
|
|
function hintPageYank() {
|
|
|
|
hintPage(hintables(), hint=>{
|
|
|
|
messageActiveTab("commandline_frame", "setClipboard", [hint.target.href])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-28 22:51:53 +00:00
|
|
|
/** Hint anchors and yank the URL on selection
|
|
|
|
*/
|
|
|
|
function hintPageAnchorYank() {
|
|
|
|
|
|
|
|
hintPage(anchors(), hint=>{
|
|
|
|
|
|
|
|
let anchorUrl = new URL(window.location.href)
|
|
|
|
|
|
|
|
anchorUrl.hash = hint.target.id || hint.target.name;
|
|
|
|
|
|
|
|
messageActiveTab("commandline_frame", "setClipboard", [anchorUrl.href])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-22 20:38:02 +00:00
|
|
|
/** Hint images, opening in the same tab, or in a background tab
|
|
|
|
*
|
|
|
|
* @param inBackground opens the image source URL in a background tab,
|
|
|
|
* as opposed to the current tab
|
|
|
|
*/
|
|
|
|
function hintImage(inBackground) {
|
|
|
|
hintPage(hintableImages(), hint=>{
|
|
|
|
let img_src = hint.target.getAttribute("src")
|
|
|
|
|
|
|
|
if (inBackground) {
|
|
|
|
openInBackground(new URL(img_src, window.location.href).href)
|
|
|
|
} else {
|
|
|
|
window.location.href = img_src
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-24 13:01:44 +00:00
|
|
|
/** Hint elements to focus */
|
|
|
|
function hintFocus() {
|
|
|
|
hintPage(hintables(), hint=>{
|
|
|
|
hint.target.focus()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-30 04:11:49 +00:00
|
|
|
/** Hint items and read out the content of the selection */
|
|
|
|
function hintRead() {
|
|
|
|
hintPage(elementswithtext(), hint=>{
|
|
|
|
TTS.readText(hint.target.textContent)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-28 22:51:53 +00:00
|
|
|
/** Hint elements and delete the selection from the page
|
|
|
|
*/
|
|
|
|
function hintKill() {
|
|
|
|
hintPage(killables(), hint=>{
|
|
|
|
hint.target.remove();
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-27 14:43:01 +00:00
|
|
|
/** Hint link elements to save
|
|
|
|
*
|
|
|
|
* @param hintType the type of elements to hint and save:
|
|
|
|
* - "link": elements that point to another resource (eg
|
|
|
|
* links to pages/files) - the link targer is saved
|
|
|
|
* - "img": image elements
|
|
|
|
* @param saveAs prompt for save location
|
|
|
|
*/
|
|
|
|
function hintSave(hintType: HintSaveType, saveAs: boolean) {
|
|
|
|
|
|
|
|
function saveHintElems(hintType) {
|
|
|
|
return (hintType === "link") ? saveableElements() : hintableImages()
|
|
|
|
}
|
|
|
|
|
|
|
|
function urlFromElem(hintType, elem) {
|
|
|
|
return (hintType === "link") ? elem.href : elem.src
|
|
|
|
}
|
|
|
|
|
|
|
|
hintPage(saveHintElems(hintType), hint=>{
|
|
|
|
|
|
|
|
const urlToSave = new URL(urlFromElem(hintType, hint.target),
|
|
|
|
window.location.href)
|
|
|
|
|
|
|
|
// Pass to background context to allow saving from data URLs.
|
|
|
|
// Convert to href because can't clone URL across contexts
|
|
|
|
message('download_background', "downloadUrl",
|
|
|
|
[urlToSave.href, saveAs])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-08 23:20:41 +00:00
|
|
|
function selectFocusedHint() {
|
2017-12-30 00:46:26 +00:00
|
|
|
logger.debug("Selecting hint.", state.mode)
|
2017-11-19 16:48:21 +00:00
|
|
|
const focused = modeState.focusedHint
|
2017-11-09 00:41:07 +00:00
|
|
|
reset()
|
2017-11-19 16:48:21 +00:00
|
|
|
focused.select()
|
2017-11-08 23:20:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
import {addListener, attributeCaller} from './messaging'
|
2017-11-09 00:41:07 +00:00
|
|
|
addListener('hinting_content', attributeCaller({
|
|
|
|
pushKey,
|
|
|
|
selectFocusedHint,
|
|
|
|
reset,
|
|
|
|
hintPageSimple,
|
2017-11-22 19:09:52 +00:00
|
|
|
hintPageYank,
|
2017-11-22 20:47:35 +00:00
|
|
|
hintPageTextYank,
|
2017-11-28 22:51:53 +00:00
|
|
|
hintPageAnchorYank,
|
2017-11-18 01:51:46 +00:00
|
|
|
hintPageOpenInBackground,
|
2017-11-22 20:38:02 +00:00
|
|
|
hintImage,
|
2017-11-24 13:01:44 +00:00
|
|
|
hintFocus,
|
2017-11-30 04:11:49 +00:00
|
|
|
hintRead,
|
2017-11-28 22:51:53 +00:00
|
|
|
hintKill,
|
2017-11-27 14:43:01 +00:00
|
|
|
hintSave,
|
2017-11-09 00:41:07 +00:00
|
|
|
}))
|