[PATCH V7] hgweb: expose a followlines UI in filerevision view
Gregory Szorc
gregory.szorc at gmail.com
Sat Apr 1 01:53:28 UTC 2017
On Fri, Mar 31, 2017 at 4:47 AM, Denis Laxalde <denis at laxalde.org> wrote:
> # HG changeset patch
> # User Denis Laxalde <denis.laxalde at 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 at 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 at mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.mercurial-scm.org/pipermail/mercurial-devel/attachments/20170331/ba81101a/attachment-0002.html>
More information about the Mercurial-devel
mailing list