web-view: support title navigation shortcuts

Like Vim mode, support following title navigtion shortcuts:

[[, ]], [], ][, [{, ]}.
This commit is contained in:
Le Tan 2017-07-14 19:37:51 +08:00
parent 5ad99ee9b8
commit fea491d938
3 changed files with 255 additions and 34 deletions

View File

@ -33,6 +33,13 @@ Zoom in/out the page.
Zoom in/out the page through the mouse scroll. Zoom in/out the page through the mouse scroll.
- `Ctrl+0` - `Ctrl+0`
Recover the page zoom factor to 100%. Recover the page zoom factor to 100%.
- Jump between titles
- `[[`: jump to previous title;
- `]]`: jump to next title;
- `[]`: jump to previous title at the same level;
- `][`: jump to next title at the same level;
- `[{`: jump to previous title at a higher level;
- `]}`: jump to next title at a higher level;
### Edit Mode ### Edit Mode
- `Ctrl+S` - `Ctrl+S`

View File

@ -33,6 +33,13 @@
鼠标滚轮实现放大/缩小页面。 鼠标滚轮实现放大/缩小页面。
- `Ctrl+0` - `Ctrl+0`
恢复页面大小为100%。 恢复页面大小为100%。
- 标题跳转
- `[[`:跳转到上一个标题;
- `]]`: 跳转到下一个标题;
- `[]`:跳转到上一个同层级的标题;
- `][`:跳转到下一个同层级的标题;
- `[{`:跳转到上一个高一层级的标题;
- `]}`:跳转到下一个高一层级的标题;
### 编辑模式 ### 编辑模式
- `Ctrl+S` - `Ctrl+S`

View File

@ -1,5 +1,10 @@
var content; var content;
var keyState = 0;
// Current header index in all headers.
var currentHeaderIdx = -1;
// Pending keys for keydown.
var pendingKeys = [];
var VMermaidDivClass = 'mermaid-diagram'; var VMermaidDivClass = 'mermaid-diagram';
var VFlowchartDivClass = 'flowchart-diagram'; var VFlowchartDivClass = 'flowchart-diagram';
@ -50,6 +55,7 @@ var g_muteScroll = false;
var scrollToAnchor = function(anchor) { var scrollToAnchor = function(anchor) {
g_muteScroll = true; g_muteScroll = true;
currentHeaderIdx = -1;
if (!anchor) { if (!anchor) {
window.scrollTo(0, 0); window.scrollTo(0, 0);
g_muteScroll = false; g_muteScroll = false;
@ -59,6 +65,13 @@ var scrollToAnchor = function(anchor) {
var anc = document.getElementById(anchor); var anc = document.getElementById(anchor);
if (anc != null) { if (anc != null) {
anc.scrollIntoView(); anc.scrollIntoView();
var headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
for (var i = 0; i < headers.length; ++i) {
if (headers[i] == anc) {
currentHeaderIdx = i;
break;
}
}
} }
// Disable scroll temporarily. // Disable scroll temporarily.
@ -78,6 +91,7 @@ window.onscroll = function() {
return; return;
} }
currentHeaderIdx = -1;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset; var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset;
var eles = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); var eles = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
@ -86,25 +100,30 @@ window.onscroll = function() {
return; return;
} }
var curIdx = -1;
var biaScrollTop = scrollTop + 50; var biaScrollTop = scrollTop + 50;
for (var i = 0; i < eles.length; ++i) { for (var i = 0; i < eles.length; ++i) {
if (biaScrollTop >= eles[i].offsetTop) { if (biaScrollTop >= eles[i].offsetTop) {
curIdx = i; currentHeaderIdx = i;
} else { } else {
break; break;
} }
} }
var curHeader = null; var curHeader = null;
if (curIdx != -1) { if (currentHeaderIdx != -1) {
curHeader = eles[curIdx].getAttribute("id"); curHeader = eles[currentHeaderIdx].getAttribute("id");
} }
content.setHeader(curHeader ? curHeader : ""); content.setHeader(curHeader ? curHeader : "");
}; };
document.onkeydown = function(e) { document.onkeydown = function(e) {
// Need to clear pending kyes.
var clear = true;
// This even has been handled completely. No need to call the default handler.
var accept = true;
e = e || window.event; e = e || window.event;
var key; var key;
var shift; var shift;
@ -114,72 +133,182 @@ document.onkeydown = function(e) {
} else { } else {
key = e.keyCode; key = e.keyCode;
} }
shift = !!e.shiftKey; shift = !!e.shiftKey;
ctrl = !!e.ctrlKey; ctrl = !!e.ctrlKey;
switch (key) { switch (key) {
// Skip Ctrl, Shift, Alt, Supper.
case 16:
case 17:
case 18:
case 91:
clear = false;
break;
case 74: // J case 74: // J
window.scrollBy(0, 100); window.scrollBy(0, 100);
keyState = 0;
break; break;
case 75: // K case 75: // K
window.scrollBy(0, -100); window.scrollBy(0, -100);
keyState = 0;
break; break;
case 72: // H case 72: // H
window.scrollBy(-100, 0); window.scrollBy(-100, 0);
keyState = 0;
break; break;
case 76: // L case 76: // L
window.scrollBy(100, 0); window.scrollBy(100, 0);
keyState = 0;
break; break;
case 71: // G case 71: // G
if (shift) { if (shift) {
if (pendingKeys.length == 0) {
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || window.pageXOffset; var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || window.pageXOffset;
var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight; var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
window.scrollTo(scrollLeft, scrollHeight); window.scrollTo(scrollLeft, scrollHeight);
keyState = 0;
break; break;
}
} else { } else {
if (keyState == 0) { if (pendingKeys.length == 0) {
keyState = 1; // First g, pend it.
} else if (keyState == 1) { pendingKeys.push({
keyState = 0; key: key,
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || window.pageXOffset; ctrl: ctrl,
shift: shift
});
clear = false;
break;
} else if (pendingKeys.length == 1) {
var pendKey = pendingKeys[0];
if (pendKey.key == key && !pendKey.shift && !pendKey.ctrl) {
var scrollLeft = document.documentElement.scrollLeft
|| document.body.scrollLeft
|| window.pageXOffset;
window.scrollTo(scrollLeft, 0); window.scrollTo(scrollLeft, 0);
}
break; break;
} }
return; }
}
accept = false;
break;
case 85: // U case 85: // U
keyState = 0;
if (ctrl) { if (ctrl) {
var clientHeight = document.documentElement.clientHeight; var clientHeight = document.documentElement.clientHeight;
window.scrollBy(0, -clientHeight / 2); window.scrollBy(0, -clientHeight / 2);
break; break;
} }
return;
accept = false;
break;
case 68: // D case 68: // D
keyState = 0;
if (ctrl) { if (ctrl) {
var clientHeight = document.documentElement.clientHeight; var clientHeight = document.documentElement.clientHeight;
window.scrollBy(0, clientHeight / 2); window.scrollBy(0, clientHeight / 2);
break; break;
} }
return;
accept = false;
break;
case 219: // [ or {
if (shift) {
// {
if (pendingKeys.length == 1) {
var pendKey = pendingKeys[0];
if (pendKey.key == key && !pendKey.shift && !pendKey.ctrl) {
// [{, jump to previous title at a higher level.
jumpTitle(false, -1, 1);
break;
}
}
} else {
// [
if (pendingKeys.length == 0) {
// First [, pend it.
pendingKeys.push({
key: key,
ctrl: ctrl,
shift: shift
});
clear = false;
break;
} else if (pendingKeys.length == 1) {
var pendKey = pendingKeys[0];
if (pendKey.key == key && !pendKey.shift && !pendKey.ctrl) {
// [[, jump to previous title.
jumpTitle(false, 1, 1);
break;
} else if (pendKey.key == 221 && !pendKey.shift && !pendKey.ctrl) {
// ][, jump to next title at the same level.
jumpTitle(true, 0, 1);
break;
}
}
}
accept = false;
break;
case 221: // ] or }
if (shift) {
// }
if (pendingKeys.length == 1) {
var pendKey = pendingKeys[0];
if (pendKey.key == key && !pendKey.shift && !pendKey.ctrl) {
// ]}, jump to next title at a higher level.
jumpTitle(true, -1, 1);
break;
}
}
} else {
// ]
if (pendingKeys.length == 0) {
// First ], pend it.
pendingKeys.push({
key: key,
ctrl: ctrl,
shift: shift
});
clear = false;
break;
} else if (pendingKeys.length == 1) {
var pendKey = pendingKeys[0];
if (pendKey.key == key && !pendKey.shift && !pendKey.ctrl) {
// ]], jump to next title.
jumpTitle(true, 1, 1);
break;
} else if (pendKey.key == 219 && !pendKey.shift && !pendKey.ctrl) {
// [], jump to previous title at the same level.
jumpTitle(false, 0, 1);
break;
}
}
}
accept = false;
break;
default: default:
content.keyPressEvent(key, ctrl, shift); accept = false;
keyState = 0; break;
return;
} }
if (clear) {
pendingKeys = [];
}
if (accept) {
e.preventDefault(); e.preventDefault();
} else {
content.keyPressEvent(key, ctrl, shift);
}
}; };
var mermaidParserErr = false; var mermaidParserErr = false;
@ -581,3 +710,81 @@ window.onmousemove = function(e) {
e.preventDefault(); e.preventDefault();
} }
}; };
// @forward: jump forward or backward.
// @relativeLevel: 0 for the same level as current header;
// negative value for upper level;
// positive value is ignored.
var jumpTitle = function(forward, relativeLevel, repeat) {
var headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
if (headers.length == 0) {
return;
}
if (currentHeaderIdx == -1) {
// At the beginning, before any headers.
if (relativeLevel < 0 || !forward) {
return;
}
}
var targetIdx = -1;
// -1: skip level check.
var targetLevel = 0;
var delta = 1;
if (!forward) {
delta = -1;
}
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset;
var firstHeader = true;
for (targetIdx = (currentHeaderIdx == -1 ? 0 : currentHeaderIdx);
targetIdx >= 0 && targetIdx < headers.length;
targetIdx += delta) {
var header = headers[targetIdx];
var level = parseInt(header.tagName.substr(1));
if (targetLevel == 0) {
targetLevel = level;
if (relativeLevel < 0) {
targetLevel += relativeLevel;
if (targetLevel < 1) {
// Invalid level.
return false;
}
} else if (relativeLevel > 0) {
targetLevel = -1;
}
}
if (targetLevel == -1 || level == targetLevel) {
if (targetIdx == currentHeaderIdx) {
// If current header is visible, skip it.
content.setLog("scroll " + scrollTop + " " + headers[targetIdx].offsetTop);
if (forward || scrollTop <= headers[targetIdx].offsetTop) {
continue;
}
}
if (--repeat == 0) {
break;
}
} else if (level < targetLevel) {
return;
}
firstHeader = false;
}
if (targetIdx < 0 || targetIdx >= headers.length) {
return;
}
// Disable scroll temporarily.
g_muteScroll = true;
headers[targetIdx].scrollIntoView();
currentHeaderIdx = targetIdx;
content.setHeader(headers[targetIdx].getAttribute("id"));
setTimeout("g_muteScroll = false", 100);
};