summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIvan Habunek <ivan@habunek.com>2019-01-23 14:33:01 +0100
committerDrew DeVault <sir@cmpwn.com>2019-01-23 21:09:03 -0500
commit012f968a122a892844b9efddca81931e06dc76a4 (patch)
tree9332d074e65aeefa5a1580616f0007d47a423af1
parent84cb142b803fd1247c4db0529cca4b353eefa31f (diff)
Allow block selection via js
* Clicking a line selects only that line * Ctrl+clicking a line toggles the selection of that line * Shift+clicking a line selects the span from the previously clicked line
-rw-r--r--gitsrht/templates/blob.html251
1 files changed, 251 insertions, 0 deletions
diff --git a/gitsrht/templates/blob.html b/gitsrht/templates/blob.html
index db29dd4..d8c19ef 100644
--- a/gitsrht/templates/blob.html
+++ b/gitsrht/templates/blob.html
@@ -98,3 +98,254 @@ pre, body {
{% endif %}
</div>
{% endblock %}
+
+{% block scripts %}
+<script type="text/javascript">
+
+/**
+ * Matches URL hash selecting one or more lines:
+ * - #L10 - single line
+ * - #L10,20 - multiple lines
+ * - #L10-15 - span of lines
+ * - #L10-15,20-25 - multiple spans
+ * - #L10,15-20,30 - combination of above
+ */
+const hashPattern = /^#L(\d+(-\d+)?)(,\d+(-\d+)?)*$/;
+
+const isValidHash = hash => hash.match(hashPattern);
+
+const getLine = no => document.getElementById(`L${no}`);
+
+const getLineCount = () => document.querySelectorAll('.lines > a').length;
+
+const lineNumber = line => Number(line.id.substring(1));
+
+function* range(start, end) {
+ if (end < start) {
+ [start, end] = [end, start];
+ }
+
+ for (let n = start; n <= end; n += 1) {
+ yield n;
+ }
+}
+
+/**
+ * Given a string representation of a span returns the numbers contained in it.
+ * Numbers greater than max are ignored.
+ */
+const parseSpan = (span, max) => {
+ const [sStart, sEnd] = span.includes("-") ? span.split("-") : [span, span];
+ const [start, end] = [sStart, sEnd].map(Number).sort((a, b) => a - b);
+
+ if (start > max) {
+ return [];
+ } else if (end > max) {
+ return range(start, max);
+ } else {
+ return range(start, end);
+ }
+}
+
+/**
+ * Returns a set of line numbers matching the hash.
+ */
+const lineNumbersFromHash = hash => {
+ const lineCount = getLineCount();
+ const lineNos = new Set();
+
+ if (isValidHash(hash)) {
+ const spans = location.hash.substring(2).split(",");
+ for (let span of spans) {
+ for (let no of parseSpan(span, lineCount)) {
+ lineNos.add(no);
+ }
+ }
+ }
+
+ return lineNos;
+}
+
+/**
+ * Given a set of line numbers, groups them into spans.
+ * Yields tuples of [startNo, endNo].
+ */
+const spansFromLineNumbers = lineNos => {
+ if (lineNos.size === 0) {
+ return [];
+ }
+
+ const sorted = Array.from(lineNos).sort((a, b) => a - b);
+ const spans = [];
+ let current, prev;
+ let start = sorted[0];
+
+ for (current of sorted) {
+ if (prev && current !== prev + 1) {
+ spans.push([start, prev]);
+ start = current;
+ }
+ prev = current;
+ }
+ spans.push([start, current]);
+
+ return spans;
+}
+
+/**
+ * Returns a hash matching the given set of line numbers.
+ */
+const hashFromLineNumbers = lineNos => {
+ const spans = spansFromLineNumbers(lineNos);
+ const parts = [];
+
+ for ([start, end] of spans) {
+ if (start == end) {
+ parts.push(start);
+ } else {
+ parts.push([start, end].join("-"));
+ }
+ }
+
+ return "#L" + parts.join(",");
+}
+
+const selectLine = lineNo => {
+ const line = getLine(lineNo);
+ if (line) {
+ line.classList.add("selected");
+ }
+}
+
+const selectLines = lineNos => {
+ for (lineNo of lineNos) {
+ selectLine(lineNo);
+ }
+}
+
+const unselectLine = lineNo => {
+ const line = getLine(lineNo);
+ if (line) {
+ line.classList.remove("selected");
+ }
+}
+
+const unselectAll = () => {
+ const selected = document.querySelectorAll(".lines .selected");
+ for (let line of selected) {
+ line.classList.remove("selected");
+ }
+}
+
+const handlePlainClick = (selected, lineNo) => {
+ selected.clear();
+ selected.add(lineNo);
+ unselectAll();
+ selectLine(lineNo);
+}
+
+const handleCtrlClick = (selected, lineNo) => {
+ if (selected.has(lineNo)) {
+ selected.delete(lineNo);
+ unselectLine(lineNo);
+ } else {
+ selected.add(lineNo);
+ selectLine(lineNo);
+ }
+}
+
+const handleShiftClick = (selected, lineNo, lastNo) => {
+ if (lastNo) {
+ for (no of range(lastNo, lineNo)) {
+ selected.add(no);
+ selectLine(no);
+ }
+ }
+}
+
+/**
+ * Scroll the selected lines into view.
+ */
+const scrollToSelected = (selected) => {
+ if (selected.size > 0) {
+ const firstNo = Math.min(...selected);
+ const scrollNo = Math.max(firstNo - 5, 1); // add top padding
+ const line = getLine(scrollNo);
+ if (line) {
+ line.scrollIntoView();
+ }
+ }
+}
+
+/**
+ * Returns true if two sets contain the same elements.
+ */
+const setsEqual = (a, b) => {
+ if (a.size != b.size) {
+ return false;
+ }
+ for (n of a) {
+ if (!b.has(n)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * A set of currently selected line numbers.
+ */
+let selected = lineNumbersFromHash(location.hash);
+
+/**
+ * The number of the last line to be clicked. Used to select spans of lines.
+ * If a single line is selected initially, set to that line.
+ */
+let lastNo = selected.size == 1 ? Array.from(selected)[0] : null;
+
+/**
+ * Overrides default click handler for line numbers.
+ */
+const handleLineClicked = event => {
+ event.preventDefault();
+
+ const lineNo = lineNumber(event.target);
+ if (event.ctrlKey) {
+ handleCtrlClick(selected, lineNo);
+ } else if (event.shiftKey) {
+ handleShiftClick(selected, lineNo, lastNo);
+ } else {
+ handlePlainClick(selected, lineNo);
+ }
+
+ lastNo = lineNo;
+
+ const hash = hashFromLineNumbers(selected);
+ if (hash) {
+ window.location.hash = hash;
+ } else {
+ // Hacky way to clear the hash (https://stackoverflow.com/a/15323220)
+ history.pushState('', document.title, window.location.pathname);
+ }
+}
+
+// Catch when the hash is changed from the outside and update the selection
+// e.g. when the user edits the hash in the URL
+window.onhashchange = () => {
+ let newSelected = lineNumbersFromHash(location.hash);
+ if (!setsEqual(selected, newSelected)) {
+ selected = newSelected;
+ unselectAll();
+ selectLines(selected);
+ }
+}
+
+document.querySelectorAll('.lines a').forEach(
+ line => line.addEventListener("click", handleLineClicked)
+);
+
+// Initially select lines matching hash and scroll them into view
+selectLines(selected);
+scrollToSelected(selected);
+</script>
+{% endblock %}