From fea491d938c60628e5a23f78c368c1397c497010 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 14 Jul 2017 19:37:51 +0800 Subject: [PATCH] web-view: support title navigation shortcuts Like Vim mode, support following title navigtion shortcuts: [[, ]], [], ][, [{, ]}. --- src/resources/docs/shortcuts_en.md | 7 + src/resources/docs/shortcuts_zh.md | 7 + src/resources/markdown_template.js | 275 +++++++++++++++++++++++++---- 3 files changed, 255 insertions(+), 34 deletions(-) diff --git a/src/resources/docs/shortcuts_en.md b/src/resources/docs/shortcuts_en.md index 9299e442..7b78f55c 100644 --- a/src/resources/docs/shortcuts_en.md +++ b/src/resources/docs/shortcuts_en.md @@ -33,6 +33,13 @@ Zoom in/out the page. Zoom in/out the page through the mouse scroll. - `Ctrl+0` 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 - `Ctrl+S` diff --git a/src/resources/docs/shortcuts_zh.md b/src/resources/docs/shortcuts_zh.md index 2636cb2c..6958449d 100644 --- a/src/resources/docs/shortcuts_zh.md +++ b/src/resources/docs/shortcuts_zh.md @@ -33,6 +33,13 @@ 鼠标滚轮实现放大/缩小页面。 - `Ctrl+0` 恢复页面大小为100%。 +- 标题跳转 + - `[[`:跳转到上一个标题; + - `]]`: 跳转到下一个标题; + - `[]`:跳转到上一个同层级的标题; + - `][`:跳转到下一个同层级的标题; + - `[{`:跳转到上一个高一层级的标题; + - `]}`:跳转到下一个高一层级的标题; ### 编辑模式 - `Ctrl+S` diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 1a8d760f..77a3035b 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -1,5 +1,10 @@ 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 VFlowchartDivClass = 'flowchart-diagram'; @@ -50,6 +55,7 @@ var g_muteScroll = false; var scrollToAnchor = function(anchor) { g_muteScroll = true; + currentHeaderIdx = -1; if (!anchor) { window.scrollTo(0, 0); g_muteScroll = false; @@ -59,6 +65,13 @@ var scrollToAnchor = function(anchor) { var anc = document.getElementById(anchor); if (anc != null) { 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. @@ -78,6 +91,7 @@ window.onscroll = function() { return; } + currentHeaderIdx = -1; var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset; var eles = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); @@ -86,25 +100,30 @@ window.onscroll = function() { return; } - var curIdx = -1; var biaScrollTop = scrollTop + 50; for (var i = 0; i < eles.length; ++i) { if (biaScrollTop >= eles[i].offsetTop) { - curIdx = i; + currentHeaderIdx = i; } else { break; } } var curHeader = null; - if (curIdx != -1) { - curHeader = eles[curIdx].getAttribute("id"); + if (currentHeaderIdx != -1) { + curHeader = eles[currentHeaderIdx].getAttribute("id"); } content.setHeader(curHeader ? curHeader : ""); }; 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; var key; var shift; @@ -114,72 +133,182 @@ document.onkeydown = function(e) { } else { key = e.keyCode; } + shift = !!e.shiftKey; ctrl = !!e.ctrlKey; switch (key) { + // Skip Ctrl, Shift, Alt, Supper. + case 16: + case 17: + case 18: + case 91: + clear = false; + break; + case 74: // J window.scrollBy(0, 100); - keyState = 0; - break; + break; case 75: // K window.scrollBy(0, -100); - keyState = 0; - break; + break; case 72: // H window.scrollBy(-100, 0); - keyState = 0; - break; + break; case 76: // L window.scrollBy(100, 0); - keyState = 0; - break; + break; case 71: // G if (shift) { - var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || window.pageXOffset; - var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight; - window.scrollTo(scrollLeft, scrollHeight); - keyState = 0; - break; - } else { - if (keyState == 0) { - keyState = 1; - } else if (keyState == 1) { - keyState = 0; + if (pendingKeys.length == 0) { var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || window.pageXOffset; - window.scrollTo(scrollLeft, 0); + var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight; + window.scrollTo(scrollLeft, scrollHeight); + break; + } + } else { + if (pendingKeys.length == 0) { + // First g, 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) { + var scrollLeft = document.documentElement.scrollLeft + || document.body.scrollLeft + || window.pageXOffset; + window.scrollTo(scrollLeft, 0); + break; + } } - break; } - return; + + accept = false; + break; case 85: // U - keyState = 0; if (ctrl) { var clientHeight = document.documentElement.clientHeight; window.scrollBy(0, -clientHeight / 2); break; } - return; + + accept = false; + break; case 68: // D - keyState = 0; if (ctrl) { var clientHeight = document.documentElement.clientHeight; window.scrollBy(0, clientHeight / 2); 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: - content.keyPressEvent(key, ctrl, shift); - keyState = 0; - return; + accept = false; + break; + } + + if (clear) { + pendingKeys = []; + } + + if (accept) { + e.preventDefault(); + } else { + content.keyPressEvent(key, ctrl, shift); } - e.preventDefault(); }; var mermaidParserErr = false; @@ -581,3 +710,81 @@ window.onmousemove = function(e) { 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); +};