followlines.js
286 lines
| 10.6 KiB
| application/javascript
|
JavascriptLexer
|
r31785 | // 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; | ||||
r35157 | if (typeof targetUri === 'undefined') { | |||
|
r31785 | return; | ||
} | ||||
|
r32993 | // Tag of children of "sourcelines" element on which to add "line | ||
// selection" style. | ||||
var selectableTag = sourcelines.dataset.selectabletag; | ||||
if (typeof selectableTag === 'undefined') { | ||||
return; | ||||
} | ||||
|
r32070 | var isHead = parseInt(sourcelines.dataset.ishead || "0"); | ||
|
r31849 | //* 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; | ||||
} | ||||
|
r32993 | // retrieve all direct *selectable* children of class="sourcelines" | ||
// element | ||||
var selectableElements = Array.prototype.filter.call( | ||||
|
r31785 | sourcelines.children, | ||
r35027 | function(x) { return x.tagName === selectableTag; }); | |||
|
r31785 | |||
|
r33390 | 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 | ||||
r35027 | var followlinesButtons = []; | |||
|
r32993 | for (var i = 0; i < selectableElements.length; i++) { | ||
selectableElements[i].classList.add('followlines-select'); | ||||
|
r33390 | 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.children[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'); | ||||
} | ||||
|
r31785 | } | ||
var lineSelectedCSSClass = 'followlines-selected'; | ||||
|
r32993 | //** add CSS class on selectable elements in `from`-`to` line range */ | ||
|
r31785 | function addSelectedCSSClass(from, to) { | ||
for (var i = from; i <= to; i++) { | ||||
|
r32993 | selectableElements[i].classList.add(lineSelectedCSSClass); | ||
|
r31785 | } | ||
} | ||||
//** remove CSS class from previously selected lines */ | ||||
function removeSelectedCSSClass() { | ||||
var elements = sourcelines.getElementsByClassName( | ||||
lineSelectedCSSClass); | ||||
while (elements.length) { | ||||
elements[0].classList.remove(lineSelectedCSSClass); | ||||
} | ||||
} | ||||
|
r32993 | // ** return the element of type "selectableTag" parent of `element` */ | ||
function selectableParent(element) { | ||||
|
r31785 | var parent = element.parentElement; | ||
if (parent === null) { | ||||
return null; | ||||
} | ||||
r35157 | if (element.tagName === selectableTag && parent.isSameNode(sourcelines)) { | |||
|
r31785 | return element; | ||
} | ||||
|
r32993 | return selectableParent(parent); | ||
|
r31785 | } | ||
|
r33390 | // ** 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); | ||||
} | ||||
} | ||||
|
r31785 | //** event handler for "click" on the first line of a block */ | ||
function lineSelectStart(e) { | ||||
|
r33390 | var startElement = selectableParent(e.target.parentElement); | ||
|
r31785 | if (startElement === null) { | ||
|
r32993 | // not a "selectable" element (maybe <a>): abort, keeping event | ||
// listener registered for other click with a "selectable" target | ||||
|
r31785 | return; | ||
} | ||||
|
r31848 | |||
|
r33390 | // update button tooltip text and CSS | ||
updateButtons(startElement); | ||||
|
r31848 | |||
|
r31785 | var startId = parseInt(startElement.id.slice(1)); | ||
startElement.classList.add(lineSelectedCSSClass); // CSS | ||||
// remove this event listener | ||||
|
r33390 | buttonsRemoveEventListener(lineSelectStart); | ||
|
r31785 | |||
//** event handler for "click" on the last line of the block */ | ||||
function lineSelectEnd(e) { | ||||
|
r33390 | var endElement = selectableParent(e.target.parentElement); | ||
|
r31785 | if (endElement === null) { | ||
// not a <span> (maybe <a>): abort, keeping event listener | ||||
// registered for other click with <span> target | ||||
return; | ||||
} | ||||
// remove this event listener | ||||
|
r33390 | buttonsRemoveEventListener(lineSelectEnd); | ||
|
r31785 | |||
|
r33390 | // reset button tooltip text | ||
resetButtons(); | ||||
|
r31848 | |||
|
r31785 | // compute line range (startId, endId) | ||
var endId = parseInt(endElement.id.slice(1)); | ||||
r35157 | if (endId === startId) { | |||
|
r31785 | // clicked twice the same line, cancel and reset initial state | ||
|
r33390 | // (CSS, event listener for selection start) | ||
|
r31785 | removeSelectedCSSClass(); | ||
|
r33390 | buttonsAddEventListener(lineSelectStart); | ||
|
r31785 | 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 | ||||
|
r32070 | var divAndButton = followlinesBox(targetUri, startId, endId, isHead); | ||
|
r31785 | var div = divAndButton[0], | ||
button = divAndButton[1]; | ||||
inviteElement.appendChild(div); | ||||
|
r31849 | // set position close to cursor (top-right) | ||
positionTopRight(div, e); | ||||
|
r33390 | // hide all buttons | ||
for (var i = 0; i < followlinesButtons.length; i++) { | ||||
followlinesButtons[i].classList.add('btn-followlines-hidden'); | ||||
} | ||||
|
r31785 | |||
//** event handler for cancelling selection */ | ||||
function cancel() { | ||||
// remove invite box | ||||
div.parentNode.removeChild(div); | ||||
// restore initial event listeners | ||||
|
r33390 | buttonsAddEventListener(lineSelectStart); | ||
buttonsRemoveEventListener(cancel); | ||||
for (var i = 0; i < followlinesButtons.length; i++) { | ||||
followlinesButtons[i].classList.remove('btn-followlines-hidden'); | ||||
} | ||||
|
r31785 | // remove styles on selected lines | ||
removeSelectedCSSClass(); | ||||
|
r33390 | resetButtons(); | ||
|
r31785 | } | ||
// bind cancel event to click on <button> | ||||
button.addEventListener('click', cancel); | ||||
// as well as on an click on any source line | ||||
|
r33390 | buttonsAddEventListener(cancel); | ||
|
r31785 | } | ||
|
r33390 | buttonsAddEventListener(lineSelectEnd); | ||
|
r31785 | |||
} | ||||
|
r33390 | buttonsAddEventListener(lineSelectStart); | ||
|
r31785 | |||
//** return a <div id="followlines"> and inner cancel <button> elements */ | ||||
|
r32070 | function followlinesBox(targetUri, fromline, toline, isHead) { | ||
|
r31785 | // <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'); | ||||
|
r31940 | 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'); | ||||
|
r31785 | var url = targetUri + '?patch=&linerange=' + fromline + ':' + toline; | ||
|
r31940 | aAsc.setAttribute('href', url); | ||
|
r32071 | aAsc.textContent = 'older'; | ||
|
r31940 | aDiv.appendChild(aAsc); | ||
|
r32070 | |||
if (!isHead) { | ||||
var sep = document.createTextNode(' / '); | ||||
aDiv.appendChild(sep); | ||||
// link to "descending" followlines | ||||
var aDesc = document.createElement('a'); | ||||
aDesc.setAttribute('href', url + '&descend='); | ||||
|
r32071 | aDesc.textContent = 'newer'; | ||
|
r32070 | aDiv.appendChild(aDesc); | ||
} | ||||
|
r31940 | |||
|
r31785 | div.appendChild(aDiv); | ||
return [div, button]; | ||||
} | ||||
}, false); | ||||