Merge branch 'port_hints_to_new_fangled_way'

This commit is contained in:
Oliver Blanthorn 2018-08-18 18:28:47 +01:00
commit 4f1d96767b
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
3 changed files with 314 additions and 275 deletions

View file

@ -147,6 +147,8 @@ const DEFAULTS = o({
F: "hint -b",
gF: "hint -br",
";i": "hint -i",
";b": "hint -b",
";o": "hint",
";I": "hint -I",
";k": "hint -k",
";y": "hint -y",
@ -161,6 +163,25 @@ const DEFAULTS = o({
";#": "hint -#",
";v": "hint -W exclaim_quiet mpv",
";w": "hint -w",
";O": "hint -W fillcmdline_notrail open ",
";W": "hint -W fillcmdline_notrail winopen ",
";T": "hint -W fillcmdline_notrail tabopen ",
"g;i": "hint -qi",
"g;I": "hint -qI",
"g;k": "hint -qk",
"g;y": "hint -qy",
"g;p": "hint -qp",
"g;P": "hint -qP",
"g;r": "hint -qr",
"g;s": "hint -qs",
"g;S": "hint -qS",
"g;a": "hint -qa",
"g;A": "hint -qA",
"g;;": "hint -q;",
"g;#": "hint -q#",
"g;v": "hint -qW exclaim_quiet mpv",
"g;w": "hint -qw",
"g;b": "hint -qb",
"<S-Insert>": "mode ignore",
"<CA-Esc>": "mode ignore",
"<CA-`>": "mode ignore",

View file

@ -96,7 +96,7 @@
// Shared
import * as Messaging from "./messaging"
import { browserBg, activeTabId, activeTabContainerId, openInNewTab } from "./lib/webext"
import { browserBg, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow } from "./lib/webext"
import * as Container from "./lib/containers"
import state from "./state"
import * as UrlUtil from "./url_util"
@ -2893,7 +2893,6 @@ import * as hinting from "./hinting"
@param option
- -b open in background
- -br repeatedly open in background
- -y copy (yank) link's target to clipboard
- -p copy an element's text to the clipboard
- -P copy an element's title/alt text to the clipboard
@ -2910,14 +2909,13 @@ import * as hinting from "./hinting"
- -c [selector] hint links that match the css selector
- `bind ;c hint -c [class*="expand"],[class="togg"]` works particularly well on reddit and HN
- -w open in new window
-wp open in new private window
- `-pipe selector key` e.g, `-pipe * href` returns the key. Only makes sense with `composite`, e.g, `composite hint -pipe * textContent | yank`.
- **DEPRECATED** `-W excmd...` append hint href to excmd and execute, e.g, `hint -W exclaim mpv` to open YouTube videos. Use `composite hint -pipe | [excmd]` instead.
- -wp open in new private window
- `-pipe selector key` e.g, `-pipe * href` returns the key. Only makes sense with `composite`, e.g, `composite hint -pipe * textContent | yank`. If you don't select a hint (i.e. press <Esc>), will return an empty string.
- `-W excmd...` append hint href to excmd and execute, e.g, `hint -W exclaim mpv` to open YouTube videos.
- -q* quick (or rapid) hints mode. Stay in hint mode until you press <Esc>, e.g. `:hint -qb` to open multiple hints in the background or `:hint -qW excmd` to execute excmd once for each hint. This will return an array containing all elements or the result of executed functions (e.g. `hint -qpipe a href` will return an array of links).
- -br deprecated, use `-qb` instead
Excepting the custom selector mode and background hint mode, each of these
hint modes is available by default as `;<option character>`, so e.g. `;y`
to yank a link's target.
Excepting the custom selector mode and background hint mode, each of these hint modes is available by default as `;<option character>`, so e.g. `;y` to yank a link's target; `g;<option character>` starts rapid hint mode for all modes where it makes sense, and some others.
To open a hint in the background, the default bind is `F`.
@ -2944,150 +2942,257 @@ import * as hinting from "./hinting"
*/
//#content
export async function hint(option?: string, selectors?: string, ...rest: string[]) {
// NB: if you want something to work with rapid hinting, make it return a tuple of [something, hintCount] see option === "-b" below.
if (!option) option = ""
if (option == "-br") option = "-qb"
let rapid = false
if (option.startsWith("-q")) {
option = "-" + option.slice(2)
rapid = true
}
let selectHints = new Promise(r => r())
let onSelected = a => a
let hintTabOpen = async (href, active = !rapid) => {
let containerId = await activeTabContainerId()
if (containerId) {
return await openInNewTab(href, {
active,
related: true,
cookieStoreId: containerId,
})
} else {
return await openInNewTab(href, {
active,
related: true,
})
}
}
// Open in background
if (option === "-b") {
selectHints = hinting.pipe(DOM.HINTTAGS_selectors)
onSelected = async result => {
let [link, hintCount] = result as [HTMLAnchorElement, number]
link.focus()
if (link.href) {
let containerId = await activeTabContainerId()
if (containerId) {
openInNewTab(link.href, {
active: false,
related: true,
cookieStoreId: containerId,
}).catch(() => DOM.simulateClick(link))
} else {
openInNewTab(link.href, {
active: false,
related: true,
}).catch(() => DOM.simulateClick(link))
}
switch (option) {
case "-b":
// Open in background
selectHints = hinting.pipe(
DOM.HINTTAGS_selectors,
async link => {
link.focus()
if (link.href) {
hintTabOpen(link.href, false).catch(() => DOM.simulateClick(link))
} else {
DOM.simulateClick(link)
}
return link
},
rapid,
)
break
case "-y":
// Yank link
selectHints = hinting.pipe(
DOM.HINTTAGS_selectors,
elem => {
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + elem["href"])
return elem
},
rapid,
)
break
case "-p":
// Yank text content
selectHints = hinting.pipe_elements(
DOM.elementsWithText(),
elem => {
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + elem["textContent"])
return elem
},
rapid,
)
break
case "-P":
// Yank link alt text
// ???: Neither anchors nor links posses an "alt" attribute. I'm assuming that the person who wrote this code also wanted to select the alt text of images
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link
selectHints = hinting.pipe_elements(
DOM.getElemsBySelector("[title], [alt]", [DOM.isVisible]),
link => {
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + (link.title ? link.title : link.alt))
return link
},
rapid,
)
break
case "-#":
// Yank anchor
selectHints = hinting.pipe_elements(
DOM.anchors(),
link => {
let anchorUrl = new URL(window.location.href)
// ???: What purpose does selecting elements with a name attribute have? Selecting values that only have meaning in forms doesn't seem very useful.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
anchorUrl.hash = link.id || link.name
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + anchorUrl.href)
return link
},
rapid,
)
break
case "-c":
selectHints = hinting.pipe(
selectors,
elem => {
DOM.simulateClick(elem as HTMLElement)
return elem
},
rapid,
)
break
case "-W":
selectHints = hinting.pipe(
DOM.HINTTAGS_selectors,
elem => {
// /!\ RACY RACY RACY!
run_exstr(selectors + " " + rest.join(" ") + " " + elem)
return elem
},
rapid,
)
break
case "-pipe":
selectHints = hinting.pipe(selectors, elem => elem[rest.join(" ")], rapid)
break
case "-i":
selectHints = hinting.pipe_elements(
hinting.hintableImages(),
elem => {
open(new URL(elem.getAttribute("src"), window.location.href).href)
return elem
},
rapid,
)
break
case "-I":
selectHints = hinting.pipe_elements(
hinting.hintableImages(),
async elem => {
await hintTabOpen(new URL(elem.getAttribute("src"), window.location.href).href)
return elem
},
rapid,
)
break
case "-k":
selectHints = hinting.pipe_elements(
hinting.killables(),
elem => {
elem.remove()
return elem
},
rapid,
)
break
case "-s":
case "-a":
case "-S":
case "-A":
let elems = []
// s: don't ask the user where to save the file
// a: ask the user where to save the file
let saveAs = true
if (option[1].toLowerCase() == "s") saveAs = false
// Lowercase: anchors
// Uppercase: images
let attr = "href"
if (option[1].toLowerCase() == option[1]) {
attr = "href"
elems = hinting.saveableElements()
} else {
DOM.simulateClick(link)
attr = "src"
elems = hinting.hintableImages()
}
return [link.href, hintCount]
}
selectHints = hinting.pipe_elements(
elems,
elem => {
Messaging.message("download_background", "downloadUrl", [new URL(elem[attr], window.location.href).href, saveAs])
return elem
},
rapid,
)
break
case "-;":
selectHints = hinting.pipe_elements(
hinting.hintables(selectors),
elem => {
elem.focus()
return elem
},
rapid,
)
break
case "-r":
selectHints = hinting.pipe_elements(
DOM.elementsWithText(),
elem => {
TTS.readText(elem.textContent)
return elem
},
rapid,
)
break
case "-w":
selectHints = hinting.pipe_elements(
hinting.hintables(),
elem => {
elem.focus()
if (elem.href) openInNewWindow({ url: new URL(elem.href, window.location.href).href })
else DOM.simulateClick(elem)
return elem
},
rapid,
)
break
case "-wp":
selectHints = hinting.pipe_elements(
hinting.hintables(),
elem => {
elem.focus()
if (elem.href) return openInNewWindow({ url: elem.href, incognito: true })
},
rapid,
)
break
default:
selectHints = hinting.pipe(
DOM.HINTTAGS_selectors,
elem => {
DOM.simulateClick(elem as HTMLElement)
return elem
},
rapid,
)
}
// Yank link
else if (option === "-y") {
selectHints = hinting.pipe(DOM.HINTTAGS_selectors)
onSelected = result => {
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + result[0]["href"])
return result
}
}
// Yank text content
else if (option === "-p") {
selectHints = hinting.pipe_elements(DOM.elementsWithText())
onSelected = result => {
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + result["textContent"])
return result
}
}
// Yank link alt text
// ???: Neither anchors nor links posses an "alt" attribute. I'm assuming that the person who wrote this code also wanted to select the alt text of images
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link
else if (option === "-P") {
selectHints = hinting.pipe_elements(DOM.getElemsBySelector("[title], [alt]", [DOM.isVisible]))
onSelected = result => {
let link = result[0] as HTMLAnchorElement & HTMLImageElement
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + (link.title ? link.title : link.alt))
return result
}
}
// Yank anchor
else if (option === "-#") {
selectHints = hinting.pipe_elements(DOM.anchors())
onSelected = result => {
let anchorUrl = new URL(window.location.href)
let link = result[0] as any
// ???: What purpose does selecting elements with a name attribute have? Selecting values that only have meaning in forms doesn't seem very useful.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
anchorUrl.hash = link.id || link.name
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + anchorUrl.href)
return result
}
} else if (option === "-c") {
selectHints = hinting.pipe(selectors)
onSelected = result => {
DOM.simulateClick(result[0] as HTMLElement)
return result
}
}
// Deprecated: hint exstr
else if (option === "-W") {
selectHints = hinting.pipe(DOM.HINTTAGS_selectors)
onSelected = result => {
// /!\ RACY RACY RACY!
run_exstr(selectors + " " + rest.join(" ") + " " + result[0])
return result
}
} else if (option === "-pipe") {
selectHints = hinting.pipe(selectors)
onSelected = result => result[0][rest.join(" ")]
} else if (option === "-br") {
while (true) {
// The typecast can be removed once the function is completely ported
let result = (await hint("-b")) as [HTMLElement, number]
if (result === null) return null
let [_, hintCount] = result
if (hintCount < 2) break
}
}
// TODO: port these to new fangled way
else if (option === "-i") hinting.hintImage(false)
else if (option === "-I") hinting.hintImage(true)
else if (option === "-k") hinting.hintKill()
else if (option === "-s") hinting.hintSave("link", false)
else if (option === "-S") hinting.hintSave("img", false)
else if (option === "-a") hinting.hintSave("link", true)
else if (option === "-A") hinting.hintSave("img", true)
else if (option === "-;") hinting.hintFocus(selectors)
else if (option === "-r") hinting.hintRead()
else if (option === "-w") hinting.hintPageWindow()
else if (option === "-wp") hinting.hintPageWindowPrivate()
else {
selectHints = hinting.pipe(DOM.HINTTAGS_selectors)
onSelected = result => {
DOM.simulateClick(result[0] as HTMLElement)
return result
}
}
return new Promise((resolve, reject) =>
selectHints.then(
async result => resolve(await onSelected(result)),
rejectionReason => {
// We have to resolve when we don't want to have our messages be logged in the command line but this feels wrong since no hint has been selected
// Perhaps we should implement a mechanism to allow specific errors to go unreported?
if (rejectionReason == hinting.HintRejectionReason.User) {
logger.debug("Hint promise rejected because user left hint mode without selecting a hint")
resolve(null)
} else if (rejectionReason == hinting.HintRejectionReason.NoHints) {
logger.debug("Hint promise rejected because there are no hints to select")
resolve(null)
} else {
reject(rejectionReason)
}
},
),
)
return selectHints
}
// how 2 crash pc

View file

@ -28,31 +28,25 @@ import Logger from "./logging"
import * as Messaging from "./messaging"
const logger = new Logger("hinting")
export enum HintRejectionReason {
Err,
User,
NoHints,
}
/** Simple container for the state of a single frame's hints. */
class HintState {
public focusedHint: Hint
readonly hintHost = document.createElement("div")
readonly hints: Hint[] = []
public selectedHints: Hint[] = []
public filter = ""
public hintchars = ""
constructor(
public filterFunc: HintFilter,
public resolve: (Hint) => void,
public reject: (HintRejectionReason) => any,
public reject: (any) => void,
public rapid: boolean,
) {
this.hintHost.classList.add("TridactylHintHost", "cleanslate")
}
destructor(abort) {
if (!this.focusedHint) this.focusedHint = this.hints[0]
destructor() {
// Undo any alterations of the hinted elements
for (const hint of this.hints) {
hint.hidden = true
@ -61,9 +55,10 @@ class HintState {
// Remove all hints from the DOM.
this.hintHost.remove()
if (abort) this.reject(HintRejectionReason.User)
else if (!this.focusedHint) this.reject(HintRejectionReason.NoHints)
else this.resolve([this.focusedHint.target, this.hints.length])
if (this.rapid)
this.resolve(this.selectedHints.map(h => h.result))
else
this.resolve(this.selectedHints[0] ? this.selectedHints[0].result : "")
}
}
@ -75,13 +70,25 @@ export function hintPage(
onSelect: HintSelectedCallback,
resolve = () => {},
reject = () => {},
rapid = false,
) {
let buildHints: HintBuilder = defaultHintBuilder()
let filterHints: HintFilter = defaultHintFilter()
state.mode = "hint"
modeState = new HintState(filterHints, resolve, reject)
modeState = new HintState(filterHints, resolve, reject, rapid)
buildHints(hintableElements, onSelect)
if (rapid == false) {
buildHints(hintableElements, hint => {
hint.result = onSelect(hint.target)
modeState.selectedHints.push(hint)
reset()
})
} else {
buildHints(hintableElements, hint => {
hint.result = onSelect(hint.target)
modeState.selectedHints.push(hint)
})
}
if (modeState.hints.length) {
let firstTarget = modeState.hints[0].target
@ -227,6 +234,7 @@ type HintSelectedCallback = (Hint) => any
/** Place a flag by each hintworthy element */
class Hint {
public readonly flag = document.createElement("span")
public result: any = null
constructor(
public readonly target: Element,
@ -444,8 +452,10 @@ function filterHintsVimperator(fstr, reflow = false) {
* If abort is true, we're resetting because the user pressed escape.
* If it is false, we're resetting because the user selected a hint.
**/
function reset(abort = false) {
modeState.destructor(abort)
function reset() {
if (modeState) {
modeState.destructor()
}
modeState = undefined
state.mode = "normal"
}
@ -473,7 +483,7 @@ function pushKey(ke) {
1. Within viewport
2. Not hidden by another element
*/
function hintables(selectors = DOM.HINTTAGS_selectors, withjs = false) {
export function hintables(selectors = DOM.HINTTAGS_selectors, withjs = false) {
let elems = DOM.getElemsBySelector(selectors, [])
if (withjs) {
elems = elems.concat(DOM.hintworthy_js_elems)
@ -484,19 +494,19 @@ function hintables(selectors = DOM.HINTTAGS_selectors, withjs = false) {
/** Returns elements that point to a saveable resource
*/
function saveableElements() {
export function saveableElements() {
return DOM.getElemsBySelector(DOM.HINTTAGS_saveable, [DOM.isVisible])
}
/** Get array of images in the viewport
*/
function hintableImages() {
export function hintableImages() {
return DOM.getElemsBySelector(DOM.HINTTAGS_img_selectors, [DOM.isVisible])
}
/** Array of items that can be killed with hint kill
*/
function killables() {
export function killables() {
return DOM.getElemsBySelector(DOM.HINTTAGS_killable_selectors, [
DOM.isVisible,
])
@ -505,130 +515,33 @@ function killables() {
import { openInNewTab, activeTabContainerId } from "./lib/webext"
import { openInNewWindow } from "./lib/webext"
export function hintPageWindow() {
hintPage(hintables(), hint => {
hint.target.focus()
if (hint.target.href) {
openInNewWindow({ url: hint.target.href })
} else {
// This is to mirror vimperator behaviour.
DOM.simulateClick(hint.target)
}
})
}
export function hintPageWindowPrivate() {
hintPage(hintables(), hint => {
hint.target.focus()
if (hint.target.href) {
openInNewWindow({ url: hint.target.href, incognito: true })
}
})
}
export function pipe(
selectors = DOM.HINTTAGS_selectors,
action: HintSelectedCallback = _ => _,
rapid = false,
): Promise<[Element, number]> {
return new Promise((resolve, reject) => {
hintPage(hintables(selectors, true), () => {}, resolve, reject)
hintPage(hintables(selectors, true), action, resolve, reject, rapid)
})
}
export function pipe_elements(
elements: any = DOM.elementsWithText,
action: HintSelectedCallback = _ => _,
rapid = false,
): Promise<[Element, number]> {
return new Promise((resolve, reject) => {
hintPage(elements, () => {}, resolve, reject)
})
}
/** 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
*/
export function hintImage(inBackground) {
hintPage(hintableImages(), hint => {
let img_src = hint.target.getAttribute("src")
if (inBackground) {
openInNewTab(new URL(img_src, window.location.href).href, {
active: false,
related: true,
})
} else {
window.location.href = img_src
}
})
}
/** Hint elements to focus */
export function hintFocus(selectors?) {
hintPage(hintables(selectors), hint => {
hint.target.focus()
})
}
/** Hint items and read out the content of the selection */
export function hintRead() {
hintPage(DOM.elementsWithText(), hint => {
TTS.readText(hint.target.textContent)
})
}
/** Hint elements and delete the selection from the page
*/
export function hintKill() {
hintPage(killables(), hint => {
hint.target.remove()
})
}
/** Type for "hint save" actions:
* - "link": elements that point to another resource (eg
* links to pages/files) - the link target is saved
* - "img": image elements
*/
export type HintSaveType = "link" | "img"
/** 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
*/
export 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])
hintPage(elements, action, resolve, reject, rapid)
})
}
function selectFocusedHint(delay = false) {
logger.debug("Selecting hint.", state.mode)
const focused = modeState.focusedHint
let select = () => {
reset()
focused.select()
}
if (delay) setTimeout(select, config.get("hintdelay"))
else select()
modeState.filter = ""
modeState.hints.forEach(h => (h.hidden = false))
if (delay) setTimeout(() => focused.select(), config.get("hintdelay"))
else focused.select()
}
import { addListener, attributeCaller } from "./messaging"