Tagger: Saka-key like keyboard navigation link tagging
This commit is contained in:
@@ -16,6 +16,7 @@ protocol VIMBindings
|
|||||||
|
|
||||||
extension BrowserViewController: VIMBindings
|
extension BrowserViewController: VIMBindings
|
||||||
{
|
{
|
||||||
|
static let keyboardScrollingEnabled = false // this is tricky...
|
||||||
static let keyboardScrollAmount: CGFloat = 33.0
|
static let keyboardScrollAmount: CGFloat = 33.0
|
||||||
|
|
||||||
override var keyCommands: [UIKeyCommand]? {
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
@@ -29,7 +30,7 @@ extension BrowserViewController: VIMBindings
|
|||||||
|
|
||||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||||
if action == #selector(VIMBindings.scrollDownPressed) || action == #selector(VIMBindings.scrollUpPressed) {
|
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)
|
return super.canPerformAction(action, withSender: sender)
|
||||||
|
|||||||
181
App/Resources/Tagger.js
Normal file
181
App/Resources/Tagger.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
|
|
||||||
_WKUserStyleSheet *_darkModeStyleSheet;
|
_WKUserStyleSheet *_darkModeStyleSheet;
|
||||||
WKUserScript *_readabilityScript;
|
WKUserScript *_readabilityScript;
|
||||||
|
|
||||||
|
NSArray<WKUserScript *> *_userScripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)tearDown
|
- (void)tearDown
|
||||||
@@ -91,6 +93,12 @@
|
|||||||
|
|
||||||
_webViewConfiguration = webViewConfiguration;
|
_webViewConfiguration = webViewConfiguration;
|
||||||
|
|
||||||
|
// User scripts
|
||||||
|
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
|
||||||
|
for (WKUserScript *script in [self _userScripts]) {
|
||||||
|
[userContentController addUserScript:script];
|
||||||
|
}
|
||||||
|
|
||||||
// Instantiate web view
|
// Instantiate web view
|
||||||
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:webViewConfiguration];
|
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:webViewConfiguration];
|
||||||
|
|
||||||
@@ -106,6 +114,25 @@
|
|||||||
return self;
|
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>
|
#pragma mark <SBRWebProcessDelegate>
|
||||||
|
|
||||||
- (void)webProcessDidConnect
|
- (void)webProcessDidConnect
|
||||||
@@ -168,10 +195,7 @@
|
|||||||
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
|
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
|
||||||
|
|
||||||
if (!_readabilityScript) {
|
if (!_readabilityScript) {
|
||||||
NSURL *readabilityJSURL = [[NSBundle mainBundle] URLForResource:@"Readability" withExtension:@"js"];
|
_readabilityScript = [self _loadScriptForResource:@"Readability" withExtension:@"js"];
|
||||||
NSString *readabilityJSSource = [NSString stringWithContentsOfURL:readabilityJSURL encoding:NSUTF8StringEncoding error:nil];
|
|
||||||
|
|
||||||
_readabilityScript = [[WKUserScript alloc] initWithSource:readabilityJSSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[userContentController _addUserScriptImmediately:_readabilityScript];
|
[userContentController _addUserScriptImmediately:_readabilityScript];
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; };
|
CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; };
|
||||||
CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; };
|
CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; };
|
||||||
CD97CF9225D5BE6F00288FEE /* NavigationControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */; };
|
CD97CF9225D5BE6F00288FEE /* NavigationControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */; };
|
||||||
|
CDC4A1CF25E9D8F7007D33C6 /* Tagger.js in Resources */ = {isa = PBXBuildFile; fileRef = CDC4A1CE25E9D8F7007D33C6 /* Tagger.js */; };
|
||||||
CDC5DA3A25DB774D00BA8D99 /* Readability.js in Resources */ = {isa = PBXBuildFile; fileRef = CDC5DA3925DB774D00BA8D99 /* Readability.js */; };
|
CDC5DA3A25DB774D00BA8D99 /* Readability.js in Resources */ = {isa = PBXBuildFile; fileRef = CDC5DA3925DB774D00BA8D99 /* Readability.js */; };
|
||||||
CDC5DA3E25DB7C2C00BA8D99 /* ReaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC5DA3D25DB7C2C00BA8D99 /* ReaderViewController.swift */; };
|
CDC5DA3E25DB7C2C00BA8D99 /* ReaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC5DA3D25DB7C2C00BA8D99 /* ReaderViewController.swift */; };
|
||||||
CDC5DA4025DB7EAC00BA8D99 /* reader.html in Resources */ = {isa = PBXBuildFile; fileRef = CDC5DA3F25DB7EAC00BA8D99 /* reader.html */; };
|
CDC5DA4025DB7EAC00BA8D99 /* reader.html in Resources */ = {isa = PBXBuildFile; fileRef = CDC5DA3F25DB7EAC00BA8D99 /* reader.html */; };
|
||||||
@@ -138,6 +139,7 @@
|
|||||||
CD853BD024E778B800D2BDCC /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = "<group>"; };
|
CD853BD024E778B800D2BDCC /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = "<group>"; };
|
||||||
CD853BD324E77BF900D2BDCC /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; };
|
CD853BD324E77BF900D2BDCC /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; };
|
||||||
CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControlsView.swift; sourceTree = "<group>"; };
|
CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControlsView.swift; sourceTree = "<group>"; };
|
||||||
|
CDC4A1CE25E9D8F7007D33C6 /* Tagger.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Tagger.js; sourceTree = "<group>"; };
|
||||||
CDC5DA3925DB774D00BA8D99 /* Readability.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = Readability.js; sourceTree = "<group>"; };
|
CDC5DA3925DB774D00BA8D99 /* Readability.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = Readability.js; sourceTree = "<group>"; };
|
||||||
CDC5DA3D25DB7C2C00BA8D99 /* ReaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderViewController.swift; sourceTree = "<group>"; };
|
CDC5DA3D25DB7C2C00BA8D99 /* ReaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderViewController.swift; sourceTree = "<group>"; };
|
||||||
CDC5DA3F25DB7EAC00BA8D99 /* reader.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = reader.html; sourceTree = "<group>"; };
|
CDC5DA3F25DB7EAC00BA8D99 /* reader.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = reader.html; sourceTree = "<group>"; };
|
||||||
@@ -182,6 +184,7 @@
|
|||||||
children = (
|
children = (
|
||||||
1A14FC2524D251BD009B3F83 /* darkmode.css */,
|
1A14FC2524D251BD009B3F83 /* darkmode.css */,
|
||||||
CDC5DA3925DB774D00BA8D99 /* Readability.js */,
|
CDC5DA3925DB774D00BA8D99 /* Readability.js */,
|
||||||
|
CDC4A1CE25E9D8F7007D33C6 /* Tagger.js */,
|
||||||
CDC5DA3F25DB7EAC00BA8D99 /* reader.html */,
|
CDC5DA3F25DB7EAC00BA8D99 /* reader.html */,
|
||||||
);
|
);
|
||||||
path = Resources;
|
path = Resources;
|
||||||
@@ -465,6 +468,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
CDC4A1CF25E9D8F7007D33C6 /* Tagger.js in Resources */,
|
||||||
1A14FC2624D251BD009B3F83 /* darkmode.css in Resources */,
|
1A14FC2624D251BD009B3F83 /* darkmode.css in Resources */,
|
||||||
CDC5DA4025DB7EAC00BA8D99 /* reader.html in Resources */,
|
CDC5DA4025DB7EAC00BA8D99 /* reader.html in Resources */,
|
||||||
1ADFF46C24C7DE54006DC7AE /* LaunchScreen.storyboard in Resources */,
|
1ADFF46C24C7DE54006DC7AE /* LaunchScreen.storyboard in Resources */,
|
||||||
|
|||||||
Reference in New Issue
Block a user