summaryrefslogtreecommitdiff
path: root/src/fossil.bootstrap.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/fossil.bootstrap.js')
-rw-r--r--src/fossil.bootstrap.js327
1 files changed, 327 insertions, 0 deletions
diff --git a/src/fossil.bootstrap.js b/src/fossil.bootstrap.js
new file mode 100644
index 0000000..3b3604f
--- /dev/null
+++ b/src/fossil.bootstrap.js
@@ -0,0 +1,327 @@
+"use strict";
+(function () {
+ /* CustomEvent polyfill, courtesy of Mozilla:
+ https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
+ */
+ if(typeof window.CustomEvent === "function") return false;
+ window.CustomEvent = function(event, params) {
+ if(!params) params = {bubbles: false, cancelable: false, detail: null};
+ const evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent( event, !!params.bubbles, !!params.cancelable, params.detail );
+ return evt;
+ };
+})();
+(function(global){
+ /* Bootstrapping bits for the global.fossil object. Must be loaded
+ after style.c:builtin_emit_script_fossil_bootstrap() has
+ initialized that object.
+ */
+
+ const F = global.fossil;
+
+ /** Creates a prototype-less plain object with properties derived
+ from all of its object-type arguments. */
+ F.nu = (...obj)=>Object.assign(Object.create(null),...obj);
+
+ /**
+ Returns the current time in something approximating
+ ISO-8601 format.
+ */
+ const timestring = function f(){
+ if(!f.rx1){
+ f.rx1 = /\.\d+Z$/;
+ }
+ const d = new Date();
+ return d.toISOString().replace(f.rx1,'').split('T').join(' ');
+ };
+
+ /** Returns the local time string of Date object d, defaulting
+ to the current time. */
+ const localTimeString = function ff(d){
+ if(!ff.pad){
+ ff.pad = (x)=>(''+x).length>1 ? x : '0'+x;
+ }
+ d || (d = new Date());
+ return [
+ d.getFullYear(),'-',ff.pad(d.getMonth()+1/*sigh*/),
+ '-',ff.pad(d.getDate()),
+ ' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()),
+ ':',ff.pad(d.getSeconds())
+ ].join('');
+ };
+
+ /*
+ ** By default fossil.message() sends its arguments console.debug(). If
+ ** fossil.message.targetElement is set, it is assumed to be a DOM
+ ** element, its innerText gets assigned to the concatenation of all
+ ** arguments (with a space between each), and the CSS 'error' class is
+ ** removed from the object. Pass it a falsy value to clear the target
+ ** element.
+ **
+ ** Returns this object.
+ */
+ F.message = function f(){
+ const args = Array.prototype.slice.call(arguments,0);
+ const tgt = f.targetElement;
+ if(args.length) args.unshift(
+ localTimeString()+':'
+ //timestring(),'UTC:'
+ );
+ if(tgt){
+ tgt.classList.remove('error');
+ tgt.innerText = args.join(' ');
+ }
+ else{
+ if(args.length){
+ args.unshift('Fossil status:');
+ console.debug.apply(console,args);
+ }
+ }
+ return this;
+ };
+ /*
+ ** Set default message.targetElement to #fossil-status-bar, if found.
+ */
+ F.message.targetElement =
+ document.querySelector('#fossil-status-bar');
+ if(F.message.targetElement){
+ F.message.targetElement.addEventListener(
+ 'dblclick', ()=>F.message(), false
+ );
+ }
+ /*
+ ** By default fossil.error() sends all arguments to
+ ** console.error(). If fossil.message.targetElement (yes,
+ ** fossil.message) is set, it adds the 'error' CSS class to
+ ** that element and sets its content as defined for message().
+ **
+ ** Returns this object.
+ */
+ F.error = function f(){
+ const args = Array.prototype.slice.call(arguments,0);
+ const tgt = F.message.targetElement;
+ args.unshift(timestring(),'UTC:');
+ if(tgt){
+ tgt.classList.add('error');
+ tgt.innerText = args.join(' ');
+ }else{
+ args.unshift('Fossil error:');
+ console.error.apply(console,args);
+ }
+ return this;
+ };
+
+ /**
+ For each property in the given object, its key/value are encoded
+ for use as URL parameters and the combined string is
+ returned. e.g. {a:1,b:2} encodes to "a=1&b=2".
+
+ If the 2nd argument is an array, each encoded element is appended
+ to that array and tgtArray is returned. The above object would be
+ appended as ['a','=','1','&','b','=','2']. This form is used for
+ building up parameter lists before join('')ing the array to create
+ the result string.
+
+ If passed a truthy 3rd argument, it does not really encode each
+ component - it simply concatenates them together.
+ */
+ F.encodeUrlArgs = function(obj,tgtArray,fakeEncode){
+ if(!obj) return '';
+ const a = (tgtArray instanceof Array) ? tgtArray : [],
+ enc = fakeEncode ? (x)=>x : encodeURIComponent;
+ let k, i = 0;
+ for( k in obj ){
+ if(i++) a.push('&');
+ a.push(enc(k),'=',enc(obj[k]));
+ }
+ return a===tgtArray ? a : a.join('');
+ };
+ /**
+ repoUrl( repoRelativePath [,urlParams] )
+
+ Creates a URL by prepending this.rootPath to the given path
+ (which must be relative from the top of the site, without a
+ leading slash). If urlParams is a string, it must be
+ parameters encoded in the form "key=val&key2=val2..." WITHOUT
+ a leading '?'. If it's an object, all of its properties get
+ appended to the URL in that form.
+ */
+ F.repoUrl = function(path,urlParams){
+ if(!urlParams) return this.rootPath+path;
+ const url=[this.rootPath,path];
+ url.push('?');
+ if('string'===typeof urlParams) url.push(urlParams);
+ else if(urlParams && 'object'===typeof urlParams){
+ this.encodeUrlArgs(urlParams, url);
+ }
+ return url.join('');
+ };
+
+ /**
+ Returns true if v appears to be a plain object.
+ */
+ F.isObject = function(v){
+ return v &&
+ (v instanceof Object) &&
+ ('[object Object]' === Object.prototype.toString.apply(v) );
+ };
+
+ /**
+ For each object argument, this function combines their properties,
+ using a last-one-wins policy, and returns a new object with the
+ combined properties. If passed a single object, it effectively
+ shallowly clones that object.
+ */
+ F.mergeLastWins = function(){
+ var k, o, i;
+ const n = arguments.length, rc={};
+ for(i = 0; i < n; ++i){
+ if(!F.isObject(o = arguments[i])) continue;
+ for( k in o ){
+ if(o.hasOwnProperty(k)) rc[k] = o[k];
+ }
+ }
+ return rc;
+ };
+
+ /**
+ Expects to be passed as hash code as its first argument. It
+ returns a "shortened" form of hash, with a length which depends
+ on the 2nd argument: truthy = fossil.config.hashDigitsUrl, falsy
+ = fossil.config.hashDigits, number == that many digits. The
+ fossil.config values are derived from the 'hash-digits'
+ repo-level config setting or the
+ FOSSIL_HASH_DIGITS_URL/FOSSIL_HASH_DIGITS compile-time options.
+
+ If its first arugment is a non-string, that value is returned
+ as-is.
+ */
+ F.hashDigits = function(hash,forUrl){
+ const n = ('number'===typeof forUrl)
+ ? forUrl : F.config[forUrl ? 'hashDigitsUrl' : 'hashDigits'];
+ return ('string'==typeof hash ? hash.substr(
+ 0, n
+ ) : hash);
+ };
+
+ /**
+ Convenience wrapper which adds an onload event listener to the
+ window object. Returns this.
+ */
+ F.onPageLoad = function(callback){
+ window.addEventListener('load', callback, false);
+ return this;
+ };
+
+ /**
+ Convenience wrapper which adds a DOMContentLoadedevent listener
+ to the window object. Returns this.
+ */
+ F.onDOMContentLoaded = function(callback){
+ window.addEventListener('DOMContentLoaded', callback, false);
+ return this;
+ };
+
+ /**
+ Assuming name is a repo-style filename, this function returns
+ a shortened form of that name:
+
+ .../LastDirectoryPart/FilenamePart
+
+ If the name has 0-1 directory parts, it is returned as-is.
+
+ Design note: in practice it is generally not helpful to elide the
+ *last* directory part because embedded docs (in particular) often
+ include x/y/index.md and x/z/index.md, both of which would be
+ shortened to something like x/.../index.md.
+ */
+ F.shortenFilename = function(name){
+ const a = name.split('/');
+ if(a.length<=2) return name;
+ while(a.length>2) a.shift();
+ return '.../'+a.join('/');
+ };
+
+ /**
+ Adds a listener for fossil-level custom events. Events are
+ delivered to their callbacks as CustomEvent objects with a
+ 'detail' property holding the event's app-level data.
+
+ The exact events fired differ by page, and not all pages trigger
+ events.
+
+ Pedantic sidebar: the custom event's 'target' property is an
+ unspecified DOM element. Clients must not rely on its value being
+ anything specific or useful.
+
+ Returns this object.
+ */
+ F.page.addEventListener = function f(eventName, callback){
+ if(!f.proxy){
+ f.proxy = document.createElement('span');
+ }
+ f.proxy.addEventListener(eventName, callback, false);
+ return this;
+ };
+
+ /**
+ Internal. Dispatches a new CustomEvent to all listeners
+ registered for the given eventName via
+ fossil.page.addEventListener(), passing on a new CustomEvent with
+ a 'detail' property equal to the 2nd argument. Returns this
+ object.
+ */
+ F.page.dispatchEvent = function(eventName, eventDetail){
+ if(this.addEventListener.proxy){
+ try{
+ this.addEventListener.proxy.dispatchEvent(
+ new CustomEvent(eventName,{detail: eventDetail})
+ );
+ }catch(e){
+ console.error(eventName,"event listener threw:",e);
+ }
+ }
+ return this;
+ };
+
+ /**
+ Sets the innerText of the page's TITLE tag to
+ the given text and returns this object.
+ */
+ F.page.setPageTitle = function(title){
+ const t = document.querySelector('title');
+ if(t) t.innerText = title;
+ return this;
+ };
+
+ /**
+ Returns a function, that, as long as it continues to be invoked,
+ will not be triggered. The function will be called after it stops
+ being called for N milliseconds. If `immediate` is passed, call
+ the callback immediately and hinder future invocations until at
+ least the given time has passed.
+
+ If passed only 1 argument, or passed a falsy 2nd argument,
+ the default wait time set in this function's $defaultDelay
+ property is used.
+
+ Inspiration: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
+ */
+ F.debounce = function f(func, waitMs, immediate) {
+ 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);
+ };
+ };
+ F.debounce.$defaultDelay = 500 /*arbitrary*/;
+
+})(window);