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);