changelog shortlog graph tags branches changeset file revisions annotate raw help

Mercurial > infra > home / static/followlines.js

revision 53: d25f982fb8a6
     1.1--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2+++ b/static/followlines.js	Sat Jul 20 22:31:54 2024 -0400
     1.3@@ -0,0 +1,286 @@
     1.4+// followlines.js - JavaScript utilities for followlines UI
     1.5+//
     1.6+// Copyright 2017 Logilab SA <contact@logilab.fr>
     1.7+//
     1.8+// This software may be used and distributed according to the terms of the
     1.9+// GNU General Public License version 2 or any later version.
    1.10+
    1.11+//** Install event listeners for line block selection and followlines action */
    1.12+document.addEventListener('DOMContentLoaded', function() {
    1.13+    var sourcelines = document.getElementsByClassName('sourcelines')[0];
    1.14+    if (typeof sourcelines === 'undefined') {
    1.15+        return;
    1.16+    }
    1.17+    // URL to complement with "linerange" query parameter
    1.18+    var targetUri = sourcelines.dataset.logurl;
    1.19+    if (typeof targetUri === 'undefined') {
    1.20+        return;
    1.21+    }
    1.22+
    1.23+    // Tag of children of "sourcelines" element on which to add "line
    1.24+    // selection" style.
    1.25+    var selectableTag = sourcelines.dataset.selectabletag;
    1.26+    if (typeof selectableTag === 'undefined') {
    1.27+        return;
    1.28+    }
    1.29+
    1.30+    var isHead = parseInt(sourcelines.dataset.ishead || "0");
    1.31+
    1.32+    //* position "element" on top-right of cursor */
    1.33+    function positionTopRight(element, event) {
    1.34+        var x = (event.clientX + 10) + 'px',
    1.35+            y = (event.clientY - 20) + 'px';
    1.36+        element.style.top = y;
    1.37+        element.style.left = x;
    1.38+    }
    1.39+
    1.40+    // retrieve all direct *selectable* children of class="sourcelines"
    1.41+    // element
    1.42+    var selectableElements = Array.prototype.filter.call(
    1.43+        sourcelines.children,
    1.44+        function(x) { return x.tagName === selectableTag; });
    1.45+
    1.46+    var btnTitleStart = 'start following lines history from here';
    1.47+    var btnTitleEnd = 'terminate line block selection here';
    1.48+
    1.49+    //** return a <button> element with +/- spans */
    1.50+    function createButton() {
    1.51+        var btn = document.createElement('button');
    1.52+        btn.title = btnTitleStart;
    1.53+        btn.classList.add('btn-followlines');
    1.54+        var plusSpan = document.createElement('span');
    1.55+        plusSpan.classList.add('followlines-plus');
    1.56+        plusSpan.textContent = '+';
    1.57+        btn.appendChild(plusSpan);
    1.58+        var br = document.createElement('br');
    1.59+        btn.appendChild(br);
    1.60+        var minusSpan = document.createElement('span');
    1.61+        minusSpan.classList.add('followlines-minus');
    1.62+        minusSpan.textContent = '−';
    1.63+        btn.appendChild(minusSpan);
    1.64+        return btn;
    1.65+    }
    1.66+
    1.67+    // extend DOM with CSS class for selection highlight and action buttons
    1.68+    var followlinesButtons = [];
    1.69+    for (var i = 0; i < selectableElements.length; i++) {
    1.70+        selectableElements[i].classList.add('followlines-select');
    1.71+        var btn = createButton();
    1.72+        followlinesButtons.push(btn);
    1.73+        // insert the <button> as child of `selectableElements[i]` unless the
    1.74+        // latter has itself a child  with a "followlines-btn-parent" class
    1.75+        // (annotate view)
    1.76+        var btnSupportElm = selectableElements[i];
    1.77+        var childSupportElms = btnSupportElm.getElementsByClassName(
    1.78+            'followlines-btn-parent');
    1.79+        if ( childSupportElms.length > 0 ) {
    1.80+            btnSupportElm = childSupportElms[0];
    1.81+        }
    1.82+        var refNode = btnSupportElm.childNodes[0]; // node to insert <button> before
    1.83+        btnSupportElm.insertBefore(btn, refNode);
    1.84+    }
    1.85+
    1.86+    // ** re-initialize followlines buttons */
    1.87+    function resetButtons() {
    1.88+        for (var i = 0; i < followlinesButtons.length; i++) {
    1.89+            var btn = followlinesButtons[i];
    1.90+            btn.title = btnTitleStart;
    1.91+            btn.classList.remove('btn-followlines-end');
    1.92+            btn.classList.remove('btn-followlines-hidden');
    1.93+        }
    1.94+    }
    1.95+
    1.96+    var lineSelectedCSSClass = 'followlines-selected';
    1.97+
    1.98+    //** add CSS class on selectable elements in `from`-`to` line range */
    1.99+    function addSelectedCSSClass(from, to) {
   1.100+        for (var i = from; i <= to; i++) {
   1.101+            selectableElements[i].classList.add(lineSelectedCSSClass);
   1.102+        }
   1.103+    }
   1.104+
   1.105+    //** remove CSS class from previously selected lines */
   1.106+    function removeSelectedCSSClass() {
   1.107+        var elements = sourcelines.getElementsByClassName(
   1.108+            lineSelectedCSSClass);
   1.109+        while (elements.length) {
   1.110+            elements[0].classList.remove(lineSelectedCSSClass);
   1.111+        }
   1.112+    }
   1.113+
   1.114+    // ** return the element of type "selectableTag" parent of `element` */
   1.115+    function selectableParent(element) {
   1.116+        var parent = element.parentElement;
   1.117+        if (parent === null) {
   1.118+            return null;
   1.119+        }
   1.120+        if (element.tagName === selectableTag && parent.isSameNode(sourcelines)) {
   1.121+            return element;
   1.122+        }
   1.123+        return selectableParent(parent);
   1.124+    }
   1.125+
   1.126+    // ** update buttons title and style upon first click */
   1.127+    function updateButtons(selectable) {
   1.128+        for (var i = 0; i < followlinesButtons.length; i++) {
   1.129+            var btn = followlinesButtons[i];
   1.130+            btn.title = btnTitleEnd;
   1.131+            btn.classList.add('btn-followlines-end');
   1.132+        }
   1.133+        // on clicked button, change title to "cancel"
   1.134+        var clicked = selectable.getElementsByClassName('btn-followlines')[0];
   1.135+        clicked.title = 'cancel';
   1.136+        clicked.classList.remove('btn-followlines-end');
   1.137+    }
   1.138+
   1.139+    //** add `listener` on "click" event for all `followlinesButtons` */
   1.140+    function buttonsAddEventListener(listener) {
   1.141+        for (var i = 0; i < followlinesButtons.length; i++) {
   1.142+            followlinesButtons[i].addEventListener('click', listener);
   1.143+        }
   1.144+    }
   1.145+
   1.146+    //** remove `listener` on "click" event for all `followlinesButtons` */
   1.147+    function buttonsRemoveEventListener(listener) {
   1.148+        for (var i = 0; i < followlinesButtons.length; i++) {
   1.149+            followlinesButtons[i].removeEventListener('click', listener);
   1.150+        }
   1.151+    }
   1.152+
   1.153+    //** event handler for "click" on the first line of a block */
   1.154+    function lineSelectStart(e) {
   1.155+        var startElement = selectableParent(e.target.parentElement);
   1.156+        if (startElement === null) {
   1.157+            // not a "selectable" element (maybe <a>): abort, keeping event
   1.158+            // listener registered for other click with a "selectable" target
   1.159+            return;
   1.160+        }
   1.161+
   1.162+        // update button tooltip text and CSS
   1.163+        updateButtons(startElement);
   1.164+
   1.165+        var startId = parseInt(startElement.id.slice(1));
   1.166+        startElement.classList.add(lineSelectedCSSClass); // CSS
   1.167+
   1.168+        // remove this event listener
   1.169+        buttonsRemoveEventListener(lineSelectStart);
   1.170+
   1.171+        //** event handler for "click" on the last line of the block */
   1.172+        function lineSelectEnd(e) {
   1.173+            var endElement = selectableParent(e.target.parentElement);
   1.174+            if (endElement === null) {
   1.175+                // not a <span> (maybe <a>): abort, keeping event listener
   1.176+                // registered for other click with <span> target
   1.177+                return;
   1.178+            }
   1.179+
   1.180+            // remove this event listener
   1.181+            buttonsRemoveEventListener(lineSelectEnd);
   1.182+
   1.183+            // reset button tooltip text
   1.184+            resetButtons();
   1.185+
   1.186+            // compute line range (startId, endId)
   1.187+            var endId = parseInt(endElement.id.slice(1));
   1.188+            if (endId === startId) {
   1.189+                // clicked twice the same line, cancel and reset initial state
   1.190+                // (CSS, event listener for selection start)
   1.191+                removeSelectedCSSClass();
   1.192+                buttonsAddEventListener(lineSelectStart);
   1.193+                return;
   1.194+            }
   1.195+            var inviteElement = endElement;
   1.196+            if (endId < startId) {
   1.197+                var tmp = endId;
   1.198+                endId = startId;
   1.199+                startId = tmp;
   1.200+                inviteElement = startElement;
   1.201+            }
   1.202+
   1.203+            addSelectedCSSClass(startId - 1, endId -1);  // CSS
   1.204+
   1.205+            // append the <div id="followlines"> element to last line of the
   1.206+            // selection block
   1.207+            var divAndButton = followlinesBox(targetUri, startId, endId, isHead);
   1.208+            var div = divAndButton[0],
   1.209+                button = divAndButton[1];
   1.210+            inviteElement.appendChild(div);
   1.211+            // set position close to cursor (top-right)
   1.212+            positionTopRight(div, e);
   1.213+            // hide all buttons
   1.214+            for (var i = 0; i < followlinesButtons.length; i++) {
   1.215+                followlinesButtons[i].classList.add('btn-followlines-hidden');
   1.216+            }
   1.217+
   1.218+            //** event handler for cancelling selection */
   1.219+            function cancel() {
   1.220+                // remove invite box
   1.221+                div.parentNode.removeChild(div);
   1.222+                // restore initial event listeners
   1.223+                buttonsAddEventListener(lineSelectStart);
   1.224+                buttonsRemoveEventListener(cancel);
   1.225+                for (var i = 0; i < followlinesButtons.length; i++) {
   1.226+                    followlinesButtons[i].classList.remove('btn-followlines-hidden');
   1.227+                }
   1.228+                // remove styles on selected lines
   1.229+                removeSelectedCSSClass();
   1.230+                resetButtons();
   1.231+            }
   1.232+
   1.233+            // bind cancel event to click on <button>
   1.234+            button.addEventListener('click', cancel);
   1.235+            // as well as on an click on any source line
   1.236+            buttonsAddEventListener(cancel);
   1.237+        }
   1.238+
   1.239+        buttonsAddEventListener(lineSelectEnd);
   1.240+
   1.241+    }
   1.242+
   1.243+    buttonsAddEventListener(lineSelectStart);
   1.244+
   1.245+    //** return a <div id="followlines"> and inner cancel <button> elements */
   1.246+    function followlinesBox(targetUri, fromline, toline, isHead) {
   1.247+        // <div id="followlines">
   1.248+        var div = document.createElement('div');
   1.249+        div.id = 'followlines';
   1.250+
   1.251+        //   <div class="followlines-cancel">
   1.252+        var buttonDiv = document.createElement('div');
   1.253+        buttonDiv.classList.add('followlines-cancel');
   1.254+
   1.255+        //     <button>x</button>
   1.256+        var button = document.createElement('button');
   1.257+        button.textContent = 'x';
   1.258+        buttonDiv.appendChild(button);
   1.259+        div.appendChild(buttonDiv);
   1.260+
   1.261+        //   <div class="followlines-link">
   1.262+        var aDiv = document.createElement('div');
   1.263+        aDiv.classList.add('followlines-link');
   1.264+        aDiv.textContent = 'follow history of lines ' + fromline + ':' + toline + ':';
   1.265+        var linesep = document.createElement('br');
   1.266+        aDiv.appendChild(linesep);
   1.267+        //     link to "ascending" followlines
   1.268+        var aAsc = document.createElement('a');
   1.269+        var url = targetUri + '?patch=&linerange=' + fromline + ':' + toline;
   1.270+        aAsc.setAttribute('href', url);
   1.271+        aAsc.textContent = 'older';
   1.272+        aDiv.appendChild(aAsc);
   1.273+
   1.274+        if (!isHead) {
   1.275+            var sep = document.createTextNode(' / ');
   1.276+            aDiv.appendChild(sep);
   1.277+            //     link to "descending" followlines
   1.278+            var aDesc = document.createElement('a');
   1.279+            aDesc.setAttribute('href', url + '&descend=');
   1.280+            aDesc.textContent = 'newer';
   1.281+            aDiv.appendChild(aDesc);
   1.282+        }
   1.283+
   1.284+        div.appendChild(aDiv);
   1.285+
   1.286+        return [div, button];
   1.287+    }
   1.288+
   1.289+}, false);