Fossil SCM
Optimized the mouse-based line number selection considerably, requiring far less DOM traversal.
Commit
0096aa4644dfde8648fc0d191be35ba0fb97ee4ed7b3d07c29d662a2fd8dad13
Parent
738bea54c0b73fe…
2 files changed
+18
-17
+20
-14
+18
-17
| --- src/fossil.numbered-lines.js | ||
| +++ src/fossil.numbered-lines.js | ||
| @@ -42,12 +42,10 @@ | ||
| 42 | 42 | ); |
| 43 | 43 | }else{ |
| 44 | 44 | D.append(link, "No lines selected."); |
| 45 | 45 | } |
| 46 | 46 | }, |
| 47 | - adjustX: (x)=>x, | |
| 48 | - adjustY: (y)=>y, | |
| 49 | 47 | init: function(){ |
| 50 | 48 | const e = this.e; |
| 51 | 49 | const btnCopy = D.span(), |
| 52 | 50 | link = D.span(); |
| 53 | 51 | this.state = {link}; |
| @@ -65,52 +63,55 @@ | ||
| 65 | 63 | |
| 66 | 64 | tdLn.addEventListener('click', function f(ev){ |
| 67 | 65 | if('SPAN'!==ev.target.tagName) return; |
| 68 | 66 | else if('number' !== typeof f.mode){ |
| 69 | 67 | f.mode = 0 /*0=none selected, 1=1 selected, 2=2 selected*/; |
| 68 | + f.spans = tdLn.querySelectorAll('span'); | |
| 69 | + f.selected = tdLn.querySelectorAll('span.selected-line'); | |
| 70 | + f.unselect = (e)=>D.removeClass(e, 'selected-line','start','end'); | |
| 70 | 71 | } |
| 71 | 72 | ev.stopPropagation(); |
| 72 | 73 | const ln = +ev.target.innerText; |
| 73 | - if(2===f.mode){/*reset selection*/ | |
| 74 | + if(2===f.mode){/*Reset selection*/ | |
| 74 | 75 | f.mode = 0; |
| 75 | 76 | } |
| 76 | - if(0===f.mode){ | |
| 77 | + if(0===f.mode){/*Select single line*/ | |
| 77 | 78 | lineState.end = 0; |
| 78 | 79 | lineState.start = ln; |
| 79 | 80 | f.mode = 1; |
| 80 | 81 | }else if(1===f.mode){ |
| 81 | - if(ln === lineState.start){/*unselect line*/ | |
| 82 | - //console.debug("Unselected line #"+ln); | |
| 82 | + if(ln === lineState.start){/*Unselect line*/ | |
| 83 | 83 | lineState.start = 0; |
| 84 | 84 | f.mode = 0; |
| 85 | - }else{ | |
| 85 | + }else{/*Select range*/ | |
| 86 | 86 | if(ln<lineState.start){ |
| 87 | 87 | lineState.end = lineState.start; |
| 88 | 88 | lineState.start = ln; |
| 89 | 89 | }else{ |
| 90 | 90 | lineState.end = ln; |
| 91 | 91 | } |
| 92 | - //console.debug("Selected range: ",rng); | |
| 93 | 92 | f.mode = 2; |
| 94 | 93 | } |
| 95 | 94 | } |
| 96 | - tdLn.querySelectorAll('span.selected-line').forEach( | |
| 97 | - (e)=>D.removeClass(e, 'selected-line','start','end')); | |
| 98 | - if(f.mode>0){ | |
| 95 | + if(f.selected){/*Unmark previously-selected lines.*/ | |
| 96 | + f.selected.forEach(f.unselect); | |
| 97 | + f.selected = undefined; | |
| 98 | + } | |
| 99 | + if(f.mode>0){/*Mark selected lines*/ | |
| 99 | 100 | const rect = ev.target.getBoundingClientRect(); |
| 100 | - lineTip.show(rect.right+3, rect.top-4); | |
| 101 | - const spans = tdLn.querySelectorAll('span'); | |
| 102 | - if(spans.length>=lineState.start){ | |
| 103 | - let i = lineState.start, end = lineState.end || lineState.start, span = spans[i-1]; | |
| 104 | - for( ; i<=end && span; span = spans[i++] ){ | |
| 101 | + f.selected = []; | |
| 102 | + if(f.spans.length>=lineState.start){ | |
| 103 | + let i = lineState.start, end = lineState.end || lineState.start, span = f.spans[i-1]; | |
| 104 | + for( ; i<=end && span; span = f.spans[i++] ){ | |
| 105 | 105 | span.classList.add('selected-line'); |
| 106 | + f.selected.push(span); | |
| 106 | 107 | if(i===lineState.start) span.classList.add('start'); |
| 107 | 108 | if(i===end) span.classList.add('end'); |
| 108 | 109 | } |
| 109 | 110 | } |
| 110 | - lineTip.refresh(); | |
| 111 | + lineTip.refresh().show(rect.right+3, rect.top-4); | |
| 111 | 112 | }else{ |
| 112 | 113 | lineTip.show(false); |
| 113 | 114 | } |
| 114 | 115 | }, false); |
| 115 | 116 | |
| 116 | 117 | })(); |
| 117 | 118 |
| --- src/fossil.numbered-lines.js | |
| +++ src/fossil.numbered-lines.js | |
| @@ -42,12 +42,10 @@ | |
| 42 | ); |
| 43 | }else{ |
| 44 | D.append(link, "No lines selected."); |
| 45 | } |
| 46 | }, |
| 47 | adjustX: (x)=>x, |
| 48 | adjustY: (y)=>y, |
| 49 | init: function(){ |
| 50 | const e = this.e; |
| 51 | const btnCopy = D.span(), |
| 52 | link = D.span(); |
| 53 | this.state = {link}; |
| @@ -65,52 +63,55 @@ | |
| 65 | |
| 66 | tdLn.addEventListener('click', function f(ev){ |
| 67 | if('SPAN'!==ev.target.tagName) return; |
| 68 | else if('number' !== typeof f.mode){ |
| 69 | f.mode = 0 /*0=none selected, 1=1 selected, 2=2 selected*/; |
| 70 | } |
| 71 | ev.stopPropagation(); |
| 72 | const ln = +ev.target.innerText; |
| 73 | if(2===f.mode){/*reset selection*/ |
| 74 | f.mode = 0; |
| 75 | } |
| 76 | if(0===f.mode){ |
| 77 | lineState.end = 0; |
| 78 | lineState.start = ln; |
| 79 | f.mode = 1; |
| 80 | }else if(1===f.mode){ |
| 81 | if(ln === lineState.start){/*unselect line*/ |
| 82 | //console.debug("Unselected line #"+ln); |
| 83 | lineState.start = 0; |
| 84 | f.mode = 0; |
| 85 | }else{ |
| 86 | if(ln<lineState.start){ |
| 87 | lineState.end = lineState.start; |
| 88 | lineState.start = ln; |
| 89 | }else{ |
| 90 | lineState.end = ln; |
| 91 | } |
| 92 | //console.debug("Selected range: ",rng); |
| 93 | f.mode = 2; |
| 94 | } |
| 95 | } |
| 96 | tdLn.querySelectorAll('span.selected-line').forEach( |
| 97 | (e)=>D.removeClass(e, 'selected-line','start','end')); |
| 98 | if(f.mode>0){ |
| 99 | const rect = ev.target.getBoundingClientRect(); |
| 100 | lineTip.show(rect.right+3, rect.top-4); |
| 101 | const spans = tdLn.querySelectorAll('span'); |
| 102 | if(spans.length>=lineState.start){ |
| 103 | let i = lineState.start, end = lineState.end || lineState.start, span = spans[i-1]; |
| 104 | for( ; i<=end && span; span = spans[i++] ){ |
| 105 | span.classList.add('selected-line'); |
| 106 | if(i===lineState.start) span.classList.add('start'); |
| 107 | if(i===end) span.classList.add('end'); |
| 108 | } |
| 109 | } |
| 110 | lineTip.refresh(); |
| 111 | }else{ |
| 112 | lineTip.show(false); |
| 113 | } |
| 114 | }, false); |
| 115 | |
| 116 | })(); |
| 117 |
| --- src/fossil.numbered-lines.js | |
| +++ src/fossil.numbered-lines.js | |
| @@ -42,12 +42,10 @@ | |
| 42 | ); |
| 43 | }else{ |
| 44 | D.append(link, "No lines selected."); |
| 45 | } |
| 46 | }, |
| 47 | init: function(){ |
| 48 | const e = this.e; |
| 49 | const btnCopy = D.span(), |
| 50 | link = D.span(); |
| 51 | this.state = {link}; |
| @@ -65,52 +63,55 @@ | |
| 63 | |
| 64 | tdLn.addEventListener('click', function f(ev){ |
| 65 | if('SPAN'!==ev.target.tagName) return; |
| 66 | else if('number' !== typeof f.mode){ |
| 67 | f.mode = 0 /*0=none selected, 1=1 selected, 2=2 selected*/; |
| 68 | f.spans = tdLn.querySelectorAll('span'); |
| 69 | f.selected = tdLn.querySelectorAll('span.selected-line'); |
| 70 | f.unselect = (e)=>D.removeClass(e, 'selected-line','start','end'); |
| 71 | } |
| 72 | ev.stopPropagation(); |
| 73 | const ln = +ev.target.innerText; |
| 74 | if(2===f.mode){/*Reset selection*/ |
| 75 | f.mode = 0; |
| 76 | } |
| 77 | if(0===f.mode){/*Select single line*/ |
| 78 | lineState.end = 0; |
| 79 | lineState.start = ln; |
| 80 | f.mode = 1; |
| 81 | }else if(1===f.mode){ |
| 82 | if(ln === lineState.start){/*Unselect line*/ |
| 83 | lineState.start = 0; |
| 84 | f.mode = 0; |
| 85 | }else{/*Select range*/ |
| 86 | if(ln<lineState.start){ |
| 87 | lineState.end = lineState.start; |
| 88 | lineState.start = ln; |
| 89 | }else{ |
| 90 | lineState.end = ln; |
| 91 | } |
| 92 | f.mode = 2; |
| 93 | } |
| 94 | } |
| 95 | if(f.selected){/*Unmark previously-selected lines.*/ |
| 96 | f.selected.forEach(f.unselect); |
| 97 | f.selected = undefined; |
| 98 | } |
| 99 | if(f.mode>0){/*Mark selected lines*/ |
| 100 | const rect = ev.target.getBoundingClientRect(); |
| 101 | f.selected = []; |
| 102 | if(f.spans.length>=lineState.start){ |
| 103 | let i = lineState.start, end = lineState.end || lineState.start, span = f.spans[i-1]; |
| 104 | for( ; i<=end && span; span = f.spans[i++] ){ |
| 105 | span.classList.add('selected-line'); |
| 106 | f.selected.push(span); |
| 107 | if(i===lineState.start) span.classList.add('start'); |
| 108 | if(i===end) span.classList.add('end'); |
| 109 | } |
| 110 | } |
| 111 | lineTip.refresh().show(rect.right+3, rect.top-4); |
| 112 | }else{ |
| 113 | lineTip.show(false); |
| 114 | } |
| 115 | }, false); |
| 116 | |
| 117 | })(); |
| 118 |
+20
-14
| --- src/fossil.tooltip.js | ||
| +++ src/fossil.tooltip.js | ||
| @@ -8,25 +8,26 @@ | ||
| 8 | 8 | |
| 9 | 9 | /** |
| 10 | 10 | Creates a new tooltip widget using the given options object. |
| 11 | 11 | |
| 12 | 12 | The options are available to clients after this returns via |
| 13 | - theTooltip.options. | |
| 13 | + theTooltip.options, and default values for any options not | |
| 14 | + provided are pulled from TooltipWidget.defaultOptions. | |
| 14 | 15 | |
| 15 | 16 | Options: |
| 16 | 17 | |
| 17 | 18 | .refresh: required callback which is called whenever the tooltip |
| 18 | 19 | is revealed or moved. It must refresh the contents of the |
| 19 | - tooltip, if needed, by applying it to this.e, which is the base | |
| 20 | - DOM element for the tooltip. | |
| 20 | + tooltip, if needed, by applying the content to/within this.e, | |
| 21 | + which is the base DOM element for the tooltip. | |
| 21 | 22 | |
| 22 | 23 | .adjustX: an optional callback which is called when the tooltip |
| 23 | 24 | is to be displayed at a given position and passed the X |
| 24 | - coordinate. This routine must either return its argument as-is | |
| 25 | - or return an adjusted value. This API assumes that clients give it | |
| 26 | - viewport-relative coordinates, and it will take care to translate | |
| 27 | - those to page-relative. | |
| 25 | + viewport-relative coordinate. This routine must either return its | |
| 26 | + argument as-is or return an adjusted value. This API assumes that | |
| 27 | + clients give it viewport-relative coordinates, and it will take | |
| 28 | + care to translate those to page-relative. | |
| 28 | 29 | |
| 29 | 30 | .adjustY: the Y counterpart of adjustX. |
| 30 | 31 | |
| 31 | 32 | .init: optional callback called one time to initialize the |
| 32 | 33 | state of the tooltip. This is called after the this.e has |
| @@ -75,32 +76,38 @@ | ||
| 75 | 76 | F.TooltipWidget.defaultOptions = { |
| 76 | 77 | cssClass: 'fossil-tooltip', |
| 77 | 78 | style: {/*properties copied as-is into element.style*/}, |
| 78 | 79 | adjustX: (x)=>x, |
| 79 | 80 | adjustY: (y)=>y, |
| 80 | - refresh: function(hw){ | |
| 81 | - console.error("TooltipWidget refresh() option must be provided by the client."); | |
| 81 | + refresh: function(){ | |
| 82 | + console.error("The TooltipWidget refresh() option must be provided by the client."); | |
| 82 | 83 | } |
| 83 | 84 | }; |
| 84 | 85 | |
| 85 | 86 | F.TooltipWidget.prototype = { |
| 86 | 87 | |
| 87 | 88 | isShown: function(){return !this.e.classList.contains('hidden')}, |
| 88 | 89 | |
| 89 | - /** Calls the refresh() method of the options object. */ | |
| 90 | - refresh: function(){this.options.refresh.call(this)}, | |
| 90 | + /** Calls the refresh() method of the options object and returns | |
| 91 | + this object. */ | |
| 92 | + refresh: function(){ | |
| 93 | + this.options.refresh.call(this); | |
| 94 | + return this; | |
| 95 | + }, | |
| 91 | 96 | |
| 92 | 97 | /** |
| 98 | + Shows or hides the tooltip. | |
| 99 | + | |
| 93 | 100 | Usages: |
| 94 | 101 | |
| 95 | 102 | (bool showIt) => hide it or reveal it at its last position. |
| 96 | 103 | |
| 97 | 104 | (x, y) => reveal/move it at/to the given |
| 98 | 105 | relative-to-the-viewport position, which will be adjusted to make |
| 99 | 106 | it page-relative. |
| 100 | 107 | |
| 101 | - (DOM element) => reveal/move it ad/to a position based on the | |
| 108 | + (DOM element) => reveal/move it at/to a position based on the | |
| 102 | 109 | the given element (adjusted slightly). |
| 103 | 110 | |
| 104 | 111 | For the latter two, this.options.adjustX() and adjustY() will |
| 105 | 112 | be called to adjust it further. |
| 106 | 113 | |
| @@ -132,12 +139,11 @@ | ||
| 132 | 139 | } |
| 133 | 140 | D[showIt ? 'removeClass' : 'addClass'](this.e, 'hidden'); |
| 134 | 141 | if(x || y){ |
| 135 | 142 | this.e.style.left = x+"px"; |
| 136 | 143 | this.e.style.top = y+"px"; |
| 137 | - //console.debug("TooltipWidget.show()", arguments, x, y); | |
| 138 | 144 | } |
| 139 | 145 | return this; |
| 140 | 146 | } |
| 141 | - }/*/F.TooltipWidget.prototype*/; | |
| 147 | + }/*F.TooltipWidget.prototype*/; | |
| 142 | 148 | |
| 143 | 149 | })(window.fossil); |
| 144 | 150 |
| --- src/fossil.tooltip.js | |
| +++ src/fossil.tooltip.js | |
| @@ -8,25 +8,26 @@ | |
| 8 | |
| 9 | /** |
| 10 | Creates a new tooltip widget using the given options object. |
| 11 | |
| 12 | The options are available to clients after this returns via |
| 13 | theTooltip.options. |
| 14 | |
| 15 | Options: |
| 16 | |
| 17 | .refresh: required callback which is called whenever the tooltip |
| 18 | is revealed or moved. It must refresh the contents of the |
| 19 | tooltip, if needed, by applying it to this.e, which is the base |
| 20 | DOM element for the tooltip. |
| 21 | |
| 22 | .adjustX: an optional callback which is called when the tooltip |
| 23 | is to be displayed at a given position and passed the X |
| 24 | coordinate. This routine must either return its argument as-is |
| 25 | or return an adjusted value. This API assumes that clients give it |
| 26 | viewport-relative coordinates, and it will take care to translate |
| 27 | those to page-relative. |
| 28 | |
| 29 | .adjustY: the Y counterpart of adjustX. |
| 30 | |
| 31 | .init: optional callback called one time to initialize the |
| 32 | state of the tooltip. This is called after the this.e has |
| @@ -75,32 +76,38 @@ | |
| 75 | F.TooltipWidget.defaultOptions = { |
| 76 | cssClass: 'fossil-tooltip', |
| 77 | style: {/*properties copied as-is into element.style*/}, |
| 78 | adjustX: (x)=>x, |
| 79 | adjustY: (y)=>y, |
| 80 | refresh: function(hw){ |
| 81 | console.error("TooltipWidget refresh() option must be provided by the client."); |
| 82 | } |
| 83 | }; |
| 84 | |
| 85 | F.TooltipWidget.prototype = { |
| 86 | |
| 87 | isShown: function(){return !this.e.classList.contains('hidden')}, |
| 88 | |
| 89 | /** Calls the refresh() method of the options object. */ |
| 90 | refresh: function(){this.options.refresh.call(this)}, |
| 91 | |
| 92 | /** |
| 93 | Usages: |
| 94 | |
| 95 | (bool showIt) => hide it or reveal it at its last position. |
| 96 | |
| 97 | (x, y) => reveal/move it at/to the given |
| 98 | relative-to-the-viewport position, which will be adjusted to make |
| 99 | it page-relative. |
| 100 | |
| 101 | (DOM element) => reveal/move it ad/to a position based on the |
| 102 | the given element (adjusted slightly). |
| 103 | |
| 104 | For the latter two, this.options.adjustX() and adjustY() will |
| 105 | be called to adjust it further. |
| 106 | |
| @@ -132,12 +139,11 @@ | |
| 132 | } |
| 133 | D[showIt ? 'removeClass' : 'addClass'](this.e, 'hidden'); |
| 134 | if(x || y){ |
| 135 | this.e.style.left = x+"px"; |
| 136 | this.e.style.top = y+"px"; |
| 137 | //console.debug("TooltipWidget.show()", arguments, x, y); |
| 138 | } |
| 139 | return this; |
| 140 | } |
| 141 | }/*/F.TooltipWidget.prototype*/; |
| 142 | |
| 143 | })(window.fossil); |
| 144 |
| --- src/fossil.tooltip.js | |
| +++ src/fossil.tooltip.js | |
| @@ -8,25 +8,26 @@ | |
| 8 | |
| 9 | /** |
| 10 | Creates a new tooltip widget using the given options object. |
| 11 | |
| 12 | The options are available to clients after this returns via |
| 13 | theTooltip.options, and default values for any options not |
| 14 | provided are pulled from TooltipWidget.defaultOptions. |
| 15 | |
| 16 | Options: |
| 17 | |
| 18 | .refresh: required callback which is called whenever the tooltip |
| 19 | is revealed or moved. It must refresh the contents of the |
| 20 | tooltip, if needed, by applying the content to/within this.e, |
| 21 | which is the base DOM element for the tooltip. |
| 22 | |
| 23 | .adjustX: an optional callback which is called when the tooltip |
| 24 | is to be displayed at a given position and passed the X |
| 25 | viewport-relative coordinate. This routine must either return its |
| 26 | argument as-is or return an adjusted value. This API assumes that |
| 27 | clients give it viewport-relative coordinates, and it will take |
| 28 | care to translate those to page-relative. |
| 29 | |
| 30 | .adjustY: the Y counterpart of adjustX. |
| 31 | |
| 32 | .init: optional callback called one time to initialize the |
| 33 | state of the tooltip. This is called after the this.e has |
| @@ -75,32 +76,38 @@ | |
| 76 | F.TooltipWidget.defaultOptions = { |
| 77 | cssClass: 'fossil-tooltip', |
| 78 | style: {/*properties copied as-is into element.style*/}, |
| 79 | adjustX: (x)=>x, |
| 80 | adjustY: (y)=>y, |
| 81 | refresh: function(){ |
| 82 | console.error("The TooltipWidget refresh() option must be provided by the client."); |
| 83 | } |
| 84 | }; |
| 85 | |
| 86 | F.TooltipWidget.prototype = { |
| 87 | |
| 88 | isShown: function(){return !this.e.classList.contains('hidden')}, |
| 89 | |
| 90 | /** Calls the refresh() method of the options object and returns |
| 91 | this object. */ |
| 92 | refresh: function(){ |
| 93 | this.options.refresh.call(this); |
| 94 | return this; |
| 95 | }, |
| 96 | |
| 97 | /** |
| 98 | Shows or hides the tooltip. |
| 99 | |
| 100 | Usages: |
| 101 | |
| 102 | (bool showIt) => hide it or reveal it at its last position. |
| 103 | |
| 104 | (x, y) => reveal/move it at/to the given |
| 105 | relative-to-the-viewport position, which will be adjusted to make |
| 106 | it page-relative. |
| 107 | |
| 108 | (DOM element) => reveal/move it at/to a position based on the |
| 109 | the given element (adjusted slightly). |
| 110 | |
| 111 | For the latter two, this.options.adjustX() and adjustY() will |
| 112 | be called to adjust it further. |
| 113 | |
| @@ -132,12 +139,11 @@ | |
| 139 | } |
| 140 | D[showIt ? 'removeClass' : 'addClass'](this.e, 'hidden'); |
| 141 | if(x || y){ |
| 142 | this.e.style.left = x+"px"; |
| 143 | this.e.style.top = y+"px"; |
| 144 | } |
| 145 | return this; |
| 146 | } |
| 147 | }/*F.TooltipWidget.prototype*/; |
| 148 | |
| 149 | })(window.fossil); |
| 150 |