Tagger: Saka-key like keyboard navigation link tagging

This commit is contained in:
James Magahern
2021-02-26 17:43:53 -08:00
parent 48a7c07551
commit 225761473d
4 changed files with 215 additions and 5 deletions

View File

@@ -16,6 +16,7 @@ protocol VIMBindings
extension BrowserViewController: VIMBindings
{
static let keyboardScrollingEnabled = false // this is tricky...
static let keyboardScrollAmount: CGFloat = 33.0
override var keyCommands: [UIKeyCommand]? {
@@ -29,7 +30,7 @@ extension BrowserViewController: VIMBindings
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(VIMBindings.scrollDownPressed) || action == #selector(VIMBindings.scrollUpPressed) {
return webView._contentViewIsFirstResponder && webView._currentContentView().isFocusingElement == false
return Self.keyboardScrollingEnabled && webView._contentViewIsFirstResponder && webView._currentContentView().isFocusingElement == false
}
return super.canPerformAction(action, withSender: sender)

181
App/Resources/Tagger.js Normal file
View File

@@ -0,0 +1,181 @@
class ElementTagger {
constructor() {
this.isTagged = false;
this.tagList = {};
const baseMnemonics = [
'a', 's', 'd', /* 'f', */ 'g',
'q', 'w', 'e', 'r', 't',
'z', 'x', 'c', 'v', 'b',
'y', 'u', 'i', 'o', 'p',
'h', 'j', 'k', 'l',
'n', 'm'
];
let additionalMnemonics = [];
baseMnemonics.forEach( (letter) => {
additionalMnemonics.push("" + letter.toUpperCase() + letter);
});
let mnemonics = baseMnemonics.concat(additionalMnemonics);
mnemonics.forEach( (letter) => {
this.tagList[letter] = undefined;
});
}
tagElement(elem) {
if (!this.isBoundedByViewport(elem)) {
return;
}
let keys = Object.keys(this.tagList);
for (let key_i in keys) {
let key = keys[key_i];
if (this.tagList[key] === undefined) {
if ( this.addOverlay(elem, key) ) {
this.tagList[key] = elem;
}
break;
}
}
}
isBoundedByViewport(elem) {
const viewport = window.visualViewport;
const rect = elem.getClientRects()[0];
if (rect == undefined) {
return false;
}
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const elemTop = (scrollTop + rect.top);
if (elemTop > viewport.pageTop && elemTop < (viewport.pageTop + viewport.height)) {
return true;
}
return false;
}
addOverlay(parentElem, mnemonic) {
var rects = parentElem.getClientRects();
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
if (parentElem._overlayElem == undefined) {
var rect = undefined;
for (let i = 0; i < rects.length; i++) {
if (rects[i] !== undefined) {
rect = rects[i];
break;
}
}
if (rect === undefined) {
return false;
}
let elem = document.createElement('div');
elem.style.position = 'absolute';
elem.style.border = '1px solid blue';
elem.style.borderRadius = '5px';
elem.style.background = 'rgba(0, 0, 255, 0.8)';
elem.style.color = 'white';
elem.style.font = '12px Helvetica bold';
elem.style.padding = '2px';
elem.innerText = "[" + mnemonic + "]";
elem.style.top = (rect.top + scrollTop) + 'px';
elem.style.left = (rect.left + scrollLeft) + 'px';
document.body.appendChild(elem);
parentElem._overlayElem = elem;
return true;
}
return false;
}
removeOverlay(elem) {
let overlayElem = elem._overlayElem;
if (overlayElem != undefined) {
document.body.removeChild(overlayElem);
elem._overlayElem = undefined;
}
}
tagDocument() {
var elt = document.getElementsByTagName("a");
for (let i = 0; i < elt.length; i++) {
this.tagElement(elt[i]);
}
this.isTagged = true;
}
untagDocument() {
const keys = Object.keys(this.tagList);
for (let key_i in keys) {
let elem = this.tagList[keys[key_i]];
if (elem !== undefined) {
this.removeOverlay(elem);
this.tagList[keys[key_i]] = undefined;
}
}
this.isTagged = false;
}
clickLinkWithTag(tag) {
if (this.tagList[tag] !== undefined) {
this.tagList[tag].click();
}
}
}
(function() {
let tagger = new ElementTagger();
var counter = 0;
var tagAccum = [];
document.addEventListener('keypress', (event) => {
if (document.activeElement !== document.body) {
return;
}
const keyName = String.fromCharCode(event.charCode);
if (keyName == undefined) {
return;
}
if (keyName == 'f') {
if (++counter == 2) {
counter = 0;
if (tagger.isTagged) {
tagger.untagDocument();
} else {
tagger.tagDocument();
}
}
} else {
// Uppercase
if (tagAccum.length > 0 || keyName.toUpperCase() == keyName) {
tagAccum.push(keyName);
}
if (tagAccum.length == 2) {
tagger.clickLinkWithTag(tagAccum.join(''));
tagger.untagDocument();
tagAccum = [];
} else if (tagAccum.length == 0) {
tagger.clickLinkWithTag(keyName);
tagger.untagDocument();
}
}
});
})();

View File

@@ -32,6 +32,8 @@
_WKUserStyleSheet *_darkModeStyleSheet;
WKUserScript *_readabilityScript;
NSArray<WKUserScript *> *_userScripts;
}
- (void)tearDown
@@ -91,6 +93,12 @@
_webViewConfiguration = webViewConfiguration;
// User scripts
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
for (WKUserScript *script in [self _userScripts]) {
[userContentController addUserScript:script];
}
// Instantiate web view
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:webViewConfiguration];
@@ -106,6 +114,25 @@
return self;
}
- (WKUserScript *)_loadScriptForResource:(NSString *)resourceName withExtension:(NSString *)extension
{
NSURL *url = [[NSBundle mainBundle] URLForResource:resourceName withExtension:extension];
NSString *source = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];
return [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
}
- (NSArray<WKUserScript *> *)_userScripts
{
if (!_userScripts) {
_userScripts = @[
[self _loadScriptForResource:@"Tagger" withExtension:@"js"],
];
}
return _userScripts;
}
#pragma mark <SBRWebProcessDelegate>
- (void)webProcessDidConnect
@@ -168,10 +195,7 @@
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
if (!_readabilityScript) {
NSURL *readabilityJSURL = [[NSBundle mainBundle] URLForResource:@"Readability" withExtension:@"js"];
NSString *readabilityJSSource = [NSString stringWithContentsOfURL:readabilityJSURL encoding:NSUTF8StringEncoding error:nil];
_readabilityScript = [[WKUserScript alloc] initWithSource:readabilityJSSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
_readabilityScript = [self _loadScriptForResource:@"Readability" withExtension:@"js"];
}
[userContentController _addUserScriptImmediately:_readabilityScript];