diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/fossil.bootstrap.js | |
| download | relic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz | |
Diffstat (limited to 'src/fossil.bootstrap.js')
| -rw-r--r-- | src/fossil.bootstrap.js | 327 |
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); |
