changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > infra > home / templates/static/followlines.js

changeset 53: d25f982fb8a6
author: Richard Westhaver <ellis@rwest.io>
date: Sat, 20 Jul 2024 22:31:54 -0400
permissions: -rw-r--r--
description: init vc
1 // followlines.js - JavaScript utilities for followlines UI
2 //
3 // Copyright 2017 Logilab SA <contact@logilab.fr>
4 //
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
7 
8 //** Install event listeners for line block selection and followlines action */
9 document.addEventListener('DOMContentLoaded', function() {
10  var sourcelines = document.getElementsByClassName('sourcelines')[0];
11  if (typeof sourcelines === 'undefined') {
12  return;
13  }
14  // URL to complement with "linerange" query parameter
15  var targetUri = sourcelines.dataset.logurl;
16  if (typeof targetUri === 'undefined') {
17  return;
18  }
19 
20  // Tag of children of "sourcelines" element on which to add "line
21  // selection" style.
22  var selectableTag = sourcelines.dataset.selectabletag;
23  if (typeof selectableTag === 'undefined') {
24  return;
25  }
26 
27  var isHead = parseInt(sourcelines.dataset.ishead || "0");
28 
29  //* position "element" on top-right of cursor */
30  function positionTopRight(element, event) {
31  var x = (event.clientX + 10) + 'px',
32  y = (event.clientY - 20) + 'px';
33  element.style.top = y;
34  element.style.left = x;
35  }
36 
37  // retrieve all direct *selectable* children of class="sourcelines"
38  // element
39  var selectableElements = Array.prototype.filter.call(
40  sourcelines.children,
41  function(x) { return x.tagName === selectableTag; });
42 
43  var btnTitleStart = 'start following lines history from here';
44  var btnTitleEnd = 'terminate line block selection here';
45 
46  //** return a <button> element with +/- spans */
47  function createButton() {
48  var btn = document.createElement('button');
49  btn.title = btnTitleStart;
50  btn.classList.add('btn-followlines');
51  var plusSpan = document.createElement('span');
52  plusSpan.classList.add('followlines-plus');
53  plusSpan.innerHTML = '&plus;';
54  btn.appendChild(plusSpan);
55  var br = document.createElement('br');
56  btn.appendChild(br);
57  var minusSpan = document.createElement('span');
58  minusSpan.classList.add('followlines-minus');
59  minusSpan.innerHTML = '&minus;';
60  btn.appendChild(minusSpan);
61  return btn;
62  }
63 
64  // extend DOM with CSS class for selection highlight and action buttons
65  var followlinesButtons = [];
66  for (var i = 0; i < selectableElements.length; i++) {
67  selectableElements[i].classList.add('followlines-select');
68  var btn = createButton();
69  followlinesButtons.push(btn);
70  // insert the <button> as child of `selectableElements[i]` unless the
71  // latter has itself a child with a "followlines-btn-parent" class
72  // (annotate view)
73  var btnSupportElm = selectableElements[i];
74  var childSupportElms = btnSupportElm.getElementsByClassName(
75  'followlines-btn-parent');
76  if ( childSupportElms.length > 0 ) {
77  btnSupportElm = childSupportElms[0];
78  }
79  var refNode = btnSupportElm.childNodes[0]; // node to insert <button> before
80  btnSupportElm.insertBefore(btn, refNode);
81  }
82 
83  // ** re-initialize followlines buttons */
84  function resetButtons() {
85  for (var i = 0; i < followlinesButtons.length; i++) {
86  var btn = followlinesButtons[i];
87  btn.title = btnTitleStart;
88  btn.classList.remove('btn-followlines-end');
89  btn.classList.remove('btn-followlines-hidden');
90  }
91  }
92 
93  var lineSelectedCSSClass = 'followlines-selected';
94 
95  //** add CSS class on selectable elements in `from`-`to` line range */
96  function addSelectedCSSClass(from, to) {
97  for (var i = from; i <= to; i++) {
98  selectableElements[i].classList.add(lineSelectedCSSClass);
99  }
100  }
101 
102  //** remove CSS class from previously selected lines */
103  function removeSelectedCSSClass() {
104  var elements = sourcelines.getElementsByClassName(
105  lineSelectedCSSClass);
106  while (elements.length) {
107  elements[0].classList.remove(lineSelectedCSSClass);
108  }
109  }
110 
111  // ** return the element of type "selectableTag" parent of `element` */
112  function selectableParent(element) {
113  var parent = element.parentElement;
114  if (parent === null) {
115  return null;
116  }
117  if (element.tagName === selectableTag && parent.isSameNode(sourcelines)) {
118  return element;
119  }
120  return selectableParent(parent);
121  }
122 
123  // ** update buttons title and style upon first click */
124  function updateButtons(selectable) {
125  for (var i = 0; i < followlinesButtons.length; i++) {
126  var btn = followlinesButtons[i];
127  btn.title = btnTitleEnd;
128  btn.classList.add('btn-followlines-end');
129  }
130  // on clicked button, change title to "cancel"
131  var clicked = selectable.getElementsByClassName('btn-followlines')[0];
132  clicked.title = 'cancel';
133  clicked.classList.remove('btn-followlines-end');
134  }
135 
136  //** add `listener` on "click" event for all `followlinesButtons` */
137  function buttonsAddEventListener(listener) {
138  for (var i = 0; i < followlinesButtons.length; i++) {
139  followlinesButtons[i].addEventListener('click', listener);
140  }
141  }
142 
143  //** remove `listener` on "click" event for all `followlinesButtons` */
144  function buttonsRemoveEventListener(listener) {
145  for (var i = 0; i < followlinesButtons.length; i++) {
146  followlinesButtons[i].removeEventListener('click', listener);
147  }
148  }
149 
150  //** event handler for "click" on the first line of a block */
151  function lineSelectStart(e) {
152  var startElement = selectableParent(e.target.parentElement);
153  if (startElement === null) {
154  // not a "selectable" element (maybe <a>): abort, keeping event
155  // listener registered for other click with a "selectable" target
156  return;
157  }
158 
159  // update button tooltip text and CSS
160  updateButtons(startElement);
161 
162  var startId = parseInt(startElement.id.slice(1));
163  startElement.classList.add(lineSelectedCSSClass); // CSS
164 
165  // remove this event listener
166  buttonsRemoveEventListener(lineSelectStart);
167 
168  //** event handler for "click" on the last line of the block */
169  function lineSelectEnd(e) {
170  var endElement = selectableParent(e.target.parentElement);
171  if (endElement === null) {
172  // not a <span> (maybe <a>): abort, keeping event listener
173  // registered for other click with <span> target
174  return;
175  }
176 
177  // remove this event listener
178  buttonsRemoveEventListener(lineSelectEnd);
179 
180  // reset button tooltip text
181  resetButtons();
182 
183  // compute line range (startId, endId)
184  var endId = parseInt(endElement.id.slice(1));
185  if (endId === startId) {
186  // clicked twice the same line, cancel and reset initial state
187  // (CSS, event listener for selection start)
188  removeSelectedCSSClass();
189  buttonsAddEventListener(lineSelectStart);
190  return;
191  }
192  var inviteElement = endElement;
193  if (endId < startId) {
194  var tmp = endId;
195  endId = startId;
196  startId = tmp;
197  inviteElement = startElement;
198  }
199 
200  addSelectedCSSClass(startId - 1, endId -1); // CSS
201 
202  // append the <div id="followlines"> element to last line of the
203  // selection block
204  var divAndButton = followlinesBox(targetUri, startId, endId, isHead);
205  var div = divAndButton[0],
206  button = divAndButton[1];
207  inviteElement.appendChild(div);
208  // set position close to cursor (top-right)
209  positionTopRight(div, e);
210  // hide all buttons
211  for (var i = 0; i < followlinesButtons.length; i++) {
212  followlinesButtons[i].classList.add('btn-followlines-hidden');
213  }
214 
215  //** event handler for cancelling selection */
216  function cancel() {
217  // remove invite box
218  div.parentNode.removeChild(div);
219  // restore initial event listeners
220  buttonsAddEventListener(lineSelectStart);
221  buttonsRemoveEventListener(cancel);
222  for (var i = 0; i < followlinesButtons.length; i++) {
223  followlinesButtons[i].classList.remove('btn-followlines-hidden');
224  }
225  // remove styles on selected lines
226  removeSelectedCSSClass();
227  resetButtons();
228  }
229 
230  // bind cancel event to click on <button>
231  button.addEventListener('click', cancel);
232  // as well as on an click on any source line
233  buttonsAddEventListener(cancel);
234  }
235 
236  buttonsAddEventListener(lineSelectEnd);
237 
238  }
239 
240  buttonsAddEventListener(lineSelectStart);
241 
242  //** return a <div id="followlines"> and inner cancel <button> elements */
243  function followlinesBox(targetUri, fromline, toline, isHead) {
244  // <div id="followlines">
245  var div = document.createElement('div');
246  div.id = 'followlines';
247 
248  // <div class="followlines-cancel">
249  var buttonDiv = document.createElement('div');
250  buttonDiv.classList.add('followlines-cancel');
251 
252  // <button>x</button>
253  var button = document.createElement('button');
254  button.textContent = 'x';
255  buttonDiv.appendChild(button);
256  div.appendChild(buttonDiv);
257 
258  // <div class="followlines-link">
259  var aDiv = document.createElement('div');
260  aDiv.classList.add('followlines-link');
261  aDiv.textContent = 'follow history of lines ' + fromline + ':' + toline + ':';
262  var linesep = document.createElement('br');
263  aDiv.appendChild(linesep);
264  // link to "ascending" followlines
265  var aAsc = document.createElement('a');
266  var url = targetUri + '?patch=&linerange=' + fromline + ':' + toline;
267  aAsc.setAttribute('href', url);
268  aAsc.textContent = 'older';
269  aDiv.appendChild(aAsc);
270 
271  if (!isHead) {
272  var sep = document.createTextNode(' / ');
273  aDiv.appendChild(sep);
274  // link to "descending" followlines
275  var aDesc = document.createElement('a');
276  aDesc.setAttribute('href', url + '&descend=');
277  aDesc.textContent = 'newer';
278  aDiv.appendChild(aDesc);
279  }
280 
281  div.appendChild(aDiv);
282 
283  return [div, button];
284  }
285 
286 }, false);