|
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
|
|