Fossil SCM

fossil-scm / src / fossil.bootstrap.js
Blame History Raw 325 lines
1
"use strict";
2
(function () {
3
/* CustomEvent polyfill, courtesy of Mozilla:
4
https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
5
*/
6
if(typeof window.CustomEvent === "function") return false;
7
window.CustomEvent = function(event, params) {
8
if(!params) params = {bubbles: false, cancelable: false, detail: null};
9
const evt = document.createEvent('CustomEvent');
10
evt.initCustomEvent( event, !!params.bubbles, !!params.cancelable, params.detail );
11
return evt;
12
};
13
})();
14
(function(global){
15
/* Bootstrapping bits for the global.fossil object. Must be loaded
16
after style.c:builtin_emit_script_fossil_bootstrap() has
17
initialized that object.
18
*/
19
20
const F = global.fossil;
21
22
/**
23
Returns the current time in something approximating
24
ISO-8601 format.
25
*/
26
const timestring = function f(){
27
if(!f.rx1){
28
f.rx1 = /\.\d+Z$/;
29
}
30
const d = new Date();
31
return d.toISOString().replace(f.rx1,'').split('T').join(' ');
32
};
33
34
/** Returns the local time string of Date object d, defaulting
35
to the current time. */
36
const localTimeString = function ff(d){
37
if(!ff.pad){
38
ff.pad = (x)=>(''+x).length>1 ? x : '0'+x;
39
}
40
d || (d = new Date());
41
return [
42
d.getFullYear(),'-',ff.pad(d.getMonth()+1/*sigh*/),
43
'-',ff.pad(d.getDate()),
44
' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()),
45
':',ff.pad(d.getSeconds())
46
].join('');
47
};
48
49
/*
50
** By default fossil.message() sends its arguments console.debug(). If
51
** fossil.message.targetElement is set, it is assumed to be a DOM
52
** element, its innerText gets assigned to the concatenation of all
53
** arguments (with a space between each), and the CSS 'error' class is
54
** removed from the object. Pass it a falsy value to clear the target
55
** element.
56
**
57
** Returns this object.
58
*/
59
F.message = function f(msg){
60
const args = Array.prototype.slice.call(arguments,0);
61
const tgt = f.targetElement;
62
if(args.length) args.unshift(
63
localTimeString()+':'
64
//timestring(),'UTC:'
65
);
66
if(tgt){
67
tgt.classList.remove('error');
68
tgt.innerText = args.join(' ');
69
}
70
else{
71
if(args.length){
72
args.unshift('Fossil status:');
73
console.debug.apply(console,args);
74
}
75
}
76
return this;
77
};
78
/*
79
** Set default message.targetElement to #fossil-status-bar, if found.
80
*/
81
F.message.targetElement =
82
document.querySelector('#fossil-status-bar');
83
if(F.message.targetElement){
84
F.message.targetElement.addEventListener(
85
'dblclick', ()=>F.message(), false
86
);
87
}
88
/*
89
** By default fossil.error() sends its first argument to
90
** console.error(). If fossil.message.targetElement (yes,
91
** fossil.message) is set, it adds the 'error' CSS class to
92
** that element and sets its content as defined for message().
93
**
94
** Returns this object.
95
*/
96
F.error = function f(msg){
97
const args = Array.prototype.slice.call(arguments,0);
98
const tgt = F.message.targetElement;
99
args.unshift(timestring(),'UTC:');
100
if(tgt){
101
tgt.classList.add('error');
102
tgt.innerText = args.join(' ');
103
}
104
else{
105
args.unshift('Fossil error:');
106
console.error.apply(console,args);
107
}
108
return this;
109
};
110
111
/**
112
For each property in the given object, its key/value are encoded
113
for use as URL parameters and the combined string is
114
returned. e.g. {a:1,b:2} encodes to "a=1&b=2".
115
116
If the 2nd argument is an array, each encoded element is appended
117
to that array and tgtArray is returned. The above object would be
118
appended as ['a','=','1','&','b','=','2']. This form is used for
119
building up parameter lists before join('')ing the array to create
120
the result string.
121
122
If passed a truthy 3rd argument, it does not really encode each
123
component - it simply concatenates them together.
124
*/
125
F.encodeUrlArgs = function(obj,tgtArray,fakeEncode){
126
if(!obj) return '';
127
const a = (tgtArray instanceof Array) ? tgtArray : [],
128
enc = fakeEncode ? (x)=>x : encodeURIComponent;
129
let k, i = 0;
130
for( k in obj ){
131
if(i++) a.push('&');
132
a.push(enc(k),'=',enc(obj[k]));
133
}
134
return a===tgtArray ? a : a.join('');
135
};
136
/**
137
repoUrl( repoRelativePath [,urlParams] )
138
139
Creates a URL by prepending this.rootPath to the given path
140
(which must be relative from the top of the site, without a
141
leading slash). If urlParams is a string, it must be
142
parameters encoded in the form "key=val&key2=val2..." WITHOUT
143
a leading '?'. If it's an object, all of its properties get
144
appended to the URL in that form.
145
*/
146
F.repoUrl = function(path,urlParams){
147
if(!urlParams) return this.rootPath+path;
148
const url=[this.rootPath,path];
149
url.push('?');
150
if('string'===typeof urlParams) url.push(urlParams);
151
else if(urlParams && 'object'===typeof urlParams){
152
this.encodeUrlArgs(urlParams, url);
153
}
154
return url.join('');
155
};
156
157
/**
158
Returns true if v appears to be a plain object.
159
*/
160
F.isObject = function(v){
161
return v &&
162
(v instanceof Object) &&
163
('[object Object]' === Object.prototype.toString.apply(v) );
164
};
165
166
/**
167
For each object argument, this function combines their properties,
168
using a last-one-wins policy, and returns a new object with the
169
combined properties. If passed a single object, it effectively
170
shallowly clones that object.
171
*/
172
F.mergeLastWins = function(){
173
var k, o, i;
174
const n = arguments.length, rc={};
175
for(i = 0; i < n; ++i){
176
if(!F.isObject(o = arguments[i])) continue;
177
for( k in o ){
178
if(o.hasOwnProperty(k)) rc[k] = o[k];
179
}
180
}
181
return rc;
182
};
183
184
/**
185
Expects to be passed as hash code as its first argument. It
186
returns a "shortened" form of hash, with a length which depends
187
on the 2nd argument: truthy = fossil.config.hashDigitsUrl, falsy
188
= fossil.config.hashDigits, number == that many digits. The
189
fossil.config values are derived from the 'hash-digits'
190
repo-level config setting or the
191
FOSSIL_HASH_DIGITS_URL/FOSSIL_HASH_DIGITS compile-time options.
192
193
If its first arugment is a non-string, that value is returned
194
as-is.
195
*/
196
F.hashDigits = function(hash,forUrl){
197
const n = ('number'===typeof forUrl)
198
? forUrl : F.config[forUrl ? 'hashDigitsUrl' : 'hashDigits'];
199
return ('string'==typeof hash ? hash.substr(
200
0, n
201
) : hash);
202
};
203
204
/**
205
Convenience wrapper which adds an onload event listener to the
206
window object. Returns this.
207
*/
208
F.onPageLoad = function(callback){
209
window.addEventListener('load', callback, false);
210
return this;
211
};
212
213
/**
214
Convenience wrapper which adds a DOMContentLoadedevent listener
215
to the window object. Returns this.
216
*/
217
F.onDOMContentLoaded = function(callback){
218
window.addEventListener('DOMContentLoaded', callback, false);
219
return this;
220
};
221
222
/**
223
Assuming name is a repo-style filename, this function returns
224
a shortened form of that name:
225
226
.../LastDirectoryPart/FilenamePart
227
228
If the name has 0-1 directory parts, it is returned as-is.
229
230
Design note: in practice it is generally not helpful to elide the
231
*last* directory part because embedded docs (in particular) often
232
include x/y/index.md and x/z/index.md, both of which would be
233
shortened to something like x/.../index.md.
234
*/
235
F.shortenFilename = function(name){
236
const a = name.split('/');
237
if(a.length<=2) return name;
238
while(a.length>2) a.shift();
239
return '.../'+a.join('/');
240
};
241
242
/**
243
Adds a listener for fossil-level custom events. Events are
244
delivered to their callbacks as CustomEvent objects with a
245
'detail' property holding the event's app-level data.
246
247
The exact events fired differ by page, and not all pages trigger
248
events.
249
250
Pedantic sidebar: the custom event's 'target' property is an
251
unspecified DOM element. Clients must not rely on its value being
252
anything specific or useful.
253
254
Returns this object.
255
*/
256
F.page.addEventListener = function f(eventName, callback){
257
if(!f.proxy){
258
f.proxy = document.createElement('span');
259
}
260
f.proxy.addEventListener(eventName, callback, false);
261
return this;
262
};
263
264
/**
265
Internal. Dispatches a new CustomEvent to all listeners
266
registered for the given eventName via
267
fossil.page.addEventListener(), passing on a new CustomEvent with
268
a 'detail' property equal to the 2nd argument. Returns this
269
object.
270
*/
271
F.page.dispatchEvent = function(eventName, eventDetail){
272
if(this.addEventListener.proxy){
273
try{
274
this.addEventListener.proxy.dispatchEvent(
275
new CustomEvent(eventName,{detail: eventDetail})
276
);
277
}catch(e){
278
console.error(eventName,"event listener threw:",e);
279
}
280
}
281
return this;
282
};
283
284
/**
285
Sets the innerText of the page's TITLE tag to
286
the given text and returns this object.
287
*/
288
F.page.setPageTitle = function(title){
289
const t = document.querySelector('title');
290
if(t) t.innerText = title;
291
return this;
292
};
293
294
/**
295
Returns a function, that, as long as it continues to be invoked,
296
will not be triggered. The function will be called after it stops
297
being called for N milliseconds. If `immediate` is passed, call
298
the callback immediately and hinder future invocations until at
299
least the given time has passed.
300
301
If passed only 1 argument, or passed a falsy 2nd argument,
302
the default wait time set in this function's $defaultDelay
303
property is used.
304
305
Inspiration: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
306
*/
307
F.debounce = function f(func, waitMs, immediate) {
308
var timeoutId;
309
if(!waitMs) waitMs = f.$defaultDelay;
310
return function() {
311
const context = this, args = Array.prototype.slice.call(arguments);
312
const later = function() {
313
timeoutId = undefined;
314
if(!immediate) func.apply(context, args);
315
};
316
const callNow = immediate && !timeoutId;
317
clearTimeout(timeoutId);
318
timeoutId = setTimeout(later, waitMs);
319
if(callNow) func.apply(context, args);
320
};
321
};
322
F.debounce.$defaultDelay = 500 /*arbitrary*/;
323
324
})(window);
325

Keyboard Shortcuts

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