diff templates/static/followlines.js @ 0:dfc36e7ed22c

init
author Vadim Filimonov <fffilimonov@yandex.ru>
date Thu, 12 May 2022 13:51:59 +0400
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/static/followlines.js	Thu May 12 13:51:59 2022 +0400
@@ -0,0 +1,286 @@
+// followlines.js - JavaScript utilities for followlines UI
+//
+// Copyright 2017 Logilab SA <contact@logilab.fr>
+//
+// This software may be used and distributed according to the terms of the
+// GNU General Public License version 2 or any later version.
+
+//** Install event listeners for line block selection and followlines action */
+document.addEventListener('DOMContentLoaded', function() {
+    var sourcelines = document.getElementsByClassName('sourcelines')[0];
+    if (typeof sourcelines === 'undefined') {
+        return;
+    }
+    // URL to complement with "linerange" query parameter
+    var targetUri = sourcelines.dataset.logurl;
+    if (typeof targetUri === 'undefined') {
+        return;
+    }
+
+    // Tag of children of "sourcelines" element on which to add "line
+    // selection" style.
+    var selectableTag = sourcelines.dataset.selectabletag;
+    if (typeof selectableTag === 'undefined') {
+        return;
+    }
+
+    var isHead = parseInt(sourcelines.dataset.ishead || "0");
+
+    //* position "element" on top-right of cursor */
+    function positionTopRight(element, event) {
+        var x = (event.clientX + 10) + 'px',
+            y = (event.clientY - 20) + 'px';
+        element.style.top = y;
+        element.style.left = x;
+    }
+
+    // retrieve all direct *selectable* children of class="sourcelines"
+    // element
+    var selectableElements = Array.prototype.filter.call(
+        sourcelines.children,
+        function(x) { return x.tagName === selectableTag; });
+
+    var btnTitleStart = 'start following lines history from here';
+    var btnTitleEnd = 'terminate line block selection here';
+
+    //** return a <button> element with +/- spans */
+    function createButton() {
+        var btn = document.createElement('button');
+        btn.title = btnTitleStart;
+        btn.classList.add('btn-followlines');
+        var plusSpan = document.createElement('span');
+        plusSpan.classList.add('followlines-plus');
+        plusSpan.textContent = '+';
+        btn.appendChild(plusSpan);
+        var br = document.createElement('br');
+        btn.appendChild(br);
+        var minusSpan = document.createElement('span');
+        minusSpan.classList.add('followlines-minus');
+        minusSpan.textContent = '−';
+        btn.appendChild(minusSpan);
+        return btn;
+    }
+
+    // extend DOM with CSS class for selection highlight and action buttons
+    var followlinesButtons = [];
+    for (var i = 0; i < selectableElements.length; i++) {
+        selectableElements[i].classList.add('followlines-select');
+        var btn = createButton();
+        followlinesButtons.push(btn);
+        // insert the <button> as child of `selectableElements[i]` unless the
+        // latter has itself a child  with a "followlines-btn-parent" class
+        // (annotate view)
+        var btnSupportElm = selectableElements[i];
+        var childSupportElms = btnSupportElm.getElementsByClassName(
+            'followlines-btn-parent');
+        if ( childSupportElms.length > 0 ) {
+            btnSupportElm = childSupportElms[0];
+        }
+        var refNode = btnSupportElm.childNodes[0]; // node to insert <button> before
+        btnSupportElm.insertBefore(btn, refNode);
+    }
+
+    // ** re-initialize followlines buttons */
+    function resetButtons() {
+        for (var i = 0; i < followlinesButtons.length; i++) {
+            var btn = followlinesButtons[i];
+            btn.title = btnTitleStart;
+            btn.classList.remove('btn-followlines-end');
+            btn.classList.remove('btn-followlines-hidden');
+        }
+    }
+
+    var lineSelectedCSSClass = 'followlines-selected';
+
+    //** add CSS class on selectable elements in `from`-`to` line range */
+    function addSelectedCSSClass(from, to) {
+        for (var i = from; i <= to; i++) {
+            selectableElements[i].classList.add(lineSelectedCSSClass);
+        }
+    }
+
+    //** remove CSS class from previously selected lines */
+    function removeSelectedCSSClass() {
+        var elements = sourcelines.getElementsByClassName(
+            lineSelectedCSSClass);
+        while (elements.length) {
+            elements[0].classList.remove(lineSelectedCSSClass);
+        }
+    }
+
+    // ** return the element of type "selectableTag" parent of `element` */
+    function selectableParent(element) {
+        var parent = element.parentElement;
+        if (parent === null) {
+            return null;
+        }
+        if (element.tagName === selectableTag && parent.isSameNode(sourcelines)) {
+            return element;
+        }
+        return selectableParent(parent);
+    }
+
+    // ** update buttons title and style upon first click */
+    function updateButtons(selectable) {
+        for (var i = 0; i < followlinesButtons.length; i++) {
+            var btn = followlinesButtons[i];
+            btn.title = btnTitleEnd;
+            btn.classList.add('btn-followlines-end');
+        }
+        // on clicked button, change title to "cancel"
+        var clicked = selectable.getElementsByClassName('btn-followlines')[0];
+        clicked.title = 'cancel';
+        clicked.classList.remove('btn-followlines-end');
+    }
+
+    //** add `listener` on "click" event for all `followlinesButtons` */
+    function buttonsAddEventListener(listener) {
+        for (var i = 0; i < followlinesButtons.length; i++) {
+            followlinesButtons[i].addEventListener('click', listener);
+        }
+    }
+
+    //** remove `listener` on "click" event for all `followlinesButtons` */
+    function buttonsRemoveEventListener(listener) {
+        for (var i = 0; i < followlinesButtons.length; i++) {
+            followlinesButtons[i].removeEventListener('click', listener);
+        }
+    }
+
+    //** event handler for "click" on the first line of a block */
+    function lineSelectStart(e) {
+        var startElement = selectableParent(e.target.parentElement);
+        if (startElement === null) {
+            // not a "selectable" element (maybe <a>): abort, keeping event
+            // listener registered for other click with a "selectable" target
+            return;
+        }
+
+        // update button tooltip text and CSS
+        updateButtons(startElement);
+
+        var startId = parseInt(startElement.id.slice(1));
+        startElement.classList.add(lineSelectedCSSClass); // CSS
+
+        // remove this event listener
+        buttonsRemoveEventListener(lineSelectStart);
+
+        //** event handler for "click" on the last line of the block */
+        function lineSelectEnd(e) {
+            var endElement = selectableParent(e.target.parentElement);
+            if (endElement === null) {
+                // not a <span> (maybe <a>): abort, keeping event listener
+                // registered for other click with <span> target
+                return;
+            }
+
+            // remove this event listener
+            buttonsRemoveEventListener(lineSelectEnd);
+
+            // reset button tooltip text
+            resetButtons();
+
+            // compute line range (startId, endId)
+            var endId = parseInt(endElement.id.slice(1));
+            if (endId === startId) {
+                // clicked twice the same line, cancel and reset initial state
+                // (CSS, event listener for selection start)
+                removeSelectedCSSClass();
+                buttonsAddEventListener(lineSelectStart);
+                return;
+            }
+            var inviteElement = endElement;
+            if (endId < startId) {
+                var tmp = endId;
+                endId = startId;
+                startId = tmp;
+                inviteElement = startElement;
+            }
+
+            addSelectedCSSClass(startId - 1, endId -1);  // CSS
+
+            // append the <div id="followlines"> element to last line of the
+            // selection block
+            var divAndButton = followlinesBox(targetUri, startId, endId, isHead);
+            var div = divAndButton[0],
+                button = divAndButton[1];
+            inviteElement.appendChild(div);
+            // set position close to cursor (top-right)
+            positionTopRight(div, e);
+            // hide all buttons
+            for (var i = 0; i < followlinesButtons.length; i++) {
+                followlinesButtons[i].classList.add('btn-followlines-hidden');
+            }
+
+            //** event handler for cancelling selection */
+            function cancel() {
+                // remove invite box
+                div.parentNode.removeChild(div);
+                // restore initial event listeners
+                buttonsAddEventListener(lineSelectStart);
+                buttonsRemoveEventListener(cancel);
+                for (var i = 0; i < followlinesButtons.length; i++) {
+                    followlinesButtons[i].classList.remove('btn-followlines-hidden');
+                }
+                // remove styles on selected lines
+                removeSelectedCSSClass();
+                resetButtons();
+            }
+
+            // bind cancel event to click on <button>
+            button.addEventListener('click', cancel);
+            // as well as on an click on any source line
+            buttonsAddEventListener(cancel);
+        }
+
+        buttonsAddEventListener(lineSelectEnd);
+
+    }
+
+    buttonsAddEventListener(lineSelectStart);
+
+    //** return a <div id="followlines"> and inner cancel <button> elements */
+    function followlinesBox(targetUri, fromline, toline, isHead) {
+        // <div id="followlines">
+        var div = document.createElement('div');
+        div.id = 'followlines';
+
+        //   <div class="followlines-cancel">
+        var buttonDiv = document.createElement('div');
+        buttonDiv.classList.add('followlines-cancel');
+
+        //     <button>x</button>
+        var button = document.createElement('button');
+        button.textContent = 'x';
+        buttonDiv.appendChild(button);
+        div.appendChild(buttonDiv);
+
+        //   <div class="followlines-link">
+        var aDiv = document.createElement('div');
+        aDiv.classList.add('followlines-link');
+        aDiv.textContent = 'follow history of lines ' + fromline + ':' + toline + ':';
+        var linesep = document.createElement('br');
+        aDiv.appendChild(linesep);
+        //     link to "ascending" followlines
+        var aAsc = document.createElement('a');
+        var url = targetUri + '?patch=&linerange=' + fromline + ':' + toline;
+        aAsc.setAttribute('href', url);
+        aAsc.textContent = 'older';
+        aDiv.appendChild(aAsc);
+
+        if (!isHead) {
+            var sep = document.createTextNode(' / ');
+            aDiv.appendChild(sep);
+            //     link to "descending" followlines
+            var aDesc = document.createElement('a');
+            aDesc.setAttribute('href', url + '&descend=');
+            aDesc.textContent = 'newer';
+            aDiv.appendChild(aDesc);
+        }
+
+        div.appendChild(aDiv);
+
+        return [div, button];
+    }
+
+}, false);