Submitter | Denis Laxalde |
---|---|
Date | March 31, 2017, 11:47 a.m. |
Message ID | <be7965e3afe82be35d25.1490960857@sh77.tls.logilab.fr> |
Download | mbox | patch |
Permalink | /patch/19871/ |
State | Accepted |
Headers | show |
Comments
On Fri, Mar 31, 2017 at 4:47 AM, Denis Laxalde <denis@laxalde.org> wrote: > # HG changeset patch > # User Denis Laxalde <denis.laxalde@logilab.fr> > # Date 1490819176 -7200 > # Wed Mar 29 22:26:16 2017 +0200 > # Node ID be7965e3afe82be35d258a2cff12b389a857ef88 > # Parent dea2a17cbfd00bf08ee87b3e44b1c71499189f89 > # Available At http://hg.logilab.org/users/dlaxalde/hg > # hg pull http://hg.logilab.org/users/dlaxalde/hg -r > be7965e3afe8 > hgweb: expose a followlines UI in filerevision view > This looks good to me and I think it can be queued. There is obviously some follow-up work: * Add support for gitweb (and other styles if we care) * Add floating labels (or similar) to help draw attention to the feature * Add support on the blame page (and anywhere else it makes sense) * CSS tweaking (if others want to) But this can be deferred until after landing. Perfect is the enemy of good. This is an awesome feature and I can wait to use it! Before I forget, I think someone should record a demo video or an animated GIF of this to put in the 4.2 release notes. AFAIK no other VCS has this feature and I think we could turn some heads by calling attention to it. > > In filerevision view (/file/<rev>/<fname>) we add some event listeners on > mouse clicks of <span> elements in the <pre class="sourcelines"> block. > Those listeners will capture a range of lines selected between two mouse > clicks and a box inviting to follow the history of selected lines will then > show up. Selected lines (i.e. the block of lines) get a CSS class which > make > them highlighted. Selection can be cancelled (and restarted) by either > clicking on the cancel ("x") button in the invite box or clicking on any > other > source line. Also clicking twice on the same line will abort the selection > and > reset event listeners to restart the process. > > As a first step, this action is only advertised by the "cursor: cell" CSS > rule > on source lines elements as any other mechanisms would make the code > significantly more complicated. This might be improved later. > > All JavaScript code lives in a new "linerangelog.js" file, sourced in > filerevision template (only in "paper" style for now). > > diff --git a/contrib/wix/templates.wxs b/contrib/wix/templates.wxs > --- a/contrib/wix/templates.wxs > +++ b/contrib/wix/templates.wxs > @@ -225,6 +225,7 @@ > <File Id="static.coal.file.png" Name="coal-file.png" /> > <File Id="static.coal.folder.png" Name="coal-folder.png" /> > <File Id="static.excanvas.js" Name="excanvas.js" /> > + <File Id="static.linerangelog.js" Name="linerangelog.js" /> > <File Id="static.mercurial.js" Name="mercurial.js" /> > <File Id="static.hgicon.png" Name="hgicon.png" /> > <File Id="static.hglogo.png" Name="hglogo.png" /> > diff --git a/mercurial/templates/paper/filerevision.tmpl > b/mercurial/templates/paper/filerevision.tmpl > --- a/mercurial/templates/paper/filerevision.tmpl > +++ b/mercurial/templates/paper/filerevision.tmpl > @@ -71,8 +71,11 @@ > <div class="overflow"> > <div class="sourcefirst linewraptoggle">line wrap: <a > class="linewraplink" href="javascript:toggleLinewrap()">on</a></div> > <div class="sourcefirst"> line source</div> > -<pre class="sourcelines stripes4 wrap bottomline">{text%fileline}</pre> > +<pre class="sourcelines stripes4 wrap bottomline" > data-logurl="{url|urlescape}log/{symrev}/{file|urlescape}" > >{text%fileline}</pre> > </div> > + > +<script type="text/javascript" src="{staticurl|urlescape} > linerangelog.js"></script> > + > </div> > </div> > > diff --git a/mercurial/templates/static/linerangelog.js > b/mercurial/templates/static/linerangelog.js > new file mode 100644 > --- /dev/null > +++ b/mercurial/templates/static/linerangelog.js > @@ -0,0 +1,163 @@ > +// linerangelog.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 */ > +function installLineSelect() { > + 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; > + } > + > + // retrieve all direct <span> children of <pre class="sourcelines"> > + var spans = Array.prototype.filter.call( > + sourcelines.children, > + function(x) { return x.tagName === 'SPAN' }); > + > + var lineSelectedCSSClass = 'followlines-selected'; > + > + //** add CSS class on <span> element in `from`-`to` line range */ > + function addSelectedCSSClass(from, to) { > + for (var i = from; i <= to; i++) { > + spans[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 <span> element parent of `element` */ > + function findParentSpan(element) { > + var parent = element.parentElement; > + if (parent === null) { > + return null; > + } > + if (element.tagName == 'SPAN' && parent.isSameNode(sourcelines)) > { > + return element; > + } > + return findParentSpan(parent); > + } > + > + //** event handler for "click" on the first line of a block */ > + function lineSelectStart(e) { > + var startElement = findParentSpan(e.target); > + if (startElement === null) { > + // not a <span> (maybe <a>): abort, keeping event listener > + // registered for other click with <span> target > + return; > + } > + var startId = parseInt(startElement.id.slice(1)); > + startElement.classList.add(lineSelectedCSSClass); // CSS > + > + // remove this event listener > + sourcelines.removeEventListener('click', lineSelectStart); > + > + //** event handler for "click" on the last line of the block */ > + function lineSelectEnd(e) { > + var endElement = findParentSpan(e.target); > + if (endElement === null) { > + // not a <span> (maybe <a>): abort, keeping event listener > + // registered for other click with <span> target > + return; > + } > + > + // remove this event listener > + sourcelines.removeEventListener('click', lineSelectEnd); > + > + // 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 and event listener for selection start) > + removeSelectedCSSClass(); > + sourcelines.addEventListener('click', 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); > + var div = divAndButton[0], > + button = divAndButton[1]; > + inviteElement.appendChild(div); > + > + //** event handler for cancelling selection */ > + function cancel() { > + // remove invite box > + div.parentNode.removeChild(div); > + // restore initial event listeners > + sourcelines.addEventListener('click', lineSelectStart); > + sourcelines.removeEventListener('click', cancel); > + // remove styles on selected lines > + removeSelectedCSSClass(); > + } > + > + // bind cancel event to click on <button> > + button.addEventListener('click', cancel); > + // as well as on an click on any source line > + sourcelines.addEventListener('click', cancel); > + } > + > + sourcelines.addEventListener('click', lineSelectEnd); > + > + } > + > + sourcelines.addEventListener('click', lineSelectStart); > + > +} > + > +//** return a <div id="followlines"> and inner cancel <button> elements */ > +function followlinesBox(targetUri, fromline, toline) { > + // <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'); > + > + // <a href="/log/<rev>/<file>?patch=&linerange=..."> > + var a = document.createElement('a'); > + var url = targetUri + '?patch=&linerange=' + fromline + ':' + toline; > + a.setAttribute('href', url); > + a.textContent = 'follow lines ' + fromline + ':' + toline; > + aDiv.appendChild(a); > + div.appendChild(aDiv); > + > + return [div, button]; > +} > + > +document.addEventListener('DOMContentLoaded', installLineSelect, false); > diff --git a/mercurial/templates/static/style-paper.css > b/mercurial/templates/static/style-paper.css > --- a/mercurial/templates/static/style-paper.css > +++ b/mercurial/templates/static/style-paper.css > @@ -280,6 +280,46 @@ td.annotate:hover div.annotate-info { di > background-color: #bfdfff; > } > > +div.overflow pre.sourcelines > span:hover { > + cursor: cell; > +} > + > +pre.sourcelines > span.followlines-selected { > + background-color: #99C7E9; > +} > + > +div#followlines { > + background-color: #B7B7B7; > + border: 1px solid #CCC; > + border-radius: 5px; > + padding: 4px; > + position: absolute; > +} > + > +div.followlines-cancel { > + text-align: right; > +} > + > +div.followlines-cancel > button { > + line-height: 80%; > + padding: 0; > + border: 0; > + border-radius: 2px; > + background-color: inherit; > + font-weight: bold; > +} > + > +div.followlines-cancel > button:hover { > + color: #FFFFFF; > + background-color: #CF1F1F; > +} > + > +div.followlines-link { > + margin: 2px; > + margin-top: 4px; > + font-family: sans-serif; > +} > + > .sourcelines > a { > display: inline-block; > position: absolute; > diff --git a/tests/test-hgweb-commands.t b/tests/test-hgweb-commands.t > --- a/tests/test-hgweb-commands.t > +++ b/tests/test-hgweb-commands.t > @@ -1343,9 +1343,12 @@ File-related > <div class="overflow"> > <div class="sourcefirst linewraptoggle">line wrap: <a > class="linewraplink" href="javascript:toggleLinewrap()">on</a></div> > <div class="sourcefirst"> line source</div> > - <pre class="sourcelines stripes4 wrap bottomline"> > + <pre class="sourcelines stripes4 wrap bottomline" > data-logurl="/log/1/foo"> > <span id="l1">foo</span><a href="#l1"></a></pre> > </div> > + > + <script type="text/javascript" src="/static/linerangelog.js"></script> > + > </div> > </div> > > @@ -1468,9 +1471,12 @@ File-related > <div class="overflow"> > <div class="sourcefirst linewraptoggle">line wrap: <a > class="linewraplink" href="javascript:toggleLinewrap()">on</a></div> > <div class="sourcefirst"> line source</div> > - <pre class="sourcelines stripes4 wrap bottomline"> > + <pre class="sourcelines stripes4 wrap bottomline" > data-logurl="/log/2/foo"> > <span id="l1">another</span><a href="#l1"></a></pre> > </div> > + > + <script type="text/javascript" src="/static/linerangelog.js"></script> > + > </div> > </div> > > _______________________________________________ > Mercurial-devel mailing list > Mercurial-devel@mercurial-scm.org > https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel >
On Fri, 31 Mar 2017 18:53:28 -0700, Gregory Szorc wrote: > On Fri, Mar 31, 2017 at 4:47 AM, Denis Laxalde <denis@laxalde.org> wrote: > > # HG changeset patch > > # User Denis Laxalde <denis.laxalde@logilab.fr> > > # Date 1490819176 -7200 > > # Wed Mar 29 22:26:16 2017 +0200 > > # Node ID be7965e3afe82be35d258a2cff12b389a857ef88 > > # Parent dea2a17cbfd00bf08ee87b3e44b1c71499189f89 > > # Available At http://hg.logilab.org/users/dlaxalde/hg > > # hg pull http://hg.logilab.org/users/dlaxalde/hg -r > > be7965e3afe8 > > hgweb: expose a followlines UI in filerevision view > > This looks good to me and I think it can be queued. Queued per Gregory's review, thanks. > > diff --git a/mercurial/templates/static/linerangelog.js > > b/mercurial/templates/static/linerangelog.js > > new file mode 100644 > > --- /dev/null > > +++ b/mercurial/templates/static/linerangelog.js > > @@ -0,0 +1,163 @@ > > +// linerangelog.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 */ > > +function installLineSelect() { Do we need (function () {})() trick to not pollute the global namespace? > > --- a/mercurial/templates/static/style-paper.css > > +++ b/mercurial/templates/static/style-paper.css > > @@ -280,6 +280,46 @@ td.annotate:hover div.annotate-info { di > > background-color: #bfdfff; > > } > > > > +div.overflow pre.sourcelines > span:hover { > > + cursor: cell; > > +} I saw this cursor in other pages such as "rev/{node}". Perhaps the js function needs to insert new class to change the cursor?
Patch
diff --git a/contrib/wix/templates.wxs b/contrib/wix/templates.wxs --- a/contrib/wix/templates.wxs +++ b/contrib/wix/templates.wxs @@ -225,6 +225,7 @@ <File Id="static.coal.file.png" Name="coal-file.png" /> <File Id="static.coal.folder.png" Name="coal-folder.png" /> <File Id="static.excanvas.js" Name="excanvas.js" /> + <File Id="static.linerangelog.js" Name="linerangelog.js" /> <File Id="static.mercurial.js" Name="mercurial.js" /> <File Id="static.hgicon.png" Name="hgicon.png" /> <File Id="static.hglogo.png" Name="hglogo.png" /> diff --git a/mercurial/templates/paper/filerevision.tmpl b/mercurial/templates/paper/filerevision.tmpl --- a/mercurial/templates/paper/filerevision.tmpl +++ b/mercurial/templates/paper/filerevision.tmpl @@ -71,8 +71,11 @@ <div class="overflow"> <div class="sourcefirst linewraptoggle">line wrap: <a class="linewraplink" href="javascript:toggleLinewrap()">on</a></div> <div class="sourcefirst"> line source</div> -<pre class="sourcelines stripes4 wrap bottomline">{text%fileline}</pre> +<pre class="sourcelines stripes4 wrap bottomline" data-logurl="{url|urlescape}log/{symrev}/{file|urlescape}">{text%fileline}</pre> </div> + +<script type="text/javascript" src="{staticurl|urlescape}linerangelog.js"></script> + </div> </div> diff --git a/mercurial/templates/static/linerangelog.js b/mercurial/templates/static/linerangelog.js new file mode 100644 --- /dev/null +++ b/mercurial/templates/static/linerangelog.js @@ -0,0 +1,163 @@ +// linerangelog.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 */ +function installLineSelect() { + 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; + } + + // retrieve all direct <span> children of <pre class="sourcelines"> + var spans = Array.prototype.filter.call( + sourcelines.children, + function(x) { return x.tagName === 'SPAN' }); + + var lineSelectedCSSClass = 'followlines-selected'; + + //** add CSS class on <span> element in `from`-`to` line range */ + function addSelectedCSSClass(from, to) { + for (var i = from; i <= to; i++) { + spans[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 <span> element parent of `element` */ + function findParentSpan(element) { + var parent = element.parentElement; + if (parent === null) { + return null; + } + if (element.tagName == 'SPAN' && parent.isSameNode(sourcelines)) { + return element; + } + return findParentSpan(parent); + } + + //** event handler for "click" on the first line of a block */ + function lineSelectStart(e) { + var startElement = findParentSpan(e.target); + if (startElement === null) { + // not a <span> (maybe <a>): abort, keeping event listener + // registered for other click with <span> target + return; + } + var startId = parseInt(startElement.id.slice(1)); + startElement.classList.add(lineSelectedCSSClass); // CSS + + // remove this event listener + sourcelines.removeEventListener('click', lineSelectStart); + + //** event handler for "click" on the last line of the block */ + function lineSelectEnd(e) { + var endElement = findParentSpan(e.target); + if (endElement === null) { + // not a <span> (maybe <a>): abort, keeping event listener + // registered for other click with <span> target + return; + } + + // remove this event listener + sourcelines.removeEventListener('click', lineSelectEnd); + + // 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 and event listener for selection start) + removeSelectedCSSClass(); + sourcelines.addEventListener('click', 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); + var div = divAndButton[0], + button = divAndButton[1]; + inviteElement.appendChild(div); + + //** event handler for cancelling selection */ + function cancel() { + // remove invite box + div.parentNode.removeChild(div); + // restore initial event listeners + sourcelines.addEventListener('click', lineSelectStart); + sourcelines.removeEventListener('click', cancel); + // remove styles on selected lines + removeSelectedCSSClass(); + } + + // bind cancel event to click on <button> + button.addEventListener('click', cancel); + // as well as on an click on any source line + sourcelines.addEventListener('click', cancel); + } + + sourcelines.addEventListener('click', lineSelectEnd); + + } + + sourcelines.addEventListener('click', lineSelectStart); + +} + +//** return a <div id="followlines"> and inner cancel <button> elements */ +function followlinesBox(targetUri, fromline, toline) { + // <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'); + + // <a href="/log/<rev>/<file>?patch=&linerange=..."> + var a = document.createElement('a'); + var url = targetUri + '?patch=&linerange=' + fromline + ':' + toline; + a.setAttribute('href', url); + a.textContent = 'follow lines ' + fromline + ':' + toline; + aDiv.appendChild(a); + div.appendChild(aDiv); + + return [div, button]; +} + +document.addEventListener('DOMContentLoaded', installLineSelect, false); diff --git a/mercurial/templates/static/style-paper.css b/mercurial/templates/static/style-paper.css --- a/mercurial/templates/static/style-paper.css +++ b/mercurial/templates/static/style-paper.css @@ -280,6 +280,46 @@ td.annotate:hover div.annotate-info { di background-color: #bfdfff; } +div.overflow pre.sourcelines > span:hover { + cursor: cell; +} + +pre.sourcelines > span.followlines-selected { + background-color: #99C7E9; +} + +div#followlines { + background-color: #B7B7B7; + border: 1px solid #CCC; + border-radius: 5px; + padding: 4px; + position: absolute; +} + +div.followlines-cancel { + text-align: right; +} + +div.followlines-cancel > button { + line-height: 80%; + padding: 0; + border: 0; + border-radius: 2px; + background-color: inherit; + font-weight: bold; +} + +div.followlines-cancel > button:hover { + color: #FFFFFF; + background-color: #CF1F1F; +} + +div.followlines-link { + margin: 2px; + margin-top: 4px; + font-family: sans-serif; +} + .sourcelines > a { display: inline-block; position: absolute; diff --git a/tests/test-hgweb-commands.t b/tests/test-hgweb-commands.t --- a/tests/test-hgweb-commands.t +++ b/tests/test-hgweb-commands.t @@ -1343,9 +1343,12 @@ File-related <div class="overflow"> <div class="sourcefirst linewraptoggle">line wrap: <a class="linewraplink" href="javascript:toggleLinewrap()">on</a></div> <div class="sourcefirst"> line source</div> - <pre class="sourcelines stripes4 wrap bottomline"> + <pre class="sourcelines stripes4 wrap bottomline" data-logurl="/log/1/foo"> <span id="l1">foo</span><a href="#l1"></a></pre> </div> + + <script type="text/javascript" src="/static/linerangelog.js"></script> + </div> </div> @@ -1468,9 +1471,12 @@ File-related <div class="overflow"> <div class="sourcefirst linewraptoggle">line wrap: <a class="linewraplink" href="javascript:toggleLinewrap()">on</a></div> <div class="sourcefirst"> line source</div> - <pre class="sourcelines stripes4 wrap bottomline"> + <pre class="sourcelines stripes4 wrap bottomline" data-logurl="/log/2/foo"> <span id="l1">another</span><a href="#l1"></a></pre> </div> + + <script type="text/javascript" src="/static/linerangelog.js"></script> + </div> </div>