Fossil SCM

Optimized the mouse-based line number selection considerably, requiring far less DOM traversal.

stephan 2020-08-15 08:29 line-number-selection
Commit 0096aa4644dfde8648fc0d191be35ba0fb97ee4ed7b3d07c29d662a2fd8dad13
--- src/fossil.numbered-lines.js
+++ src/fossil.numbered-lines.js
@@ -42,12 +42,10 @@
4242
);
4343
}else{
4444
D.append(link, "No lines selected.");
4545
}
4646
},
47
- adjustX: (x)=>x,
48
- adjustY: (y)=>y,
4947
init: function(){
5048
const e = this.e;
5149
const btnCopy = D.span(),
5250
link = D.span();
5351
this.state = {link};
@@ -65,52 +63,55 @@
6563
6664
tdLn.addEventListener('click', function f(ev){
6765
if('SPAN'!==ev.target.tagName) return;
6866
else if('number' !== typeof f.mode){
6967
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');
7071
}
7172
ev.stopPropagation();
7273
const ln = +ev.target.innerText;
73
- if(2===f.mode){/*reset selection*/
74
+ if(2===f.mode){/*Reset selection*/
7475
f.mode = 0;
7576
}
76
- if(0===f.mode){
77
+ if(0===f.mode){/*Select single line*/
7778
lineState.end = 0;
7879
lineState.start = ln;
7980
f.mode = 1;
8081
}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*/
8383
lineState.start = 0;
8484
f.mode = 0;
85
- }else{
85
+ }else{/*Select range*/
8686
if(ln<lineState.start){
8787
lineState.end = lineState.start;
8888
lineState.start = ln;
8989
}else{
9090
lineState.end = ln;
9191
}
92
- //console.debug("Selected range: ",rng);
9392
f.mode = 2;
9493
}
9594
}
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*/
99100
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++] ){
105105
span.classList.add('selected-line');
106
+ f.selected.push(span);
106107
if(i===lineState.start) span.classList.add('start');
107108
if(i===end) span.classList.add('end');
108109
}
109110
}
110
- lineTip.refresh();
111
+ lineTip.refresh().show(rect.right+3, rect.top-4);
111112
}else{
112113
lineTip.show(false);
113114
}
114115
}, false);
115116
116117
})();
117118
--- 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
--- src/fossil.tooltip.js
+++ src/fossil.tooltip.js
@@ -8,25 +8,26 @@
88
99
/**
1010
Creates a new tooltip widget using the given options object.
1111
1212
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.
1415
1516
Options:
1617
1718
.refresh: required callback which is called whenever the tooltip
1819
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.
2122
2223
.adjustX: an optional callback which is called when the tooltip
2324
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.
2829
2930
.adjustY: the Y counterpart of adjustX.
3031
3132
.init: optional callback called one time to initialize the
3233
state of the tooltip. This is called after the this.e has
@@ -75,32 +76,38 @@
7576
F.TooltipWidget.defaultOptions = {
7677
cssClass: 'fossil-tooltip',
7778
style: {/*properties copied as-is into element.style*/},
7879
adjustX: (x)=>x,
7980
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.");
8283
}
8384
};
8485
8586
F.TooltipWidget.prototype = {
8687
8788
isShown: function(){return !this.e.classList.contains('hidden')},
8889
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
+ },
9196
9297
/**
98
+ Shows or hides the tooltip.
99
+
93100
Usages:
94101
95102
(bool showIt) => hide it or reveal it at its last position.
96103
97104
(x, y) => reveal/move it at/to the given
98105
relative-to-the-viewport position, which will be adjusted to make
99106
it page-relative.
100107
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
102109
the given element (adjusted slightly).
103110
104111
For the latter two, this.options.adjustX() and adjustY() will
105112
be called to adjust it further.
106113
@@ -132,12 +139,11 @@
132139
}
133140
D[showIt ? 'removeClass' : 'addClass'](this.e, 'hidden');
134141
if(x || y){
135142
this.e.style.left = x+"px";
136143
this.e.style.top = y+"px";
137
- //console.debug("TooltipWidget.show()", arguments, x, y);
138144
}
139145
return this;
140146
}
141
- }/*/F.TooltipWidget.prototype*/;
147
+ }/*F.TooltipWidget.prototype*/;
142148
143149
})(window.fossil);
144150
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button