1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
|
/**
JS code for link-tester.html. We cannot host this JS inline in that
file because fossil's default Content Security Policy won't let it
run that way.
*/
window.addEventListener("DOMContentLoaded", function(){
const E = function(s){
const e = document.querySelector(s);
if( !e ) throw new Error("Missing element: "+s);
return e;
};
const EAll = function(s){
const e = document.querySelectorAll(s);
if( !e || !e.length ) throw new Error("Missing elements: "+s);
return e;
};
const eIframe = E('#iframe');
const eSelect = E('#selectPage');
const eCurrentUrl = E('#currentUrl');
/*
Prepend the fossil instance's URL to each link. We have to guess
which part of the URL is the fossil CGI/server instance. The
following works when run (A) from under /uv or /ext and (B) from
/doc/branchname/test/link-tester.html.
*/
let urlTop;
let loc = (''+window.location);
let aLoc = loc.split('/')
aLoc.pop(); /* file name */
const thisDir = aLoc.join('/');
const rxDoc = /.*\/doc\/[^/]+\/.*/;
//console.log(rxDoc, loc, aLoc);
if( loc.match(rxDoc) ){
/* We're hopefully now at the top-most fossil-served
URL. */
aLoc.pop(); aLoc.pop(); /* /doc/foo */
aLoc.pop(); /* current dir name */
}else{
aLoc.pop(); /* current dir name */
}
urlTop = aLoc.join('/');
//console.log(urlTop, aLoc);
for( const o of eSelect.options ){
o.value = urlTop + (o.value || o.innerText);
}
const updateUrl = function(opt){
if( opt ){
let url = (opt.value || opt.innerText);
eCurrentUrl.innerText = url.replace(urlTop,'');
eCurrentUrl.setAttribute('href', url);
}else{
eCurrentUrl.innerText = '';
}
};
eSelect.addEventListener('change',function(ev){
const so = ev.target.options[ev.target.selectedIndex];
if( so ){
eIframe.setAttribute('src', so.value || so.innerText);
updateUrl(so);
}
});
/** Select the entry at the given ndx and fire a change event. */
const selectEntry = function(ndx){
if( ndx>=0 ){
eSelect.selectedIndex = ndx;
eSelect.dispatchEvent(new Event('change',{target:eSelect}));
}
};
/* Cycle to the next link in the list, accounting for separators and
wrapping around at either end. */
const cycleLink = function(dir/*<0 = prev, >0 = next*/){
let n = eSelect.selectedIndex + dir;
if( n < 0 ) n = eSelect.options.length-1;
else if( n>=eSelect.options.length ){
n = 0;
}
const opt = eSelect.options[n];
if( opt && opt.disabled ){
/* If that OPTION element is disabled, skip over it. */
eSelect.selectedIndex = n;
cycleLink(dir);
}else{
selectEntry(n);
}
};
E('#btn-prev').addEventListener('click', ()=>cycleLink(-1), false);
E('#btn-next').addEventListener('click', ()=>cycleLink(1), false);
/**
We have to adjust the iframe's size dynamically to account for
other widgets around it. iframes don't simply like to fill up all
available space without some help. If #controlWrapper only
contained the one SELECT element, CSS would be sufficient, but
once we add text around it, #controlWrapper's size becomes
unpredictable and we need JS to calculate it. We do this every
time the window size changes.
*/
const effectiveHeight = function f(e){
// Copied from fossil.dom.js
if(!e) return 0;
if(!f.measure){
f.measure = function callee(e, depth){
if(!e) return;
const m = e.getBoundingClientRect();
if(0===depth){
callee.top = m.top;
callee.bottom = m.bottom;
}else{
callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
callee.bottom = Math.max(callee.bottom, m.bottom);
}
Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
if(0===depth){
//console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
f.extra += callee.bottom - callee.top;
}
return f.extra;
};
}
f.extra = 0;
f.measure(e,0);
return f.extra;
};
/* Helper for the window-resized event handler below, to avoid
handling the resize until after it's finished. */
const debounce = function f(func, waitMs, immediate) {
// Copied from fossil.bootstrap.js
var timeoutId;
if(!waitMs) waitMs = f.$defaultDelay;
return function() {
const context = this, args = Array.prototype.slice.call(arguments);
const later = function() {
timeoutId = undefined;
if(!immediate) func.apply(context, args);
};
const callNow = immediate && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(later, waitMs);
if(callNow) func.apply(context, args);
};
};
/**
Resize eConstrained (the ifame element) so that it fits within
the page space not occupied by the list of elements eToAvoid.
*/
const ForceResizeKludge = (function(eToAvoid, eConstrained){
const resized = function f(){
if( f.$disabled ) return;
const wh = window.innerHeight;
let ht;
let extra = 0;
eToAvoid.forEach((e)=>e ? extra += effectiveHeight(e) : false);
ht = wh - extra;
if( ht < 100 ) ht = 100;
eConstrained.style.top = 'calc('+extra+'px + 2em)';
eConstrained.style.height =
eConstrained.style.maxHeight = "calc("+ ht+ "px - 2em)";
};
resized.$disabled = true/* gets deleted later */;
window.addEventListener('resize', debounce(resized, 250), false);
return resized;
})(
EAll('body > *:not(iframe)'),
eIframe
);
delete ForceResizeKludge.$disabled;
ForceResizeKludge();
selectEntry(0);
/**
Read link-tester.json, which should live in the same directory
as this file. It's expected to be an array with entries
in one of the following forms:
- "string" = Separator label (disabled)
- ["/path"] = path with itself as a label
- ["label", "/path"] = path with the given label
All paths are expected to have a "/" prefix and this script
accounts for mapping that to the fossil part of this script's
URL.
*/
window.fetch(thisDir+'/link-tester.json').then((r)=>r.json()).then(j=>{
//console.log("fetched",j);
eSelect.innerHTML = '';
const opt = function(arg){
const o = document.createElement('option');
//console.warn(arguments);
let rc = true;
if( 'string' === typeof arg ){
/* Grouping separator */
o.innerText = "--- " + arg + " ---";
o.setAttribute('disabled','');
rc = false;
}else if( 1===arg.length ){
o.innerText = arg[0];
o.value = urlTop + arg[0];
}else if( 2==arg.length ){
o.innerText = arg[0];
o.value = urlTop + arg[1];
}
eSelect.appendChild(o);
return rc;
};
let ndx = -1/*index of first non-disabled entry*/, i = 0;
for(const e of j){
if( opt(e) && ndx<0 ){
ndx = i;
}
++i;
}
selectEntry(ndx);
});
});
|