decaffeinate: Convert app.coffee and 75 other files to JS

pull/1441/head
decaffeinate 1 year ago committed by Simon Legner
parent 6cc430ffc4
commit e4fbca722b

@ -1,283 +1,352 @@
@app = /*
_$: $ * decaffeinate suggestions:
_$$: $$ * DS101: Remove unnecessary use of Array.from
_page: page * DS102: Remove unnecessary code created because of implicit returns
collections: {} * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
models: {} * DS207: Consider shorter variations of null checks
templates: {} * DS208: Avoid top-level this
views: {} * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
init: -> this.app = {
try @initErrorTracking() catch _$: $,
return unless @browserCheck() _$$: $$,
_page: page,
@el = $('._app') collections: {},
@localStorage = new LocalStorageStore models: {},
@serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled() templates: {},
@settings = new app.Settings views: {},
@db = new app.DB()
init() {
@settings.initLayout() try { this.initErrorTracking(); } catch (error) {}
if (!this.browserCheck()) { return; }
@docs = new app.collections.Docs
@disabledDocs = new app.collections.Docs this.el = $('._app');
@entries = new app.collections.Entries this.localStorage = new LocalStorageStore;
if (app.ServiceWorker.isEnabled()) { this.serviceWorker = new app.ServiceWorker; }
@router = new app.Router this.settings = new app.Settings;
@shortcuts = new app.Shortcuts this.db = new app.DB();
@document = new app.views.Document
@mobile = new app.views.Mobile if @isMobile() this.settings.initLayout();
if document.body.hasAttribute('data-doc') this.docs = new app.collections.Docs;
@DOC = JSON.parse(document.body.getAttribute('data-doc')) this.disabledDocs = new app.collections.Docs;
@bootOne() this.entries = new app.collections.Entries;
else if @DOCS
@bootAll() this.router = new app.Router;
else this.shortcuts = new app.Shortcuts;
@onBootError() this.document = new app.views.Document;
return if (this.isMobile()) { this.mobile = new app.views.Mobile; }
browserCheck: -> if (document.body.hasAttribute('data-doc')) {
return true if @isSupportedBrowser() this.DOC = JSON.parse(document.body.getAttribute('data-doc'));
document.body.innerHTML = app.templates.unsupportedBrowser this.bootOne();
@hideLoadingScreen() } else if (this.DOCS) {
false this.bootAll();
} else {
initErrorTracking: -> this.onBootError();
# Show a warning message and don't track errors when the app is loaded }
# from a domain other than our own, because things are likely to break. },
# (e.g. cross-domain requests)
if @isInvalidLocation() browserCheck() {
new app.views.Notif 'InvalidLocation' if (this.isSupportedBrowser()) { return true; }
else document.body.innerHTML = app.templates.unsupportedBrowser;
if @config.sentry_dsn this.hideLoadingScreen();
Raven.config @config.sentry_dsn, return false;
release: @config.release },
whitelistUrls: [/devdocs/]
includePaths: [/devdocs/] initErrorTracking() {
ignoreErrors: [/NPObject/, /NS_ERROR/, /^null$/, /EvalError/] // Show a warning message and don't track errors when the app is loaded
tags: // from a domain other than our own, because things are likely to break.
mode: if @isSingleDoc() then 'single' else 'full' // (e.g. cross-domain requests)
iframe: (window.top isnt window).toString() if (this.isInvalidLocation()) {
electron: (!!window.process?.versions?.electron).toString() new app.views.Notif('InvalidLocation');
shouldSendCallback: => } else {
try if (this.config.sentry_dsn) {
if @isInjectionError() Raven.config(this.config.sentry_dsn, {
@onInjectionError() release: this.config.release,
return false whitelistUrls: [/devdocs/],
if @isAndroidWebview() includePaths: [/devdocs/],
return false ignoreErrors: [/NPObject/, /NS_ERROR/, /^null$/, /EvalError/],
true tags: {
dataCallback: (data) -> mode: this.isSingleDoc() ? 'single' : 'full',
try iframe: (window.top !== window).toString(),
$.extend(data.user ||= {}, app.settings.dump()) electron: (!!__guard__(window.process != null ? window.process.versions : undefined, x => x.electron)).toString()
data.user.docs = data.user.docs.split('/') if data.user.docs },
data.user.lastIDBTransaction = app.lastIDBTransaction if app.lastIDBTransaction shouldSendCallback: () => {
data.tags.scriptCount = document.scripts.length try {
data if (this.isInjectionError()) {
.install() this.onInjectionError();
@previousErrorHandler = onerror return false;
window.onerror = @onWindowError.bind(@) }
CookiesStore.onBlocked = @onCookieBlocked if (this.isAndroidWebview()) {
return return false;
}
bootOne: -> } catch (error) {}
@doc = new app.models.Doc @DOC return true;
@docs.reset [@doc] },
@doc.load @start.bind(@), @onBootError.bind(@), readCache: true dataCallback(data) {
new app.views.Notice 'singleDoc', @doc try {
delete @DOC $.extend(data.user || (data.user = {}), app.settings.dump());
return if (data.user.docs) { data.user.docs = data.user.docs.split('/'); }
if (app.lastIDBTransaction) { data.user.lastIDBTransaction = app.lastIDBTransaction; }
bootAll: -> data.tags.scriptCount = document.scripts.length;
docs = @settings.getDocs() } catch (error) {}
for doc in @DOCS return data;
(if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc) }
@migrateDocs() }).install();
@docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true }
delete @DOCS this.previousErrorHandler = onerror;
return window.onerror = this.onWindowError.bind(this);
CookiesStore.onBlocked = this.onCookieBlocked;
start: -> }
@entries.add doc.toEntry() for doc in @docs.all() },
@entries.add doc.toEntry() for doc in @disabledDocs.all()
@initDoc(doc) for doc in @docs.all() bootOne() {
@trigger 'ready' this.doc = new app.models.Doc(this.DOC);
@router.start() this.docs.reset([this.doc]);
@hideLoadingScreen() this.doc.load(this.start.bind(this), this.onBootError.bind(this), {readCache: true});
setTimeout => new app.views.Notice('singleDoc', this.doc);
@welcomeBack() unless @doc delete this.DOC;
@removeEvent 'ready bootError' },
, 50
return bootAll() {
const docs = this.settings.getDocs();
initDoc: (doc) -> for (var doc of Array.from(this.DOCS)) {
doc.entries.add type.toEntry() for type in doc.types.all() (docs.indexOf(doc.slug) >= 0 ? this.docs : this.disabledDocs).add(doc);
@entries.add doc.entries.all() }
return this.migrateDocs();
this.docs.load(this.start.bind(this), this.onBootError.bind(this), {readCache: true, writeCache: true});
migrateDocs: -> delete this.DOCS;
for slug in @settings.getDocs() when not @docs.findBy('slug', slug) },
needsSaving = true
doc = @disabledDocs.findBy('slug', 'webpack') if slug == 'webpack~2' start() {
doc = @disabledDocs.findBy('slug', 'angular') if slug == 'angular~4_typescript' let doc;
doc = @disabledDocs.findBy('slug', 'angular~2') if slug == 'angular~2_typescript' for (doc of Array.from(this.docs.all())) { this.entries.add(doc.toEntry()); }
doc ||= @disabledDocs.findBy('slug_without_version', slug) for (doc of Array.from(this.disabledDocs.all())) { this.entries.add(doc.toEntry()); }
if doc for (doc of Array.from(this.docs.all())) { this.initDoc(doc); }
@disabledDocs.remove(doc) this.trigger('ready');
@docs.add(doc) this.router.start();
this.hideLoadingScreen();
@saveDocs() if needsSaving setTimeout(() => {
return if (!this.doc) { this.welcomeBack(); }
return this.removeEvent('ready bootError');
enableDoc: (doc, _onSuccess, onError) -> }
return if @docs.contains(doc) , 50);
},
onSuccess = =>
return if @docs.contains(doc) initDoc(doc) {
@disabledDocs.remove(doc) for (var type of Array.from(doc.types.all())) { doc.entries.add(type.toEntry()); }
@docs.add(doc) this.entries.add(doc.entries.all());
@docs.sort() },
@initDoc(doc)
@saveDocs() migrateDocs() {
if app.settings.get('autoInstall') let needsSaving;
doc.install(_onSuccess, onError) for (var slug of Array.from(this.settings.getDocs())) {
else if (!this.docs.findBy('slug', slug)) {var doc;
_onSuccess()
return needsSaving = true;
if (slug === 'webpack~2') { doc = this.disabledDocs.findBy('slug', 'webpack'); }
doc.load onSuccess, onError, writeCache: true if (slug === 'angular~4_typescript') { doc = this.disabledDocs.findBy('slug', 'angular'); }
return if (slug === 'angular~2_typescript') { doc = this.disabledDocs.findBy('slug', 'angular~2'); }
if (!doc) { doc = this.disabledDocs.findBy('slug_without_version', slug); }
saveDocs: -> if (doc) {
@settings.setDocs(doc.slug for doc in @docs.all()) this.disabledDocs.remove(doc);
@db.migrate() this.docs.add(doc);
@serviceWorker?.updateInBackground() }
}
welcomeBack: -> }
visitCount = @settings.get('count')
@settings.set 'count', ++visitCount if (needsSaving) { this.saveDocs(); }
new app.views.Notif 'Share', autoHide: null if visitCount is 5 },
new app.views.News()
new app.views.Updates() enableDoc(doc, _onSuccess, onError) {
@updateChecker = new app.UpdateChecker() if (this.docs.contains(doc)) { return; }
reboot: -> const onSuccess = () => {
if location.pathname isnt '/' and location.pathname isnt '/settings' if (this.docs.contains(doc)) { return; }
window.location = "/##{location.pathname}" this.disabledDocs.remove(doc);
else this.docs.add(doc);
window.location = '/' this.docs.sort();
return this.initDoc(doc);
this.saveDocs();
reload: -> if (app.settings.get('autoInstall')) {
@docs.clearCache() doc.install(_onSuccess, onError);
@disabledDocs.clearCache() } else {
if @serviceWorker then @serviceWorker.reload() else @reboot() _onSuccess();
return }
};
reset: ->
@localStorage.reset() doc.load(onSuccess, onError, {writeCache: true});
@settings.reset() },
@db?.reset()
@serviceWorker?.update() saveDocs() {
window.location = '/' this.settings.setDocs(Array.from(this.docs.all()).map((doc) => doc.slug));
return this.db.migrate();
return (this.serviceWorker != null ? this.serviceWorker.updateInBackground() : undefined);
showTip: (tip) -> },
return if @isSingleDoc()
tips = @settings.getTips() welcomeBack() {
if tips.indexOf(tip) is -1 let visitCount = this.settings.get('count');
tips.push(tip) this.settings.set('count', ++visitCount);
@settings.setTips(tips) if (visitCount === 5) { new app.views.Notif('Share', {autoHide: null}); }
new app.views.Tip(tip) new app.views.News();
return new app.views.Updates();
return this.updateChecker = new app.UpdateChecker();
hideLoadingScreen: -> },
document.body.classList.add '_overlay-scrollbars' if $.overlayScrollbarsEnabled()
document.documentElement.classList.remove '_booting' reboot() {
return if ((location.pathname !== '/') && (location.pathname !== '/settings')) {
window.location = `/#${location.pathname}`;
indexHost: -> } else {
# Can't load the index files from the host/CDN when service worker is window.location = '/';
# enabled because it doesn't support caching URLs that use CORS. }
@config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin'] },
onBootError: (args...) -> reload() {
@trigger 'bootError' this.docs.clearCache();
@hideLoadingScreen() this.disabledDocs.clearCache();
return if (this.serviceWorker) { this.serviceWorker.reload(); } else { this.reboot(); }
},
onQuotaExceeded: ->
return if @quotaExceeded reset() {
@quotaExceeded = true this.localStorage.reset();
new app.views.Notif 'QuotaExceeded', autoHide: null this.settings.reset();
return if (this.db != null) {
this.db.reset();
onCookieBlocked: (key, value, actual) -> }
return if @cookieBlocked if (this.serviceWorker != null) {
@cookieBlocked = true this.serviceWorker.update();
new app.views.Notif 'CookieBlocked', autoHide: null }
Raven.captureMessage "CookieBlocked/#{key}", level: 'warning', extra: {value, actual} window.location = '/';
return },
onWindowError: (args...) -> showTip(tip) {
return if @cookieBlocked if (this.isSingleDoc()) { return; }
if @isInjectionError args... const tips = this.settings.getTips();
@onInjectionError() if (tips.indexOf(tip) === -1) {
else if @isAppError args... tips.push(tip);
@previousErrorHandler? args... this.settings.setTips(tips);
@hideLoadingScreen() new app.views.Tip(tip);
@errorNotif or= new app.views.Notif 'Error' }
@errorNotif.show() },
return
hideLoadingScreen() {
onInjectionError: -> if ($.overlayScrollbarsEnabled()) { document.body.classList.add('_overlay-scrollbars'); }
unless @injectionError document.documentElement.classList.remove('_booting');
@injectionError = true },
alert """
indexHost() {
// Can't load the index files from the host/CDN when service worker is
// enabled because it doesn't support caching URLs that use CORS.
return this.config[this.serviceWorker && this.settings.hasDocs() ? 'index_path' : 'docs_origin'];
},
onBootError(...args) {
this.trigger('bootError');
this.hideLoadingScreen();
},
onQuotaExceeded() {
if (this.quotaExceeded) { return; }
this.quotaExceeded = true;
new app.views.Notif('QuotaExceeded', {autoHide: null});
},
onCookieBlocked(key, value, actual) {
if (this.cookieBlocked) { return; }
this.cookieBlocked = true;
new app.views.Notif('CookieBlocked', {autoHide: null});
Raven.captureMessage(`CookieBlocked/${key}`, {level: 'warning', extra: {value, actual}});
},
onWindowError(...args) {
if (this.cookieBlocked) { return; }
if (this.isInjectionError(...Array.from(args || []))) {
this.onInjectionError();
} else if (this.isAppError(...Array.from(args || []))) {
if (typeof this.previousErrorHandler === 'function') {
this.previousErrorHandler(...Array.from(args || []));
}
this.hideLoadingScreen();
if (!this.errorNotif) { this.errorNotif = new app.views.Notif('Error'); }
this.errorNotif.show();
}
},
onInjectionError() {
if (!this.injectionError) {
this.injectionError = true;
alert(`\
JavaScript code has been injected in the page which prevents DevDocs from running correctly. JavaScript code has been injected in the page which prevents DevDocs from running correctly.
Please check your browser extensions/addons. """ Please check your browser extensions/addons. `
Raven.captureMessage 'injection error', level: 'info' );
return Raven.captureMessage('injection error', {level: 'info'});
}
isInjectionError: -> },
# Some browser extensions expect the entire web to use jQuery.
# I gave up trying to fight back. isInjectionError() {
window.$ isnt app._$ or window.$$ isnt app._$$ or window.page isnt app._page or typeof $.empty isnt 'function' or typeof page.show isnt 'function' // Some browser extensions expect the entire web to use jQuery.
// I gave up trying to fight back.
isAppError: (error, file) -> return (window.$ !== app._$) || (window.$$ !== app._$$) || (window.page !== app._page) || (typeof $.empty !== 'function') || (typeof page.show !== 'function');
# Ignore errors from external scripts. },
file and file.indexOf('devdocs') isnt -1 and file.indexOf('.js') is file.length - 3
isAppError(error, file) {
isSupportedBrowser: -> // Ignore errors from external scripts.
try return file && (file.indexOf('devdocs') !== -1) && (file.indexOf('.js') === (file.length - 3));
features = },
bind: !!Function::bind
pushState: !!history.pushState isSupportedBrowser() {
matchMedia: !!window.matchMedia try {
insertAdjacentHTML: !!document.body.insertAdjacentHTML const features = {
defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false bind: !!Function.prototype.bind,
cssVariables: !!CSS?.supports?('(--t: 0)') pushState: !!history.pushState,
matchMedia: !!window.matchMedia,
for key, value of features when not value insertAdjacentHTML: !!document.body.insertAdjacentHTML,
Raven.captureMessage "unsupported/#{key}", level: 'info' defaultPrevented: document.createEvent('CustomEvent').defaultPrevented === false,
return false cssVariables: !!__guardMethod__(CSS, 'supports', o => o.supports('(--t: 0)'))
};
true
catch error for (var key in features) {
Raven.captureMessage 'unsupported/exception', level: 'info', extra: { error: error } var value = features[key];
false if (!value) {
Raven.captureMessage(`unsupported/${key}`, {level: 'info'});
isSingleDoc: -> return false;
document.body.hasAttribute('data-doc') }
}
isMobile: ->
@_isMobile ?= app.views.Mobile.detect() return true;
} catch (error) {
isAndroidWebview: -> Raven.captureMessage('unsupported/exception', {level: 'info', extra: { error }});
@_isAndroidWebview ?= app.views.Mobile.detectAndroidWebview() return false;
}
isInvalidLocation: -> },
@config.env is 'production' and location.host.indexOf(app.config.production_host) isnt 0
isSingleDoc() {
$.extend app, Events return document.body.hasAttribute('data-doc');
},
isMobile() {
return this._isMobile != null ? this._isMobile : (this._isMobile = app.views.Mobile.detect());
},
isAndroidWebview() {
return this._isAndroidWebview != null ? this._isAndroidWebview : (this._isAndroidWebview = app.views.Mobile.detectAndroidWebview());
},
isInvalidLocation() {
return (this.config.env === 'production') && (location.host.indexOf(app.config.production_host) !== 0);
}
};
$.extend(app, Events);
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}
function __guardMethod__(obj, methodName, transform) {
if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') {
return transform(obj, methodName);
} else {
return undefined;
}
}

@ -1,382 +1,486 @@
class app.DB /*
NAME = 'docs' * decaffeinate suggestions:
VERSION = 15 * DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
constructor: -> * DS205: Consider reworking code to avoid use of IIFEs
@versionMultipler = if $.isIE() then 1e5 else 1e9 * DS206: Consider reworking classes to avoid initClass
@useIndexedDB = @useIndexedDB() * DS207: Consider shorter variations of null checks
@callbacks = [] * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
db: (fn) -> (function() {
return fn() unless @useIndexedDB let NAME = undefined;
@callbacks.push(fn) if fn let VERSION = undefined;
return if @open const Cls = (app.DB = class DB {
static initClass() {
try NAME = 'docs';
@open = true VERSION = 15;
req = indexedDB.open(NAME, VERSION * @versionMultipler + @userVersion()) }
req.onsuccess = @onOpenSuccess
req.onerror = @onOpenError constructor() {
req.onupgradeneeded = @onUpgradeNeeded this.onOpenSuccess = this.onOpenSuccess.bind(this);
catch error this.onOpenError = this.onOpenError.bind(this);
@fail 'exception', error this.checkForCorruptedDocs = this.checkForCorruptedDocs.bind(this);
return this.deleteCorruptedDocs = this.deleteCorruptedDocs.bind(this);
this.versionMultipler = $.isIE() ? 1e5 : 1e9;
onOpenSuccess: (event) => this.useIndexedDB = this.useIndexedDB();
db = event.target.result this.callbacks = [];
}
if db.objectStoreNames.length is 0
try db.close() db(fn) {
@open = false if (!this.useIndexedDB) { return fn(); }
@fail 'empty' if (fn) { this.callbacks.push(fn); }
else if error = @buggyIDB(db) if (this.open) { return; }
try db.close()
@open = false try {
@fail 'buggy', error this.open = true;
else const req = indexedDB.open(NAME, (VERSION * this.versionMultipler) + this.userVersion());
@runCallbacks(db) req.onsuccess = this.onOpenSuccess;
@open = false req.onerror = this.onOpenError;
db.close() req.onupgradeneeded = this.onUpgradeNeeded;
return } catch (error) {
this.fail('exception', error);
onOpenError: (event) => }
event.preventDefault() }
@open = false
error = event.target.error onOpenSuccess(event) {
let error;
switch error.name const db = event.target.result;
when 'QuotaExceededError'
@onQuotaExceededError() if (db.objectStoreNames.length === 0) {
when 'VersionError' try { db.close(); } catch (error1) {}
@onVersionError() this.open = false;
when 'InvalidStateError' this.fail('empty');
@fail 'private_mode' } else if (error = this.buggyIDB(db)) {
else try { db.close(); } catch (error2) {}
@fail 'cant_open', error this.open = false;
return this.fail('buggy', error);
} else {
fail: (reason, error) -> this.runCallbacks(db);
@cachedDocs = null this.open = false;
@useIndexedDB = false db.close();
@reason or= reason }
@error or= error }
console.error? 'IDB error', error if error
@runCallbacks() onOpenError(event) {
if error and reason is 'cant_open' event.preventDefault();
Raven.captureMessage "#{error.name}: #{error.message}", level: 'warning', fingerprint: [error.name] this.open = false;
return const {
error
onQuotaExceededError: -> } = event.target;
@reset()
@db() switch (error.name) {
app.onQuotaExceeded() case 'QuotaExceededError':
Raven.captureMessage 'QuotaExceededError', level: 'warning' this.onQuotaExceededError();
return break;
case 'VersionError':
onVersionError: -> this.onVersionError();
req = indexedDB.open(NAME) break;
req.onsuccess = (event) => case 'InvalidStateError':
@handleVersionMismatch event.target.result.version this.fail('private_mode');
req.onerror = (event) -> break;
event.preventDefault() default:
@fail 'cant_open', error this.fail('cant_open', error);
return }
}
handleVersionMismatch: (actualVersion) ->
if Math.floor(actualVersion / @versionMultipler) isnt VERSION fail(reason, error) {
@fail 'version' this.cachedDocs = null;
else this.useIndexedDB = false;
@setUserVersion actualVersion - VERSION * @versionMultipler if (!this.reason) { this.reason = reason; }
@db() if (!this.error) { this.error = error; }
return if (error) { if (typeof console.error === 'function') {
console.error('IDB error', error);
buggyIDB: (db) -> } }
return if @checkedBuggyIDB this.runCallbacks();
@checkedBuggyIDB = true if (error && (reason === 'cant_open')) {
try Raven.captureMessage(`${error.name}: ${error.message}`, {level: 'warning', fingerprint: [error.name]});
@idbTransaction(db, stores: $.makeArray(db.objectStoreNames)[0..1], mode: 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937 }
return }
catch error
return error onQuotaExceededError() {
this.reset();
runCallbacks: (db) -> this.db();
fn(db) while fn = @callbacks.shift() app.onQuotaExceeded();
return Raven.captureMessage('QuotaExceededError', {level: 'warning'});
}
onUpgradeNeeded: (event) ->
return unless db = event.target.result onVersionError() {
const req = indexedDB.open(NAME);
objectStoreNames = $.makeArray(db.objectStoreNames) req.onsuccess = event => {
return this.handleVersionMismatch(event.target.result.version);
unless $.arrayDelete(objectStoreNames, 'docs') };
try db.createObjectStore('docs') req.onerror = function(event) {
event.preventDefault();
for doc in app.docs.all() when not $.arrayDelete(objectStoreNames, doc.slug) return this.fail('cant_open', error);
try db.createObjectStore(doc.slug) };
}
for name in objectStoreNames
try db.deleteObjectStore(name) handleVersionMismatch(actualVersion) {
return if (Math.floor(actualVersion / this.versionMultipler) !== VERSION) {
this.fail('version');
store: (doc, data, onSuccess, onError, _retry = true) -> } else {
@db (db) => this.setUserVersion(actualVersion - (VERSION * this.versionMultipler));
unless db this.db();
onError() }
return }
txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false buggyIDB(db) {
txn.oncomplete = => if (this.checkedBuggyIDB) { return; }
@cachedDocs?[doc.slug] = doc.mtime this.checkedBuggyIDB = true;
onSuccess() try {
return this.idbTransaction(db, {stores: $.makeArray(db.objectStoreNames).slice(0, 2), mode: 'readwrite'}).abort(); // https://bugs.webkit.org/show_bug.cgi?id=136937
txn.onerror = (event) => return;
event.preventDefault() } catch (error) {
if txn.error?.name is 'NotFoundError' and _retry return error;
@migrate() }
setTimeout => }
@store(doc, data, onSuccess, onError, false)
, 0 runCallbacks(db) {
else let fn;
onError(event) while ((fn = this.callbacks.shift())) { fn(db); }
return }
store = txn.objectStore(doc.slug) onUpgradeNeeded(event) {
store.clear() let db;
store.add(content, path) for path, content of data if (!(db = event.target.result)) { return; }
store = txn.objectStore('docs') const objectStoreNames = $.makeArray(db.objectStoreNames);
store.put(doc.mtime, doc.slug)
return if (!$.arrayDelete(objectStoreNames, 'docs')) {
return try { db.createObjectStore('docs'); } catch (error) {}
}
unstore: (doc, onSuccess, onError, _retry = true) ->
@db (db) => for (var doc of Array.from(app.docs.all())) {
unless db if (!$.arrayDelete(objectStoreNames, doc.slug)) {
onError() try { db.createObjectStore(doc.slug); } catch (error1) {}
return }
}
txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false
txn.oncomplete = => for (var name of Array.from(objectStoreNames)) {
delete @cachedDocs?[doc.slug] try { db.deleteObjectStore(name); } catch (error2) {}
onSuccess() }
return }
txn.onerror = (event) ->
event.preventDefault() store(doc, data, onSuccess, onError, _retry) {
if txn.error?.name is 'NotFoundError' and _retry if (_retry == null) { _retry = true; }
@migrate() this.db(db => {
setTimeout => if (!db) {
@unstore(doc, onSuccess, onError, false) onError();
, 0 return;
else }
onError(event)
return const txn = this.idbTransaction(db, {stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false});
txn.oncomplete = () => {
store = txn.objectStore('docs') if (this.cachedDocs != null) {
store.delete(doc.slug) this.cachedDocs[doc.slug] = doc.mtime;
}
store = txn.objectStore(doc.slug) onSuccess();
store.clear() };
return txn.onerror = event => {
return event.preventDefault();
if (((txn.error != null ? txn.error.name : undefined) === 'NotFoundError') && _retry) {
version: (doc, fn) -> this.migrate();
if (version = @cachedVersion(doc))? setTimeout(() => {
fn(version) return this.store(doc, data, onSuccess, onError, false);
return }
, 0);
@db (db) => } else {
unless db onError(event);
fn(false) }
return };
txn = @idbTransaction db, stores: ['docs'], mode: 'readonly' let store = txn.objectStore(doc.slug);
store = txn.objectStore('docs') store.clear();
for (var path in data) { var content = data[path]; store.add(content, path); }
req = store.get(doc.slug)
req.onsuccess = -> store = txn.objectStore('docs');
fn(req.result) store.put(doc.mtime, doc.slug);
return });
req.onerror = (event) -> }
event.preventDefault()
fn(false) unstore(doc, onSuccess, onError, _retry) {
return if (_retry == null) { _retry = true; }
return this.db(db => {
return if (!db) {
onError();
cachedVersion: (doc) -> return;
return unless @cachedDocs }
@cachedDocs[doc.slug] or false
const txn = this.idbTransaction(db, {stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false});
versions: (docs, fn) -> txn.oncomplete = () => {
if versions = @cachedVersions(docs) if (this.cachedDocs != null) {
fn(versions) delete this.cachedDocs[doc.slug];
return }
onSuccess();
@db (db) => };
unless db txn.onerror = function(event) {
fn(false) event.preventDefault();
return if (((txn.error != null ? txn.error.name : undefined) === 'NotFoundError') && _retry) {
this.migrate();
txn = @idbTransaction db, stores: ['docs'], mode: 'readonly' setTimeout(() => {
txn.oncomplete = -> return this.unstore(doc, onSuccess, onError, false);
fn(result) }
return , 0);
store = txn.objectStore('docs') } else {
result = {} onError(event);
}
docs.forEach (doc) -> };
req = store.get(doc.slug)
req.onsuccess = -> let store = txn.objectStore('docs');
result[doc.slug] = req.result store.delete(doc.slug);
return
req.onerror = (event) -> store = txn.objectStore(doc.slug);
event.preventDefault() store.clear();
result[doc.slug] = false });
return }
return
return version(doc, fn) {
let version;
cachedVersions: (docs) -> if ((version = this.cachedVersion(doc)) != null) {
return unless @cachedDocs fn(version);
result = {} return;
result[doc.slug] = @cachedVersion(doc) for doc in docs }
result
this.db(db => {
load: (entry, onSuccess, onError) -> if (!db) {
if @shouldLoadWithIDB(entry) fn(false);
onError = @loadWithXHR.bind(@, entry, onSuccess, onError) return;
@loadWithIDB entry, onSuccess, onError }
else
@loadWithXHR entry, onSuccess, onError const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'});
const store = txn.objectStore('docs');
loadWithXHR: (entry, onSuccess, onError) ->
ajax const req = store.get(doc.slug);
url: entry.fileUrl() req.onsuccess = function() {
dataType: 'html' fn(req.result);
success: onSuccess };
req.onerror = function(event) {
event.preventDefault();
fn(false);
};
});
}
cachedVersion(doc) {
if (!this.cachedDocs) { return; }
return this.cachedDocs[doc.slug] || false;
}
versions(docs, fn) {
let versions;
if (versions = this.cachedVersions(docs)) {
fn(versions);
return;
}
return this.db(db => {
if (!db) {
fn(false);
return;
}
const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'});
txn.oncomplete = function() {
fn(result);
};
const store = txn.objectStore('docs');
var result = {};
docs.forEach(function(doc) {
const req = store.get(doc.slug);
req.onsuccess = function() {
result[doc.slug] = req.result;
};
req.onerror = function(event) {
event.preventDefault();
result[doc.slug] = false;
};
});
});
}
cachedVersions(docs) {
if (!this.cachedDocs) { return; }
const result = {};
for (var doc of Array.from(docs)) { result[doc.slug] = this.cachedVersion(doc); }
return result;
}
load(entry, onSuccess, onError) {
if (this.shouldLoadWithIDB(entry)) {
onError = this.loadWithXHR.bind(this, entry, onSuccess, onError);
return this.loadWithIDB(entry, onSuccess, onError);
} else {
return this.loadWithXHR(entry, onSuccess, onError);
}
}
loadWithXHR(entry, onSuccess, onError) {
return ajax({
url: entry.fileUrl(),
dataType: 'html',
success: onSuccess,
error: onError error: onError
});
loadWithIDB: (entry, onSuccess, onError) -> }
@db (db) =>
unless db loadWithIDB(entry, onSuccess, onError) {
onError() return this.db(db => {
return if (!db) {
onError();
unless db.objectStoreNames.contains(entry.doc.slug) return;
onError() }
@loadDocsCache(db)
return if (!db.objectStoreNames.contains(entry.doc.slug)) {
onError();
txn = @idbTransaction db, stores: [entry.doc.slug], mode: 'readonly' this.loadDocsCache(db);
store = txn.objectStore(entry.doc.slug) return;
}
req = store.get(entry.dbPath())
req.onsuccess = -> const txn = this.idbTransaction(db, {stores: [entry.doc.slug], mode: 'readonly'});
if req.result then onSuccess(req.result) else onError() const store = txn.objectStore(entry.doc.slug);
return
req.onerror = (event) -> const req = store.get(entry.dbPath());
event.preventDefault() req.onsuccess = function() {
onError() if (req.result) { onSuccess(req.result); } else { onError(); }
return };
@loadDocsCache(db) req.onerror = function(event) {
return event.preventDefault();
onError();
loadDocsCache: (db) -> };
return if @cachedDocs this.loadDocsCache(db);
@cachedDocs = {} });
}
txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
txn.oncomplete = => loadDocsCache(db) {
setTimeout(@checkForCorruptedDocs, 50) if (this.cachedDocs) { return; }
return this.cachedDocs = {};
req = txn.objectStore('docs').openCursor() const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'});
req.onsuccess = (event) => txn.oncomplete = () => {
return unless cursor = event.target.result setTimeout(this.checkForCorruptedDocs, 50);
@cachedDocs[cursor.key] = cursor.value };
cursor.continue()
return const req = txn.objectStore('docs').openCursor();
req.onerror = (event) -> req.onsuccess = event => {
event.preventDefault() let cursor;
return if (!(cursor = event.target.result)) { return; }
return this.cachedDocs[cursor.key] = cursor.value;
cursor.continue();
checkForCorruptedDocs: => };
@db (db) => req.onerror = function(event) {
@corruptedDocs = [] event.preventDefault();
docs = (key for key, value of @cachedDocs when value) };
return if docs.length is 0 }
for slug in docs when not app.docs.findBy('slug', slug) checkForCorruptedDocs() {
@corruptedDocs.push(slug) this.db(db => {
let slug;
for slug in @corruptedDocs this.corruptedDocs = [];
$.arrayDelete(docs, slug) const docs = ((() => {
const result = [];
if docs.length is 0 for (var key in this.cachedDocs) {
setTimeout(@deleteCorruptedDocs, 0) var value = this.cachedDocs[key];
return if (value) {
result.push(key);
txn = @idbTransaction(db, stores: docs, mode: 'readonly', ignoreError: false) }
txn.oncomplete = => }
setTimeout(@deleteCorruptedDocs, 0) if @corruptedDocs.length > 0 return result;
return })());
if (docs.length === 0) { return; }
for doc in docs
txn.objectStore(doc).get('index').onsuccess = (event) => for (slug of Array.from(docs)) {
@corruptedDocs.push(event.target.source.name) unless event.target.result if (!app.docs.findBy('slug', slug)) {
return this.corruptedDocs.push(slug);
return }
return }
deleteCorruptedDocs: => for (slug of Array.from(this.corruptedDocs)) {
@db (db) => $.arrayDelete(docs, slug);
txn = @idbTransaction(db, stores: ['docs'], mode: 'readwrite', ignoreError: false) }
store = txn.objectStore('docs')
while doc = @corruptedDocs.pop() if (docs.length === 0) {
@cachedDocs[doc] = false setTimeout(this.deleteCorruptedDocs, 0);
store.delete(doc) return;
return }
Raven.captureMessage 'corruptedDocs', level: 'info', extra: { docs: @corruptedDocs.join(',') }
return const txn = this.idbTransaction(db, {stores: docs, mode: 'readonly', ignoreError: false});
txn.oncomplete = () => {
shouldLoadWithIDB: (entry) -> if (this.corruptedDocs.length > 0) { setTimeout(this.deleteCorruptedDocs, 0); }
@useIndexedDB and (not @cachedDocs or @cachedDocs[entry.doc.slug]) };
idbTransaction: (db, options) -> for (var doc of Array.from(docs)) {
app.lastIDBTransaction = [options.stores, options.mode] txn.objectStore(doc).get('index').onsuccess = event => {
txn = db.transaction(options.stores, options.mode) if (!event.target.result) { this.corruptedDocs.push(event.target.source.name); }
unless options.ignoreError is false };
txn.onerror = (event) -> }
event.preventDefault() });
return }
unless options.ignoreAbort is false
txn.onabort = (event) -> deleteCorruptedDocs() {
event.preventDefault() this.db(db => {
return let doc;
txn const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readwrite', ignoreError: false});
const store = txn.objectStore('docs');
reset: -> while ((doc = this.corruptedDocs.pop())) {
try indexedDB?.deleteDatabase(NAME) catch this.cachedDocs[doc] = false;
return store.delete(doc);
}
useIndexedDB: -> });
try Raven.captureMessage('corruptedDocs', {level: 'info', extra: { docs: this.corruptedDocs.join(',') }});
if !app.isSingleDoc() and window.indexedDB }
true
else shouldLoadWithIDB(entry) {
@reason = 'not_supported' return this.useIndexedDB && (!this.cachedDocs || this.cachedDocs[entry.doc.slug]);
false }
catch
false idbTransaction(db, options) {
app.lastIDBTransaction = [options.stores, options.mode];
migrate: -> const txn = db.transaction(options.stores, options.mode);
app.settings.set('schema', @userVersion() + 1) if (options.ignoreError !== false) {
return txn.onerror = function(event) {
event.preventDefault();
setUserVersion: (version) -> };
app.settings.set('schema', version) }
return if (options.ignoreAbort !== false) {
txn.onabort = function(event) {
userVersion: -> event.preventDefault();
app.settings.get('schema') };
}
return txn;
}
reset() {
try { if (typeof indexedDB !== 'undefined' && indexedDB !== null) {
indexedDB.deleteDatabase(NAME);
} } catch (error) {}
}
useIndexedDB() {
try {
if (!app.isSingleDoc() && window.indexedDB) {
return true;
} else {
this.reason = 'not_supported';
return false;
}
} catch (error) {
return false;
}
}
migrate() {
app.settings.set('schema', this.userVersion() + 1);
}
setUserVersion(version) {
app.settings.set('schema', version);
}
userVersion() {
return app.settings.get('schema');
}
});
Cls.initClass();
return Cls;
})();

@ -1,154 +1,199 @@
class app.Router /*
$.extend @prototype, Events * decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
@routes: [ * DS102: Remove unnecessary code created because of implicit returns
['*', 'before' ] * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
['/', 'root' ] * DS206: Consider reworking classes to avoid initClass
['/settings', 'settings' ] * DS207: Consider shorter variations of null checks
['/offline', 'offline' ] * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
['/about', 'about' ] */
['/news', 'news' ] const Cls = (app.Router = class Router {
['/help', 'help' ] static initClass() {
['/:doc-:type/', 'type' ] $.extend(this.prototype, Events);
['/:doc/', 'doc' ]
['/:doc/:path(*)', 'entry' ] this.routes = [
['*', 'before' ],
['/', 'root' ],
['/settings', 'settings' ],
['/offline', 'offline' ],
['/about', 'about' ],
['/news', 'news' ],
['/help', 'help' ],
['/:doc-:type/', 'type' ],
['/:doc/', 'doc' ],
['/:doc/:path(*)', 'entry' ],
['*', 'notFound' ] ['*', 'notFound' ]
] ];
}
constructor: ->
for [path, method] in @constructor.routes constructor() {
page path, @[method].bind(@) for (var [path, method] of Array.from(this.constructor.routes)) {
@setInitialPath() page(path, this[method].bind(this));
}
start: -> this.setInitialPath();
page.start() }
return
start() {
show: (path) -> page.start();
page.show(path) }
return
show(path) {
triggerRoute: (name) -> page.show(path);
@trigger name, @context }
@trigger 'after', name, @context
return triggerRoute(name) {
this.trigger(name, this.context);
before: (context, next) -> this.trigger('after', name, this.context);
previousContext = @context }
@context = context
@trigger 'before', context before(context, next) {
let res;
if res = next() const previousContext = this.context;
@context = previousContext this.context = context;
return res this.trigger('before', context);
else
return if (res = next()) {
this.context = previousContext;
doc: (context, next) -> return res;
if doc = app.docs.findBySlug(context.params.doc) or app.disabledDocs.findBySlug(context.params.doc) } else {
context.doc = doc return;
context.entry = doc.toEntry() }
@triggerRoute 'entry' }
return
else doc(context, next) {
return next() let doc;
if (doc = app.docs.findBySlug(context.params.doc) || app.disabledDocs.findBySlug(context.params.doc)) {
type: (context, next) -> context.doc = doc;
doc = app.docs.findBySlug(context.params.doc) context.entry = doc.toEntry();
this.triggerRoute('entry');
if type = doc?.types.findBy 'slug', context.params.type return;
context.doc = doc } else {
context.type = type return next();
@triggerRoute 'type' }
return }
else
return next() type(context, next) {
let type;
entry: (context, next) -> const doc = app.docs.findBySlug(context.params.doc);
doc = app.docs.findBySlug(context.params.doc)
return next() unless doc if (type = doc != null ? doc.types.findBy('slug', context.params.type) : undefined) {
path = context.params.path context.doc = doc;
hash = context.hash context.type = type;
this.triggerRoute('type');
if entry = doc.findEntryByPathAndHash(path, hash) return;
context.doc = doc } else {
context.entry = entry return next();
@triggerRoute 'entry' }
return }
else if path.slice(-6) is '/index'
path = path.substr(0, path.length - 6) entry(context, next) {
return entry.fullPath() if entry = doc.findEntryByPathAndHash(path, hash) let entry;
else const doc = app.docs.findBySlug(context.params.doc);
path = "#{path}/index" if (!doc) { return next(); }
return entry.fullPath() if entry = doc.findEntryByPathAndHash(path, hash) let {
return next()
root: ->
return '/' if app.isSingleDoc()
@triggerRoute 'root'
return
settings: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
@triggerRoute 'settings'
return
offline: (context)->
return "/#/#{context.path}" if app.isSingleDoc()
@triggerRoute 'offline'
return
about: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'about'
@triggerRoute 'page'
return
news: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'news'
@triggerRoute 'page'
return
help: (context) ->
return "/#/#{context.path}" if app.isSingleDoc()
context.page = 'help'
@triggerRoute 'page'
return
notFound: (context) ->
@triggerRoute 'notFound'
return
isIndex: ->
@context?.path is '/' or (app.isSingleDoc() and @context?.entry?.isIndex())
isSettings: ->
@context?.path is '/settings'
setInitialPath: ->
# Remove superfluous forward slashes at the beginning of the path
if (path = location.pathname.replace /^\/{2,}/g, '/') isnt location.pathname
page.replace path + location.search + location.hash, null, true
if location.pathname is '/'
if path = @getInitialPathFromHash()
page.replace path + location.search, null, true
else if path = @getInitialPathFromCookie()
page.replace path + location.search + location.hash, null, true
return
getInitialPathFromHash: ->
try
(new RegExp "#/(.+)").exec(decodeURIComponent location.hash)?[1]
catch
getInitialPathFromCookie: ->
if path = Cookies.get('initial_path')
Cookies.expire('initial_path')
path path
} = context.params;
replaceHash: (hash) -> const {
page.replace location.pathname + location.search + (hash or ''), null, true hash
return } = context;
if (entry = doc.findEntryByPathAndHash(path, hash)) {
context.doc = doc;
context.entry = entry;
this.triggerRoute('entry');
return;
} else if (path.slice(-6) === '/index') {
path = path.substr(0, path.length - 6);
if (entry = doc.findEntryByPathAndHash(path, hash)) { return entry.fullPath(); }
} else {
path = `${path}/index`;
if (entry = doc.findEntryByPathAndHash(path, hash)) { return entry.fullPath(); }
}
return next();
}
root() {
if (app.isSingleDoc()) { return '/'; }
this.triggerRoute('root');
}
settings(context) {
if (app.isSingleDoc()) { return `/#/${context.path}`; }
this.triggerRoute('settings');
}
offline(context){
if (app.isSingleDoc()) { return `/#/${context.path}`; }
this.triggerRoute('offline');
}
about(context) {
if (app.isSingleDoc()) { return `/#/${context.path}`; }
context.page = 'about';
this.triggerRoute('page');
}
news(context) {
if (app.isSingleDoc()) { return `/#/${context.path}`; }
context.page = 'news';
this.triggerRoute('page');
}
help(context) {
if (app.isSingleDoc()) { return `/#/${context.path}`; }
context.page = 'help';
this.triggerRoute('page');
}
notFound(context) {
this.triggerRoute('notFound');
}
isIndex() {
return ((this.context != null ? this.context.path : undefined) === '/') || (app.isSingleDoc() && __guard__(this.context != null ? this.context.entry : undefined, x => x.isIndex()));
}
isSettings() {
return (this.context != null ? this.context.path : undefined) === '/settings';
}
setInitialPath() {
// Remove superfluous forward slashes at the beginning of the path
let path;
if ((path = location.pathname.replace(/^\/{2,}/g, '/')) !== location.pathname) {
page.replace(path + location.search + location.hash, null, true);
}
if (location.pathname === '/') {
if (path = this.getInitialPathFromHash()) {
page.replace(path + location.search, null, true);
} else if (path = this.getInitialPathFromCookie()) {
page.replace(path + location.search + location.hash, null, true);
}
}
}
getInitialPathFromHash() {
try {
return __guard__((new RegExp("#/(.+)")).exec(decodeURIComponent(location.hash)), x => x[1]);
} catch (error) {}
}
getInitialPathFromCookie() {
let path;
if (path = Cookies.get('initial_path')) {
Cookies.expire('initial_path');
return path;
}
}
replaceHash(hash) {
page.replace(location.pathname + location.search + (hash || ''), null, true);
}
});
Cls.initClass();
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,292 +1,373 @@
# /*
# Match functions * decaffeinate suggestions:
# * DS002: Fix invalid constructor
* DS101: Remove unnecessary use of Array.from
SEPARATOR = '.' * DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
query = * DS202: Simplify dynamic range loops
queryLength = * DS206: Consider reworking classes to avoid initClass
value = * DS207: Consider shorter variations of null checks
valueLength = * DS209: Avoid top-level return
matcher = # current match function * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
fuzzyRegexp = # query fuzzy regexp */
index = # position of the query in the string being matched //
lastIndex = # last position of the query in the string being matched // Match functions
match = # regexp match data //
matchIndex =
matchLength = let fuzzyRegexp, i, index, lastIndex, match, matcher, matchIndex, matchLength, queryLength, score, separators, value, valueLength;
score = # score for the current match const SEPARATOR = '.';
separators = # counter
i = null # cursor let query =
(queryLength =
`function exactMatch() {` (value =
index = value.indexOf(query) (valueLength =
return unless index >= 0 (matcher = // current match function
(fuzzyRegexp = // query fuzzy regexp
lastIndex = value.lastIndexOf(query) (index = // position of the query in the string being matched
(lastIndex = // last position of the query in the string being matched
if index isnt lastIndex (match = // regexp match data
return Math.max(scoreExactMatch(), ((index = lastIndex) and scoreExactMatch()) or 0) (matchIndex =
else (matchLength =
return scoreExactMatch() (score = // score for the current match
`}` (separators = // counter
(i = null))))))))))))); // cursor
`function scoreExactMatch() {`
# Remove one point for each unmatched character. function exactMatch() {
score = 100 - (valueLength - queryLength) index = value.indexOf(query);
if (!(index >= 0)) { return; }
if index > 0
# If the character preceding the query is a dot, assign the same score lastIndex = value.lastIndexOf(query);
# as if the query was found at the beginning of the string, minus one.
if value.charAt(index - 1) is SEPARATOR if (index !== lastIndex) {
score += index - 1 return Math.max(scoreExactMatch(), ((index = lastIndex) && scoreExactMatch()) || 0);
# Don't match a single-character query unless it's found at the beginning } else {
# of the string or is preceded by a dot. return scoreExactMatch();
else if queryLength is 1 }
return }
# (1) Remove one point for each unmatched character up to the nearest
# preceding dot or the beginning of the string. function scoreExactMatch() {
# (2) Remove one point for each unmatched character following the query. // Remove one point for each unmatched character.
else score = 100 - (valueLength - queryLength);
i = index - 2
i-- while i >= 0 and value.charAt(i) isnt SEPARATOR if (index > 0) {
score -= (index - i) + # (1) // If the character preceding the query is a dot, assign the same score
(valueLength - queryLength - index) # (2) // as if the query was found at the beginning of the string, minus one.
if (value.charAt(index - 1) === SEPARATOR) {
# Remove one point for each dot preceding the query, except for the one score += index - 1;
# immediately before the query. // Don't match a single-character query unless it's found at the beginning
separators = 0 // of the string or is preceded by a dot.
i = index - 2 } else if (queryLength === 1) {
while i >= 0 return;
separators++ if value.charAt(i) is SEPARATOR // (1) Remove one point for each unmatched character up to the nearest
i-- // preceding dot or the beginning of the string.
score -= separators // (2) Remove one point for each unmatched character following the query.
} else {
# Remove five points for each dot following the query. i = index - 2;
separators = 0 while ((i >= 0) && (value.charAt(i) !== SEPARATOR)) { i--; }
i = valueLength - queryLength - index - 1 score -= (index - i) + // (1)
while i >= 0 (valueLength - queryLength - index); // (2)
separators++ if value.charAt(index + queryLength + i) is SEPARATOR }
i--
score -= separators * 5 // Remove one point for each dot preceding the query, except for the one
// immediately before the query.
return Math.max 1, score separators = 0;
`}` i = index - 2;
while (i >= 0) {
`function fuzzyMatch() {` if (value.charAt(i) === SEPARATOR) { separators++; }
return if valueLength <= queryLength or value.indexOf(query) >= 0 i--;
return unless match = fuzzyRegexp.exec(value) }
matchIndex = match.index score -= separators;
matchLength = match[0].length }
score = scoreFuzzyMatch()
if match = fuzzyRegexp.exec(value.slice(i = value.lastIndexOf(SEPARATOR) + 1)) // Remove five points for each dot following the query.
matchIndex = i + match.index separators = 0;
matchLength = match[0].length i = valueLength - queryLength - index - 1;
return Math.max(score, scoreFuzzyMatch()) while (i >= 0) {
else if (value.charAt(index + queryLength + i) === SEPARATOR) { separators++; }
return score i--;
`}` }
score -= separators * 5;
`function scoreFuzzyMatch() {`
# When the match is at the beginning of the string or preceded by a dot. return Math.max(1, score);
if matchIndex is 0 or value.charAt(matchIndex - 1) is SEPARATOR }
return Math.max 66, 100 - matchLength
# When the match is at the end of the string. function fuzzyMatch() {
else if matchIndex + matchLength is valueLength if ((valueLength <= queryLength) || (value.indexOf(query) >= 0)) { return; }
return Math.max 33, 67 - matchLength if (!(match = fuzzyRegexp.exec(value))) { return; }
# When the match is in the middle of the string. matchIndex = match.index;
else matchLength = match[0].length;
return Math.max 1, 34 - matchLength score = scoreFuzzyMatch();
`}` if (match = fuzzyRegexp.exec(value.slice(i = value.lastIndexOf(SEPARATOR) + 1))) {
matchIndex = i + match.index;
# matchLength = match[0].length;
# Searchers return Math.max(score, scoreFuzzyMatch());
# } else {
return score;
class app.Searcher }
$.extend @prototype, Events }
CHUNK_SIZE = 20000 function scoreFuzzyMatch() {
// When the match is at the beginning of the string or preceded by a dot.
DEFAULTS = if ((matchIndex === 0) || (value.charAt(matchIndex - 1) === SEPARATOR)) {
max_results: app.config.max_results return Math.max(66, 100 - matchLength);
// When the match is at the end of the string.
} else if ((matchIndex + matchLength) === valueLength) {
return Math.max(33, 67 - matchLength);
// When the match is in the middle of the string.
} else {
return Math.max(1, 34 - matchLength);
}
}
//
// Searchers
//
(function() {
let CHUNK_SIZE = undefined;
let DEFAULTS = undefined;
let SEPARATORS_REGEXP = undefined;
let EOS_SEPARATORS_REGEXP = undefined;
let INFO_PARANTHESES_REGEXP = undefined;
let EMPTY_PARANTHESES_REGEXP = undefined;
let EVENT_REGEXP = undefined;
let DOT_REGEXP = undefined;
let WHITESPACE_REGEXP = undefined;
let EMPTY_STRING = undefined;
let ELLIPSIS = undefined;
let STRING = undefined;
const Cls = (app.Searcher = class Searcher {
static initClass() {
$.extend(this.prototype, Events);
CHUNK_SIZE = 20000;
DEFAULTS = {
max_results: app.config.max_results,
fuzzy_min_length: 3 fuzzy_min_length: 3
};
SEPARATORS_REGEXP = /#|::|:-|->|\$(?=\w)|\-(?=\w)|\:(?=\w)|\ [\/\-&]\ |:\ |\ /g
EOS_SEPARATORS_REGEXP = /(\w)[\-:]$/ SEPARATORS_REGEXP = /#|::|:-|->|\$(?=\w)|\-(?=\w)|\:(?=\w)|\ [\/\-&]\ |:\ |\ /g;
INFO_PARANTHESES_REGEXP = /\ \(\w+?\)$/ EOS_SEPARATORS_REGEXP = /(\w)[\-:]$/;
EMPTY_PARANTHESES_REGEXP = /\(\)/ INFO_PARANTHESES_REGEXP = /\ \(\w+?\)$/;
EVENT_REGEXP = /\ event$/ EMPTY_PARANTHESES_REGEXP = /\(\)/;
DOT_REGEXP = /\.+/g EVENT_REGEXP = /\ event$/;
WHITESPACE_REGEXP = /\s/g DOT_REGEXP = /\.+/g;
WHITESPACE_REGEXP = /\s/g;
EMPTY_STRING = ''
ELLIPSIS = '...' EMPTY_STRING = '';
STRING = 'string' ELLIPSIS = '...';
STRING = 'string';
@normalizeString: (string) -> }
string
static normalizeString(string) {
return string
.toLowerCase() .toLowerCase()
.replace ELLIPSIS, EMPTY_STRING .replace(ELLIPSIS, EMPTY_STRING)
.replace EVENT_REGEXP, EMPTY_STRING .replace(EVENT_REGEXP, EMPTY_STRING)
.replace INFO_PARANTHESES_REGEXP, EMPTY_STRING .replace(INFO_PARANTHESES_REGEXP, EMPTY_STRING)
.replace SEPARATORS_REGEXP, SEPARATOR .replace(SEPARATORS_REGEXP, SEPARATOR)
.replace DOT_REGEXP, SEPARATOR .replace(DOT_REGEXP, SEPARATOR)
.replace EMPTY_PARANTHESES_REGEXP, EMPTY_STRING .replace(EMPTY_PARANTHESES_REGEXP, EMPTY_STRING)
.replace WHITESPACE_REGEXP, EMPTY_STRING .replace(WHITESPACE_REGEXP, EMPTY_STRING);
}
@normalizeQuery: (string) ->
string = @normalizeString(string) static normalizeQuery(string) {
string.replace EOS_SEPARATORS_REGEXP, '$1.' string = this.normalizeString(string);
return string.replace(EOS_SEPARATORS_REGEXP, '$1.');
constructor: (options = {}) -> }
@options = $.extend {}, DEFAULTS, options
constructor(options) {
find: (data, attr, q) -> this.match = this.match.bind(this);
@kill() this.matchChunks = this.matchChunks.bind(this);
if (options == null) { options = {}; }
@data = data this.options = $.extend({}, DEFAULTS, options);
@attr = attr }
@query = q
@setup() find(data, attr, q) {
this.kill();
if @isValid() then @match() else @end()
return this.data = data;
this.attr = attr;
setup: -> this.query = q;
query = @query = @constructor.normalizeQuery(@query) this.setup();
queryLength = query.length
@dataLength = @data.length if (this.isValid()) { this.match(); } else { this.end(); }
@matchers = [exactMatch] }
@totalResults = 0
@setupFuzzy() setup() {
return query = (this.query = this.constructor.normalizeQuery(this.query));
queryLength = query.length;
setupFuzzy: -> this.dataLength = this.data.length;
if queryLength >= @options.fuzzy_min_length this.matchers = [exactMatch];
fuzzyRegexp = @queryToFuzzyRegexp(query) this.totalResults = 0;
@matchers.push(fuzzyMatch) this.setupFuzzy();
else }
fuzzyRegexp = null
return setupFuzzy() {
if (queryLength >= this.options.fuzzy_min_length) {
isValid: -> fuzzyRegexp = this.queryToFuzzyRegexp(query);
queryLength > 0 and query isnt SEPARATOR this.matchers.push(fuzzyMatch);
} else {
end: -> fuzzyRegexp = null;
@triggerResults [] unless @totalResults }
@trigger 'end' }
@free()
return isValid() {
return (queryLength > 0) && (query !== SEPARATOR);
kill: -> }
if @timeout
clearTimeout @timeout end() {
@free() if (!this.totalResults) { this.triggerResults([]); }
return this.trigger('end');
this.free();
free: -> }
@data = @attr = @dataLength = @matchers = @matcher = @query =
@totalResults = @scoreMap = @cursor = @timeout = null kill() {
return if (this.timeout) {
clearTimeout(this.timeout);
match: => this.free();
if not @foundEnough() and @matcher = @matchers.shift() }
@setupMatcher() }
@matchChunks()
else free() {
@end() this.data = (this.attr = (this.dataLength = (this.matchers = (this.matcher = (this.query =
return (this.totalResults = (this.scoreMap = (this.cursor = (this.timeout = null)))))))));
}
setupMatcher: ->
@cursor = 0 match() {
@scoreMap = new Array(101) if (!this.foundEnough() && (this.matcher = this.matchers.shift())) {
return this.setupMatcher();
this.matchChunks();
matchChunks: => } else {
@matchChunk() this.end();
}
if @cursor is @dataLength or @scoredEnough() }
@delay @match
@sendResults() setupMatcher() {
else this.cursor = 0;
@delay @matchChunks this.scoreMap = new Array(101);
return }
matchChunk: -> matchChunks() {
matcher = @matcher this.matchChunk();
for [0...@chunkSize()]
value = @data[@cursor][@attr] if ((this.cursor === this.dataLength) || this.scoredEnough()) {
if value.split # string this.delay(this.match);
valueLength = value.length this.sendResults();
@addResult(@data[@cursor], score) if score = matcher() } else {
else # array this.delay(this.matchChunks);
score = 0 }
for value in @data[@cursor][@attr] }
valueLength = value.length
score = Math.max(score, matcher() || 0) matchChunk() {
@addResult(@data[@cursor], score) if score > 0 ({
@cursor++ matcher
return } = this);
for (let j = 0, end = this.chunkSize(), asc = 0 <= end; asc ? j < end : j > end; asc ? j++ : j--) {
chunkSize: -> value = this.data[this.cursor][this.attr];
if @cursor + CHUNK_SIZE > @dataLength if (value.split) { // string
@dataLength % CHUNK_SIZE valueLength = value.length;
else if (score = matcher()) { this.addResult(this.data[this.cursor], score); }
CHUNK_SIZE } else { // array
score = 0;
scoredEnough: -> for (value of Array.from(this.data[this.cursor][this.attr])) {
@scoreMap[100]?.length >= @options.max_results valueLength = value.length;
score = Math.max(score, matcher() || 0);
foundEnough: -> }
@totalResults >= @options.max_results if (score > 0) { this.addResult(this.data[this.cursor], score); }
}
addResult: (object, score) -> this.cursor++;
(@scoreMap[Math.round(score)] or= []).push(object) }
@totalResults++ }
return
chunkSize() {
getResults: -> if ((this.cursor + CHUNK_SIZE) > this.dataLength) {
results = [] return this.dataLength % CHUNK_SIZE;
for objects in @scoreMap by -1 when objects } else {
results.push.apply results, objects return CHUNK_SIZE;
results[0...@options.max_results] }
}
sendResults: ->
results = @getResults() scoredEnough() {
@triggerResults results if results.length return (this.scoreMap[100] != null ? this.scoreMap[100].length : undefined) >= this.options.max_results;
return }
triggerResults: (results) -> foundEnough() {
@trigger 'results', results return this.totalResults >= this.options.max_results;
return }
delay: (fn) -> addResult(object, score) {
@timeout = setTimeout(fn, 1) let name;
(this.scoreMap[name = Math.round(score)] || (this.scoreMap[name] = [])).push(object);
queryToFuzzyRegexp: (string) -> this.totalResults++;
chars = string.split '' }
chars[i] = $.escapeRegexp(char) for char, i in chars
new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/ getResults() {
const results = [];
class app.SynchronousSearcher extends app.Searcher for (let j = this.scoreMap.length - 1; j >= 0; j--) {
match: => var objects = this.scoreMap[j];
if @matcher if (objects) {
@allResults or= [] results.push.apply(results, objects);
@allResults.push.apply @allResults, @getResults() }
super }
return results.slice(0, this.options.max_results);
free: -> }
@allResults = null
super sendResults() {
const results = this.getResults();
end: -> if (results.length) { this.triggerResults(results); }
@sendResults true }
super
triggerResults(results) {
sendResults: (end) -> this.trigger('results', results);
if end and @allResults?.length }
@triggerResults @allResults
delay(fn) {
delay: (fn) -> return this.timeout = setTimeout(fn, 1);
fn() }
queryToFuzzyRegexp(string) {
const chars = string.split('');
for (i = 0; i < chars.length; i++) { var char = chars[i]; chars[i] = $.escapeRegexp(char); }
return new RegExp(chars.join('.*?'));
}
});
Cls.initClass();
return Cls; // abc -> /a.*?b.*?c.*?/
})();
app.SynchronousSearcher = class SynchronousSearcher extends app.Searcher {
constructor(...args) {
this.match = this.match.bind(this);
super(...args);
}
match() {
if (this.matcher) {
if (!this.allResults) { this.allResults = []; }
this.allResults.push.apply(this.allResults, this.getResults());
}
return super.match(...arguments);
}
free() {
this.allResults = null;
return super.free(...arguments);
}
end() {
this.sendResults(true);
return super.end(...arguments);
}
sendResults(end) {
if (end && (this.allResults != null ? this.allResults.length : undefined)) {
return this.triggerResults(this.allResults);
}
}
delay(fn) {
return fn();
}
};

@ -1,49 +1,66 @@
class app.ServiceWorker /*
$.extend @prototype, Events * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.ServiceWorker = class ServiceWorker {
static initClass() {
$.extend(this.prototype, Events);
}
@isEnabled: -> static isEnabled() {
!!navigator.serviceWorker and app.config.service_worker_enabled return !!navigator.serviceWorker && app.config.service_worker_enabled;
}
constructor: -> constructor() {
@registration = null this.onUpdateFound = this.onUpdateFound.bind(this);
@notifyUpdate = true this.onStateChange = this.onStateChange.bind(this);
this.registration = null;
this.notifyUpdate = true;
navigator.serviceWorker.register(app.config.service_worker_path, {scope: '/'}) navigator.serviceWorker.register(app.config.service_worker_path, {scope: '/'})
.then( .then(
(registration) => @updateRegistration(registration), registration => this.updateRegistration(registration),
(error) -> console.error('Could not register service worker:', error) error => console.error('Could not register service worker:', error));
) }
update: -> update() {
return unless @registration if (!this.registration) { return; }
@notifyUpdate = true this.notifyUpdate = true;
return @registration.update().catch(->) return this.registration.update().catch(function() {});
}
updateInBackground: ->
return unless @registration updateInBackground() {
@notifyUpdate = false if (!this.registration) { return; }
return @registration.update().catch(->) this.notifyUpdate = false;
return this.registration.update().catch(function() {});
reload: -> }
return @updateInBackground().then(() -> app.reboot())
reload() {
updateRegistration: (registration) -> return this.updateInBackground().then(() => app.reboot());
@registration = registration }
$.on @registration, 'updatefound', @onUpdateFound
return updateRegistration(registration) {
this.registration = registration;
onUpdateFound: => $.on(this.registration, 'updatefound', this.onUpdateFound);
$.off @installingRegistration, 'statechange', @onStateChange() if @installingRegistration }
@installingRegistration = @registration.installing
$.on @installingRegistration, 'statechange', @onStateChange onUpdateFound() {
return if (this.installingRegistration) { $.off(this.installingRegistration, 'statechange', this.onStateChange()); }
this.installingRegistration = this.registration.installing;
onStateChange: => $.on(this.installingRegistration, 'statechange', this.onStateChange);
if @installingRegistration and @installingRegistration.state == 'installed' and navigator.serviceWorker.controller }
@installingRegistration = null
@onUpdateReady() onStateChange() {
return if (this.installingRegistration && (this.installingRegistration.state === 'installed') && navigator.serviceWorker.controller) {
this.installingRegistration = null;
onUpdateReady: -> this.onUpdateReady();
@trigger 'updateready' if @notifyUpdate }
return }
onUpdateReady() {
if (this.notifyUpdate) { this.trigger('updateready'); }
}
});
Cls.initClass();

@ -1,170 +1,219 @@
class app.Settings /*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
* DS104: Avoid inline assignments
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
(function() {
let PREFERENCE_KEYS = undefined;
let INTERNAL_KEYS = undefined;
const Cls = (app.Settings = class Settings {
static initClass() {
PREFERENCE_KEYS = [ PREFERENCE_KEYS = [
'hideDisabled' 'hideDisabled',
'hideIntro' 'hideIntro',
'manualUpdate' 'manualUpdate',
'fastScroll' 'fastScroll',
'arrowScroll' 'arrowScroll',
'analyticsConsent' 'analyticsConsent',
'docs' 'docs',
'dark' # legacy 'dark', // legacy
'theme' 'theme',
'layout' 'layout',
'size' 'size',
'tips' 'tips',
'noAutofocus' 'noAutofocus',
'autoInstall' 'autoInstall',
'spaceScroll' 'spaceScroll',
'spaceTimeout' 'spaceTimeout'
] ];
INTERNAL_KEYS = [ INTERNAL_KEYS = [
'count' 'count',
'schema' 'schema',
'version' 'version',
'news' 'news'
] ];
LAYOUTS: [ this.prototype.LAYOUTS = [
'_max-width' '_max-width',
'_sidebar-hidden' '_sidebar-hidden',
'_native-scrollbars' '_native-scrollbars',
'_text-justify-hyphenate' '_text-justify-hyphenate'
] ];
@defaults: this.defaults = {
count: 0 count: 0,
hideDisabled: false hideDisabled: false,
hideIntro: false hideIntro: false,
news: 0 news: 0,
manualUpdate: false manualUpdate: false,
schema: 1 schema: 1,
analyticsConsent: false analyticsConsent: false,
theme: 'auto' theme: 'auto',
spaceScroll: 1 spaceScroll: 1,
spaceTimeout: 0.5 spaceTimeout: 0.5
};
constructor: -> }
@store = new CookiesStore
@cache = {} constructor() {
@autoSupported = window.matchMedia('(prefers-color-scheme)').media != 'not all' this.store = new CookiesStore;
if @autoSupported this.cache = {};
@darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)') this.autoSupported = window.matchMedia('(prefers-color-scheme)').media !== 'not all';
@darkModeQuery.addListener => @setTheme(@get('theme')) if (this.autoSupported) {
this.darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.darkModeQuery.addListener(() => this.setTheme(this.get('theme')));
get: (key) -> }
return @cache[key] if @cache.hasOwnProperty(key) }
@cache[key] = @store.get(key) ? @constructor.defaults[key]
if key == 'theme' and @cache[key] == 'auto' and !@darkModeQuery
@cache[key] = 'default' get(key) {
else let left;
@cache[key] if (this.cache.hasOwnProperty(key)) { return this.cache[key]; }
this.cache[key] = (left = this.store.get(key)) != null ? left : this.constructor.defaults[key];
set: (key, value) -> if ((key === 'theme') && (this.cache[key] === 'auto') && !this.darkModeQuery) {
@store.set(key, value) return this.cache[key] = 'default';
delete @cache[key] } else {
@setTheme(value) if key == 'theme' return this.cache[key];
return }
}
del: (key) ->
@store.del(key) set(key, value) {
delete @cache[key] this.store.set(key, value);
return delete this.cache[key];
if (key === 'theme') { this.setTheme(value); }
hasDocs: -> }
try !!@store.get('docs')
del(key) {
getDocs: -> this.store.del(key);
@store.get('docs')?.split('/') or app.config.default_docs delete this.cache[key];
}
setDocs: (docs) ->
@set 'docs', docs.join('/') hasDocs() {
return try { return !!this.store.get('docs'); } catch (error) {}
}
getTips: ->
@store.get('tips')?.split('/') or [] getDocs() {
return __guard__(this.store.get('docs'), x => x.split('/')) || app.config.default_docs;
setTips: (tips) -> }
@set 'tips', tips.join('/')
return setDocs(docs) {
this.set('docs', docs.join('/'));
setLayout: (name, enable) -> }
@toggleLayout(name, enable)
getTips() {
layout = (@store.get('layout') || '').split(' ') return __guard__(this.store.get('tips'), x => x.split('/')) || [];
$.arrayDelete(layout, '') }
if enable setTips(tips) {
layout.push(name) if layout.indexOf(name) is -1 this.set('tips', tips.join('/'));
else }
$.arrayDelete(layout, name)
setLayout(name, enable) {
if layout.length > 0 this.toggleLayout(name, enable);
@set 'layout', layout.join(' ')
else const layout = (this.store.get('layout') || '').split(' ');
@del 'layout' $.arrayDelete(layout, '');
return
if (enable) {
hasLayout: (name) -> if (layout.indexOf(name) === -1) { layout.push(name); }
layout = (@store.get('layout') || '').split(' ') } else {
layout.indexOf(name) isnt -1 $.arrayDelete(layout, name);
}
setSize: (value) ->
@set 'size', value if (layout.length > 0) {
return this.set('layout', layout.join(' '));
} else {
dump: -> this.del('layout');
@store.dump() }
}
export: ->
data = @dump() hasLayout(name) {
delete data[key] for key in INTERNAL_KEYS const layout = (this.store.get('layout') || '').split(' ');
data return layout.indexOf(name) !== -1;
}
import: (data) ->
for key, value of @export() setSize(value) {
@del key unless data.hasOwnProperty(key) this.set('size', value);
for key, value of data }
@set key, value if PREFERENCE_KEYS.indexOf(key) isnt -1
return dump() {
return this.store.dump();
reset: -> }
@store.reset()
@cache = {} export() {
return const data = this.dump();
for (var key of Array.from(INTERNAL_KEYS)) { delete data[key]; }
initLayout: -> return data;
if @get('dark') is 1 }
@set('theme', 'dark')
@del 'dark' import(data) {
@setTheme(@get('theme')) let key, value;
@toggleLayout(layout, @hasLayout(layout)) for layout in @LAYOUTS const object = this.export();
@initSidebarWidth() for (key in object) {
return value = object[key];
if (!data.hasOwnProperty(key)) { this.del(key); }
setTheme: (theme) -> }
if theme is 'auto' for (key in data) {
theme = if @darkModeQuery.matches then 'dark' else 'default' value = data[key];
classList = document.documentElement.classList if (PREFERENCE_KEYS.indexOf(key) !== -1) { this.set(key, value); }
classList.remove('_theme-default', '_theme-dark') }
classList.add('_theme-' + theme) }
@updateColorMeta()
return reset() {
this.store.reset();
updateColorMeta: -> this.cache = {};
color = getComputedStyle(document.documentElement).getPropertyValue('--headerBackground').trim() }
$('meta[name=theme-color]').setAttribute('content', color)
return initLayout() {
if (this.get('dark') === 1) {
toggleLayout: (layout, enable) -> this.set('theme', 'dark');
classList = document.body.classList this.del('dark');
# sidebar is always shown for settings; its state is updated in app.views.Settings }
classList.toggle(layout, enable) unless layout is '_sidebar-hidden' and app.router?.isSettings this.setTheme(this.get('theme'));
classList.toggle('_overlay-scrollbars', $.overlayScrollbarsEnabled()) for (var layout of Array.from(this.LAYOUTS)) { this.toggleLayout(layout, this.hasLayout(layout)); }
return this.initSidebarWidth();
}
initSidebarWidth: ->
size = @get('size') setTheme(theme) {
document.documentElement.style.setProperty('--sidebarWidth', size + 'px') if size if (theme === 'auto') {
return theme = this.darkModeQuery.matches ? 'dark' : 'default';
}
const {
classList
} = document.documentElement;
classList.remove('_theme-default', '_theme-dark');
classList.add('_theme-' + theme);
this.updateColorMeta();
}
updateColorMeta() {
const color = getComputedStyle(document.documentElement).getPropertyValue('--headerBackground').trim();
$('meta[name=theme-color]').setAttribute('content', color);
}
toggleLayout(layout, enable) {
const {
classList
} = document.body;
// sidebar is always shown for settings; its state is updated in app.views.Settings
if ((layout !== '_sidebar-hidden') || !(app.router != null ? app.router.isSettings : undefined)) { classList.toggle(layout, enable); }
classList.toggle('_overlay-scrollbars', $.overlayScrollbarsEnabled());
}
initSidebarWidth() {
const size = this.get('size');
if (size) { document.documentElement.style.setProperty('--sidebarWidth', size + 'px'); }
}
});
Cls.initClass();
return Cls;
})();
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,193 +1,259 @@
class app.Shortcuts /*
$.extend @prototype, Events * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
constructor: -> * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
@isMac = $.isMac() * DS205: Consider reworking code to avoid use of IIFEs
@start() * DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
start: -> */
$.on document, 'keydown', @onKeydown const Cls = (app.Shortcuts = class Shortcuts {
$.on document, 'keypress', @onKeypress static initClass() {
return $.extend(this.prototype, Events);
}
stop: ->
$.off document, 'keydown', @onKeydown constructor() {
$.off document, 'keypress', @onKeypress this.onKeydown = this.onKeydown.bind(this);
return this.onKeypress = this.onKeypress.bind(this);
this.isMac = $.isMac();
swapArrowKeysBehavior: -> this.start();
app.settings.get('arrowScroll') }
spaceScroll: -> start() {
app.settings.get('spaceScroll') $.on(document, 'keydown', this.onKeydown);
$.on(document, 'keypress', this.onKeypress);
showTip: -> }
app.showTip('KeyNav')
@showTip = null stop() {
$.off(document, 'keydown', this.onKeydown);
spaceTimeout: -> $.off(document, 'keypress', this.onKeypress);
app.settings.get('spaceTimeout') }
onKeydown: (event) => swapArrowKeysBehavior() {
return if @buggyEvent(event) return app.settings.get('arrowScroll');
result = if event.ctrlKey or event.metaKey }
@handleKeydownSuperEvent event unless event.altKey or event.shiftKey
else if event.shiftKey spaceScroll() {
@handleKeydownShiftEvent event unless event.altKey return app.settings.get('spaceScroll');
else if event.altKey }
@handleKeydownAltEvent event
else showTip() {
@handleKeydownEvent event app.showTip('KeyNav');
return this.showTip = null;
event.preventDefault() if result is false }
return
spaceTimeout() {
onKeypress: (event) => return app.settings.get('spaceTimeout');
return if @buggyEvent(event) or (event.charCode == 63 and document.activeElement.tagName == 'INPUT') }
unless event.ctrlKey or event.metaKey
result = @handleKeypressEvent event onKeydown(event) {
event.preventDefault() if result is false if (this.buggyEvent(event)) { return; }
return const result = (() => {
if (event.ctrlKey || event.metaKey) {
handleKeydownEvent: (event, _force) -> if (!event.altKey && !event.shiftKey) { return this.handleKeydownSuperEvent(event); }
return @handleKeydownAltEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior() } else if (event.shiftKey) {
if (!event.altKey) { return this.handleKeydownShiftEvent(event); }
if not event.target.form and (48 <= event.which <= 57 or 65 <= event.which <= 90) } else if (event.altKey) {
@trigger 'typing' return this.handleKeydownAltEvent(event);
return } else {
return this.handleKeydownEvent(event);
switch event.which }
when 8 })();
@trigger 'typing' unless event.target.form
when 13 if (result === false) { event.preventDefault(); }
@trigger 'enter' }
when 27
@trigger 'escape' onKeypress(event) {
false if (this.buggyEvent(event) || ((event.charCode === 63) && (document.activeElement.tagName === 'INPUT'))) { return; }
when 32 if (!event.ctrlKey && !event.metaKey) {
if event.target.type is 'search' and @spaceScroll() and (not @lastKeypress or @lastKeypress < Date.now() - (@spaceTimeout() * 1000)) const result = this.handleKeypressEvent(event);
@trigger 'pageDown' if (result === false) { event.preventDefault(); }
false }
when 33 }
@trigger 'pageUp'
when 34 handleKeydownEvent(event, _force) {
@trigger 'pageDown' if (!_force && [37, 38, 39, 40].includes(event.which) && this.swapArrowKeysBehavior()) { return this.handleKeydownAltEvent(event, true); }
when 35
@trigger 'pageBottom' unless event.target.form if (!event.target.form && ((48 <= event.which && event.which <= 57) || (65 <= event.which && event.which <= 90))) {
when 36 this.trigger('typing');
@trigger 'pageTop' unless event.target.form return;
when 37 }
@trigger 'left' unless event.target.value
when 38 switch (event.which) {
@trigger 'up' case 8:
@showTip?() if (!event.target.form) { return this.trigger('typing'); }
false break;
when 39 case 13:
@trigger 'right' unless event.target.value return this.trigger('enter');
when 40 case 27:
@trigger 'down' this.trigger('escape');
@showTip?() return false;
false case 32:
when 191 if ((event.target.type === 'search') && this.spaceScroll() && (!this.lastKeypress || (this.lastKeypress < (Date.now() - (this.spaceTimeout() * 1000))))) {
unless event.target.form this.trigger('pageDown');
@trigger 'typing' return false;
false }
break;
handleKeydownSuperEvent: (event) -> case 33:
switch event.which return this.trigger('pageUp');
when 13 case 34:
@trigger 'superEnter' return this.trigger('pageDown');
when 37 case 35:
if @isMac if (!event.target.form) { return this.trigger('pageBottom'); }
@trigger 'superLeft' break;
false case 36:
when 38 if (!event.target.form) { return this.trigger('pageTop'); }
@trigger 'pageTop' break;
false case 37:
when 39 if (!event.target.value) { return this.trigger('left'); }
if @isMac break;
@trigger 'superRight' case 38:
false this.trigger('up');
when 40 if (typeof this.showTip === 'function') {
@trigger 'pageBottom' this.showTip();
false }
when 188 return false;
@trigger 'preferences' case 39:
false if (!event.target.value) { return this.trigger('right'); }
break;
handleKeydownShiftEvent: (event, _force) -> case 40:
return @handleKeydownEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior() this.trigger('down');
if (typeof this.showTip === 'function') {
if not event.target.form and 65 <= event.which <= 90 this.showTip();
@trigger 'typing' }
return return false;
case 191:
switch event.which if (!event.target.form) {
when 32 this.trigger('typing');
@trigger 'pageUp' return false;
false }
when 38 break;
unless getSelection()?.toString() }
@trigger 'altUp' }
false
when 40 handleKeydownSuperEvent(event) {
unless getSelection()?.toString() switch (event.which) {
@trigger 'altDown' case 13:
false return this.trigger('superEnter');
case 37:
handleKeydownAltEvent: (event, _force) -> if (this.isMac) {
return @handleKeydownEvent(event, true) if not _force and event.which in [37, 38, 39, 40] and @swapArrowKeysBehavior() this.trigger('superLeft');
return false;
switch event.which }
when 9 break;
@trigger 'altRight', event case 38:
when 37 this.trigger('pageTop');
unless @isMac return false;
@trigger 'superLeft' case 39:
false if (this.isMac) {
when 38 this.trigger('superRight');
@trigger 'altUp' return false;
false }
when 39 break;
unless @isMac case 40:
@trigger 'superRight' this.trigger('pageBottom');
false return false;
when 40 case 188:
@trigger 'altDown' this.trigger('preferences');
false return false;
when 67 }
@trigger 'altC' }
false
when 68 handleKeydownShiftEvent(event, _force) {
@trigger 'altD' if (!_force && [37, 38, 39, 40].includes(event.which) && this.swapArrowKeysBehavior()) { return this.handleKeydownEvent(event, true); }
false
when 70 if (!event.target.form && (65 <= event.which && event.which <= 90)) {
@trigger 'altF', event this.trigger('typing');
when 71 return;
@trigger 'altG' }
false
when 79 switch (event.which) {
@trigger 'altO' case 32:
false this.trigger('pageUp');
when 82 return false;
@trigger 'altR' case 38:
false if (!__guard__(getSelection(), x => x.toString())) {
when 83 this.trigger('altUp');
@trigger 'altS' return false;
false }
break;
handleKeypressEvent: (event) -> case 40:
if event.which is 63 and not event.target.value if (!__guard__(getSelection(), x1 => x1.toString())) {
@trigger 'help' this.trigger('altDown');
false return false;
else }
@lastKeypress = Date.now() break;
}
buggyEvent: (event) -> }
try
event.target handleKeydownAltEvent(event, _force) {
event.ctrlKey if (!_force && [37, 38, 39, 40].includes(event.which) && this.swapArrowKeysBehavior()) { return this.handleKeydownEvent(event, true); }
event.which
return false switch (event.which) {
catch case 9:
return true return this.trigger('altRight', event);
case 37:
if (!this.isMac) {
this.trigger('superLeft');
return false;
}
break;
case 38:
this.trigger('altUp');
return false;
case 39:
if (!this.isMac) {
this.trigger('superRight');
return false;
}
break;
case 40:
this.trigger('altDown');
return false;
case 67:
this.trigger('altC');
return false;
case 68:
this.trigger('altD');
return false;
case 70:
return this.trigger('altF', event);
case 71:
this.trigger('altG');
return false;
case 79:
this.trigger('altO');
return false;
case 82:
this.trigger('altR');
return false;
case 83:
this.trigger('altS');
return false;
}
}
handleKeypressEvent(event) {
if ((event.which === 63) && !event.target.value) {
this.trigger('help');
return false;
} else {
return this.lastKeypress = Date.now();
}
}
buggyEvent(event) {
try {
event.target;
event.ctrlKey;
event.which;
return false;
} catch (error) {
return true;
}
}
});
Cls.initClass();
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,39 +1,54 @@
class app.UpdateChecker /*
constructor: -> * decaffeinate suggestions:
@lastCheck = Date.now() * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.UpdateChecker = class UpdateChecker {
constructor() {
this.checkDocs = this.checkDocs.bind(this);
this.onFocus = this.onFocus.bind(this);
this.lastCheck = Date.now();
$.on window, 'focus', @onFocus $.on(window, 'focus', this.onFocus);
app.serviceWorker?.on 'updateready', @onUpdateReady if (app.serviceWorker != null) {
app.serviceWorker.on('updateready', this.onUpdateReady);
}
setTimeout @checkDocs, 0 setTimeout(this.checkDocs, 0);
}
check: -> check() {
if app.serviceWorker if (app.serviceWorker) {
app.serviceWorker.update() app.serviceWorker.update();
else } else {
ajax ajax({
url: $('script[src*="application"]').getAttribute('src') url: $('script[src*="application"]').getAttribute('src'),
dataType: 'application/javascript' dataType: 'application/javascript',
error: (_, xhr) => @onUpdateReady() if xhr.status is 404 error: (_, xhr) => { if (xhr.status === 404) { return this.onUpdateReady(); } }
return });
}
}
onUpdateReady: -> onUpdateReady() {
new app.views.Notif 'UpdateReady', autoHide: null new app.views.Notif('UpdateReady', {autoHide: null});
return }
checkDocs: => checkDocs() {
unless app.settings.get('manualUpdate') if (!app.settings.get('manualUpdate')) {
app.docs.updateInBackground() app.docs.updateInBackground();
else } else {
app.docs.checkForUpdates (i) => @onDocsUpdateReady() if i > 0 app.docs.checkForUpdates(i => { if (i > 0) { return this.onDocsUpdateReady(); } });
return }
}
onDocsUpdateReady: -> onDocsUpdateReady() {
new app.views.Notif 'UpdateDocs', autoHide: null new app.views.Notif('UpdateDocs', {autoHide: null});
return }
onFocus: => onFocus() {
if Date.now() - @lastCheck > 21600e3 if ((Date.now() - this.lastCheck) > 21600e3) {
@lastCheck = Date.now() this.lastCheck = Date.now();
@check() this.check();
return }
}
};

@ -1,31 +1,38 @@
#= require_tree ./vendor /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
//= require_tree ./vendor
#= require lib/license //= require lib/license
#= require_tree ./lib //= require_tree ./lib
#= require app/app //= require app/app
#= require app/config //= require app/config
#= require_tree ./app //= require_tree ./app
#= require collections/collection //= require collections/collection
#= require_tree ./collections //= require_tree ./collections
#= require models/model //= require models/model
#= require_tree ./models //= require_tree ./models
#= require views/view //= require views/view
#= require_tree ./views //= require_tree ./views
#= require_tree ./templates //= require_tree ./templates
#= require tracking //= require tracking
init = -> var init = function() {
document.removeEventListener 'DOMContentLoaded', init, false document.removeEventListener('DOMContentLoaded', init, false);
if document.body if (document.body) {
app.init() return app.init();
else } else {
setTimeout(init, 42) return setTimeout(init, 42);
}
};
document.addEventListener 'DOMContentLoaded', init, false document.addEventListener('DOMContentLoaded', init, false);

@ -1,55 +1,75 @@
class app.Collection /*
constructor: (objects = []) -> * decaffeinate suggestions:
@reset objects * DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
model: -> * DS207: Consider shorter variations of null checks
app.models[@constructor.model] * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
reset: (objects = []) -> app.Collection = class Collection {
@models = [] constructor(objects) {
@add object for object in objects if (objects == null) { objects = []; }
return this.reset(objects);
}
add: (object) ->
if object instanceof app.Model model() {
@models.push object return app.models[this.constructor.model];
else if object instanceof Array }
@add obj for obj in object
else if object instanceof app.Collection reset(objects) {
@models.push object.all()... if (objects == null) { objects = []; }
else this.models = [];
@models.push new (@model())(object) for (var object of Array.from(objects)) { this.add(object); }
return }
remove: (model) -> add(object) {
@models.splice @models.indexOf(model), 1 if (object instanceof app.Model) {
return this.models.push(object);
} else if (object instanceof Array) {
size: -> for (var obj of Array.from(object)) { this.add(obj); }
@models.length } else if (object instanceof app.Collection) {
this.models.push(...Array.from(object.all() || []));
isEmpty: -> } else {
@models.length is 0 this.models.push(new (this.model())(object));
}
each: (fn) -> }
fn(model) for model in @models
return remove(model) {
this.models.splice(this.models.indexOf(model), 1);
all: -> }
@models
size() {
contains: (model) -> return this.models.length;
@models.indexOf(model) >= 0 }
findBy: (attr, value) -> isEmpty() {
for model in @models return this.models.length === 0;
return model if model[attr] is value }
return
each(fn) {
findAllBy: (attr, value) -> for (var model of Array.from(this.models)) { fn(model); }
model for model in @models when model[attr] is value }
countAllBy: (attr, value) -> all() {
i = 0 return this.models;
i += 1 for model in @models when model[attr] is value }
i
contains(model) {
return this.models.indexOf(model) >= 0;
}
findBy(attr, value) {
for (var model of Array.from(this.models)) {
if (model[attr] === value) { return model; }
}
}
findAllBy(attr, value) {
return Array.from(this.models).filter((model) => model[attr] === value);
}
countAllBy(attr, value) {
let i = 0;
for (var model of Array.from(this.models)) { if (model[attr] === value) { i += 1; } }
return i;
}
};

@ -1,85 +1,117 @@
class app.collections.Docs extends app.Collection /*
@model: 'Doc' * decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS202: Simplify dynamic range loops
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
(function() {
let NORMALIZE_VERSION_RGX = undefined;
let NORMALIZE_VERSION_SUB = undefined;
let CONCURRENCY = undefined;
const Cls = (app.collections.Docs = class Docs extends app.Collection {
static initClass() {
this.model = 'Doc';
findBySlug: (slug) -> NORMALIZE_VERSION_RGX = /\.(\d)$/;
@findBy('slug', slug) or @findBy('slug_without_version', slug) NORMALIZE_VERSION_SUB = '.0$1';
NORMALIZE_VERSION_RGX = /\.(\d)$/ // Load models concurrently.
NORMALIZE_VERSION_SUB = '.0$1' // It's not pretty but I didn't want to import a promise library only for this.
sort: -> CONCURRENCY = 3;
@models.sort (a, b) -> }
if a.name is b.name
if not a.version or a.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB) > b.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB)
-1
else
1
else if a.name.toLowerCase() > b.name.toLowerCase()
1
else
-1
# Load models concurrently. findBySlug(slug) {
# It's not pretty but I didn't want to import a promise library only for this. return this.findBy('slug', slug) || this.findBy('slug_without_version', slug);
CONCURRENCY = 3 }
load: (onComplete, onError, options) -> sort() {
i = 0 return this.models.sort(function(a, b) {
if (a.name === b.name) {
if (!a.version || (a.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB) > b.version.replace(NORMALIZE_VERSION_RGX, NORMALIZE_VERSION_SUB))) {
return -1;
} else {
return 1;
}
} else if (a.name.toLowerCase() > b.name.toLowerCase()) {
return 1;
} else {
return -1;
}
});
}
load(onComplete, onError, options) {
let i = 0;
next = => var next = () => {
if i < @models.length if (i < this.models.length) {
@models[i].load(next, fail, options) this.models[i].load(next, fail, options);
else if i is @models.length + CONCURRENCY - 1 } else if (i === ((this.models.length + CONCURRENCY) - 1)) {
onComplete() onComplete();
i++ }
return i++;
};
fail = (args...) -> var fail = function(...args) {
if onError if (onError) {
onError(args...) onError(...Array.from(args || []));
onError = null onError = null;
next() }
return next();
};
next() for [0...CONCURRENCY] for (let j = 0, end = CONCURRENCY, asc = 0 <= end; asc ? j < end : j > end; asc ? j++ : j--) { next(); }
return }
clearCache: -> clearCache() {
doc.clearCache() for doc in @models for (var doc of Array.from(this.models)) { doc.clearCache(); }
return }
uninstall: (callback) -> uninstall(callback) {
i = 0 let i = 0;
next = => var next = () => {
if i < @models.length if (i < this.models.length) {
@models[i++].uninstall(next, next) this.models[i++].uninstall(next, next);
else } else {
callback() callback();
return }
next() };
return next();
}
getInstallStatuses: (callback) -> getInstallStatuses(callback) {
app.db.versions @models, (statuses) -> app.db.versions(this.models, function(statuses) {
if statuses if (statuses) {
for key, value of statuses for (var key in statuses) {
statuses[key] = installed: !!value, mtime: value var value = statuses[key];
callback(statuses) statuses[key] = {installed: !!value, mtime: value};
return }
return }
callback(statuses);
});
}
checkForUpdates: (callback) -> checkForUpdates(callback) {
@getInstallStatuses (statuses) => this.getInstallStatuses(statuses => {
i = 0 let i = 0;
if statuses if (statuses) {
i += 1 for slug, status of statuses when @findBy('slug', slug).isOutdated(status) for (var slug in statuses) { var status = statuses[slug]; if (this.findBy('slug', slug).isOutdated(status)) { i += 1; } }
callback(i) }
return callback(i);
return });
}
updateInBackground: -> updateInBackground() {
@getInstallStatuses (statuses) => this.getInstallStatuses(statuses => {
return unless statuses if (!statuses) { return; }
for slug, status of statuses for (var slug in statuses) {
doc = @findBy 'slug', slug var status = statuses[slug];
doc.install($.noop, $.noop) if doc.isOutdated(status) var doc = this.findBy('slug', slug);
return if (doc.isOutdated(status)) { doc.install($.noop, $.noop); }
return }
});
}
});
Cls.initClass();
return Cls;
})();

@ -1,2 +1,11 @@
class app.collections.Entries extends app.Collection /*
@model: 'Entry' * decaffeinate suggestions:
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.collections.Entries = class Entries extends app.Collection {
static initClass() {
this.model = 'Entry';
}
});
Cls.initClass();

@ -1,19 +1,41 @@
class app.collections.Types extends app.Collection /*
@model: 'Type' * decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
(function() {
let GUIDES_RGX = undefined;
let APPENDIX_RGX = undefined;
const Cls = (app.collections.Types = class Types extends app.Collection {
static initClass() {
this.model = 'Type';
groups: -> GUIDES_RGX = /(^|\()(guides?|tutorials?|reference|book|getting\ started|manual|examples)($|[\):])/i;
result = [] APPENDIX_RGX = /appendix/i;
for type in @models }
(result[@_groupFor(type)] ||= []).push(type)
result.filter (e) -> e.length > 0
GUIDES_RGX = /(^|\()(guides?|tutorials?|reference|book|getting\ started|manual|examples)($|[\):])/i groups() {
APPENDIX_RGX = /appendix/i const result = [];
for (var type of Array.from(this.models)) {
var name;
(result[name = this._groupFor(type)] || (result[name] = [])).push(type);
}
return result.filter(e => e.length > 0);
}
_groupFor: (type) -> _groupFor(type) {
if GUIDES_RGX.test(type.name) if (GUIDES_RGX.test(type.name)) {
0 return 0;
else if APPENDIX_RGX.test(type.name) } else if (APPENDIX_RGX.test(type.name)) {
2 return 2;
else } else {
1 return 1;
}
}
});
Cls.initClass();
return Cls;
})();

@ -1,85 +1,110 @@
return unless console?.time and console.groupCollapsed /*
* decaffeinate suggestions:
# * DS102: Remove unnecessary code created because of implicit returns
# App * DS203: Remove `|| {}` from converted for-own loops
# * DS207: Consider shorter variations of null checks
* DS208: Avoid top-level this
_init = app.init * DS209: Avoid top-level return
app.init = -> * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
console.time 'Init' */
_init.call(app) if (!(typeof console !== 'undefined' && console !== null ? console.time : undefined) || !console.groupCollapsed) { return; }
console.timeEnd 'Init'
console.time 'Load' //
// App
_start = app.start //
app.start = ->
console.timeEnd 'Load' const _init = app.init;
console.time 'Start' app.init = function() {
_start.call(app, arguments...) console.time('Init');
console.timeEnd 'Start' _init.call(app);
console.timeEnd('Init');
# return console.time('Load');
# Searcher };
#
const _start = app.start;
_super = app.Searcher app.start = function() {
_proto = app.Searcher.prototype console.timeEnd('Load');
console.time('Start');
app.Searcher = -> _start.call(app, ...arguments);
_super.apply @, arguments return console.timeEnd('Start');
};
_setup = @setup.bind(@)
@setup = -> //
console.groupCollapsed "Search: #{@query}" // Searcher
console.time 'Total' //
_setup()
const _super = app.Searcher;
_match = @match.bind(@) const _proto = app.Searcher.prototype;
@match = =>
console.timeEnd @matcher.name if @matcher app.Searcher = function() {
_match() _super.apply(this, arguments);
_setupMatcher = @setupMatcher.bind(@) const _setup = this.setup.bind(this);
@setupMatcher = -> this.setup = function() {
console.time @matcher.name console.groupCollapsed(`Search: ${this.query}`);
_setupMatcher() console.time('Total');
return _setup();
_end = @end.bind(@) };
@end = ->
console.log "Results: #{@totalResults}" const _match = this.match.bind(this);
console.timeEnd 'Total' this.match = () => {
console.groupEnd() if (this.matcher) { console.timeEnd(this.matcher.name); }
_end() return _match();
};
_kill = @kill.bind(@)
@kill = -> const _setupMatcher = this.setupMatcher.bind(this);
if @timeout this.setupMatcher = function() {
console.timeEnd @matcher.name if @matcher console.time(this.matcher.name);
console.groupEnd() return _setupMatcher();
console.timeEnd 'Total' };
console.warn 'Killed'
_kill() const _end = this.end.bind(this);
this.end = function() {
return console.log(`Results: ${this.totalResults}`);
console.timeEnd('Total');
$.extend(app.Searcher, _super) console.groupEnd();
_proto.constructor = app.Searcher return _end();
app.Searcher.prototype = _proto };
# const _kill = this.kill.bind(this);
# View tree this.kill = function() {
# if (this.timeout) {
if (this.matcher) { console.timeEnd(this.matcher.name); }
@viewTree = (view = app.document, level = 0, visited = []) -> console.groupEnd();
return if visited.indexOf(view) >= 0 console.timeEnd('Total');
visited.push(view) console.warn('Killed');
}
console.log "%c #{Array(level + 1).join(' ')}#{view.constructor.name}: #{!!view.activated}", return _kill();
'color:' + (view.activated and 'green' or 'red') };
for own key, value of view when key isnt 'view' and value };
if typeof value is 'object' and value.setupElement
@viewTree(value, level + 1, visited) $.extend(app.Searcher, _super);
else if value.constructor.toString().match(/Object\(\)/) _proto.constructor = app.Searcher;
@viewTree(v, level + 1, visited) for own k, v of value when v and typeof v is 'object' and v.setupElement app.Searcher.prototype = _proto;
return
//
// View tree
//
this.viewTree = function(view, level, visited) {
if (view == null) { view = app.document; }
if (level == null) { level = 0; }
if (visited == null) { visited = []; }
if (visited.indexOf(view) >= 0) { return; }
visited.push(view);
console.log(`%c ${Array(level + 1).join(' ')}${view.constructor.name}: ${!!view.activated}`,
'color:' + ((view.activated && 'green') || 'red'));
for (var key of Object.keys(view || {})) {
var value = view[key];
if ((key !== 'view') && value) {
if ((typeof value === 'object') && value.setupElement) {
this.viewTree(value, level + 1, visited);
} else if (value.constructor.toString().match(/Object\(\)/)) {
for (var k of Object.keys(value || {})) { var v = value[k]; if (v && (typeof v === 'object') && v.setupElement) { this.viewTree(v, level + 1, visited); } }
}
}
}
};

@ -1,118 +1,154 @@
MIME_TYPES = /*
json: 'application/json' * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* DS208: Avoid top-level this
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const MIME_TYPES = {
json: 'application/json',
html: 'text/html' html: 'text/html'
};
@ajax = (options) -> this.ajax = function(options) {
applyDefaults(options) applyDefaults(options);
serializeData(options) serializeData(options);
xhr = new XMLHttpRequest() const xhr = new XMLHttpRequest();
xhr.open(options.type, options.url, options.async) xhr.open(options.type, options.url, options.async);
applyCallbacks(xhr, options) applyCallbacks(xhr, options);
applyHeaders(xhr, options) applyHeaders(xhr, options);
xhr.send(options.data) xhr.send(options.data);
if options.async if (options.async) {
abort: abort.bind(undefined, xhr) return {abort: abort.bind(undefined, xhr)};
else } else {
parseResponse(xhr, options) return parseResponse(xhr, options);
}
};
ajax.defaults = ajax.defaults = {
async: true async: true,
dataType: 'json' dataType: 'json',
timeout: 30 timeout: 30,
type: 'GET' type: 'GET'
# contentType };
# context // contentType
# data // context
# error // data
# headers // error
# progress // headers
# success // progress
# url // success
// url
applyDefaults = (options) ->
for key of ajax.defaults var applyDefaults = function(options) {
options[key] ?= ajax.defaults[key] for (var key in ajax.defaults) {
return if (options[key] == null) { options[key] = ajax.defaults[key]; }
}
serializeData = (options) -> };
return unless options.data
var serializeData = function(options) {
if options.type is 'GET' if (!options.data) { return; }
options.url += '?' + serializeParams(options.data)
options.data = null if (options.type === 'GET') {
else options.url += '?' + serializeParams(options.data);
options.data = serializeParams(options.data) options.data = null;
return } else {
options.data = serializeParams(options.data);
serializeParams = (params) -> }
("#{encodeURIComponent key}=#{encodeURIComponent value}" for key, value of params).join '&' };
applyCallbacks = (xhr, options) -> var serializeParams = params => ((() => {
return unless options.async const result = [];
for (var key in params) {
xhr.timer = setTimeout onTimeout.bind(undefined, xhr, options), options.timeout * 1000 var value = params[key];
xhr.onprogress = options.progress if options.progress result.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
xhr.onreadystatechange = -> }
if xhr.readyState is 4 return result;
clearTimeout(xhr.timer) })()).join('&');
onComplete(xhr, options)
return var applyCallbacks = function(xhr, options) {
return if (!options.async) { return; }
applyHeaders = (xhr, options) -> xhr.timer = setTimeout(onTimeout.bind(undefined, xhr, options), options.timeout * 1000);
options.headers or= {} if (options.progress) { xhr.onprogress = options.progress; }
xhr.onreadystatechange = function() {
if options.contentType if (xhr.readyState === 4) {
options.headers['Content-Type'] = options.contentType clearTimeout(xhr.timer);
onComplete(xhr, options);
if not options.headers['Content-Type'] and options.data and options.type isnt 'GET' }
options.headers['Content-Type'] = 'application/x-www-form-urlencoded' };
};
if options.dataType
options.headers['Accept'] = MIME_TYPES[options.dataType] or options.dataType var applyHeaders = function(xhr, options) {
if (!options.headers) { options.headers = {}; }
for key, value of options.headers
xhr.setRequestHeader(key, value) if (options.contentType) {
return options.headers['Content-Type'] = options.contentType;
}
onComplete = (xhr, options) ->
if 200 <= xhr.status < 300 if (!options.headers['Content-Type'] && options.data && (options.type !== 'GET')) {
if (response = parseResponse(xhr, options))? options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
onSuccess response, xhr, options }
else
onError 'invalid', xhr, options if (options.dataType) {
else options.headers['Accept'] = MIME_TYPES[options.dataType] || options.dataType;
onError 'error', xhr, options }
return
for (var key in options.headers) {
onSuccess = (response, xhr, options) -> var value = options.headers[key];
options.success?.call options.context, response, xhr, options xhr.setRequestHeader(key, value);
return }
};
onError = (type, xhr, options) ->
options.error?.call options.context, type, xhr, options var onComplete = function(xhr, options) {
return if (200 <= xhr.status && xhr.status < 300) {
let response;
onTimeout = (xhr, options) -> if ((response = parseResponse(xhr, options)) != null) {
xhr.abort() onSuccess(response, xhr, options);
onError 'timeout', xhr, options } else {
return onError('invalid', xhr, options);
}
abort = (xhr) -> } else {
clearTimeout(xhr.timer) onError('error', xhr, options);
xhr.onreadystatechange = null }
xhr.abort() };
return
var onSuccess = function(response, xhr, options) {
parseResponse = (xhr, options) -> if (options.success != null) {
if options.dataType is 'json' options.success.call(options.context, response, xhr, options);
parseJSON(xhr.responseText) }
else };
xhr.responseText
var onError = function(type, xhr, options) {
parseJSON = (json) -> if (options.error != null) {
try JSON.parse(json) catch options.error.call(options.context, type, xhr, options);
}
};
var onTimeout = function(xhr, options) {
xhr.abort();
onError('timeout', xhr, options);
};
var abort = function(xhr) {
clearTimeout(xhr.timer);
xhr.onreadystatechange = null;
xhr.abort();
};
var parseResponse = function(xhr, options) {
if (options.dataType === 'json') {
return parseJSON(xhr.responseText);
} else {
return xhr.responseText;
}
};
var parseJSON = function(json) {
try { return JSON.parse(json); } catch (error) {}
};

@ -1,42 +1,66 @@
class @CookiesStore /*
# Intentionally called CookiesStore instead of CookieStore * decaffeinate suggestions:
# Calling it CookieStore causes issues when the Experimental Web Platform features flag is enabled in Chrome * DS101: Remove unnecessary use of Array.from
# Related issue: https://github.com/freeCodeCamp/devdocs/issues/932 * DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
INT = /^\d+$/ * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
@onBlocked: -> */
(function() {
get: (key) -> let INT = undefined;
value = Cookies.get(key) const Cls = (this.CookiesStore = class CookiesStore {
value = parseInt(value, 10) if value? and INT.test(value) static initClass() {
value // Intentionally called CookiesStore instead of CookieStore
// Calling it CookieStore causes issues when the Experimental Web Platform features flag is enabled in Chrome
set: (key, value) -> // Related issue: https://github.com/freeCodeCamp/devdocs/issues/932
if value == false
@del(key) INT = /^\d+$/;
return }
value = 1 if value == true static onBlocked() {}
value = parseInt(value, 10) if value and INT.test?(value)
Cookies.set(key, '' + value, path: '/', expires: 1e8) get(key) {
@constructor.onBlocked(key, value, @get(key)) if @get(key) != value let value = Cookies.get(key);
return if ((value != null) && INT.test(value)) { value = parseInt(value, 10); }
return value;
del: (key) -> }
Cookies.expire(key)
return set(key, value) {
if (value === false) {
reset: -> this.del(key);
try return;
for cookie in document.cookie.split(/;\s?/) }
Cookies.expire(cookie.split('=')[0])
return if (value === true) { value = 1; }
catch if (value && (typeof INT.test === 'function' ? INT.test(value) : undefined)) { value = parseInt(value, 10); }
Cookies.set(key, '' + value, {path: '/', expires: 1e8});
dump: -> if (this.get(key) !== value) { this.constructor.onBlocked(key, value, this.get(key)); }
result = {} }
for cookie in document.cookie.split(/;\s?/) when cookie[0] isnt '_'
cookie = cookie.split('=') del(key) {
result[cookie[0]] = cookie[1] Cookies.expire(key);
result }
reset() {
try {
for (var cookie of Array.from(document.cookie.split(/;\s?/))) {
Cookies.expire(cookie.split('=')[0]);
}
return;
} catch (error) {}
}
dump() {
const result = {};
for (var cookie of Array.from(document.cookie.split(/;\s?/))) {
if (cookie[0] !== '_') {
cookie = cookie.split('=');
result[cookie[0]] = cookie[1];
}
}
return result;
}
});
Cls.initClass();
return Cls;
})();

@ -1,28 +1,51 @@
@Events = /*
on: (event, callback) -> * decaffeinate suggestions:
if event.indexOf(' ') >= 0 * DS101: Remove unnecessary use of Array.from
@on name, callback for name in event.split(' ') * DS102: Remove unnecessary code created because of implicit returns
else * DS104: Avoid inline assignments
((@_callbacks ?= {})[event] ?= []).push callback * DS207: Consider shorter variations of null checks
@ * DS208: Avoid top-level this
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
this.Events = {
on(event, callback) {
if (event.indexOf(' ') >= 0) {
for (var name of Array.from(event.split(' '))) { this.on(name, callback); }
} else {
let base;
(((base = this._callbacks != null ? this._callbacks : (this._callbacks = {})))[event] != null ? base[event] : (base[event] = [])).push(callback);
}
return this;
},
off: (event, callback) -> off(event, callback) {
if event.indexOf(' ') >= 0 let callbacks, index;
@off name, callback for name in event.split(' ') if (event.indexOf(' ') >= 0) {
else if (callbacks = @_callbacks?[event]) and (index = callbacks.indexOf callback) >= 0 for (var name of Array.from(event.split(' '))) { this.off(name, callback); }
callbacks.splice index, 1 } else if ((callbacks = this._callbacks != null ? this._callbacks[event] : undefined) && ((index = callbacks.indexOf(callback)) >= 0)) {
delete @_callbacks[event] unless callbacks.length callbacks.splice(index, 1);
@ if (!callbacks.length) { delete this._callbacks[event]; }
}
return this;
},
trigger: (event, args...) -> trigger(event, ...args) {
@eventInProgress = { name: event, args: args } let callbacks;
if callbacks = @_callbacks?[event] this.eventInProgress = { name: event, args };
callback? args... for callback in callbacks.slice(0) if (callbacks = this._callbacks != null ? this._callbacks[event] : undefined) {
@eventInProgress = null for (var callback of Array.from(callbacks.slice(0))) { if (typeof callback === 'function') {
@trigger 'all', event, args... unless event is 'all' callback(...Array.from(args || []));
@ } }
}
this.eventInProgress = null;
if (event !== 'all') { this.trigger('all', event, ...Array.from(args)); }
return this;
},
removeEvent: (event) -> removeEvent(event) {
if @_callbacks? if (this._callbacks != null) {
delete @_callbacks[name] for name in event.split(' ') for (var name of Array.from(event.split(' '))) { delete this._callbacks[name]; }
@ }
return this;
}
};

@ -1,76 +1,89 @@
defaultUrl = null /*
currentSlug = null * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
imageCache = {} * DS208: Avoid top-level this
urlCache = {} * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
withImage = (url, action) -> let defaultUrl = null;
if imageCache[url] let currentSlug = null;
action(imageCache[url])
else const imageCache = {};
img = new Image() const urlCache = {};
img.crossOrigin = 'anonymous'
img.src = url const withImage = function(url, action) {
img.onload = () => if (imageCache[url]) {
imageCache[url] = img return action(imageCache[url]);
action(img) } else {
const img = new Image();
@setFaviconForDoc = (doc) -> img.crossOrigin = 'anonymous';
return if currentSlug == doc.slug img.src = url;
return img.onload = () => {
favicon = $('link[rel="icon"]') imageCache[url] = img;
return action(img);
if defaultUrl == null };
defaultUrl = favicon.href }
};
if urlCache[doc.slug]
favicon.href = urlCache[doc.slug] this.setFaviconForDoc = function(doc) {
currentSlug = doc.slug if (currentSlug === doc.slug) { return; }
return
const favicon = $('link[rel="icon"]');
iconEl = $("._icon-#{doc.slug.split('~')[0]}")
return if iconEl == null if (defaultUrl === null) {
defaultUrl = favicon.href;
styles = window.getComputedStyle(iconEl, ':before') }
backgroundPositionX = styles['background-position-x'] if (urlCache[doc.slug]) {
backgroundPositionY = styles['background-position-y'] favicon.href = urlCache[doc.slug];
return if backgroundPositionX == undefined || backgroundPositionY == undefined currentSlug = doc.slug;
return;
bgUrl = app.config.favicon_spritesheet }
sourceSize = 16
sourceX = Math.abs(parseInt(backgroundPositionX.slice(0, -2))) const iconEl = $(`._icon-${doc.slug.split('~')[0]}`);
sourceY = Math.abs(parseInt(backgroundPositionY.slice(0, -2))) if (iconEl === null) { return; }
withImage(bgUrl, (docImg) -> const styles = window.getComputedStyle(iconEl, ':before');
withImage(defaultUrl, (defaultImg) ->
size = defaultImg.width const backgroundPositionX = styles['background-position-x'];
const backgroundPositionY = styles['background-position-y'];
canvas = document.createElement('canvas') if ((backgroundPositionX === undefined) || (backgroundPositionY === undefined)) { return; }
ctx = canvas.getContext('2d')
const bgUrl = app.config.favicon_spritesheet;
canvas.width = size const sourceSize = 16;
canvas.height = size const sourceX = Math.abs(parseInt(backgroundPositionX.slice(0, -2)));
ctx.drawImage(defaultImg, 0, 0) const sourceY = Math.abs(parseInt(backgroundPositionY.slice(0, -2)));
docIconPercentage = 65 return withImage(bgUrl, docImg => withImage(defaultUrl, function(defaultImg) {
destinationCoords = size / 100 * (100 - docIconPercentage) const size = defaultImg.width;
destinationSize = size / 100 * docIconPercentage
const canvas = document.createElement('canvas');
ctx.drawImage(docImg, sourceX, sourceY, sourceSize, sourceSize, destinationCoords, destinationCoords, destinationSize, destinationSize) const ctx = canvas.getContext('2d');
try canvas.width = size;
urlCache[doc.slug] = canvas.toDataURL() canvas.height = size;
favicon.href = urlCache[doc.slug] ctx.drawImage(defaultImg, 0, 0);
currentSlug = doc.slug const docIconPercentage = 65;
catch error const destinationCoords = (size / 100) * (100 - docIconPercentage);
Raven.captureException error, { level: 'info' } const destinationSize = (size / 100) * docIconPercentage;
@resetFavicon()
) ctx.drawImage(docImg, sourceX, sourceY, sourceSize, sourceSize, destinationCoords, destinationCoords, destinationSize, destinationSize);
)
try {
@resetFavicon = () -> urlCache[doc.slug] = canvas.toDataURL();
if defaultUrl != null and currentSlug != null favicon.href = urlCache[doc.slug];
$('link[rel="icon"]').href = defaultUrl
currentSlug = null return currentSlug = doc.slug;
} catch (error) {
Raven.captureException(error, { level: 'info' });
return this.resetFavicon();
}
}));
};
this.resetFavicon = function() {
if ((defaultUrl !== null) && (currentSlug !== null)) {
$('link[rel="icon"]').href = defaultUrl;
return currentSlug = null;
}
};

@ -1,7 +1,7 @@
### /*
* Copyright 2013-2023 Thibaut Courouble and other contributors * Copyright 2013-2023 Thibaut Courouble and other contributors
* *
* This source code is licensed under the terms of the Mozilla * This source code is licensed under the terms of the Mozilla
* Public License, v. 2.0, a copy of which may be obtained at: * Public License, v. 2.0, a copy of which may be obtained at:
* http://mozilla.org/MPL/2.0/ * http://mozilla.org/MPL/2.0/
### */

@ -1,23 +1,33 @@
class @LocalStorageStore /*
get: (key) -> * decaffeinate suggestions:
try * DS102: Remove unnecessary code created because of implicit returns
JSON.parse localStorage.getItem(key) * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
catch */
this.LocalStorageStore = class LocalStorageStore {
get(key) {
try {
return JSON.parse(localStorage.getItem(key));
} catch (error) {}
}
set: (key, value) -> set(key, value) {
try try {
localStorage.setItem(key, JSON.stringify(value)) localStorage.setItem(key, JSON.stringify(value));
true return true;
catch } catch (error) {}
}
del: (key) -> del(key) {
try try {
localStorage.removeItem(key) localStorage.removeItem(key);
true return true;
catch } catch (error) {}
}
reset: -> reset() {
try try {
localStorage.clear() localStorage.clear();
true return true;
catch } catch (error) {}
}
};

@ -1,223 +1,280 @@
### /*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* DS208: Avoid top-level this
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
/*
* Based on github.com/visionmedia/page.js * Based on github.com/visionmedia/page.js
* Licensed under the MIT license * Licensed under the MIT license
* Copyright 2012 TJ Holowaychuk <tj@vision-media.ca> * Copyright 2012 TJ Holowaychuk <tj@vision-media.ca>
### */
running = false let running = false;
currentState = null let currentState = null;
callbacks = [] const callbacks = [];
@page = (value, fn) -> this.page = function(value, fn) {
if typeof value is 'function' if (typeof value === 'function') {
page '*', value page('*', value);
else if typeof fn is 'function' } else if (typeof fn === 'function') {
route = new Route(value) const route = new Route(value);
callbacks.push route.middleware(fn) callbacks.push(route.middleware(fn));
else if typeof value is 'string' } else if (typeof value === 'string') {
page.show(value, fn) page.show(value, fn);
else } else {
page.start(value) page.start(value);
return }
};
page.start = (options = {}) ->
unless running page.start = function(options) {
running = true if (options == null) { options = {}; }
addEventListener 'popstate', onpopstate if (!running) {
addEventListener 'click', onclick running = true;
page.replace currentPath(), null, null, true addEventListener('popstate', onpopstate);
return addEventListener('click', onclick);
page.replace(currentPath(), null, null, true);
page.stop = -> }
if running };
running = false
removeEventListener 'click', onclick page.stop = function() {
removeEventListener 'popstate', onpopstate if (running) {
return running = false;
removeEventListener('click', onclick);
page.show = (path, state) -> removeEventListener('popstate', onpopstate);
return if path is currentState?.path }
context = new Context(path, state) };
previousState = currentState
currentState = context.state page.show = function(path, state) {
if res = page.dispatch(context) let res;
currentState = previousState if (path === (currentState != null ? currentState.path : undefined)) { return; }
location.assign(res) const context = new Context(path, state);
else const previousState = currentState;
context.pushState() currentState = context.state;
updateCanonicalLink() if (res = page.dispatch(context)) {
track() currentState = previousState;
context location.assign(res);
} else {
page.replace = (path, state, skipDispatch, init) -> context.pushState();
context = new Context(path, state or currentState) updateCanonicalLink();
context.init = init track();
currentState = context.state }
result = page.dispatch(context) unless skipDispatch return context;
if result };
context = new Context(result)
context.init = init page.replace = function(path, state, skipDispatch, init) {
currentState = context.state let result;
page.dispatch(context) let context = new Context(path, state || currentState);
context.replaceState() context.init = init;
updateCanonicalLink() currentState = context.state;
track() unless skipDispatch if (!skipDispatch) { result = page.dispatch(context); }
context if (result) {
context = new Context(result);
page.dispatch = (context) -> context.init = init;
i = 0 currentState = context.state;
next = -> page.dispatch(context);
res = fn(context, next) if fn = callbacks[i++] }
return res context.replaceState();
return next() updateCanonicalLink();
if (!skipDispatch) { track(); }
page.canGoBack = -> return context;
not Context.isIntialState(currentState) };
page.canGoForward = -> page.dispatch = function(context) {
not Context.isLastState(currentState) let i = 0;
var next = function() {
currentPath = -> let fn, res;
location.pathname + location.search + location.hash if (fn = callbacks[i++]) { res = fn(context, next); }
return res;
class Context };
@initialPath: currentPath() return next();
@sessionId: Date.now() };
@stateId: 0
page.canGoBack = () => !Context.isIntialState(currentState);
@isIntialState: (state) ->
state.id == 0 page.canGoForward = () => !Context.isLastState(currentState);
@isLastState: (state) -> var currentPath = () => location.pathname + location.search + location.hash;
state.id == @stateId - 1
class Context {
@isInitialPopState: (state) -> static initClass() {
state.path is @initialPath and @stateId is 1 this.initialPath = currentPath();
this.sessionId = Date.now();
@isSameSession: (state) -> this.stateId = 0;
state.sessionId is @sessionId }
constructor: (@path = '/', @state = {}) -> static isIntialState(state) {
@pathname = @path.replace /(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) => return state.id === 0;
@query = query }
@hash = hash
'' static isLastState(state) {
return state.id === (this.stateId - 1);
@state.id ?= @constructor.stateId++ }
@state.sessionId ?= @constructor.sessionId
@state.path = @path static isInitialPopState(state) {
return (state.path === this.initialPath) && (this.stateId === 1);
pushState: -> }
history.pushState @state, '', @path
return static isSameSession(state) {
return state.sessionId === this.sessionId;
replaceState: -> }
try history.replaceState @state, '', @path # NS_ERROR_FAILURE in Firefox
return constructor(path, state) {
if (path == null) { path = '/'; }
class Route this.path = path;
constructor: (@path, options = {}) -> if (state == null) { state = {}; }
@keys = [] this.state = state;
@regexp = pathtoRegexp @path, @keys this.pathname = this.path.replace(/(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) => {
this.query = query;
middleware: (fn) -> this.hash = hash;
(context, next) => return '';
if @match context.pathname, params = [] });
context.params = params
return fn(context, next) if (this.state.id == null) { this.state.id = this.constructor.stateId++; }
else if (this.state.sessionId == null) { this.state.sessionId = this.constructor.sessionId; }
return next() this.state.path = this.path;
}
match: (path, params) ->
return unless matchData = @regexp.exec(path) pushState() {
history.pushState(this.state, '', this.path);
for value, i in matchData[1..] }
value = decodeURIComponent value if typeof value is 'string'
if key = @keys[i] replaceState() {
params[key.name] = value try { history.replaceState(this.state, '', this.path); } catch (error) {} // NS_ERROR_FAILURE in Firefox
else }
params.push value }
true Context.initClass();
pathtoRegexp = (path, keys) -> class Route {
return path if path instanceof RegExp constructor(path, options) {
this.path = path;
path = "(#{path.join '|'})" if path instanceof Array if (options == null) { options = {}; }
this.keys = [];
this.regexp = pathtoRegexp(this.path, this.keys);
}
middleware(fn) {
return (context, next) => {
let params;
if (this.match(context.pathname, (params = []))) {
context.params = params;
return fn(context, next);
} else {
return next();
}
};
}
match(path, params) {
let matchData;
if (!(matchData = this.regexp.exec(path))) { return; }
const iterable = matchData.slice(1);
for (let i = 0; i < iterable.length; i++) {
var key;
var value = iterable[i];
if (typeof value === 'string') { value = decodeURIComponent(value); }
if ((key = this.keys[i])) {
params[key.name] = value;
} else {
params.push(value);
}
}
return true;
}
}
var pathtoRegexp = function(path, keys) {
if (path instanceof RegExp) { return path; }
if (path instanceof Array) { path = `(${path.join('|')})`; }
path = path path = path
.replace /\/\(/g, '(?:/' .replace(/\/\(/g, '(?:/')
.replace /(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash = '', format = '', key, capture, optional) -> .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) {
keys.push name: key, optional: !!optional if (slash == null) { slash = ''; }
str = if optional then '' else slash if (format == null) { format = ''; }
str += '(?:' keys.push({name: key, optional: !!optional});
str += slash if optional let str = optional ? '' : slash;
str += format str += '(?:';
str += capture or if format then '([^/.]+?)' else '([^/]+?)' if (optional) { str += slash; }
str += ')' str += format;
str += optional if optional str += capture || (format ? '([^/.]+?)' : '([^/]+?)');
str str += ')';
.replace /([\/.])/g, '\\$1' if (optional) { str += optional; }
.replace /\*/g, '(.*)' return str;
}).replace(/([\/.])/g, '\\$1')
new RegExp "^#{path}$" .replace(/\*/g, '(.*)');
onpopstate = (event) -> return new RegExp(`^${path}$`);
return if not event.state or Context.isInitialPopState(event.state) };
if Context.isSameSession(event.state) var onpopstate = function(event) {
page.replace(event.state.path, event.state) if (!event.state || Context.isInitialPopState(event.state)) { return; }
else
location.reload() if (Context.isSameSession(event.state)) {
return page.replace(event.state.path, event.state);
} else {
onclick = (event) -> location.reload();
try }
return if event.which isnt 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.defaultPrevented };
catch
return var onclick = function(event) {
try {
link = $.eventTarget(event) if ((event.which !== 1) || event.metaKey || event.ctrlKey || event.shiftKey || event.defaultPrevented) { return; }
link = link.parentNode while link and link.tagName isnt 'A' } catch (error) {
return;
if link and not link.target and isSameOrigin(link.href) }
event.preventDefault()
path = link.pathname + link.search + link.hash let link = $.eventTarget(event);
path = path.replace /^\/\/+/, '/' # IE11 bug while (link && (link.tagName !== 'A')) { link = link.parentNode; }
page.show(path)
return if (link && !link.target && isSameOrigin(link.href)) {
event.preventDefault();
isSameOrigin = (url) -> let path = link.pathname + link.search + link.hash;
url.indexOf("#{location.protocol}//#{location.hostname}") is 0 path = path.replace(/^\/\/+/, '/'); // IE11 bug
page.show(path);
updateCanonicalLink = -> }
@canonicalLink ||= document.head.querySelector('link[rel="canonical"]') };
@canonicalLink.setAttribute('href', "https://#{location.host}#{location.pathname}")
var isSameOrigin = url => url.indexOf(`${location.protocol}//${location.hostname}`) === 0;
trackers = []
var updateCanonicalLink = function() {
page.track = (fn) -> if (!this.canonicalLink) { this.canonicalLink = document.head.querySelector('link[rel="canonical"]'); }
trackers.push(fn) return this.canonicalLink.setAttribute('href', `https://${location.host}${location.pathname}`);
return };
track = -> const trackers = [];
return unless app.config.env == 'production'
return if navigator.doNotTrack == '1' page.track = function(fn) {
return if navigator.globalPrivacyControl trackers.push(fn);
};
consentGiven = Cookies.get('analyticsConsent')
consentAsked = Cookies.get('analyticsConsentAsked') var track = function() {
if (app.config.env !== 'production') { return; }
if consentGiven == '1' if (navigator.doNotTrack === '1') { return; }
tracker.call() for tracker in trackers if (navigator.globalPrivacyControl) { return; }
else if consentGiven == undefined and consentAsked == undefined
# Only ask for consent once per browser session const consentGiven = Cookies.get('analyticsConsent');
Cookies.set('analyticsConsentAsked', '1') const consentAsked = Cookies.get('analyticsConsentAsked');
new app.views.Notif 'AnalyticsConsent', autoHide: null if (consentGiven === '1') {
return for (var tracker of Array.from(trackers)) { tracker.call(); }
} else if ((consentGiven === undefined) && (consentAsked === undefined)) {
@resetAnalytics = -> // Only ask for consent once per browser session
for cookie in document.cookie.split(/;\s?/) Cookies.set('analyticsConsentAsked', '1');
name = cookie.split('=')[0]
if name[0] == '_' && name[1] != '_' new app.views.Notif('AnalyticsConsent', {autoHide: null});
Cookies.expire(name) }
return };
this.resetAnalytics = function() {
for (var cookie of Array.from(document.cookie.split(/;\s?/))) {
var name = cookie.split('=')[0];
if ((name[0] === '_') && (name[1] !== '_')) {
Cookies.expire(name);
}
}
};

@ -1,399 +1,491 @@
# /*
# Traversing * decaffeinate suggestions:
# * DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
@$ = (selector, el = document) -> * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
try el.querySelector(selector) catch * DS104: Avoid inline assignments
* DS204: Change includes calls to have a more natural evaluation order
@$$ = (selector, el = document) -> * DS207: Consider shorter variations of null checks
try el.querySelectorAll(selector) catch * DS208: Avoid top-level this
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
$.id = (id) -> */
document.getElementById(id) //
// Traversing
$.hasChild = (parent, el) -> //
return unless parent
while el let smoothDistance, smoothDuration, smoothEnd, smoothStart;
return true if el is parent this.$ = function(selector, el) {
return if el is document.body if (el == null) { el = document; }
el = el.parentNode try { return el.querySelector(selector); } catch (error) {}
};
$.closestLink = (el, parent = document.body) ->
while el this.$$ = function(selector, el) {
return el if el.tagName is 'A' if (el == null) { el = document; }
return if el is parent try { return el.querySelectorAll(selector); } catch (error) {}
el = el.parentNode };
# $.id = id => document.getElementById(id);
# Events
# $.hasChild = function(parent, el) {
if (!parent) { return; }
$.on = (el, event, callback, useCapture = false) -> while (el) {
if event.indexOf(' ') >= 0 if (el === parent) { return true; }
$.on el, name, callback for name in event.split(' ') if (el === document.body) { return; }
else el = el.parentNode;
el.addEventListener(event, callback, useCapture) }
return };
$.off = (el, event, callback, useCapture = false) -> $.closestLink = function(el, parent) {
if event.indexOf(' ') >= 0 if (parent == null) { parent = document.body; }
$.off el, name, callback for name in event.split(' ') while (el) {
else if (el.tagName === 'A') { return el; }
el.removeEventListener(event, callback, useCapture) if (el === parent) { return; }
return el = el.parentNode;
}
$.trigger = (el, type, canBubble = true, cancelable = true) -> };
event = document.createEvent 'Event'
event.initEvent(type, canBubble, cancelable) //
el.dispatchEvent(event) // Events
return //
$.click = (el) -> $.on = function(el, event, callback, useCapture) {
event = document.createEvent 'MouseEvent' if (useCapture == null) { useCapture = false; }
event.initMouseEvent 'click', true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null if (event.indexOf(' ') >= 0) {
el.dispatchEvent(event) for (var name of Array.from(event.split(' '))) { $.on(el, name, callback); }
return } else {
el.addEventListener(event, callback, useCapture);
$.stopEvent = (event) -> }
event.preventDefault() };
event.stopPropagation()
event.stopImmediatePropagation() $.off = function(el, event, callback, useCapture) {
return if (useCapture == null) { useCapture = false; }
if (event.indexOf(' ') >= 0) {
$.eventTarget = (event) -> for (var name of Array.from(event.split(' '))) { $.off(el, name, callback); }
event.target.correspondingUseElement || event.target } else {
el.removeEventListener(event, callback, useCapture);
# }
# Manipulation };
#
$.trigger = function(el, type, canBubble, cancelable) {
buildFragment = (value) -> if (canBubble == null) { canBubble = true; }
fragment = document.createDocumentFragment() if (cancelable == null) { cancelable = true; }
const event = document.createEvent('Event');
if $.isCollection(value) event.initEvent(type, canBubble, cancelable);
fragment.appendChild(child) for child in $.makeArray(value) el.dispatchEvent(event);
else };
fragment.innerHTML = value
$.click = function(el) {
fragment const event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
$.append = (el, value) -> el.dispatchEvent(event);
if typeof value is 'string' };
el.insertAdjacentHTML 'beforeend', value
else $.stopEvent = function(event) {
value = buildFragment(value) if $.isCollection(value) event.preventDefault();
el.appendChild(value) event.stopPropagation();
return event.stopImmediatePropagation();
};
$.prepend = (el, value) ->
if not el.firstChild $.eventTarget = event => event.target.correspondingUseElement || event.target;
$.append(value)
else if typeof value is 'string' //
el.insertAdjacentHTML 'afterbegin', value // Manipulation
else //
value = buildFragment(value) if $.isCollection(value)
el.insertBefore(value, el.firstChild) const buildFragment = function(value) {
return const fragment = document.createDocumentFragment();
$.before = (el, value) -> if ($.isCollection(value)) {
if typeof value is 'string' or $.isCollection(value) for (var child of Array.from($.makeArray(value))) { fragment.appendChild(child); }
value = buildFragment(value) } else {
fragment.innerHTML = value;
el.parentNode.insertBefore(value, el) }
return
return fragment;
$.after = (el, value) -> };
if typeof value is 'string' or $.isCollection(value)
value = buildFragment(value) $.append = function(el, value) {
if (typeof value === 'string') {
if el.nextSibling el.insertAdjacentHTML('beforeend', value);
el.parentNode.insertBefore(value, el.nextSibling) } else {
else if ($.isCollection(value)) { value = buildFragment(value); }
el.parentNode.appendChild(value) el.appendChild(value);
return }
};
$.remove = (value) ->
if $.isCollection(value) $.prepend = function(el, value) {
el.parentNode?.removeChild(el) for el in $.makeArray(value) if (!el.firstChild) {
else $.append(value);
value.parentNode?.removeChild(value) } else if (typeof value === 'string') {
return el.insertAdjacentHTML('afterbegin', value);
} else {
$.empty = (el) -> if ($.isCollection(value)) { value = buildFragment(value); }
el.removeChild(el.firstChild) while el.firstChild el.insertBefore(value, el.firstChild);
return }
};
# Calls the function while the element is off the DOM to avoid triggering
# unnecessary reflows and repaints. $.before = function(el, value) {
$.batchUpdate = (el, fn) -> if ((typeof value === 'string') || $.isCollection(value)) {
parent = el.parentNode value = buildFragment(value);
sibling = el.nextSibling }
parent.removeChild(el)
el.parentNode.insertBefore(value, el);
fn(el) };
if (sibling) $.after = function(el, value) {
parent.insertBefore(el, sibling) if ((typeof value === 'string') || $.isCollection(value)) {
else value = buildFragment(value);
parent.appendChild(el) }
return
if (el.nextSibling) {
# el.parentNode.insertBefore(value, el.nextSibling);
# Offset } else {
# el.parentNode.appendChild(value);
}
$.rect = (el) -> };
el.getBoundingClientRect()
$.remove = function(value) {
$.offset = (el, container = document.body) -> if ($.isCollection(value)) {
top = 0 for (var el of Array.from($.makeArray(value))) { if (el.parentNode != null) {
left = 0 el.parentNode.removeChild(el);
} }
while el and el isnt container } else {
top += el.offsetTop if (value.parentNode != null) {
left += el.offsetLeft value.parentNode.removeChild(value);
el = el.offsetParent }
}
top: top };
left: left
$.empty = function(el) {
$.scrollParent = (el) -> while (el.firstChild) { el.removeChild(el.firstChild); }
while (el = el.parentNode) and el.nodeType is 1 };
break if el.scrollTop > 0
break if getComputedStyle(el)?.overflowY in ['auto', 'scroll'] // Calls the function while the element is off the DOM to avoid triggering
el // unnecessary reflows and repaints.
$.batchUpdate = function(el, fn) {
$.scrollTo = (el, parent, position = 'center', options = {}) -> const parent = el.parentNode;
return unless el const sibling = el.nextSibling;
parent.removeChild(el);
parent ?= $.scrollParent(el)
return unless parent fn(el);
parentHeight = parent.clientHeight if (sibling) {
parentScrollHeight = parent.scrollHeight parent.insertBefore(el, sibling);
return unless parentScrollHeight > parentHeight } else {
parent.appendChild(el);
top = $.offset(el, parent).top }
offsetTop = parent.firstElementChild.offsetTop };
switch position //
when 'top' // Offset
parent.scrollTop = top - offsetTop - (if options.margin? then options.margin else 0) //
when 'center'
parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2) $.rect = el => el.getBoundingClientRect();
when 'continuous'
scrollTop = parent.scrollTop $.offset = function(el, container) {
height = el.offsetHeight if (container == null) { container = document.body; }
let top = 0;
lastElementOffset = parent.lastElementChild.offsetTop + parent.lastElementChild.offsetHeight let left = 0;
offsetBottom = if lastElementOffset > 0 then parentScrollHeight - lastElementOffset else 0
while (el && (el !== container)) {
# If the target element is above the visible portion of its scrollable top += el.offsetTop;
# ancestor, move it near the top with a gap = options.topGap * target's height. left += el.offsetLeft;
if top - offsetTop <= scrollTop + height * (options.topGap or 1) el = el.offsetParent;
parent.scrollTop = top - offsetTop - height * (options.topGap or 1) }
# If the target element is below the visible portion of its scrollable
# ancestor, move it near the bottom with a gap = options.bottomGap * target's height. return {
else if top + offsetBottom >= scrollTop + parentHeight - height * ((options.bottomGap or 1) + 1) top,
parent.scrollTop = top + offsetBottom - parentHeight + height * ((options.bottomGap or 1) + 1) left
return };
};
$.scrollToWithImageLock = (el, parent, args...) ->
parent ?= $.scrollParent(el) $.scrollParent = function(el) {
return unless parent while ((el = el.parentNode) && (el.nodeType === 1)) {
var needle;
$.scrollTo el, parent, args... if (el.scrollTop > 0) { break; }
if ((needle = __guard__(getComputedStyle(el), x => x.overflowY), ['auto', 'scroll'].includes(needle))) { break; }
# Lock the scroll position on the target element for up to 3 seconds while }
# nearby images are loaded and rendered. return el;
for image in parent.getElementsByTagName('img') when not image.complete };
do ->
onLoad = (event) -> $.scrollTo = function(el, parent, position, options) {
clearTimeout(timeout) if (position == null) { position = 'center'; }
unbind(event.target) if (options == null) { options = {}; }
$.scrollTo el, parent, args... if (!el) { return; }
unbind = (target) -> if (parent == null) { parent = $.scrollParent(el); }
$.off target, 'load', onLoad if (!parent) { return; }
$.on image, 'load', onLoad const parentHeight = parent.clientHeight;
timeout = setTimeout unbind.bind(null, image), 3000 const parentScrollHeight = parent.scrollHeight;
return if (!(parentScrollHeight > parentHeight)) { return; }
# Calls the function while locking the element's position relative to the window. const {
$.lockScroll = (el, fn) -> top
if parent = $.scrollParent(el) } = $.offset(el, parent);
top = $.rect(el).top const {
top -= $.rect(parent).top unless parent in [document.body, document.documentElement] offsetTop
fn() } = parent.firstElementChild;
parent.scrollTop = $.offset(el, parent).top - top
else switch (position) {
fn() case 'top':
return parent.scrollTop = top - offsetTop - ((options.margin != null) ? options.margin : 0);
break;
smoothScroll = smoothStart = smoothEnd = smoothDistance = smoothDuration = null case 'center':
parent.scrollTop = top - Math.round((parentHeight / 2) - (el.offsetHeight / 2));
$.smoothScroll = (el, end) -> break;
unless window.requestAnimationFrame case 'continuous':
el.scrollTop = end var {
return scrollTop
} = parent;
smoothEnd = end var height = el.offsetHeight;
if smoothScroll var lastElementOffset = parent.lastElementChild.offsetTop + parent.lastElementChild.offsetHeight;
newDistance = smoothEnd - smoothStart var offsetBottom = lastElementOffset > 0 ? parentScrollHeight - lastElementOffset : 0;
smoothDuration += Math.min 300, Math.abs(smoothDistance - newDistance)
smoothDistance = newDistance // If the target element is above the visible portion of its scrollable
return // ancestor, move it near the top with a gap = options.topGap * target's height.
if ((top - offsetTop) <= (scrollTop + (height * (options.topGap || 1)))) {
smoothStart = el.scrollTop parent.scrollTop = top - offsetTop - (height * (options.topGap || 1));
smoothDistance = smoothEnd - smoothStart // If the target element is below the visible portion of its scrollable
smoothDuration = Math.min 300, Math.abs(smoothDistance) // ancestor, move it near the bottom with a gap = options.bottomGap * target's height.
startTime = Date.now() } else if ((top + offsetBottom) >= ((scrollTop + parentHeight) - (height * ((options.bottomGap || 1) + 1)))) {
parent.scrollTop = ((top + offsetBottom) - parentHeight) + (height * ((options.bottomGap || 1) + 1));
smoothScroll = -> }
p = Math.min 1, (Date.now() - startTime) / smoothDuration break;
y = Math.max 0, Math.floor(smoothStart + smoothDistance * (if p < 0.5 then 2 * p * p else p * (4 - p * 2) - 1)) }
el.scrollTop = y };
if p is 1
smoothScroll = null $.scrollToWithImageLock = function(el, parent, ...args) {
else if (parent == null) { parent = $.scrollParent(el); }
requestAnimationFrame(smoothScroll) if (!parent) { return; }
requestAnimationFrame(smoothScroll)
$.scrollTo(el, parent, ...Array.from(args));
#
# Utilities // Lock the scroll position on the target element for up to 3 seconds while
# // nearby images are loaded and rendered.
for (var image of Array.from(parent.getElementsByTagName('img'))) {
$.extend = (target, objects...) -> if (!image.complete) {
for object in objects when object (function() {
for key, value of object let timeout;
target[key] = value const onLoad = function(event) {
target clearTimeout(timeout);
unbind(event.target);
$.makeArray = (object) -> return $.scrollTo(el, parent, ...Array.from(args));
if Array.isArray(object) };
object
else var unbind = target => $.off(target, 'load', onLoad);
Array::slice.apply(object)
$.on(image, 'load', onLoad);
$.arrayDelete = (array, object) -> return timeout = setTimeout(unbind.bind(null, image), 3000);
index = array.indexOf(object) })();
if index >= 0 }
array.splice(index, 1) }
true };
else
false // Calls the function while locking the element's position relative to the window.
$.lockScroll = function(el, fn) {
# Returns true if the object is an array or a collection of DOM elements. let parent;
$.isCollection = (object) -> if (parent = $.scrollParent(el)) {
Array.isArray(object) or typeof object?.item is 'function' let {
top
ESCAPE_HTML_MAP = } = $.rect(el);
'&': '&amp;' if (![document.body, document.documentElement].includes(parent)) { top -= $.rect(parent).top; }
'<': '&lt;' fn();
'>': '&gt;' parent.scrollTop = $.offset(el, parent).top - top;
'"': '&quot;' } else {
"'": '&#x27;' fn();
}
};
let smoothScroll = (smoothStart = (smoothEnd = (smoothDistance = (smoothDuration = null))));
$.smoothScroll = function(el, end) {
if (!window.requestAnimationFrame) {
el.scrollTop = end;
return;
}
smoothEnd = end;
if (smoothScroll) {
const newDistance = smoothEnd - smoothStart;
smoothDuration += Math.min(300, Math.abs(smoothDistance - newDistance));
smoothDistance = newDistance;
return;
}
smoothStart = el.scrollTop;
smoothDistance = smoothEnd - smoothStart;
smoothDuration = Math.min(300, Math.abs(smoothDistance));
const startTime = Date.now();
smoothScroll = function() {
const p = Math.min(1, (Date.now() - startTime) / smoothDuration);
const y = Math.max(0, Math.floor(smoothStart + (smoothDistance * (p < 0.5 ? 2 * p * p : (p * (4 - (p * 2))) - 1))));
el.scrollTop = y;
if (p === 1) {
return smoothScroll = null;
} else {
return requestAnimationFrame(smoothScroll);
}
};
return requestAnimationFrame(smoothScroll);
};
//
// Utilities
//
$.extend = function(target, ...objects) {
for (var object of Array.from(objects)) {
if (object) {
for (var key in object) {
var value = object[key];
target[key] = value;
}
}
}
return target;
};
$.makeArray = function(object) {
if (Array.isArray(object)) {
return object;
} else {
return Array.prototype.slice.apply(object);
}
};
$.arrayDelete = function(array, object) {
const index = array.indexOf(object);
if (index >= 0) {
array.splice(index, 1);
return true;
} else {
return false;
}
};
// Returns true if the object is an array or a collection of DOM elements.
$.isCollection = object => Array.isArray(object) || (typeof (object != null ? object.item : undefined) === 'function');
const ESCAPE_HTML_MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;' '/': '&#x2F;'
};
ESCAPE_HTML_REGEXP = /[&<>"'\/]/g
const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g;
$.escape = (string) ->
string.replace ESCAPE_HTML_REGEXP, (match) -> ESCAPE_HTML_MAP[match] $.escape = string => string.replace(ESCAPE_HTML_REGEXP, match => ESCAPE_HTML_MAP[match]);
ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g;
$.escapeRegexp = (string) -> $.escapeRegexp = string => string.replace(ESCAPE_REGEXP, "\\$1");
string.replace ESCAPE_REGEXP, "\\$1"
$.urlDecode = string => decodeURIComponent(string.replace(/\+/g, '%20'));
$.urlDecode = (string) ->
decodeURIComponent string.replace(/\+/g, '%20') $.classify = function(string) {
string = string.split('_');
$.classify = (string) -> for (let i = 0; i < string.length; i++) {
string = string.split('_') var substr = string[i];
for substr, i in string string[i] = substr[0].toUpperCase() + substr.slice(1);
string[i] = substr[0].toUpperCase() + substr[1..] }
string.join('') return string.join('');
};
$.framify = (fn, obj) ->
if window.requestAnimationFrame $.framify = function(fn, obj) {
(args...) -> requestAnimationFrame(fn.bind(obj, args...)) if (window.requestAnimationFrame) {
else return (...args) => requestAnimationFrame(fn.bind(obj, ...Array.from(args)));
fn } else {
return fn;
$.requestAnimationFrame = (fn) -> }
if window.requestAnimationFrame };
requestAnimationFrame(fn)
else $.requestAnimationFrame = function(fn) {
setTimeout(fn, 0) if (window.requestAnimationFrame) {
return requestAnimationFrame(fn);
} else {
# setTimeout(fn, 0);
# Miscellaneous }
# };
$.noop = -> //
// Miscellaneous
$.popup = (value) -> //
try
win = window.open() $.noop = function() {};
win.opener = null if win.opener
win.location = value.href or value $.popup = function(value) {
catch try {
window.open value.href or value, '_blank' const win = window.open();
return if (win.opener) { win.opener = null; }
win.location = value.href || value;
isMac = null } catch (error) {
$.isMac = -> window.open(value.href || value, '_blank');
isMac ?= navigator.userAgent?.indexOf('Mac') >= 0 }
};
isIE = null
$.isIE = -> let isMac = null;
isIE ?= navigator.userAgent?.indexOf('MSIE') >= 0 || navigator.userAgent?.indexOf('rv:11.0') >= 0 $.isMac = () => isMac != null ? isMac : (isMac = (navigator.userAgent != null ? navigator.userAgent.indexOf('Mac') : undefined) >= 0);
isChromeForAndroid = null let isIE = null;
$.isChromeForAndroid = -> $.isIE = () => isIE != null ? isIE : (isIE = ((navigator.userAgent != null ? navigator.userAgent.indexOf('MSIE') : undefined) >= 0) || ((navigator.userAgent != null ? navigator.userAgent.indexOf('rv:11.0') : undefined) >= 0));
isChromeForAndroid ?= navigator.userAgent?.indexOf('Android') >= 0 && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent)
let isChromeForAndroid = null;
isAndroid = null $.isChromeForAndroid = () => isChromeForAndroid != null ? isChromeForAndroid : (isChromeForAndroid = ((navigator.userAgent != null ? navigator.userAgent.indexOf('Android') : undefined) >= 0) && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent));
$.isAndroid = ->
isAndroid ?= navigator.userAgent?.indexOf('Android') >= 0 let isAndroid = null;
$.isAndroid = () => isAndroid != null ? isAndroid : (isAndroid = (navigator.userAgent != null ? navigator.userAgent.indexOf('Android') : undefined) >= 0);
isIOS = null
$.isIOS = -> let isIOS = null;
isIOS ?= navigator.userAgent?.indexOf('iPhone') >= 0 || navigator.userAgent?.indexOf('iPad') >= 0 $.isIOS = () => isIOS != null ? isIOS : (isIOS = ((navigator.userAgent != null ? navigator.userAgent.indexOf('iPhone') : undefined) >= 0) || ((navigator.userAgent != null ? navigator.userAgent.indexOf('iPad') : undefined) >= 0));
$.overlayScrollbarsEnabled = -> $.overlayScrollbarsEnabled = function() {
return false unless $.isMac() if (!$.isMac()) { return false; }
div = document.createElement('div') const div = document.createElement('div');
div.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position: absolute') div.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position: absolute');
document.body.appendChild(div) document.body.appendChild(div);
result = div.offsetWidth is div.clientWidth const result = div.offsetWidth === div.clientWidth;
document.body.removeChild(div) document.body.removeChild(div);
result return result;
};
HIGHLIGHT_DEFAULTS =
className: 'highlight' const HIGHLIGHT_DEFAULTS = {
className: 'highlight',
delay: 1000 delay: 1000
};
$.highlight = (el, options = {}) ->
options = $.extend {}, HIGHLIGHT_DEFAULTS, options $.highlight = function(el, options) {
el.classList.add(options.className) if (options == null) { options = {}; }
setTimeout (-> el.classList.remove(options.className)), options.delay options = $.extend({}, HIGHLIGHT_DEFAULTS, options);
return el.classList.add(options.className);
setTimeout((() => el.classList.remove(options.className)), options.delay);
$.copyToClipboard = (string) -> };
textarea = document.createElement('textarea')
textarea.style.position = 'fixed' $.copyToClipboard = function(string) {
textarea.style.opacity = 0 let result;
textarea.value = string const textarea = document.createElement('textarea');
document.body.appendChild(textarea) textarea.style.position = 'fixed';
try textarea.style.opacity = 0;
textarea.select() textarea.value = string;
result = !!document.execCommand('copy') document.body.appendChild(textarea);
catch try {
result = false textarea.select();
finally result = !!document.execCommand('copy');
document.body.removeChild(textarea) } catch (error) {
result result = false;
}
finally {
document.body.removeChild(textarea);
}
return result;
};
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,147 +1,174 @@
class app.models.Doc extends app.Model /*
# Attributes: name, slug, type, version, release, db_size, mtime, links * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
constructor: -> * DS207: Consider shorter variations of null checks
super * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
@reset @ */
@slug_without_version = @slug.split('~')[0] app.models.Doc = class Doc extends app.Model {
@fullName = "#{@name}" + if @version then " #{@version}" else '' // Attributes: name, slug, type, version, release, db_size, mtime, links
@icon = @slug_without_version
@short_version = @version.split(' ')[0] if @version constructor() {
@text = @toEntry().text super(...arguments);
this.reset(this);
reset: (data) -> this.slug_without_version = this.slug.split('~')[0];
@resetEntries data.entries this.fullName = `${this.name}` + (this.version ? ` ${this.version}` : '');
@resetTypes data.types this.icon = this.slug_without_version;
return if (this.version) { this.short_version = this.version.split(' ')[0]; }
this.text = this.toEntry().text;
resetEntries: (entries) -> }
@entries = new app.collections.Entries(entries)
@entries.each (entry) => entry.doc = @ reset(data) {
return this.resetEntries(data.entries);
this.resetTypes(data.types);
resetTypes: (types) -> }
@types = new app.collections.Types(types)
@types.each (type) => type.doc = @ resetEntries(entries) {
return this.entries = new app.collections.Entries(entries);
this.entries.each(entry => { return entry.doc = this; });
fullPath: (path = '') -> }
path = "/#{path}" unless path[0] is '/'
"/#{@slug}#{path}" resetTypes(types) {
this.types = new app.collections.Types(types);
fileUrl: (path) -> this.types.each(type => { return type.doc = this; });
"#{app.config.docs_origin}#{@fullPath(path)}?#{@mtime}" }
dbUrl: -> fullPath(path) {
"#{app.config.docs_origin}/#{@slug}/#{app.config.db_filename}?#{@mtime}" if (path == null) { path = ''; }
if (path[0] !== '/') { path = `/${path}`; }
indexUrl: -> return `/${this.slug}${path}`;
"#{app.indexHost()}/#{@slug}/#{app.config.index_filename}?#{@mtime}" }
toEntry: -> fileUrl(path) {
return @entry if @entry return `${app.config.docs_origin}${this.fullPath(path)}?${this.mtime}`;
@entry = new app.models.Entry }
doc: @
name: @fullName dbUrl() {
return `${app.config.docs_origin}/${this.slug}/${app.config.db_filename}?${this.mtime}`;
}
indexUrl() {
return `${app.indexHost()}/${this.slug}/${app.config.index_filename}?${this.mtime}`;
}
toEntry() {
if (this.entry) { return this.entry; }
this.entry = new app.models.Entry({
doc: this,
name: this.fullName,
path: 'index' path: 'index'
@entry.addAlias(@name) if @version });
@entry if (this.version) { this.entry.addAlias(this.name); }
return this.entry;
findEntryByPathAndHash: (path, hash) -> }
if hash and entry = @entries.findBy 'path', "#{path}##{hash}"
entry findEntryByPathAndHash(path, hash) {
else if path is 'index' let entry;
@toEntry() if (hash && (entry = this.entries.findBy('path', `${path}#${hash}`))) {
else return entry;
@entries.findBy 'path', path } else if (path === 'index') {
return this.toEntry();
load: (onSuccess, onError, options = {}) -> } else {
return if options.readCache and @_loadFromCache(onSuccess) return this.entries.findBy('path', path);
}
callback = (data) => }
@reset data
onSuccess() load(onSuccess, onError, options) {
@_setCache data if options.writeCache if (options == null) { options = {}; }
return if (options.readCache && this._loadFromCache(onSuccess)) { return; }
ajax const callback = data => {
url: @indexUrl() this.reset(data);
success: callback onSuccess();
if (options.writeCache) { this._setCache(data); }
};
return ajax({
url: this.indexUrl(),
success: callback,
error: onError error: onError
});
clearCache: -> }
app.localStorage.del @slug
return clearCache() {
app.localStorage.del(this.slug);
_loadFromCache: (onSuccess) -> }
return unless data = @_getCache()
_loadFromCache(onSuccess) {
callback = => let data;
@reset data if (!(data = this._getCache())) { return; }
onSuccess()
return const callback = () => {
this.reset(data);
setTimeout callback, 0 onSuccess();
true };
_getCache: -> setTimeout(callback, 0);
return unless data = app.localStorage.get @slug return true;
}
if data[0] is @mtime
return data[1] _getCache() {
else let data;
@clearCache() if (!(data = app.localStorage.get(this.slug))) { return; }
return
if (data[0] === this.mtime) {
_setCache: (data) -> return data[1];
app.localStorage.set @slug, [@mtime, data] } else {
return this.clearCache();
return;
install: (onSuccess, onError, onProgress) -> }
return if @installing }
@installing = true
_setCache(data) {
error = => app.localStorage.set(this.slug, [this.mtime, data]);
@installing = null }
onError()
return install(onSuccess, onError, onProgress) {
if (this.installing) { return; }
success = (data) => this.installing = true;
@installing = null
app.db.store @, data, onSuccess, error const error = () => {
return this.installing = null;
onError();
ajax };
url: @dbUrl()
success: success const success = data => {
error: error this.installing = null;
progress: onProgress app.db.store(this, data, onSuccess, error);
};
ajax({
url: this.dbUrl(),
success,
error,
progress: onProgress,
timeout: 3600 timeout: 3600
return });
}
uninstall: (onSuccess, onError) ->
return if @installing uninstall(onSuccess, onError) {
@installing = true if (this.installing) { return; }
this.installing = true;
success = =>
@installing = null const success = () => {
onSuccess() this.installing = null;
return onSuccess();
};
error = =>
@installing = null const error = () => {
onError() this.installing = null;
return onError();
};
app.db.unstore @, success, error
return app.db.unstore(this, success, error);
}
getInstallStatus: (callback) ->
app.db.version @, (value) -> getInstallStatus(callback) {
callback installed: !!value, mtime: value app.db.version(this, value => callback({installed: !!value, mtime: value}));
return }
isOutdated: (status) -> isOutdated(status) {
return false if not status if (!status) { return false; }
isInstalled = status.installed or app.settings.get('autoInstall') const isInstalled = status.installed || app.settings.get('autoInstall');
isInstalled and @mtime isnt status.mtime return isInstalled && (this.mtime !== status.mtime);
}
};

@ -1,85 +1,116 @@
#= require app/searcher /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
//= require app/searcher
class app.models.Entry extends app.Model (function() {
# Attributes: name, type, path let applyAliases = undefined;
const Cls = (app.models.Entry = class Entry extends app.Model {
static initClass() {
constructor: -> let ALIASES;
super applyAliases = function(string) {
@text = applyAliases(app.Searcher.normalizeString(@name)) if (ALIASES.hasOwnProperty(string)) {
return [string, ALIASES[string]];
} else {
const words = string.split('.');
for (let i = 0; i < words.length; i++) {
var word = words[i];
if (ALIASES.hasOwnProperty(word)) {
words[i] = ALIASES[word];
return [string, words.join('.')];
}
}
}
return string;
};
addAlias: (name) -> this.ALIASES = (ALIASES = {
text = applyAliases(app.Searcher.normalizeString(name)) 'angular': 'ng',
@text = [@text] unless Array.isArray(@text) 'angular.js': 'ng',
@text.push(if Array.isArray(text) then text[1] else text) 'backbone.js': 'bb',
return 'c++': 'cpp',
'coffeescript': 'cs',
'crystal': 'cr',
'elixir': 'ex',
'javascript': 'js',
'julia': 'jl',
'jquery': '$',
'knockout.js': 'ko',
'kubernetes': 'k8s',
'less': 'ls',
'lodash': '_',
'löve': 'love',
'marionette': 'mn',
'markdown': 'md',
'matplotlib': 'mpl',
'modernizr': 'mdr',
'moment.js': 'mt',
'openjdk': 'java',
'nginx': 'ngx',
'numpy': 'np',
'pandas': 'pd',
'postgresql': 'pg',
'python': 'py',
'ruby.on.rails': 'ror',
'ruby': 'rb',
'rust': 'rs',
'sass': 'scss',
'tensorflow': 'tf',
'typescript': 'ts',
'underscore.js': '_'
});
}
// Attributes: name, type, path
fullPath: -> constructor() {
@doc.fullPath if @isIndex() then '' else @path super(...arguments);
this.text = applyAliases(app.Searcher.normalizeString(this.name));
}
dbPath: -> addAlias(name) {
@path.replace /#.*/, '' const text = applyAliases(app.Searcher.normalizeString(name));
if (!Array.isArray(this.text)) { this.text = [this.text]; }
this.text.push(Array.isArray(text) ? text[1] : text);
}
filePath: -> fullPath() {
@doc.fullPath @_filePath() return this.doc.fullPath(this.isIndex() ? '' : this.path);
}
fileUrl: -> dbPath() {
@doc.fileUrl @_filePath() return this.path.replace(/#.*/, '');
}
_filePath: -> filePath() {
result = @path.replace /#.*/, '' return this.doc.fullPath(this._filePath());
result += '.html' unless result[-5..-1] is '.html' }
result
isIndex: -> fileUrl() {
@path is 'index' return this.doc.fileUrl(this._filePath());
}
getType: -> _filePath() {
@doc.types.findBy 'name', @type let result = this.path.replace(/#.*/, '');
if (result.slice(-5) !== '.html') { result += '.html'; }
return result;
}
loadFile: (onSuccess, onError) -> isIndex() {
app.db.load(@, onSuccess, onError) return this.path === 'index';
}
applyAliases = (string) -> getType() {
if ALIASES.hasOwnProperty(string) return this.doc.types.findBy('name', this.type);
return [string, ALIASES[string]] }
else
words = string.split('.')
for word, i in words when ALIASES.hasOwnProperty(word)
words[i] = ALIASES[word]
return [string, words.join('.')]
return string
@ALIASES = ALIASES = loadFile(onSuccess, onError) {
'angular': 'ng' return app.db.load(this, onSuccess, onError);
'angular.js': 'ng' }
'backbone.js': 'bb' });
'c++': 'cpp' Cls.initClass();
'coffeescript': 'cs' return Cls;
'crystal': 'cr' })();
'elixir': 'ex'
'javascript': 'js'
'julia': 'jl'
'jquery': '$'
'knockout.js': 'ko'
'kubernetes': 'k8s'
'less': 'ls'
'lodash': '_'
'löve': 'love'
'marionette': 'mn'
'markdown': 'md'
'matplotlib': 'mpl'
'modernizr': 'mdr'
'moment.js': 'mt'
'openjdk': 'java'
'nginx': 'ngx'
'numpy': 'np'
'pandas': 'pd'
'postgresql': 'pg'
'python': 'py'
'ruby.on.rails': 'ror'
'ruby': 'rb'
'rust': 'rs'
'sass': 'scss'
'tensorflow': 'tf'
'typescript': 'ts'
'underscore.js': '_'

@ -1,3 +1,5 @@
class app.Model app.Model = class Model {
constructor: (attributes) -> constructor(attributes) {
@[key] = value for key, value of attributes for (var key in attributes) { var value = attributes[key]; this[key] = value; }
}
};

@ -1,14 +1,24 @@
class app.models.Type extends app.Model /*
# Attributes: name, slug, count * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.models.Type = class Type extends app.Model {
// Attributes: name, slug, count
fullPath: -> fullPath() {
"/#{@doc.slug}-#{@slug}/" return `/${this.doc.slug}-${this.slug}/`;
}
entries: -> entries() {
@doc.entries.findAllBy 'type', @name return this.doc.entries.findAllBy('type', this.name);
}
toEntry: -> toEntry() {
new app.models.Entry return new app.models.Entry({
doc: @doc doc: this.doc,
name: "#{@doc.name} / #{@name}" name: `${this.doc.name} / ${this.name}`,
path: '..' + @fullPath() path: '..' + this.fullPath()
});
}
};

@ -1,11 +1,19 @@
app.templates.render = (name, value, args...) -> /*
template = app.templates[name] * decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.templates.render = function(name, value, ...args) {
const template = app.templates[name];
if Array.isArray(value) if (Array.isArray(value)) {
result = '' let result = '';
result += template(val, args...) for val in value for (var val of Array.from(value)) { result += template(val, ...Array.from(args)); }
result return result;
else if typeof template is 'function' } else if (typeof template === 'function') {
template(value, args...) return template(value, ...Array.from(args));
else } else {
template return template;
}
};

@ -1,57 +1,69 @@
error = (title, text = '', links = '') -> /*
text = """<p class="_error-text">#{text}</p>""" if text * decaffeinate suggestions:
links = """<p class="_error-links">#{links}</p>""" if links * DS102: Remove unnecessary code created because of implicit returns
"""<div class="_error"><h1 class="_error-title">#{title}</h1>#{text}#{links}</div>""" * DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const error = function(title, text, links) {
if (text == null) { text = ''; }
if (links == null) { links = ''; }
if (text) { text = `<p class="_error-text">${text}</p>`; }
if (links) { links = `<p class="_error-links">${links}</p>`; }
return `<div class="_error"><h1 class="_error-title">${title}</h1>${text}${links}</div>`;
};
back = '<a href="#" data-behavior="back" class="_error-link">Go back</a>' const back = '<a href="#" data-behavior="back" class="_error-link">Go back</a>';
app.templates.notFoundPage = -> app.templates.notFoundPage = () => error(" Page not found. ",
error """ Page not found. """, " It may be missing from the source documentation or this could be a bug. ",
""" It may be missing from the source documentation or this could be a bug. """, back);
back
app.templates.pageLoadError = -> app.templates.pageLoadError = () => error(" The page failed to load. ",
error """ The page failed to load. """, ` It may be missing from the server (try reloading the app) or you could be offline (try <a href="/offline">installing the documentation for offline usage</a> when online again).<br>
""" It may be missing from the server (try reloading the app) or you could be offline (try <a href="/offline">installing the documentation for offline usage</a> when online again).<br> If you're online and you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. `,
If you're online and you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """, ` ${back} &middot; <a href="/#${location.pathname}" target="_top" class="_error-link">Reload</a>
""" #{back} &middot; <a href="/##{location.pathname}" target="_top" class="_error-link">Reload</a> &middot; <a href="#" class="_error-link" data-retry>Retry</a> `
&middot; <a href="#" class="_error-link" data-retry>Retry</a> """ );
app.templates.bootError = -> app.templates.bootError = () => error(" The app failed to load. ",
error """ The app failed to load. """, ` Check your Internet connection and try <a href="#" data-behavior="reload">reloading</a>.<br>
""" Check your Internet connection and try <a href="#" data-behavior="reload">reloading</a>.<br> If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. `
If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """ );
app.templates.offlineError = (reason, exception) -> app.templates.offlineError = function(reason, exception) {
if reason is 'cookie_blocked' if (reason === 'cookie_blocked') {
return error """ Cookies must be enabled to use offline mode. """ return error(" Cookies must be enabled to use offline mode. ");
}
reason = switch reason reason = (() => { switch (reason) {
when 'not_supported' case 'not_supported':
""" DevDocs requires IndexedDB to cache documentations for offline access.<br> return ` DevDocs requires IndexedDB to cache documentations for offline access.<br>
Unfortunately your browser either doesn't support IndexedDB or doesn't make it available. """ Unfortunately your browser either doesn't support IndexedDB or doesn't make it available. `;
when 'buggy' case 'buggy':
""" DevDocs requires IndexedDB to cache documentations for offline access.<br> return ` DevDocs requires IndexedDB to cache documentations for offline access.<br>
Unfortunately your browser's implementation of IndexedDB contains bugs that prevent DevDocs from using it. """ Unfortunately your browser's implementation of IndexedDB contains bugs that prevent DevDocs from using it. `;
when 'private_mode' case 'private_mode':
""" Your browser appears to be running in private mode.<br> return ` Your browser appears to be running in private mode.<br>
This prevents DevDocs from caching documentations for offline access.""" This prevents DevDocs from caching documentations for offline access.`;
when 'exception' case 'exception':
""" An error occurred when trying to open the IndexedDB database:<br> return ` An error occurred when trying to open the IndexedDB database:<br>
<code class="_label">#{exception.name}: #{exception.message}</code> """ <code class="_label">${exception.name}: ${exception.message}</code> `;
when 'cant_open' case 'cant_open':
""" An error occurred when trying to open the IndexedDB database:<br> return ` An error occurred when trying to open the IndexedDB database:<br>
<code class="_label">#{exception.name}: #{exception.message}</code><br> <code class="_label">${exception.name}: ${exception.message}</code><br>
This could be because you're browsing in private mode or have disallowed offline storage on the domain. """ This could be because you're browsing in private mode or have disallowed offline storage on the domain. `;
when 'version' case 'version':
""" The IndexedDB database was modified with a newer version of the app.<br> return ` The IndexedDB database was modified with a newer version of the app.<br>
<a href="#" data-behavior="reload">Reload the page</a> to use offline mode. """ <a href="#" data-behavior="reload">Reload the page</a> to use offline mode. `;
when 'empty' case 'empty':
""" The IndexedDB database appears to be corrupted. Try <a href="#" data-behavior="reset">resetting the app</a>. """ return " The IndexedDB database appears to be corrupted. Try <a href=\"#\" data-behavior=\"reset\">resetting the app</a>. ";
} })();
error 'Offline mode is unavailable.', reason return error('Offline mode is unavailable.', reason);
};
app.templates.unsupportedBrowser = """ app.templates.unsupportedBrowser = `\
<div class="_fail"> <div class="_fail">
<h1 class="_fail-title">Your browser is unsupported, sorry.</h1> <h1 class="_fail-title">Your browser is unsupported, sorry.</h1>
<p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers: <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:
@ -69,5 +81,5 @@ app.templates.unsupportedBrowser = """
The app uses feature detection, not user agent sniffing. The app uses feature detection, not user agent sniffing.
<p class="_fail-text"> <p class="_fail-text">
&mdash; <a href="https://twitter.com/DevDocs">@DevDocs</a> &mdash; <a href="https://twitter.com/DevDocs">@DevDocs</a>
</div> </div>\
""" `;

@ -1,9 +1,14 @@
notice = (text) -> """<p class="_notice-text">#{text}</p>""" /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const notice = text => `<p class="_notice-text">${text}</p>`;
app.templates.singleDocNotice = (doc) -> app.templates.singleDocNotice = doc => notice(` You're browsing the ${doc.fullName} documentation. To browse all docs, go to
notice """ You're browsing the #{doc.fullName} documentation. To browse all docs, go to <a href="//${app.config.production_host}" target="_top">${app.config.production_host}</a> (or press <code>esc</code>). `
<a href="//#{app.config.production_host}" target="_top">#{app.config.production_host}</a> (or press <code>esc</code>). """ );
app.templates.disabledDocNotice = -> app.templates.disabledDocNotice = () => notice(` <strong>This documentation is disabled.</strong>
notice """ <strong>This documentation is disabled.</strong> To enable it, go to <a href="/settings" class="_notice-link">Preferences</a>. `
To enable it, go to <a href="/settings" class="_notice-link">Preferences</a>. """ );

@ -1,76 +1,81 @@
notif = (title, html) -> /*
html = html.replace /<a /g, '<a class="_notif-link" ' * decaffeinate suggestions:
""" <h5 class="_notif-title">#{title}</h5> * DS101: Remove unnecessary use of Array.from
#{html} * DS102: Remove unnecessary code created because of implicit returns
<button type="button" class="_notif-close" title="Close"><svg><use xlink:href="#icon-close"/></svg>Close</a> * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
""" */
const notif = function(title, html) {
html = html.replace(/<a /g, '<a class="_notif-link" ');
return ` <h5 class="_notif-title">${title}</h5>
${html}
<button type="button" class="_notif-close" title="Close"><svg><use xlink:href="#icon-close"/></svg>Close</a>\
`;
};
textNotif = (title, message) -> const textNotif = (title, message) => notif(title, `<p class="_notif-text">${message}`);
notif title, """<p class="_notif-text">#{message}"""
app.templates.notifUpdateReady = -> app.templates.notifUpdateReady = () => textNotif("<span data-behavior=\"reboot\">DevDocs has been updated.</span>",
textNotif """<span data-behavior="reboot">DevDocs has been updated.</span>""", "<span data-behavior=\"reboot\"><a href=\"#\" data-behavior=\"reboot\">Reload the page</a> to use the new version.</span>");
"""<span data-behavior="reboot"><a href="#" data-behavior="reboot">Reload the page</a> to use the new version.</span>"""
app.templates.notifError = -> app.templates.notifError = () => textNotif(" Oops, an error occurred. ",
textNotif """ Oops, an error occurred. """, ` Try <a href="#" data-behavior="hard-reload">reloading</a>, and if the problem persists,
""" Try <a href="#" data-behavior="hard-reload">reloading</a>, and if the problem persists,
<a href="#" data-behavior="reset">resetting the app</a>.<br> <a href="#" data-behavior="reset">resetting the app</a>.<br>
You can also report this issue on <a href="https://github.com/freeCodeCamp/devdocs/issues/new" target="_blank" rel="noopener">GitHub</a>. """ You can also report this issue on <a href="https://github.com/freeCodeCamp/devdocs/issues/new" target="_blank" rel="noopener">GitHub</a>. `
);
app.templates.notifQuotaExceeded = -> app.templates.notifQuotaExceeded = () => textNotif(" The offline database has exceeded its size limitation. ",
textNotif """ The offline database has exceeded its size limitation. """, " Unfortunately this quota can't be detected programmatically, and the database can't be opened while over the quota, so it had to be reset. ");
""" Unfortunately this quota can't be detected programmatically, and the database can't be opened while over the quota, so it had to be reset. """
app.templates.notifCookieBlocked = -> app.templates.notifCookieBlocked = () => textNotif(" Please enable cookies. ",
textNotif """ Please enable cookies. """, " DevDocs will not work properly if cookies are disabled. ");
""" DevDocs will not work properly if cookies are disabled. """
app.templates.notifInvalidLocation = -> app.templates.notifInvalidLocation = () => textNotif(` DevDocs must be loaded from ${app.config.production_host} `,
textNotif """ DevDocs must be loaded from #{app.config.production_host} """, " Otherwise things are likely to break. ");
""" Otherwise things are likely to break. """
app.templates.notifImportInvalid = -> app.templates.notifImportInvalid = () => textNotif(" Oops, an error occurred. ",
textNotif """ Oops, an error occurred. """, " The file you selected is invalid. ");
""" The file you selected is invalid. """
app.templates.notifNews = (news) -> app.templates.notifNews = news => notif('Changelog', `<div class="_notif-content _notif-news">${app.templates.newsList(news, {years: false})}</div>`);
notif 'Changelog', """<div class="_notif-content _notif-news">#{app.templates.newsList(news, years: false)}</div>"""
app.templates.notifUpdates = (docs, disabledDocs) -> app.templates.notifUpdates = function(docs, disabledDocs) {
html = '<div class="_notif-content _notif-news">' let doc;
let html = '<div class="_notif-content _notif-news">';
if docs.length > 0 if (docs.length > 0) {
html += '<div class="_news-row">' html += '<div class="_news-row">';
html += '<ul class="_notif-list">' html += '<ul class="_notif-list">';
for doc in docs for (doc of Array.from(docs)) {
html += "<li>#{doc.name}" html += `<li>${doc.name}`;
html += " <code>&rarr;</code> #{doc.release}" if doc.release if (doc.release) { html += ` <code>&rarr;</code> ${doc.release}`; }
html += '</ul></div>' }
html += '</ul></div>';
}
if disabledDocs.length > 0 if (disabledDocs.length > 0) {
html += '<div class="_news-row"><p class="_news-title">Disabled:' html += '<div class="_news-row"><p class="_news-title">Disabled:';
html += '<ul class="_notif-list">' html += '<ul class="_notif-list">';
for doc in disabledDocs for (doc of Array.from(disabledDocs)) {
html += "<li>#{doc.name}" html += `<li>${doc.name}`;
html += " <code>&rarr;</code> #{doc.release}" if doc.release if (doc.release) { html += ` <code>&rarr;</code> ${doc.release}`; }
html += """<span class="_notif-info"><a href="/settings">Enable</a></span>""" html += "<span class=\"_notif-info\"><a href=\"/settings\">Enable</a></span>";
html += '</ul></div>' }
html += '</ul></div>';
}
notif 'Updates', "#{html}</div>" return notif('Updates', `${html}</div>`);
};
app.templates.notifShare = -> app.templates.notifShare = () => textNotif(" Hi there! ",
textNotif """ Hi there! """, ` Like DevDocs? Help us reach more developers by sharing the link with your friends on
""" Like DevDocs? Help us reach more developers by sharing the link with your friends on
<a href="https://out.devdocs.io/s/tw" target="_blank" rel="noopener">Twitter</a>, <a href="https://out.devdocs.io/s/fb" target="_blank" rel="noopener">Facebook</a>, <a href="https://out.devdocs.io/s/tw" target="_blank" rel="noopener">Twitter</a>, <a href="https://out.devdocs.io/s/fb" target="_blank" rel="noopener">Facebook</a>,
<a href="https://out.devdocs.io/s/re" target="_blank" rel="noopener">Reddit</a>, etc.<br>Thanks :) """ <a href="https://out.devdocs.io/s/re" target="_blank" rel="noopener">Reddit</a>, etc.<br>Thanks :) `
);
app.templates.notifUpdateDocs = -> app.templates.notifUpdateDocs = () => textNotif(" Documentation updates available. ",
textNotif """ Documentation updates available. """, " <a href=\"/offline\">Install them</a> as soon as possible to avoid broken pages. ");
""" <a href="/offline">Install them</a> as soon as possible to avoid broken pages. """
app.templates.notifAnalyticsConsent = -> app.templates.notifAnalyticsConsent = () => textNotif(" Tracking cookies ",
textNotif """ Tracking cookies """, ` We would like to gather usage data about how DevDocs is used through Google Analytics and Gauges. We only collect anonymous traffic information.
""" We would like to gather usage data about how DevDocs is used through Google Analytics and Gauges. We only collect anonymous traffic information.
Please confirm if you accept our tracking cookies. You can always change your decision in the settings. Please confirm if you accept our tracking cookies. You can always change your decision in the settings.
<br><span class="_notif-right"><a href="#" data-behavior="accept-analytics">Accept</a> or <a href="#" data-behavior="decline-analytics">Decline</a></span> """ <br><span class="_notif-right"><a href="#" data-behavior="accept-analytics">Accept</a> or <a href="#" data-behavior="decline-analytics">Decline</a></span> `
);

@ -1,9 +1,18 @@
app.templates.aboutPage = -> /*
all_docs = app.docs.all().concat(app.disabledDocs.all()...) * decaffeinate suggestions:
# de-duplicate docs by doc.name * DS101: Remove unnecessary use of Array.from
docs = [] * DS102: Remove unnecessary code created because of implicit returns
docs.push doc for doc in all_docs when not (docs.find (d) -> d.name == doc.name) * DS205: Consider reworking code to avoid use of IIFEs
""" * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.templates.aboutPage = function() {
let doc;
const all_docs = app.docs.all().concat(...Array.from(app.disabledDocs.all() || []));
// de-duplicate docs by doc.name
const docs = [];
for (doc of Array.from(all_docs)) { if (!(docs.find(d => d.name === doc.name))) { docs.push(doc); } }
return `\
<nav class="_toc" role="directory"> <nav class="_toc" role="directory">
<h3 class="_toc-title">Table of Contents</h3> <h3 class="_toc-title">Table of Contents</h3>
<ul class="_toc-list"> <ul class="_toc-list">
@ -68,13 +77,18 @@ app.templates.aboutPage = ->
<th>Documentation <th>Documentation
<th>Copyright/License <th>Copyright/License
<th>Source code <th>Source code
#{( ${((() => {
"<tr> const result = [];
<td><a href=\"#{doc.links?.home}\">#{doc.name}</a></td>
<td>#{doc.attribution}</td> for (doc of Array.from(docs)) { result.push(`<tr> \
<td><a href=\"#{doc.links?.code}\">Source code</a></td> <td><a href=\"${(doc.links != null ? doc.links.home : undefined)}\">${doc.name}</a></td> \
</tr>" for doc in docs <td>${doc.attribution}</td> \
).join('')} <td><a href=\"${(doc.links != null ? doc.links.code : undefined)}\">Source code</a></td> \
</tr>`);
}
return result;
})()).join('')}
</table> </table>
</div> </div>
@ -87,5 +101,6 @@ app.templates.aboutPage = ->
<li>The app uses cookies to store user preferences. <li>The app uses cookies to store user preferences.
<li>By using the app, you signify your acceptance of this policy. If you do not agree to this policy, please do not use the app. <li>By using the app, you signify your acceptance of this policy. If you do not agree to this policy, please do not use the app.
<li>If you have any questions regarding privacy, please email <a href="mailto:privacy@freecodecamp.org">privacy@freecodecamp.org</a>. <li>If you have any questions regarding privacy, please email <a href="mailto:privacy@freecodecamp.org">privacy@freecodecamp.org</a>.
</ul> </ul>\
""" `;
};

@ -1,16 +1,25 @@
app.templates.helpPage = -> /*
ctrlKey = if $.isMac() then 'cmd' else 'ctrl' * decaffeinate suggestions:
navKey = if $.isMac() then 'cmd' else 'alt' * DS102: Remove unnecessary code created because of implicit returns
arrowScroll = app.settings.get('arrowScroll') * DS205: Consider reworking code to avoid use of IIFEs
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.templates.helpPage = function() {
let key, value;
const ctrlKey = $.isMac() ? 'cmd' : 'ctrl';
const navKey = $.isMac() ? 'cmd' : 'alt';
const arrowScroll = app.settings.get('arrowScroll');
aliases_one = {} const aliases_one = {};
aliases_two = {} const aliases_two = {};
keys = Object.keys(app.models.Entry.ALIASES) const keys = Object.keys(app.models.Entry.ALIASES);
middle = Math.ceil(keys.length / 2) - 1 const middle = Math.ceil(keys.length / 2) - 1;
for key, i in keys for (let i = 0; i < keys.length; i++) {
(if i > middle then aliases_two else aliases_one)[key] = app.models.Entry.ALIASES[key] key = keys[i];
(i > middle ? aliases_two : aliases_one)[key] = app.models.Entry.ALIASES[key];
}
""" return `\
<nav class="_toc" role="directory"> <nav class="_toc" role="directory">
<h3 class="_toc-title">Table of Contents</h3> <h3 class="_toc-title">Table of Contents</h3>
<ul class="_toc-list"> <ul class="_toc-list">
@ -68,12 +77,12 @@ app.templates.helpPage = ->
<h3 class="_shortcuts-title">Sidebar</h3> <h3 class="_shortcuts-title">Sidebar</h3>
<dl class="_shortcuts-dl"> <dl class="_shortcuts-dl">
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
#{if arrowScroll then '<code class="_shortcut-code">shift</code> + ' else ''} ${arrowScroll ? '<code class="_shortcut-code">shift</code> + ' : ''}
<code class="_shortcut-code">&darr;</code> <code class="_shortcut-code">&darr;</code>
<code class="_shortcut-code">&uarr;</code> <code class="_shortcut-code">&uarr;</code>
<dd class="_shortcuts-dd">Move selection <dd class="_shortcuts-dd">Move selection
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
#{if arrowScroll then '<code class="_shortcut-code">shift</code> + ' else ''} ${arrowScroll ? '<code class="_shortcut-code">shift</code> + ' : ''}
<code class="_shortcut-code">&rarr;</code> <code class="_shortcut-code">&rarr;</code>
<code class="_shortcut-code">&larr;</code> <code class="_shortcut-code">&larr;</code>
<dd class="_shortcuts-dd">Show/hide sub-list <dd class="_shortcuts-dd">Show/hide sub-list
@ -81,7 +90,7 @@ app.templates.helpPage = ->
<code class="_shortcut-code">enter</code> <code class="_shortcut-code">enter</code>
<dd class="_shortcuts-dd">Open selection <dd class="_shortcuts-dd">Open selection
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">#{ctrlKey} + enter</code> <code class="_shortcut-code">${ctrlKey} + enter</code>
<dd class="_shortcuts-dd">Open selection in a new tab <dd class="_shortcuts-dd">Open selection in a new tab
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + r</code> <code class="_shortcut-code">alt + r</code>
@ -90,14 +99,14 @@ app.templates.helpPage = ->
<h3 class="_shortcuts-title">Browsing</h3> <h3 class="_shortcuts-title">Browsing</h3>
<dl class="_shortcuts-dl"> <dl class="_shortcuts-dl">
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">#{navKey} + &larr;</code> <code class="_shortcut-code">${navKey} + &larr;</code>
<code class="_shortcut-code">#{navKey} + &rarr;</code> <code class="_shortcut-code">${navKey} + &rarr;</code>
<dd class="_shortcuts-dd">Go back/forward <dd class="_shortcuts-dd">Go back/forward
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
#{if arrowScroll ${arrowScroll ?
'<code class="_shortcut-code">&darr;</code> ' + '<code class="_shortcut-code">&darr;</code> ' +
'<code class="_shortcut-code">&uarr;</code>' '<code class="_shortcut-code">&uarr;</code>'
else :
'<code class="_shortcut-code">alt + &darr;</code> ' + '<code class="_shortcut-code">alt + &darr;</code> ' +
'<code class="_shortcut-code">alt + &uarr;</code>' + '<code class="_shortcut-code">alt + &uarr;</code>' +
'<br>' + '<br>' +
@ -109,8 +118,8 @@ app.templates.helpPage = ->
<code class="_shortcut-code">shift + space</code> <code class="_shortcut-code">shift + space</code>
<dd class="_shortcuts-dd">Scroll screen by screen <dd class="_shortcuts-dd">Scroll screen by screen
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">#{ctrlKey} + &uarr;</code> <code class="_shortcut-code">${ctrlKey} + &uarr;</code>
<code class="_shortcut-code">#{ctrlKey} + &darr;</code> <code class="_shortcut-code">${ctrlKey} + &darr;</code>
<dd class="_shortcuts-dd">Scroll to the top/bottom <dd class="_shortcuts-dd">Scroll to the top/bottom
<dt class="_shortcuts-dt"> <dt class="_shortcuts-dt">
<code class="_shortcut-code">alt + f</code> <code class="_shortcut-code">alt + f</code>
@ -156,14 +165,29 @@ app.templates.helpPage = ->
<tr> <tr>
<th>Word <th>Word
<th>Alias <th>Alias
#{("<tr><td class=\"_code\">#{key}<td class=\"_code\">#{value}" for key, value of aliases_one).join('')} ${((() => {
const result = [];
for (key in aliases_one) {
value = aliases_one[key];
result.push(`<tr><td class=\"_code\">${key}<td class=\"_code\">${value}`);
}
return result;
})()).join('')}
</table> </table>
<table> <table>
<tr> <tr>
<th>Word <th>Word
<th>Alias <th>Alias
#{("<tr><td class=\"_code\">#{key}<td class=\"_code\">#{value}" for key, value of aliases_two).join('')} ${((() => {
const result1 = [];
for (key in aliases_two) {
value = aliases_two[key];
result1.push(`<tr><td class=\"_code\">${key}<td class=\"_code\">${value}`);
}
return result1;
})()).join('')}
</table> </table>
</div> </div>
<p>Feel free to suggest new aliases on <a href="https://github.com/freeCodeCamp/devdocs/issues/new">GitHub</a>. <p>Feel free to suggest new aliases on <a href="https://github.com/freeCodeCamp/devdocs/issues/new">GitHub</a>.\
""" `;
};

@ -1,9 +1,14 @@
app.templates.offlinePage = (docs) -> """ /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.templates.offlinePage = docs => `\
<h1 class="_lined-heading">Offline Documentation</h1> <h1 class="_lined-heading">Offline Documentation</h1>
<div class="_docs-tools"> <div class="_docs-tools">
<label> <label>
<input type="checkbox" name="autoUpdate" value="1" #{if app.settings.get('manualUpdate') then '' else 'checked'}>Install updates automatically <input type="checkbox" name="autoUpdate" value="1" ${app.settings.get('manualUpdate') ? '' : 'checked'}>Install updates automatically
</label> </label>
<div class="_docs-links"> <div class="_docs-links">
<button type="button" class="_btn-link" data-action-all="install">Install all</button><button type="button" class="_btn-link" data-action-all="update"><strong>Update all</strong></button><button type="button" class="_btn-link" data-action-all="uninstall">Uninstall all</button> <button type="button" class="_btn-link" data-action-all="install">Install all</button><button type="button" class="_btn-link" data-action-all="update"><strong>Update all</strong></button><button type="button" class="_btn-link" data-action-all="uninstall">Uninstall all</button>
@ -18,7 +23,7 @@ app.templates.offlinePage = (docs) -> """
<th>Status</th> <th>Status</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
#{docs} ${docs}
</table> </table>
</div> </div>
<p class="_note"><strong>Note:</strong> your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there. <p class="_note"><strong>Note:</strong> your browser may delete DevDocs's offline data if your computer is running low on disk space and you haven't used the app in a while. Load this page before going offline to make sure the data is still there.
@ -28,7 +33,7 @@ app.templates.offlinePage = (docs) -> """
<dd>Each page is cached as a key-value pair in <a href="https://devdocs.io/dom/indexeddb_api">IndexedDB</a> (downloaded from a single file).<br> <dd>Each page is cached as a key-value pair in <a href="https://devdocs.io/dom/indexeddb_api">IndexedDB</a> (downloaded from a single file).<br>
The app also uses <a href="https://devdocs.io/dom/service_worker_api/using_service_workers">Service Workers</a> and <a href="https://devdocs.io/dom/web_storage_api">localStorage</a> to cache the assets and index files. The app also uses <a href="https://devdocs.io/dom/service_worker_api/using_service_workers">Service Workers</a> and <a href="https://devdocs.io/dom/web_storage_api">localStorage</a> to cache the assets and index files.
<dt>Can I close the tab/browser? <dt>Can I close the tab/browser?
<dd>#{canICloseTheTab()} <dd>${canICloseTheTab()}
<dt>What if I don't update a documentation? <dt>What if I don't update a documentation?
<dd>You'll see outdated content and some pages will be missing or broken, because the rest of the app (including data for the search and sidebar) uses a different caching mechanism that's updated automatically. <dd>You'll see outdated content and some pages will be missing or broken, because the rest of the app (including data for the search and sidebar) uses a different caching mechanism that's updated automatically.
<dt>I found a bug, where do I report it? <dt>I found a bug, where do I report it?
@ -37,44 +42,48 @@ app.templates.offlinePage = (docs) -> """
<dd>Click <a href="#" data-behavior="reset">here</a>. <dd>Click <a href="#" data-behavior="reset">here</a>.
<dt>Why aren't all documentations listed above? <dt>Why aren't all documentations listed above?
<dd>You have to <a href="/settings">enable</a> them first. <dd>You have to <a href="/settings">enable</a> them first.
</dl> </dl>\
""" `;
canICloseTheTab = -> var canICloseTheTab = function() {
if app.ServiceWorker.isEnabled() if (app.ServiceWorker.isEnabled()) {
""" Yes! Even offline, you can open a new tab, go to <a href="//devdocs.io">devdocs.io</a>, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """ return " Yes! Even offline, you can open a new tab, go to <a href=\"//devdocs.io\">devdocs.io</a>, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). ";
else } else {
reason = "aren't available in your browser (or are disabled)" let reason = "aren't available in your browser (or are disabled)";
if app.config.env != 'production' if (app.config.env !== 'production') {
reason = "are disabled in your development instance of DevDocs (enable them by setting the <code>ENABLE_SERVICE_WORKER</code> environment variable to <code>true</code>)" reason = "are disabled in your development instance of DevDocs (enable them by setting the <code>ENABLE_SERVICE_WORKER</code> environment variable to <code>true</code>)";
}
""" No. Service Workers #{reason}, so loading <a href="//devdocs.io">devdocs.io</a> offline won't work.<br> return ` No. Service Workers ${reason}, so loading <a href="//devdocs.io">devdocs.io</a> offline won't work.<br>
The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """ The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). `;
}
};
app.templates.offlineDoc = (doc, status) -> app.templates.offlineDoc = function(doc, status) {
outdated = doc.isOutdated(status) const outdated = doc.isOutdated(status);
html = """ let html = `\
<tr data-slug="#{doc.slug}"#{if outdated then ' class="_highlight"' else ''}> <tr data-slug="${doc.slug}"${outdated ? ' class="_highlight"' : ''}>
<td class="_docs-name _icon-#{doc.icon}">#{doc.fullName}</td> <td class="_docs-name _icon-${doc.icon}">${doc.fullName}</td>
<td class="_docs-size">#{Math.ceil(doc.db_size / 100000) / 10}&nbsp;<small>MB</small></td> <td class="_docs-size">${Math.ceil(doc.db_size / 100000) / 10}&nbsp;<small>MB</small></td>\
""" `;
html += if !(status and status.installed) html += !(status && status.installed) ?
""" `\
<td>-</td> <td>-</td>
<td><button type="button" class="_btn-link" data-action="install">Install</button></td> <td><button type="button" class="_btn-link" data-action="install">Install</button></td>\
""" `
else if outdated : outdated ?
""" `\
<td><strong>Outdated</strong></td> <td><strong>Outdated</strong></td>
<td><button type="button" class="_btn-link _bold" data-action="update">Update</button> - <button type="button" class="_btn-link" data-action="uninstall">Uninstall</button></td> <td><button type="button" class="_btn-link _bold" data-action="update">Update</button> - <button type="button" class="_btn-link" data-action="uninstall">Uninstall</button></td>\
""" `
else :
""" `\
<td>Up&#8209;to&#8209;date</td> <td>Up&#8209;to&#8209;date</td>
<td><button type="button" class="_btn-link" data-action="uninstall">Uninstall</button></td> <td><button type="button" class="_btn-link" data-action="uninstall">Uninstall</button></td>\
""" `;
html + '</tr>' return html + '</tr>';
};

@ -1,22 +1,27 @@
themeOption = ({ label, value }, settings) -> """ /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const themeOption = ({ label, value }, settings) => `\
<label class="_settings-label _theme-label"> <label class="_settings-label _theme-label">
<input type="radio" name="theme" value="#{value}"#{if settings.theme == value then ' checked' else ''}> <input type="radio" name="theme" value="${value}"${settings.theme === value ? ' checked' : ''}>
#{label} ${label}
</label> </label>\
""" `;
app.templates.settingsPage = (settings) -> """ app.templates.settingsPage = settings => `\
<h1 class="_lined-heading">Preferences</h1> <h1 class="_lined-heading">Preferences</h1>
<div class="_settings-fieldset"> <div class="_settings-fieldset">
<h2 class="_settings-legend">Theme:</h2> <h2 class="_settings-legend">Theme:</h2>
<div class="_settings-inputs"> <div class="_settings-inputs">
#{if settings.autoSupported ${settings.autoSupported ?
themeOption label: "Automatic <small>Matches system setting</small>", value: "auto", settings themeOption({label: "Automatic <small>Matches system setting</small>", value: "auto"}, settings)
else :
""} ""}
#{themeOption label: "Light", value: "default", settings} ${themeOption({label: "Light", value: "default"}, settings)}
#{themeOption label: "Dark", value: "dark", settings} ${themeOption({label: "Dark", value: "dark"}, settings)}
</div> </div>
</div> </div>
@ -25,24 +30,24 @@ app.templates.settingsPage = (settings) -> """
<div class="_settings-inputs"> <div class="_settings-inputs">
<label class="_settings-label _setting-max-width"> <label class="_settings-label _setting-max-width">
<input type="checkbox" form="settings" name="layout" value="_max-width"#{if settings['_max-width'] then ' checked' else ''}>Enable fixed-width layout <input type="checkbox" form="settings" name="layout" value="_max-width"${settings['_max-width'] ? ' checked' : ''}>Enable fixed-width layout
</label> </label>
<label class="_settings-label _setting-text-justify-hyphenate"> <label class="_settings-label _setting-text-justify-hyphenate">
<input type="checkbox" form="settings" name="layout" value="_text-justify-hyphenate"#{if settings['_text-justify-hyphenate'] then ' checked' else ''}>Enable justified layout and automatic hyphenation <input type="checkbox" form="settings" name="layout" value="_text-justify-hyphenate"${settings['_text-justify-hyphenate'] ? ' checked' : ''}>Enable justified layout and automatic hyphenation
</label> </label>
<label class="_settings-label _hide-on-mobile"> <label class="_settings-label _hide-on-mobile">
<input type="checkbox" form="settings" name="layout" value="_sidebar-hidden"#{if settings['_sidebar-hidden'] then ' checked' else ''}>Automatically hide and show the sidebar <input type="checkbox" form="settings" name="layout" value="_sidebar-hidden"${settings['_sidebar-hidden'] ? ' checked' : ''}>Automatically hide and show the sidebar
<small>Tip: drag the edge of the sidebar to resize it.</small> <small>Tip: drag the edge of the sidebar to resize it.</small>
</label> </label>
<label class="_settings-label _hide-on-mobile"> <label class="_settings-label _hide-on-mobile">
<input type="checkbox" form="settings" name="noAutofocus" value="_no-autofocus"#{if settings.noAutofocus then ' checked' else ''}>Disable autofocus of search input <input type="checkbox" form="settings" name="noAutofocus" value="_no-autofocus"${settings.noAutofocus ? ' checked' : ''}>Disable autofocus of search input
</label> </label>
<label class="_settings-label"> <label class="_settings-label">
<input type="checkbox" form="settings" name="autoInstall" value="_auto-install"#{if settings.autoInstall then ' checked' else ''}>Automatically download documentation for offline use <input type="checkbox" form="settings" name="autoInstall" value="_auto-install"${settings.autoInstall ? ' checked' : ''}>Automatically download documentation for offline use
<small>Only enable this when bandwidth isn't a concern to you.</small> <small>Only enable this when bandwidth isn't a concern to you.</small>
</label> </label>
<label class="_settings-label _hide-in-development"> <label class="_settings-label _hide-in-development">
<input type="checkbox" form="settings" name="analyticsConsent"#{if settings.analyticsConsent then ' checked' else ''}>Enable tracking cookies <input type="checkbox" form="settings" name="analyticsConsent"${settings.analyticsConsent ? ' checked' : ''}>Enable tracking cookies
<small>With this checked, we enable Google Analytics and Gauges to collect anonymous traffic information.</small> <small>With this checked, we enable Google Analytics and Gauges to collect anonymous traffic information.</small>
</label> </label>
</div> </div>
@ -53,20 +58,20 @@ app.templates.settingsPage = (settings) -> """
<div class="_settings-inputs"> <div class="_settings-inputs">
<label class="_settings-label"> <label class="_settings-label">
<input type="checkbox" form="settings" name="smoothScroll" value="1"#{if settings.smoothScroll then ' checked' else ''}>Use smooth scrolling <input type="checkbox" form="settings" name="smoothScroll" value="1"${settings.smoothScroll ? ' checked' : ''}>Use smooth scrolling
</label> </label>
<label class="_settings-label _setting-native-scrollbar"> <label class="_settings-label _setting-native-scrollbar">
<input type="checkbox" form="settings" name="layout" value="_native-scrollbars"#{if settings['_native-scrollbars'] then ' checked' else ''}>Use native scrollbars <input type="checkbox" form="settings" name="layout" value="_native-scrollbars"${settings['_native-scrollbars'] ? ' checked' : ''}>Use native scrollbars
</label> </label>
<label class="_settings-label"> <label class="_settings-label">
<input type="checkbox" form="settings" name="arrowScroll" value="1"#{if settings.arrowScroll then ' checked' else ''}>Use arrow keys to scroll the main content area <input type="checkbox" form="settings" name="arrowScroll" value="1"${settings.arrowScroll ? ' checked' : ''}>Use arrow keys to scroll the main content area
<small>With this checked, use <code class="_label">shift</code> + <code class="_label">&uarr;</code><code class="_label">&darr;</code><code class="_label">&larr;</code><code class="_label">&rarr;</code> to navigate the sidebar.</small> <small>With this checked, use <code class="_label">shift</code> + <code class="_label">&uarr;</code><code class="_label">&darr;</code><code class="_label">&larr;</code><code class="_label">&rarr;</code> to navigate the sidebar.</small>
</label> </label>
<label class="_settings-label"> <label class="_settings-label">
<input type="checkbox" form="settings" name="spaceScroll" value="1"#{if settings.spaceScroll then ' checked' else ''}>Use spacebar to scroll during search <input type="checkbox" form="settings" name="spaceScroll" value="1"${settings.spaceScroll ? ' checked' : ''}>Use spacebar to scroll during search
</label> </label>
<label class="_settings-label"> <label class="_settings-label">
<input type="number" step="0.1" form="settings" name="spaceTimeout" min="0" max="5" value="#{settings.spaceTimeout}"> Delay until you can scroll by pressing space <input type="number" step="0.1" form="settings" name="spaceTimeout" min="0" max="5" value="${settings.spaceTimeout}"> Delay until you can scroll by pressing space
<small>Time in seconds</small> <small>Time in seconds</small>
</label> </label>
</div> </div>
@ -77,5 +82,5 @@ app.templates.settingsPage = (settings) -> """
<label class="_btn _file-btn"><input type="file" form="settings" name="import" accept=".json">Import</label> <label class="_btn _file-btn"><input type="file" form="settings" name="import" accept=".json">Import</label>
<p> <p>
<button type="button" class="_btn-link _reset-btn" data-behavior="reset">Reset all preferences and data</button> <button type="button" class="_btn-link _reset-btn" data-behavior="reset">Reset all preferences and data</button>\
""" `;

@ -1,6 +1,9 @@
app.templates.typePage = (type) -> /*
""" <h1>#{type.doc.fullName} / #{type.name}</h1> * decaffeinate suggestions:
<ul class="_entry-list">#{app.templates.render 'typePageEntry', type.entries()}</ul> """ * DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.templates.typePage = type => ` <h1>${type.doc.fullName} / ${type.name}</h1>
<ul class="_entry-list">${app.templates.render('typePageEntry', type.entries())}</ul> `;
app.templates.typePageEntry = (entry) -> app.templates.typePageEntry = entry => `<li><a href="${entry.fullPath()}">${$.escape(entry.name)}</a></li>`;
"""<li><a href="#{entry.fullPath()}">#{$.escape entry.name}</a></li>"""

@ -1,7 +1,13 @@
arrow = """<svg class="_path-arrow"><use xlink:href="#icon-dir"/></svg>""" /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const arrow = "<svg class=\"_path-arrow\"><use xlink:href=\"#icon-dir\"/></svg>";
app.templates.path = (doc, type, entry) -> app.templates.path = function(doc, type, entry) {
html = """<a href="#{doc.fullPath()}" class="_path-item _icon-#{doc.icon}">#{doc.fullName}</a>""" let html = `<a href="${doc.fullPath()}" class="_path-item _icon-${doc.icon}">${doc.fullName}</a>`;
html += """#{arrow}<a href="#{type.fullPath()}" class="_path-item">#{type.name}</a>""" if type if (type) { html += `${arrow}<a href="${type.fullPath()}" class="_path-item">${type.name}</a>`; }
html += """#{arrow}<span class="_path-item">#{$.escape entry.name}</span>""" if entry if (entry) { html += `${arrow}<span class="_path-item">${$.escape(entry.name)}</span>`; }
html return html;
};

@ -1,68 +1,79 @@
templates = app.templates /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const {
templates
} = app;
arrow = """<svg class="_list-arrow"><use xlink:href="#icon-dir"/></svg>""" const arrow = "<svg class=\"_list-arrow\"><use xlink:href=\"#icon-dir\"/></svg>";
templates.sidebarDoc = (doc, options = {}) -> templates.sidebarDoc = function(doc, options) {
link = """<a href="#{doc.fullPath()}" class="_list-item _icon-#{doc.icon} """ if (options == null) { options = {}; }
link += if options.disabled then '_list-disabled' else '_list-dir' let link = `<a href="${doc.fullPath()}" class="_list-item _icon-${doc.icon} `;
link += """" data-slug="#{doc.slug}" title="#{doc.fullName}" tabindex="-1">""" link += options.disabled ? '_list-disabled' : '_list-dir';
if options.disabled link += `" data-slug="${doc.slug}" title="${doc.fullName}" tabindex="-1">`;
link += """<span class="_list-enable" data-enable="#{doc.slug}">Enable</span>""" if (options.disabled) {
else link += `<span class="_list-enable" data-enable="${doc.slug}">Enable</span>`;
link += arrow } else {
link += """<span class="_list-count">#{doc.release}</span>""" if doc.release link += arrow;
link += """<span class="_list-text">#{doc.name}""" }
link += " #{doc.version}" if options.fullName or options.disabled and doc.version if (doc.release) { link += `<span class="_list-count">${doc.release}</span>`; }
link + "</span></a>" link += `<span class="_list-text">${doc.name}`;
if (options.fullName || (options.disabled && doc.version)) { link += ` ${doc.version}`; }
return link + "</span></a>";
};
templates.sidebarType = (type) -> templates.sidebarType = type => `<a href="${type.fullPath()}" class="_list-item _list-dir" data-slug="${type.slug}" tabindex="-1">${arrow}<span class="_list-count">${type.count}</span><span class="_list-text">${$.escape(type.name)}</span></a>`;
"""<a href="#{type.fullPath()}" class="_list-item _list-dir" data-slug="#{type.slug}" tabindex="-1">#{arrow}<span class="_list-count">#{type.count}</span><span class="_list-text">#{$.escape type.name}</span></a>"""
templates.sidebarEntry = (entry) -> templates.sidebarEntry = entry => `<a href="${entry.fullPath()}" class="_list-item _list-hover" tabindex="-1">${$.escape(entry.name)}</a>`;
"""<a href="#{entry.fullPath()}" class="_list-item _list-hover" tabindex="-1">#{$.escape entry.name}</a>"""
templates.sidebarResult = (entry) -> templates.sidebarResult = function(entry) {
addons = if entry.isIndex() and app.disabledDocs.contains(entry.doc) let addons = entry.isIndex() && app.disabledDocs.contains(entry.doc) ?
"""<span class="_list-enable" data-enable="#{entry.doc.slug}">Enable</span>""" `<span class="_list-enable" data-enable="${entry.doc.slug}">Enable</span>`
else :
"""<span class="_list-reveal" data-reset-list title="Reveal in list"></span>""" "<span class=\"_list-reveal\" data-reset-list title=\"Reveal in list\"></span>";
addons += """<span class="_list-count">#{entry.doc.short_version}</span>""" if entry.doc.version and not entry.isIndex() if (entry.doc.version && !entry.isIndex()) { addons += `<span class="_list-count">${entry.doc.short_version}</span>`; }
"""<a href="#{entry.fullPath()}" class="_list-item _list-hover _list-result _icon-#{entry.doc.icon}" tabindex="-1">#{addons}<span class="_list-text">#{$.escape entry.name}</span></a>""" return `<a href="${entry.fullPath()}" class="_list-item _list-hover _list-result _icon-${entry.doc.icon}" tabindex="-1">${addons}<span class="_list-text">${$.escape(entry.name)}</span></a>`;
};
templates.sidebarNoResults = -> templates.sidebarNoResults = function() {
html = """ <div class="_list-note">No results.</div> """ let html = " <div class=\"_list-note\">No results.</div> ";
html += """ if (!app.isSingleDoc() && !app.disabledDocs.isEmpty()) { html += `\
<div class="_list-note">Note: documentations must be <a href="/settings" class="_list-note-link">enabled</a> to appear in the search.</div> <div class="_list-note">Note: documentations must be <a href="/settings" class="_list-note-link">enabled</a> to appear in the search.</div>\
""" unless app.isSingleDoc() or app.disabledDocs.isEmpty() `; }
html return html;
};
templates.sidebarPageLink = (count) -> templates.sidebarPageLink = count => `<span role="link" class="_list-item _list-pagelink">Show more\u2026 (${count})</span>`;
"""<span role="link" class="_list-item _list-pagelink">Show more\u2026 (#{count})</span>"""
templates.sidebarLabel = (doc, options = {}) -> templates.sidebarLabel = function(doc, options) {
label = """<label class="_list-item""" if (options == null) { options = {}; }
label += " _icon-#{doc.icon}" unless doc.version let label = "<label class=\"_list-item";
label += """"><input type="checkbox" name="#{doc.slug}" class="_list-checkbox" """ if (!doc.version) { label += ` _icon-${doc.icon}`; }
label += "checked" if options.checked label += `"><input type="checkbox" name="${doc.slug}" class="_list-checkbox" `;
label + """><span class="_list-text">#{doc.fullName}</span></label>""" if (options.checked) { label += "checked"; }
return label + `><span class="_list-text">${doc.fullName}</span></label>`;
};
templates.sidebarVersionedDoc = (doc, versions, options = {}) -> templates.sidebarVersionedDoc = function(doc, versions, options) {
html = """<div class="_list-item _list-dir _list-rdir _icon-#{doc.icon}""" if (options == null) { options = {}; }
html += " open" if options.open let html = `<div class="_list-item _list-dir _list-rdir _icon-${doc.icon}`;
html + """" tabindex="0">#{arrow}#{doc.name}</div><div class="_list _list-sub">#{versions}</div>""" if (options.open) { html += " open"; }
return html + `" tabindex="0">${arrow}${doc.name}</div><div class="_list _list-sub">${versions}</div>`;
};
templates.sidebarDisabled = (options) -> templates.sidebarDisabled = options => `<h6 class="_list-title">${arrow}Disabled (${options.count}) <a href="/settings" class="_list-title-link" tabindex="-1">Customize</a></h6>`;
"""<h6 class="_list-title">#{arrow}Disabled (#{options.count}) <a href="/settings" class="_list-title-link" tabindex="-1">Customize</a></h6>"""
templates.sidebarDisabledList = (html) -> templates.sidebarDisabledList = html => `<div class="_disabled-list">${html}</div>`;
"""<div class="_disabled-list">#{html}</div>"""
templates.sidebarDisabledVersionedDoc = (doc, versions) -> templates.sidebarDisabledVersionedDoc = (doc, versions) => `<a class="_list-item _list-dir _icon-${doc.icon} _list-disabled" data-slug="${doc.slug_without_version}" tabindex="-1">${arrow}${doc.name}</a><div class="_list _list-sub">${versions}</div>`;
"""<a class="_list-item _list-dir _icon-#{doc.icon} _list-disabled" data-slug="#{doc.slug_without_version}" tabindex="-1">#{arrow}#{doc.name}</a><div class="_list _list-sub">#{versions}</div>"""
templates.docPickerHeader = """<div class="_list-picker-head"><span>Documentation</span> <span>Enable</span></div>""" templates.docPickerHeader = "<div class=\"_list-picker-head\"><span>Documentation</span> <span>Enable</span></div>";
templates.docPickerNote = """ templates.docPickerNote = `\
<div class="_list-note">Tip: for faster and better search results, select only the docs you need.</div> <div class="_list-note">Tip: for faster and better search results, select only the docs you need.</div>
<a href="https://trello.com/b/6BmTulfx/devdocs-documentation" class="_list-link" target="_blank" rel="noopener">Vote for new documentation</a> <a href="https://trello.com/b/6BmTulfx/devdocs-documentation" class="_list-link" target="_blank" rel="noopener">Vote for new documentation</a>\
""" `;

@ -1,10 +1,15 @@
app.templates.tipKeyNav = () -> """ /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.templates.tipKeyNav = () => `\
<p class="_notif-text"> <p class="_notif-text">
<strong>ProTip</strong> <strong>ProTip</strong>
<span class="_notif-info">(click to dismiss)</span> <span class="_notif-info">(click to dismiss)</span>
<p class="_notif-text"> <p class="_notif-text">
Hit #{if app.settings.get('arrowScroll') then '<code class="_label">shift</code> +' else ''} <code class="_label">&darr;</code> <code class="_label">&uarr;</code> <code class="_label">&larr;</code> <code class="_label">&rarr;</code> to navigate the sidebar.<br> Hit ${app.settings.get('arrowScroll') ? '<code class="_label">shift</code> +' : ''} <code class="_label">&darr;</code> <code class="_label">&uarr;</code> <code class="_label">&larr;</code> <code class="_label">&rarr;</code> to navigate the sidebar.<br>
Hit <code class="_label">space / shift space</code>#{if app.settings.get('arrowScroll') then ' or <code class="_label">&darr;/&uarr;</code>' else ', <code class="_label">alt &darr;/&uarr;</code> or <code class="_label">shift &darr;/&uarr;</code>'} to scroll the page. Hit <code class="_label">space / shift space</code>${app.settings.get('arrowScroll') ? ' or <code class="_label">&darr;/&uarr;</code>' : ', <code class="_label">alt &darr;/&uarr;</code> or <code class="_label">shift &darr;/&uarr;</code>'} to scroll the page.
<p class="_notif-text"> <p class="_notif-text">
<a href="/help#shortcuts" class="_notif-link">See all keyboard shortcuts</a> <a href="/help#shortcuts" class="_notif-link">See all keyboard shortcuts</a>\
""" `;

@ -1,195 +1,259 @@
class app.views.Content extends app.View /*
@el: '._content' * decaffeinate suggestions:
@loadingClass: '_content-loading' * DS002: Fix invalid constructor
* DS102: Remove unnecessary code created because of implicit returns
@events: * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
click: 'onClick' * DS104: Avoid inline assignments
* DS204: Change includes calls to have a more natural evaluation order
@shortcuts: * DS205: Consider reworking code to avoid use of IIFEs
altUp: 'scrollStepUp' * DS206: Consider reworking classes to avoid initClass
altDown: 'scrollStepDown' * DS207: Consider shorter variations of null checks
pageUp: 'scrollPageUp' * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
pageDown: 'scrollPageDown' */
pageTop: 'scrollToTop' const Cls = (app.views.Content = class Content extends app.View {
pageBottom: 'scrollToBottom' constructor(...args) {
this.scrollToTop = this.scrollToTop.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollStepUp = this.scrollStepUp.bind(this);
this.scrollStepDown = this.scrollStepDown.bind(this);
this.scrollPageUp = this.scrollPageUp.bind(this);
this.scrollPageDown = this.scrollPageDown.bind(this);
this.onReady = this.onReady.bind(this);
this.onBootError = this.onBootError.bind(this);
this.onEntryLoading = this.onEntryLoading.bind(this);
this.onEntryLoaded = this.onEntryLoaded.bind(this);
this.beforeRoute = this.beforeRoute.bind(this);
this.afterRoute = this.afterRoute.bind(this);
this.onClick = this.onClick.bind(this);
this.onAltF = this.onAltF.bind(this);
super(...args);
}
static initClass() {
this.el = '._content';
this.loadingClass = '_content-loading';
this.events =
{click: 'onClick'};
this.shortcuts = {
altUp: 'scrollStepUp',
altDown: 'scrollStepDown',
pageUp: 'scrollPageUp',
pageDown: 'scrollPageDown',
pageTop: 'scrollToTop',
pageBottom: 'scrollToBottom',
altF: 'onAltF' altF: 'onAltF'
};
@routes: this.routes = {
before: 'beforeRoute' before: 'beforeRoute',
after: 'afterRoute' after: 'afterRoute'
};
}
init: -> init() {
@scrollEl = if app.isMobile() this.scrollEl = app.isMobile() ?
(document.scrollingElement || document.body) (document.scrollingElement || document.body)
else :
@el this.el;
@scrollMap = {} this.scrollMap = {};
@scrollStack = [] this.scrollStack = [];
@rootPage = new app.views.RootPage this.rootPage = new app.views.RootPage;
@staticPage = new app.views.StaticPage this.staticPage = new app.views.StaticPage;
@settingsPage = new app.views.SettingsPage this.settingsPage = new app.views.SettingsPage;
@offlinePage = new app.views.OfflinePage this.offlinePage = new app.views.OfflinePage;
@typePage = new app.views.TypePage this.typePage = new app.views.TypePage;
@entryPage = new app.views.EntryPage this.entryPage = new app.views.EntryPage;
@entryPage this.entryPage
.on 'loading', @onEntryLoading .on('loading', this.onEntryLoading)
.on 'loaded', @onEntryLoaded .on('loaded', this.onEntryLoaded);
app app
.on 'ready', @onReady .on('ready', this.onReady)
.on 'bootError', @onBootError .on('bootError', this.onBootError);
return }
show: (view) -> show(view) {
@hideLoading() this.hideLoading();
unless view is @view if (view !== this.view) {
@view?.deactivate() if (this.view != null) {
@html @view = view this.view.deactivate();
@view.activate() }
return this.html(this.view = view);
this.view.activate();
showLoading: -> }
@addClass @constructor.loadingClass }
return
showLoading() {
isLoading: -> this.addClass(this.constructor.loadingClass);
@el.classList.contains @constructor.loadingClass }
hideLoading: -> isLoading() {
@removeClass @constructor.loadingClass return this.el.classList.contains(this.constructor.loadingClass);
return }
scrollTo: (value) -> hideLoading() {
@scrollEl.scrollTop = value or 0 this.removeClass(this.constructor.loadingClass);
return }
smoothScrollTo: (value) -> scrollTo(value) {
if app.settings.get('fastScroll') this.scrollEl.scrollTop = value || 0;
@scrollTo value }
else
$.smoothScroll @scrollEl, value or 0 smoothScrollTo(value) {
return if (app.settings.get('fastScroll')) {
this.scrollTo(value);
scrollBy: (n) -> } else {
@smoothScrollTo @scrollEl.scrollTop + n $.smoothScroll(this.scrollEl, value || 0);
return }
}
scrollToTop: =>
@smoothScrollTo 0 scrollBy(n) {
return this.smoothScrollTo(this.scrollEl.scrollTop + n);
}
scrollToBottom: =>
@smoothScrollTo @scrollEl.scrollHeight scrollToTop() {
return this.smoothScrollTo(0);
}
scrollStepUp: =>
@scrollBy -80 scrollToBottom() {
return this.smoothScrollTo(this.scrollEl.scrollHeight);
}
scrollStepDown: =>
@scrollBy 80 scrollStepUp() {
return this.scrollBy(-80);
}
scrollPageUp: =>
@scrollBy 40 - @scrollEl.clientHeight scrollStepDown() {
return this.scrollBy(80);
}
scrollPageDown: =>
@scrollBy @scrollEl.clientHeight - 40 scrollPageUp() {
return this.scrollBy(40 - this.scrollEl.clientHeight);
}
scrollToTarget: ->
if @routeCtx.hash and el = @findTargetByHash @routeCtx.hash scrollPageDown() {
$.scrollToWithImageLock el, @scrollEl, 'top', this.scrollBy(this.scrollEl.clientHeight - 40);
margin: if @scrollEl is @el then 0 else $.offset(@el).top }
$.highlight el, className: '_highlight'
else scrollToTarget() {
@scrollTo @scrollMap[@routeCtx.state.id] let el;
return if (this.routeCtx.hash && (el = this.findTargetByHash(this.routeCtx.hash))) {
$.scrollToWithImageLock(el, this.scrollEl, 'top',
onReady: => {margin: this.scrollEl === this.el ? 0 : $.offset(this.el).top});
@hideLoading() $.highlight(el, {className: '_highlight'});
return } else {
this.scrollTo(this.scrollMap[this.routeCtx.state.id]);
onBootError: => }
@hideLoading() }
@html @tmpl('bootError')
return onReady() {
this.hideLoading();
onEntryLoading: => }
@showLoading()
if @scrollToTargetTimeout onBootError() {
clearTimeout @scrollToTargetTimeout this.hideLoading();
@scrollToTargetTimeout = null this.html(this.tmpl('bootError'));
return }
onEntryLoaded: => onEntryLoading() {
@hideLoading() this.showLoading();
if @scrollToTargetTimeout if (this.scrollToTargetTimeout) {
clearTimeout @scrollToTargetTimeout clearTimeout(this.scrollToTargetTimeout);
@scrollToTargetTimeout = null this.scrollToTargetTimeout = null;
@scrollToTarget() }
return }
beforeRoute: (context) => onEntryLoaded() {
@cacheScrollPosition() this.hideLoading();
@routeCtx = context if (this.scrollToTargetTimeout) {
@scrollToTargetTimeout = @delay @scrollToTarget clearTimeout(this.scrollToTargetTimeout);
return this.scrollToTargetTimeout = null;
}
cacheScrollPosition: -> this.scrollToTarget();
return if not @routeCtx or @routeCtx.hash }
return if @routeCtx.path is '/'
beforeRoute(context) {
unless @scrollMap[@routeCtx.state.id]? this.cacheScrollPosition();
@scrollStack.push @routeCtx.state.id this.routeCtx = context;
while @scrollStack.length > app.config.history_cache_size this.scrollToTargetTimeout = this.delay(this.scrollToTarget);
delete @scrollMap[@scrollStack.shift()] }
@scrollMap[@routeCtx.state.id] = @scrollEl.scrollTop cacheScrollPosition() {
return if (!this.routeCtx || this.routeCtx.hash) { return; }
if (this.routeCtx.path === '/') { return; }
afterRoute: (route, context) =>
if route != 'entry' and route != 'type' if (this.scrollMap[this.routeCtx.state.id] == null) {
resetFavicon() this.scrollStack.push(this.routeCtx.state.id);
while (this.scrollStack.length > app.config.history_cache_size) {
switch route delete this.scrollMap[this.scrollStack.shift()];
when 'root' }
@show @rootPage }
when 'entry'
@show @entryPage this.scrollMap[this.routeCtx.state.id] = this.scrollEl.scrollTop;
when 'type' }
@show @typePage
when 'settings' afterRoute(route, context) {
@show @settingsPage if ((route !== 'entry') && (route !== 'type')) {
when 'offline' resetFavicon();
@show @offlinePage }
else
@show @staticPage switch (route) {
case 'root':
@view.onRoute(context) this.show(this.rootPage);
app.document.setTitle @view.getTitle?() break;
return case 'entry':
this.show(this.entryPage);
onClick: (event) => break;
link = $.closestLink $.eventTarget(event), @el case 'type':
if link and @isExternalUrl link.getAttribute('href') this.show(this.typePage);
$.stopEvent(event) break;
$.popup(link) case 'settings':
return this.show(this.settingsPage);
break;
onAltF: (event) => case 'offline':
unless document.activeElement and $.hasChild @el, document.activeElement this.show(this.offlinePage);
@find('a:not(:empty)')?.focus() break;
$.stopEvent(event) default:
this.show(this.staticPage);
findTargetByHash: (hash) -> }
el = try $.id decodeURIComponent(hash) catch
el or= try $.id(hash) catch this.view.onRoute(context);
el app.document.setTitle(typeof this.view.getTitle === 'function' ? this.view.getTitle() : undefined);
}
isExternalUrl: (url) ->
url?[0..5] in ['http:/', 'https:'] onClick(event) {
const link = $.closestLink($.eventTarget(event), this.el);
if (link && this.isExternalUrl(link.getAttribute('href'))) {
$.stopEvent(event);
$.popup(link);
}
}
onAltF(event) {
if (!document.activeElement || !$.hasChild(this.el, document.activeElement)) {
__guard__(this.find('a:not(:empty)'), x => x.focus());
return $.stopEvent(event);
}
}
findTargetByHash(hash) {
let el = (() => { try { return $.id(decodeURIComponent(hash)); } catch (error) {} })();
if (!el) { el = (() => { try { return $.id(hash); } catch (error1) {} })(); }
return el;
}
isExternalUrl(url) {
let needle;
return (needle = __guard__(url, x => x.slice(0, 6)), ['http:/', 'https:'].includes(needle));
}
});
Cls.initClass();
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,166 +1,225 @@
class app.views.EntryPage extends app.View /*
@className: '_page' * decaffeinate suggestions:
@errorClass: '_page-error' * DS002: Fix invalid constructor
* DS101: Remove unnecessary use of Array.from
@events: * DS102: Remove unnecessary code created because of implicit returns
click: 'onClick' * DS205: Consider reworking code to avoid use of IIFEs
* DS206: Consider reworking classes to avoid initClass
@shortcuts: * DS207: Consider shorter variations of null checks
altC: 'onAltC' * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
(function() {
let LINKS = undefined;
const Cls = (app.views.EntryPage = class EntryPage extends app.View {
constructor(...args) {
this.beforeRoute = this.beforeRoute.bind(this);
this.onSuccess = this.onSuccess.bind(this);
this.onError = this.onError.bind(this);
this.onClick = this.onClick.bind(this);
this.onAltC = this.onAltC.bind(this);
this.onAltO = this.onAltO.bind(this);
super(...args);
}
static initClass() {
this.className = '_page';
this.errorClass = '_page-error';
this.events =
{click: 'onClick'};
this.shortcuts = {
altC: 'onAltC',
altO: 'onAltO' altO: 'onAltO'
};
@routes: this.routes =
before: 'beforeRoute' {before: 'beforeRoute'};
init: ->
@cacheMap = {}
@cacheStack = []
return
deactivate: ->
if super
@empty()
@entry = null
return
loading: ->
@empty()
@trigger 'loading'
return
render: (content = '', fromCache = false) ->
return unless @activated
@empty()
@subview = new (@subViewClass()) @el, @entry
$.batchUpdate @el, =>
@subview.render(content, fromCache)
@addCopyButtons() unless fromCache
return
if app.disabledDocs.findBy 'slug', @entry.doc.slug
@hiddenView = new app.views.HiddenPage @el, @entry
setFaviconForDoc(@entry.doc)
@delay @polyfillMathML
@trigger 'loaded'
return
addCopyButtons: ->
unless @copyButton
@copyButton = document.createElement('button')
@copyButton.innerHTML = '<svg><use xlink:href="#icon-copy"/></svg>'
@copyButton.type = 'button'
@copyButton.className = '_pre-clip'
@copyButton.title = 'Copy to clipboard'
@copyButton.setAttribute 'aria-label', 'Copy to clipboard'
el.appendChild @copyButton.cloneNode(true) for el in @findAllByTag('pre')
return
polyfillMathML: ->
return unless window.supportsMathML is false and !@polyfilledMathML and @findByTag('math')
@polyfilledMathML = true
$.append document.head, """<link rel="stylesheet" href="#{app.config.mathml_stylesheet}">"""
return
LINKS =
home: 'Homepage'
code: 'Source code'
prepareContent: (content) -> LINKS = {
return content unless @entry.isIndex() and @entry.doc.links home: 'Homepage',
code: 'Source code'
links = for link, url of @entry.doc.links };
"""<a href="#{url}" class="_links-link">#{LINKS[link]}</a>""" }
"""<p class="_links">#{links.join('')}</p>#{content}""" init() {
this.cacheMap = {};
empty: -> this.cacheStack = [];
@subview?.deactivate() }
@subview = null
deactivate() {
@hiddenView?.deactivate() if (super.deactivate(...arguments)) {
@hiddenView = null this.empty();
this.entry = null;
@resetClass() }
super }
return
loading() {
subViewClass: -> this.empty();
app.views["#{$.classify(@entry.doc.type)}Page"] or app.views.BasePage this.trigger('loading');
}
getTitle: ->
@entry.doc.fullName + if @entry.isIndex() then ' documentation' else " / #{@entry.name}" render(content, fromCache) {
if (content == null) { content = ''; }
beforeRoute: => if (fromCache == null) { fromCache = false; }
@cache() if (!this.activated) { return; }
@abort() this.empty();
return this.subview = new (this.subViewClass())(this.el, this.entry);
onRoute: (context) -> $.batchUpdate(this.el, () => {
isSameFile = context.entry.filePath() is @entry?.filePath() this.subview.render(content, fromCache);
@entry = context.entry if (!fromCache) { this.addCopyButtons(); }
@restore() or @load() unless isSameFile });
return
if (app.disabledDocs.findBy('slug', this.entry.doc.slug)) {
load: -> this.hiddenView = new app.views.HiddenPage(this.el, this.entry);
@loading() }
@xhr = @entry.loadFile @onSuccess, @onError
return setFaviconForDoc(this.entry.doc);
this.delay(this.polyfillMathML);
abort: -> this.trigger('loaded');
if @xhr }
@xhr.abort()
@xhr = @entry = null addCopyButtons() {
return if (!this.copyButton) {
this.copyButton = document.createElement('button');
onSuccess: (response) => this.copyButton.innerHTML = '<svg><use xlink:href="#icon-copy"/></svg>';
return unless @activated this.copyButton.type = 'button';
@xhr = null this.copyButton.className = '_pre-clip';
@render @prepareContent(response) this.copyButton.title = 'Copy to clipboard';
return this.copyButton.setAttribute('aria-label', 'Copy to clipboard');
}
onError: => for (var el of Array.from(this.findAllByTag('pre'))) { el.appendChild(this.copyButton.cloneNode(true)); }
@xhr = null }
@render @tmpl('pageLoadError')
@resetClass() polyfillMathML() {
@addClass @constructor.errorClass if ((window.supportsMathML !== false) || !!this.polyfilledMathML || !this.findByTag('math')) { return; }
app.serviceWorker?.update() this.polyfilledMathML = true;
return $.append(document.head, `<link rel="stylesheet" href="${app.config.mathml_stylesheet}">`);
}
cache: ->
return if @xhr or not @entry or @cacheMap[path = @entry.filePath()] prepareContent(content) {
if (!this.entry.isIndex() || !this.entry.doc.links) { return content; }
@cacheMap[path] = @el.innerHTML
@cacheStack.push(path) const links = (() => {
const result = [];
while @cacheStack.length > app.config.history_cache_size for (var link in this.entry.doc.links) {
delete @cacheMap[@cacheStack.shift()] var url = this.entry.doc.links[link];
return result.push(`<a href="${url}" class="_links-link">${LINKS[link]}</a>`);
}
restore: -> return result;
if @cacheMap[path = @entry.filePath()] })();
@render @cacheMap[path], true
true return `<p class="_links">${links.join('')}</p>${content}`;
}
onClick: (event) =>
target = $.eventTarget(event) empty() {
if target.hasAttribute 'data-retry' if (this.subview != null) {
$.stopEvent(event) this.subview.deactivate();
@load() }
else if target.classList.contains '_pre-clip' this.subview = null;
$.stopEvent(event)
target.classList.add if $.copyToClipboard(target.parentNode.textContent) then '_pre-clip-success' else '_pre-clip-error' if (this.hiddenView != null) {
setTimeout (-> target.className = '_pre-clip'), 2000 this.hiddenView.deactivate();
return }
this.hiddenView = null;
onAltC: =>
return unless link = @find('._attribution:last-child ._attribution-link') this.resetClass();
console.log(link.href + location.hash) super.empty(...arguments);
navigator.clipboard.writeText(link.href + location.hash) }
return
subViewClass() {
onAltO: => return app.views[`${$.classify(this.entry.doc.type)}Page`] || app.views.BasePage;
return unless link = @find('._attribution:last-child ._attribution-link') }
@delay -> $.popup(link.href + location.hash)
return getTitle() {
return this.entry.doc.fullName + (this.entry.isIndex() ? ' documentation' : ` / ${this.entry.name}`);
}
beforeRoute() {
this.cache();
this.abort();
}
onRoute(context) {
const isSameFile = context.entry.filePath() === (this.entry != null ? this.entry.filePath() : undefined);
this.entry = context.entry;
if (!isSameFile) { this.restore() || this.load(); }
}
load() {
this.loading();
this.xhr = this.entry.loadFile(this.onSuccess, this.onError);
}
abort() {
if (this.xhr) {
this.xhr.abort();
this.xhr = (this.entry = null);
}
}
onSuccess(response) {
if (!this.activated) { return; }
this.xhr = null;
this.render(this.prepareContent(response));
}
onError() {
this.xhr = null;
this.render(this.tmpl('pageLoadError'));
this.resetClass();
this.addClass(this.constructor.errorClass);
if (app.serviceWorker != null) {
app.serviceWorker.update();
}
}
cache() {
let path;
if (this.xhr || !this.entry || this.cacheMap[(path = this.entry.filePath())]) { return; }
this.cacheMap[path] = this.el.innerHTML;
this.cacheStack.push(path);
while (this.cacheStack.length > app.config.history_cache_size) {
delete this.cacheMap[this.cacheStack.shift()];
}
}
restore() {
let path;
if (this.cacheMap[(path = this.entry.filePath())]) {
this.render(this.cacheMap[path], true);
return true;
}
}
onClick(event) {
const target = $.eventTarget(event);
if (target.hasAttribute('data-retry')) {
$.stopEvent(event);
this.load();
} else if (target.classList.contains('_pre-clip')) {
$.stopEvent(event);
target.classList.add($.copyToClipboard(target.parentNode.textContent) ? '_pre-clip-success' : '_pre-clip-error');
setTimeout((() => target.className = '_pre-clip'), 2000);
}
}
onAltC() {
let link;
if (!(link = this.find('._attribution:last-child ._attribution-link'))) { return; }
console.log(link.href + location.hash);
navigator.clipboard.writeText(link.href + location.hash);
}
onAltO() {
let link;
if (!(link = this.find('._attribution:last-child ._attribution-link'))) { return; }
this.delay(() => $.popup(link.href + location.hash));
}
});
Cls.initClass();
return Cls;
})();

@ -1,92 +1,128 @@
class app.views.OfflinePage extends app.View /*
@className: '_static' * decaffeinate suggestions:
* DS002: Fix invalid constructor
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.OfflinePage = class OfflinePage extends app.View {
constructor(...args) {
this.onClick = this.onClick.bind(this);
super(...args);
}
@events: static initClass() {
click: 'onClick' this.className = '_static';
this.events = {
click: 'onClick',
change: 'onChange' change: 'onChange'
};
}
deactivate() {
if (super.deactivate(...arguments)) {
this.empty();
}
}
render() {
if (app.cookieBlocked) {
this.html(this.tmpl('offlineError', 'cookie_blocked'));
return;
}
app.docs.getInstallStatuses(statuses => {
if (!this.activated) { return; }
if (statuses === false) {
this.html(this.tmpl('offlineError', app.db.reason, app.db.error));
} else {
let html = '';
for (var doc of Array.from(app.docs.all())) { html += this.renderDoc(doc, statuses[doc.slug]); }
this.html(this.tmpl('offlinePage', html));
this.refreshLinks();
}
});
}
renderDoc(doc, status) {
return app.templates.render('offlineDoc', doc, status);
}
getTitle() {
return 'Offline';
}
refreshLinks() {
for (var action of ['install', 'update', 'uninstall']) {
this.find(`[data-action-all='${action}']`).classList[this.find(`[data-action='${action}']`) ? 'add' : 'remove']('_show');
}
}
docByEl(el) {
let slug;
while (!(slug = el.getAttribute('data-slug'))) { el = el.parentNode; }
return app.docs.findBy('slug', slug);
}
docEl(doc) {
return this.find(`[data-slug='${doc.slug}']`);
}
onRoute(context) {
this.render();
}
onClick(event) {
let action;
let el = $.eventTarget(event);
if (action = el.getAttribute('data-action')) {
const doc = this.docByEl(el);
if (action === 'update') { action = 'install'; }
doc[action](this.onInstallSuccess.bind(this, doc), this.onInstallError.bind(this, doc), this.onInstallProgress.bind(this, doc));
el.parentNode.innerHTML = `${el.textContent.replace(/e$/, '')}ing…`;
} else if (action = el.getAttribute('data-action-all') || el.parentElement.getAttribute('data-action-all')) {
if ((action === 'uninstall') && !window.confirm('Uninstall all docs?')) { return; }
app.db.migrate();
for (el of Array.from(this.findAll(`[data-action='${action}']`))) { $.click(el); }
}
}
onInstallSuccess(doc) {
if (!this.activated) { return; }
doc.getInstallStatus(status => {
let el;
if (!this.activated) { return; }
if (el = this.docEl(doc)) {
el.outerHTML = this.renderDoc(doc, status);
$.highlight(el, {className: '_highlight'});
this.refreshLinks();
}
});
}
onInstallError(doc) {
let el;
if (!this.activated) { return; }
if (el = this.docEl(doc)) {
el.lastElementChild.textContent = 'Error';
}
}
onInstallProgress(doc, event) {
let el;
if (!this.activated || !event.lengthComputable) { return; }
if (el = this.docEl(doc)) {
const percentage = Math.round((event.loaded * 100) / event.total);
el.lastElementChild.textContent = el.lastElementChild.textContent.replace(/(\s.+)?$/, ` (${percentage}%)`);
}
}
deactivate: -> onChange(event) {
if super if (event.target.name === 'autoUpdate') {
@empty() app.settings.set('manualUpdate', !event.target.checked);
return }
}
render: -> });
if app.cookieBlocked Cls.initClass();
@html @tmpl('offlineError', 'cookie_blocked')
return
app.docs.getInstallStatuses (statuses) =>
return unless @activated
if statuses is false
@html @tmpl('offlineError', app.db.reason, app.db.error)
else
html = ''
html += @renderDoc(doc, statuses[doc.slug]) for doc in app.docs.all()
@html @tmpl('offlinePage', html)
@refreshLinks()
return
return
renderDoc: (doc, status) ->
app.templates.render('offlineDoc', doc, status)
getTitle: ->
'Offline'
refreshLinks: ->
for action in ['install', 'update', 'uninstall']
@find("[data-action-all='#{action}']").classList[if @find("[data-action='#{action}']") then 'add' else 'remove']('_show')
return
docByEl: (el) ->
el = el.parentNode until slug = el.getAttribute('data-slug')
app.docs.findBy('slug', slug)
docEl: (doc) ->
@find("[data-slug='#{doc.slug}']")
onRoute: (context) ->
@render()
return
onClick: (event) =>
el = $.eventTarget(event)
if action = el.getAttribute('data-action')
doc = @docByEl(el)
action = 'install' if action is 'update'
doc[action](@onInstallSuccess.bind(@, doc), @onInstallError.bind(@, doc), @onInstallProgress.bind(@, doc))
el.parentNode.innerHTML = "#{el.textContent.replace(/e$/, '')}ing…"
else if action = el.getAttribute('data-action-all') || el.parentElement.getAttribute('data-action-all')
return unless action isnt 'uninstall' or window.confirm('Uninstall all docs?')
app.db.migrate()
$.click(el) for el in @findAll("[data-action='#{action}']")
return
onInstallSuccess: (doc) ->
return unless @activated
doc.getInstallStatus (status) =>
return unless @activated
if el = @docEl(doc)
el.outerHTML = @renderDoc(doc, status)
$.highlight el, className: '_highlight'
@refreshLinks()
return
return
onInstallError: (doc) ->
return unless @activated
if el = @docEl(doc)
el.lastElementChild.textContent = 'Error'
return
onInstallProgress: (doc, event) ->
return unless @activated and event.lengthComputable
if el = @docEl(doc)
percentage = Math.round event.loaded * 100 / event.total
el.lastElementChild.textContent = el.lastElementChild.textContent.replace(/(\s.+)?$/, " (#{percentage}%)")
return
onChange: (event) ->
if event.target.name is 'autoUpdate'
app.settings.set 'manualUpdate', !event.target.checked
return

@ -1,43 +1,61 @@
class app.views.RootPage extends app.View /*
@events: * decaffeinate suggestions:
click: 'onClick' * DS002: Fix invalid constructor
* DS102: Remove unnecessary code created because of implicit returns
init: -> * DS206: Consider reworking classes to avoid initClass
@setHidden false unless @isHidden() # reserve space in local storage * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
@render() */
return const Cls = (app.views.RootPage = class RootPage extends app.View {
constructor(...args) {
render: -> this.onClick = this.onClick.bind(this);
@empty() super(...args);
}
tmpl = if app.isAndroidWebview()
static initClass() {
this.events =
{click: 'onClick'};
}
init() {
if (!this.isHidden()) { this.setHidden(false); } // reserve space in local storage
this.render();
}
render() {
this.empty();
const tmpl = app.isAndroidWebview() ?
'androidWarning' 'androidWarning'
else if @isHidden() : this.isHidden() ?
'splash' 'splash'
else if app.isMobile() : app.isMobile() ?
'mobileIntro' 'mobileIntro'
else :
'intro' 'intro';
@append @tmpl(tmpl) this.append(this.tmpl(tmpl));
return }
hideIntro: -> hideIntro() {
@setHidden true this.setHidden(true);
@render() this.render();
return }
setHidden: (value) -> setHidden(value) {
app.settings.set 'hideIntro', value app.settings.set('hideIntro', value);
return }
isHidden: -> isHidden() {
app.isSingleDoc() or app.settings.get 'hideIntro' return app.isSingleDoc() || app.settings.get('hideIntro');
}
onRoute: ->
onRoute() {}
onClick: (event) =>
if $.eventTarget(event).hasAttribute 'data-hide-intro' onClick(event) {
$.stopEvent(event) if ($.eventTarget(event).hasAttribute('data-hide-intro')) {
@hideIntro() $.stopEvent(event);
return this.hideIntro();
}
}
});
Cls.initClass();

@ -1,116 +1,151 @@
class app.views.SettingsPage extends app.View /*
@className: '_static' * decaffeinate suggestions:
* DS002: Fix invalid constructor
@events: * DS101: Remove unnecessary use of Array.from
click: 'onClick' * DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.SettingsPage = class SettingsPage extends app.View {
constructor(...args) {
this.onChange = this.onChange.bind(this);
this.onClick = this.onClick.bind(this);
super(...args);
}
static initClass() {
this.className = '_static';
this.events = {
click: 'onClick',
change: 'onChange' change: 'onChange'
};
render: -> }
@html @tmpl('settingsPage', @currentSettings())
return render() {
this.html(this.tmpl('settingsPage', this.currentSettings()));
currentSettings: -> }
settings = {}
settings.theme = app.settings.get('theme') currentSettings() {
settings.smoothScroll = !app.settings.get('fastScroll') const settings = {};
settings.arrowScroll = app.settings.get('arrowScroll') settings.theme = app.settings.get('theme');
settings.noAutofocus = app.settings.get('noAutofocus') settings.smoothScroll = !app.settings.get('fastScroll');
settings.autoInstall = app.settings.get('autoInstall') settings.arrowScroll = app.settings.get('arrowScroll');
settings.analyticsConsent = app.settings.get('analyticsConsent') settings.noAutofocus = app.settings.get('noAutofocus');
settings.spaceScroll = app.settings.get('spaceScroll') settings.autoInstall = app.settings.get('autoInstall');
settings.spaceTimeout = app.settings.get('spaceTimeout') settings.analyticsConsent = app.settings.get('analyticsConsent');
settings.autoSupported = app.settings.autoSupported settings.spaceScroll = app.settings.get('spaceScroll');
settings[layout] = app.settings.hasLayout(layout) for layout in app.settings.LAYOUTS settings.spaceTimeout = app.settings.get('spaceTimeout');
settings settings.autoSupported = app.settings.autoSupported;
for (var layout of Array.from(app.settings.LAYOUTS)) { settings[layout] = app.settings.hasLayout(layout); }
getTitle: -> return settings;
'Preferences' }
setTheme: (value) -> getTitle() {
app.settings.set('theme', value) return 'Preferences';
return }
toggleLayout: (layout, enable) -> setTheme(value) {
app.settings.setLayout(layout, enable) app.settings.set('theme', value);
return }
toggleSmoothScroll: (enable) -> toggleLayout(layout, enable) {
app.settings.set('fastScroll', !enable) app.settings.setLayout(layout, enable);
return }
toggleAnalyticsConsent: (enable) -> toggleSmoothScroll(enable) {
app.settings.set('analyticsConsent', if enable then '1' else '0') app.settings.set('fastScroll', !enable);
resetAnalytics() unless enable }
return
toggleAnalyticsConsent(enable) {
toggleSpaceScroll: (enable) -> app.settings.set('analyticsConsent', enable ? '1' : '0');
app.settings.set('spaceScroll', if enable then 1 else 0) if (!enable) { resetAnalytics(); }
return }
setScrollTimeout: (value) -> toggleSpaceScroll(enable) {
app.settings.set('spaceTimeout', value) app.settings.set('spaceScroll', enable ? 1 : 0);
}
toggle: (name, enable) ->
app.settings.set(name, enable) setScrollTimeout(value) {
return return app.settings.set('spaceTimeout', value);
}
export: ->
data = new Blob([JSON.stringify(app.settings.export())], type: 'application/json') toggle(name, enable) {
link = document.createElement('a') app.settings.set(name, enable);
link.href = URL.createObjectURL(data) }
link.download = 'devdocs.json'
link.style.display = 'none' export() {
document.body.appendChild(link) const data = new Blob([JSON.stringify(app.settings.export())], {type: 'application/json'});
link.click() const link = document.createElement('a');
document.body.removeChild(link) link.href = URL.createObjectURL(data);
return link.download = 'devdocs.json';
link.style.display = 'none';
import: (file, input) -> document.body.appendChild(link);
unless file and file.type is 'application/json' link.click();
new app.views.Notif 'ImportInvalid', autoHide: false document.body.removeChild(link);
return }
reader = new FileReader() import(file, input) {
reader.onloadend = -> if (!file || (file.type !== 'application/json')) {
data = try JSON.parse(reader.result) new app.views.Notif('ImportInvalid', {autoHide: false});
unless data and data.constructor is Object return;
new app.views.Notif 'ImportInvalid', autoHide: false }
return
app.settings.import(data) const reader = new FileReader();
$.trigger input.form, 'import' reader.onloadend = function() {
return const data = (() => { try { return JSON.parse(reader.result); } catch (error) {} })();
reader.readAsText(file) if (!data || (data.constructor !== Object)) {
return new app.views.Notif('ImportInvalid', {autoHide: false});
return;
onChange: (event) => }
input = event.target app.settings.import(data);
switch input.name $.trigger(input.form, 'import');
when 'theme' };
@setTheme input.value reader.readAsText(file);
when 'layout' }
@toggleLayout input.value, input.checked
when 'smoothScroll' onChange(event) {
@toggleSmoothScroll input.checked const input = event.target;
when 'import' switch (input.name) {
@import input.files[0], input case 'theme':
when 'analyticsConsent' this.setTheme(input.value);
@toggleAnalyticsConsent input.checked break;
when 'spaceScroll' case 'layout':
@toggleSpaceScroll input.checked this.toggleLayout(input.value, input.checked);
when 'spaceTimeout' break;
@setScrollTimeout input.value case 'smoothScroll':
else this.toggleSmoothScroll(input.checked);
@toggle input.name, input.checked break;
return case 'import':
this.import(input.files[0], input);
onClick: (event) => break;
target = $.eventTarget(event) case 'analyticsConsent':
switch target.getAttribute('data-action') this.toggleAnalyticsConsent(input.checked);
when 'export' break;
$.stopEvent(event) case 'spaceScroll':
@export() this.toggleSpaceScroll(input.checked);
return break;
case 'spaceTimeout':
onRoute: (context) -> this.setScrollTimeout(input.value);
@render() break;
return default:
this.toggle(input.name, input.checked);
}
}
onClick(event) {
const target = $.eventTarget(event);
switch (target.getAttribute('data-action')) {
case 'export':
$.stopEvent(event);
this.export();
break;
}
}
onRoute(context) {
this.render();
}
});
Cls.initClass();

@ -1,26 +1,39 @@
class app.views.StaticPage extends app.View /*
@className: '_static' * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.StaticPage = class StaticPage extends app.View {
static initClass() {
this.className = '_static';
@titles: this.titles = {
about: 'About' about: 'About',
news: 'News' news: 'News',
help: 'User Guide' help: 'User Guide',
notFound: '404' notFound: '404'
};
}
deactivate: -> deactivate() {
if super if (super.deactivate(...arguments)) {
@empty() this.empty();
@page = null this.page = null;
return }
}
render: (page) -> render(page) {
@page = page this.page = page;
@html @tmpl("#{@page}Page") this.html(this.tmpl(`${this.page}Page`));
return }
getTitle: -> getTitle() {
@constructor.titles[@page] return this.constructor.titles[this.page];
}
onRoute: (context) -> onRoute(context) {
@render context.page or 'notFound' this.render(context.page || 'notFound');
return }
});
Cls.initClass();

@ -1,20 +1,33 @@
class app.views.TypePage extends app.View /*
@className: '_page' * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.TypePage = class TypePage extends app.View {
static initClass() {
this.className = '_page';
}
deactivate: -> deactivate() {
if super if (super.deactivate(...arguments)) {
@empty() this.empty();
@type = null this.type = null;
return }
}
render: (@type) -> render(type) {
@html @tmpl('typePage', @type) this.type = type;
setFaviconForDoc(@type.doc) this.html(this.tmpl('typePage', this.type));
return setFaviconForDoc(this.type.doc);
}
getTitle: -> getTitle() {
"#{@type.doc.fullName} / #{@type.name}" return `${this.type.doc.fullName} / ${this.type.name}`;
}
onRoute: (context) -> onRoute(context) {
@render context.type this.render(context.type);
return }
});
Cls.initClass();

@ -1,85 +1,111 @@
class app.views.Document extends app.View /*
@el: document * decaffeinate suggestions:
* DS002: Fix invalid constructor
@events: * DS102: Remove unnecessary code created because of implicit returns
visibilitychange: 'onVisibilityChange' * DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
@shortcuts: * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
help: 'onHelp' */
preferences: 'onPreferences' const Cls = (app.views.Document = class Document extends app.View {
escape: 'onEscape' constructor(...args) {
superLeft: 'onBack' this.afterRoute = this.afterRoute.bind(this);
this.onVisibilityChange = this.onVisibilityChange.bind(this);
super(...args);
}
static initClass() {
this.el = document;
this.events =
{visibilitychange: 'onVisibilityChange'};
this.shortcuts = {
help: 'onHelp',
preferences: 'onPreferences',
escape: 'onEscape',
superLeft: 'onBack',
superRight: 'onForward' superRight: 'onForward'
};
@routes:
after: 'afterRoute' this.routes =
{after: 'afterRoute'};
init: -> }
@addSubview @menu = new app.views.Menu,
@addSubview @sidebar = new app.views.Sidebar init() {
@addSubview @resizer = new app.views.Resizer if app.views.Resizer.isSupported() this.addSubview((this.menu = new app.views.Menu),
@addSubview @content = new app.views.Content this.addSubview(this.sidebar = new app.views.Sidebar));
@addSubview @path = new app.views.Path unless app.isSingleDoc() or app.isMobile() if (app.views.Resizer.isSupported()) { this.addSubview(this.resizer = new app.views.Resizer); }
@settings = new app.views.Settings unless app.isSingleDoc() this.addSubview(this.content = new app.views.Content);
if (!app.isSingleDoc() && !app.isMobile()) { this.addSubview(this.path = new app.views.Path); }
$.on document.body, 'click', @onClick if (!app.isSingleDoc()) { this.settings = new app.views.Settings; }
@activate() $.on(document.body, 'click', this.onClick);
return
this.activate();
setTitle: (title) -> }
@el.title = if title then "#{title} — DevDocs" else 'DevDocs API Documentation'
setTitle(title) {
afterRoute: (route) => return this.el.title = title ? `${title} — DevDocs` : 'DevDocs API Documentation';
if route is 'settings' }
@settings?.activate()
else afterRoute(route) {
@settings?.deactivate() if (route === 'settings') {
return if (this.settings != null) {
this.settings.activate();
onVisibilityChange: => }
return unless @el.visibilityState is 'visible' } else {
@delay -> if (this.settings != null) {
location.reload() if app.isMobile() isnt app.views.Mobile.detect() this.settings.deactivate();
return }
, 300 }
return }
onHelp: -> onVisibilityChange() {
app.router.show '/help#shortcuts' if (this.el.visibilityState !== 'visible') { return; }
return this.delay(function() {
if (app.isMobile() !== app.views.Mobile.detect()) { location.reload(); }
onPreferences: -> }
app.router.show '/settings' , 300);
return }
onEscape: -> onHelp() {
path = if !app.isSingleDoc() or location.pathname is app.doc.fullPath() app.router.show('/help#shortcuts');
}
onPreferences() {
app.router.show('/settings');
}
onEscape() {
const path = !app.isSingleDoc() || (location.pathname === app.doc.fullPath()) ?
'/' '/'
else :
app.doc.fullPath() app.doc.fullPath();
app.router.show(path) app.router.show(path);
return }
onBack: -> onBack() {
history.back() history.back();
return }
onForward: -> onForward() {
history.forward() history.forward();
return }
onClick: (event) -> onClick(event) {
target = $.eventTarget(event) const target = $.eventTarget(event);
return unless target.hasAttribute('data-behavior') if (!target.hasAttribute('data-behavior')) { return; }
$.stopEvent(event) $.stopEvent(event);
switch target.getAttribute('data-behavior') switch (target.getAttribute('data-behavior')) {
when 'back' then history.back() case 'back': history.back(); break;
when 'reload' then window.location.reload() case 'reload': window.location.reload(); break;
when 'reboot' then app.reboot() case 'reboot': app.reboot(); break;
when 'hard-reload' then app.reload() case 'hard-reload': app.reload(); break;
when 'reset' then app.reset() if confirm('Are you sure you want to reset DevDocs?') case 'reset': if (confirm('Are you sure you want to reset DevDocs?')) { app.reset(); } break;
when 'accept-analytics' then Cookies.set('analyticsConsent', '1', expires: 1e8) && app.reboot() case 'accept-analytics': Cookies.set('analyticsConsent', '1', {expires: 1e8}) && app.reboot(); break;
when 'decline-analytics' then Cookies.set('analyticsConsent', '0', expires: 1e8) && app.reboot() case 'decline-analytics': Cookies.set('analyticsConsent', '0', {expires: 1e8}) && app.reboot(); break;
return }
}
});
Cls.initClass();

@ -1,23 +1,39 @@
class app.views.Menu extends app.View /*
@el: '._menu' * decaffeinate suggestions:
@activeClass: 'active' * DS002: Fix invalid constructor
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.Menu = class Menu extends app.View {
constructor(...args) {
this.onGlobalClick = this.onGlobalClick.bind(this);
super(...args);
}
@events: static initClass() {
click: 'onClick' this.el = '._menu';
this.activeClass = 'active';
init: -> this.events =
$.on document.body, 'click', @onGlobalClick {click: 'onClick'};
return }
onClick: (event) -> init() {
target = $.eventTarget(event) $.on(document.body, 'click', this.onGlobalClick);
target.blur() if target.tagName is 'A' }
return
onGlobalClick: (event) => onClick(event) {
return if event.which isnt 1 const target = $.eventTarget(event);
if event.target.hasAttribute?('data-toggle-menu') if (target.tagName === 'A') { target.blur(); }
@toggleClass @constructor.activeClass }
else if @hasClass @constructor.activeClass
@removeClass @constructor.activeClass onGlobalClick(event) {
return if (event.which !== 1) { return; }
if (typeof event.target.hasAttribute === 'function' ? event.target.hasAttribute('data-toggle-menu') : undefined) {
this.toggleClass(this.constructor.activeClass);
} else if (this.hasClass(this.constructor.activeClass)) {
this.removeClass(this.constructor.activeClass);
}
}
});
Cls.initClass();

@ -1,155 +1,195 @@
class app.views.Mobile extends app.View /*
@className: '_mobile' * decaffeinate suggestions:
* DS002: Fix invalid constructor
@elements: * DS102: Remove unnecessary code created because of implicit returns
body: 'body' * DS206: Consider reworking classes to avoid initClass
content: '._container' * DS207: Consider shorter variations of null checks
sidebar: '._sidebar' * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.Mobile = class Mobile extends app.View {
static initClass() {
this.className = '_mobile';
this.elements = {
body: 'body',
content: '._container',
sidebar: '._sidebar',
docPicker: '._settings ._sidebar' docPicker: '._settings ._sidebar'
};
@shortcuts:
escape: 'onEscape' this.shortcuts =
{escape: 'onEscape'};
@routes:
after: 'afterRoute' this.routes =
{after: 'afterRoute'};
@detect: -> }
if Cookies.get('override-mobile-detect')?
return JSON.parse Cookies.get('override-mobile-detect') static detect() {
try if (Cookies.get('override-mobile-detect') != null) {
(window.matchMedia('(max-width: 480px)').matches) or return JSON.parse(Cookies.get('override-mobile-detect'));
(window.matchMedia('(max-width: 767px)').matches) or }
(window.matchMedia('(max-height: 767px) and (max-width: 1024px)').matches) or try {
# Need to sniff the user agent because some Android and Windows Phone devices don't take return (window.matchMedia('(max-width: 480px)').matches) ||
# resolution (dpi) into account when reporting device width/height. (window.matchMedia('(max-width: 767px)').matches) ||
(navigator.userAgent.indexOf('Android') isnt -1 and navigator.userAgent.indexOf('Mobile') isnt -1) or (window.matchMedia('(max-height: 767px) and (max-width: 1024px)').matches) ||
(navigator.userAgent.indexOf('IEMobile') isnt -1) // Need to sniff the user agent because some Android and Windows Phone devices don't take
catch // resolution (dpi) into account when reporting device width/height.
false ((navigator.userAgent.indexOf('Android') !== -1) && (navigator.userAgent.indexOf('Mobile') !== -1)) ||
(navigator.userAgent.indexOf('IEMobile') !== -1);
@detectAndroidWebview: -> } catch (error) {
try return false;
/(Android).*( Version\/.\.. ).*(Chrome)/.test(navigator.userAgent) }
catch }
false
static detectAndroidWebview() {
constructor: -> try {
@el = document.documentElement return /(Android).*( Version\/.\.. ).*(Chrome)/.test(navigator.userAgent);
super } catch (error) {
return false;
init: -> }
$.on $('._search'), 'touchend', @onTapSearch }
@toggleSidebar = $('button[data-toggle-sidebar]') constructor() {
@toggleSidebar.removeAttribute('hidden') this.showSidebar = this.showSidebar.bind(this);
$.on @toggleSidebar, 'click', @onClickToggleSidebar this.hideSidebar = this.hideSidebar.bind(this);
this.onClickBack = this.onClickBack.bind(this);
@back = $('button[data-back]') this.onClickForward = this.onClickForward.bind(this);
@back.removeAttribute('hidden') this.onClickToggleSidebar = this.onClickToggleSidebar.bind(this);
$.on @back, 'click', @onClickBack this.onClickDocPickerTab = this.onClickDocPickerTab.bind(this);
this.onClickSettingsTab = this.onClickSettingsTab.bind(this);
@forward = $('button[data-forward]') this.onTapSearch = this.onTapSearch.bind(this);
@forward.removeAttribute('hidden') this.onEscape = this.onEscape.bind(this);
$.on @forward, 'click', @onClickForward this.afterRoute = this.afterRoute.bind(this);
this.el = document.documentElement;
@docPickerTab = $('button[data-tab="doc-picker"]') super(...arguments);
@docPickerTab.removeAttribute('hidden') }
$.on @docPickerTab, 'click', @onClickDocPickerTab
init() {
@settingsTab = $('button[data-tab="settings"]') $.on($('._search'), 'touchend', this.onTapSearch);
@settingsTab.removeAttribute('hidden')
$.on @settingsTab, 'click', @onClickSettingsTab this.toggleSidebar = $('button[data-toggle-sidebar]');
this.toggleSidebar.removeAttribute('hidden');
$.on(this.toggleSidebar, 'click', this.onClickToggleSidebar);
this.back = $('button[data-back]');
this.back.removeAttribute('hidden');
$.on(this.back, 'click', this.onClickBack);
this.forward = $('button[data-forward]');
this.forward.removeAttribute('hidden');
$.on(this.forward, 'click', this.onClickForward);
this.docPickerTab = $('button[data-tab="doc-picker"]');
this.docPickerTab.removeAttribute('hidden');
$.on(this.docPickerTab, 'click', this.onClickDocPickerTab);
this.settingsTab = $('button[data-tab="settings"]');
this.settingsTab.removeAttribute('hidden');
$.on(this.settingsTab, 'click', this.onClickSettingsTab);
app.document.sidebar.search app.document.sidebar.search
.on 'searching', @showSidebar .on('searching', this.showSidebar);
@activate() this.activate();
return }
showSidebar: => showSidebar() {
if @isSidebarShown() let selection;
window.scrollTo 0, 0 if (this.isSidebarShown()) {
return window.scrollTo(0, 0);
return;
@contentTop = window.scrollY }
@content.style.display = 'none'
@sidebar.style.display = 'block' this.contentTop = window.scrollY;
this.content.style.display = 'none';
if selection = @findByClass app.views.ListSelect.activeClass this.sidebar.style.display = 'block';
scrollContainer = if window.scrollY is @body.scrollTop then @body else document.documentElement
$.scrollTo selection, scrollContainer, 'center' if (selection = this.findByClass(app.views.ListSelect.activeClass)) {
else const scrollContainer = window.scrollY === this.body.scrollTop ? this.body : document.documentElement;
window.scrollTo 0, @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0 $.scrollTo(selection, scrollContainer, 'center');
return } else {
window.scrollTo(0, (this.findByClass(app.views.ListFold.activeClass) && this.sidebarTop) || 0);
hideSidebar: => }
return unless @isSidebarShown() }
@sidebarTop = window.scrollY
@sidebar.style.display = 'none' hideSidebar() {
@content.style.display = 'block' if (!this.isSidebarShown()) { return; }
window.scrollTo 0, @contentTop or 0 this.sidebarTop = window.scrollY;
return this.sidebar.style.display = 'none';
this.content.style.display = 'block';
isSidebarShown: -> window.scrollTo(0, this.contentTop || 0);
@sidebar.style.display isnt 'none' }
onClickBack: => isSidebarShown() {
history.back() return this.sidebar.style.display !== 'none';
}
onClickForward: =>
history.forward() onClickBack() {
return history.back();
onClickToggleSidebar: => }
if @isSidebarShown() then @hideSidebar() else @showSidebar()
return onClickForward() {
return history.forward();
onClickDocPickerTab: (event) => }
$.stopEvent(event)
@showDocPicker() onClickToggleSidebar() {
return if (this.isSidebarShown()) { this.hideSidebar(); } else { this.showSidebar(); }
}
onClickSettingsTab: (event) =>
$.stopEvent(event) onClickDocPickerTab(event) {
@showSettings() $.stopEvent(event);
return this.showDocPicker();
}
showDocPicker: ->
window.scrollTo 0, 0 onClickSettingsTab(event) {
@docPickerTab.classList.add 'active' $.stopEvent(event);
@settingsTab.classList.remove 'active' this.showSettings();
@docPicker.style.display = 'block' }
@content.style.display = 'none'
return showDocPicker() {
window.scrollTo(0, 0);
showSettings: -> this.docPickerTab.classList.add('active');
window.scrollTo 0, 0 this.settingsTab.classList.remove('active');
@docPickerTab.classList.remove 'active' this.docPicker.style.display = 'block';
@settingsTab.classList.add 'active' this.content.style.display = 'none';
@docPicker.style.display = 'none' }
@content.style.display = 'block'
return showSettings() {
window.scrollTo(0, 0);
onTapSearch: => this.docPickerTab.classList.remove('active');
window.scrollTo 0, 0 this.settingsTab.classList.add('active');
this.docPicker.style.display = 'none';
onEscape: => this.content.style.display = 'block';
@hideSidebar() }
afterRoute: (route) => onTapSearch() {
@hideSidebar() return window.scrollTo(0, 0);
}
if route is 'settings'
@showDocPicker() onEscape() {
else return this.hideSidebar();
@content.style.display = 'block' }
if page.canGoBack() afterRoute(route) {
@back.removeAttribute('disabled') this.hideSidebar();
else
@back.setAttribute('disabled', 'disabled') if (route === 'settings') {
this.showDocPicker();
if page.canGoForward() } else {
@forward.removeAttribute('disabled') this.content.style.display = 'block';
else }
@forward.setAttribute('disabled', 'disabled')
return if (page.canGoBack()) {
this.back.removeAttribute('disabled');
} else {
this.back.setAttribute('disabled', 'disabled');
}
if (page.canGoForward()) {
this.forward.removeAttribute('disabled');
} else {
this.forward.setAttribute('disabled', 'disabled');
}
}
});
Cls.initClass();

@ -1,43 +1,64 @@
class app.views.Path extends app.View /*
@className: '_path' * decaffeinate suggestions:
@attributes: * DS002: Fix invalid constructor
role: 'complementary' * DS101: Remove unnecessary use of Array.from
* DS206: Consider reworking classes to avoid initClass
@events: * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
click: 'onClick' */
const Cls = (app.views.Path = class Path extends app.View {
@routes: constructor(...args) {
after: 'afterRoute' this.onClick = this.onClick.bind(this);
this.afterRoute = this.afterRoute.bind(this);
render: (args...) -> super(...args);
@html @tmpl 'path', args... }
@show()
return static initClass() {
this.className = '_path';
show: -> this.attributes =
@prependTo app.el unless @el.parentNode {role: 'complementary'};
return
this.events =
hide: -> {click: 'onClick'};
$.remove @el if @el.parentNode
return this.routes =
{after: 'afterRoute'};
onClick: (event) => }
@clicked = true if link = $.closestLink event.target, @el
return render(...args) {
this.html(this.tmpl('path', ...Array.from(args)));
afterRoute: (route, context) => this.show();
if context.type }
@render context.doc, context.type
else if context.entry show() {
if context.entry.isIndex() if (!this.el.parentNode) { this.prependTo(app.el); }
@render context.doc }
else
@render context.doc, context.entry.getType(), context.entry hide() {
else if (this.el.parentNode) { $.remove(this.el); }
@hide() }
if @clicked onClick(event) {
@clicked = null let link;
app.document.sidebar.reset() if (link = $.closestLink(event.target, this.el)) { this.clicked = true; }
return }
afterRoute(route, context) {
if (context.type) {
this.render(context.doc, context.type);
} else if (context.entry) {
if (context.entry.isIndex()) {
this.render(context.doc);
} else {
this.render(context.doc, context.entry.getType(), context.entry);
}
} else {
this.hide();
}
if (this.clicked) {
this.clicked = null;
app.document.sidebar.reset();
}
}
});
Cls.initClass();

@ -1,49 +1,75 @@
class app.views.Resizer extends app.View /*
@className: '_resizer' * decaffeinate suggestions:
* DS002: Fix invalid constructor
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
(function() {
let MIN = undefined;
let MAX = undefined;
const Cls = (app.views.Resizer = class Resizer extends app.View {
constructor(...args) {
this.onDragStart = this.onDragStart.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
super(...args);
}
@events: static initClass() {
dragstart: 'onDragStart' this.className = '_resizer';
this.events = {
dragstart: 'onDragStart',
dragend: 'onDragEnd' dragend: 'onDragEnd'
};
MIN = 260;
MAX = 600;
}
static isSupported() {
return 'ondragstart' in document.createElement('div') && !app.isMobile();
}
init() {
this.el.setAttribute('draggable', 'true');
this.appendTo($('._app'));
}
resize(value, save) {
value -= app.el.offsetLeft;
if (!(value > 0)) { return; }
value = Math.min(Math.max(Math.round(value), MIN), MAX);
const newSize = `${value}px`;
document.documentElement.style.setProperty('--sidebarWidth', newSize);
if (save) { app.settings.setSize(value); }
}
onDragStart(event) {
event.dataTransfer.effectAllowed = 'link';
event.dataTransfer.setData('Text', '');
$.on(window, 'dragover', this.onDrag);
}
onDrag(event) {
const value = event.pageX;
if (!(value > 0)) { return; }
this.lastDragValue = value;
if (this.lastDrag && (this.lastDrag > (Date.now() - 50))) { return; }
this.lastDrag = Date.now();
this.resize(value, false);
}
@isSupported: -> onDragEnd(event) {
'ondragstart' of document.createElement('div') and !app.isMobile() $.off(window, 'dragover', this.onDrag);
let value = event.pageX || (event.screenX - window.screenX);
init: -> if (this.lastDragValue && !(this.lastDragValue - 5 < value && value < this.lastDragValue + 5)) { // https://github.com/freeCodeCamp/devdocs/issues/265
@el.setAttribute('draggable', 'true') value = this.lastDragValue;
@appendTo $('._app') }
return this.resize(value, true);
}
MIN = 260 });
MAX = 600 Cls.initClass();
return Cls;
resize: (value, save) -> })();
value -= app.el.offsetLeft
return unless value > 0
value = Math.min(Math.max(Math.round(value), MIN), MAX)
newSize = "#{value}px"
document.documentElement.style.setProperty('--sidebarWidth', newSize)
app.settings.setSize(value) if save
return
onDragStart: (event) =>
event.dataTransfer.effectAllowed = 'link'
event.dataTransfer.setData('Text', '')
$.on(window, 'dragover', @onDrag)
return
onDrag: (event) =>
value = event.pageX
return unless value > 0
@lastDragValue = value
return if @lastDrag and @lastDrag > Date.now() - 50
@lastDrag = Date.now()
@resize(value, false)
return
onDragEnd: (event) =>
$.off(window, 'dragover', @onDrag)
value = event.pageX or (event.screenX - window.screenX)
if @lastDragValue and not (@lastDragValue - 5 < value < @lastDragValue + 5) # https://github.com/freeCodeCamp/devdocs/issues/265
value = @lastDragValue
@resize(value, true)
return

@ -1,83 +1,127 @@
class app.views.Settings extends app.View /*
SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden' * decaffeinate suggestions:
* DS002: Fix invalid constructor
@el: '._settings' * DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
@elements: * DS205: Consider reworking code to avoid use of IIFEs
sidebar: '._sidebar' * DS206: Consider reworking classes to avoid initClass
saveBtn: 'button[type="submit"]' * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
(function() {
let SIDEBAR_HIDDEN_LAYOUT = undefined;
const Cls = (app.views.Settings = class Settings extends app.View {
constructor(...args) {
this.onChange = this.onChange.bind(this);
this.onEnter = this.onEnter.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.onImport = this.onImport.bind(this);
this.onClick = this.onClick.bind(this);
super(...args);
}
static initClass() {
SIDEBAR_HIDDEN_LAYOUT = '_sidebar-hidden';
this.el = '._settings';
this.elements = {
sidebar: '._sidebar',
saveBtn: 'button[type="submit"]',
backBtn: 'button[data-back]' backBtn: 'button[data-back]'
};
@events: this.events = {
import: 'onImport' import: 'onImport',
change: 'onChange' change: 'onChange',
submit: 'onSubmit' submit: 'onSubmit',
click: 'onClick' click: 'onClick'
};
@shortcuts:
enter: 'onEnter' this.shortcuts =
{enter: 'onEnter'};
init: -> }
@addSubview @docPicker = new app.views.DocPicker
return init() {
this.addSubview(this.docPicker = new app.views.DocPicker);
activate: -> }
if super
@render() activate() {
document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT) if (super.activate(...arguments)) {
return this.render();
document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT);
deactivate: -> }
if super }
@resetClass()
@docPicker.detach() deactivate() {
document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT) if (super.deactivate(...arguments)) {
return this.resetClass();
this.docPicker.detach();
render: -> if (app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)) { document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT); }
@docPicker.appendTo @sidebar }
@refreshElements() }
@addClass '_in'
return render() {
this.docPicker.appendTo(this.sidebar);
save: (options = {}) -> this.refreshElements();
unless @saving this.addClass('_in');
@saving = true }
if options.import save(options) {
docs = app.settings.getDocs() if (options == null) { options = {}; }
else if (!this.saving) {
docs = @docPicker.getSelectedDocs() let docs;
app.settings.setDocs(docs) this.saving = true;
@saveBtn.textContent = 'Saving\u2026' if (options.import) {
disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1) docs = app.settings.getDocs();
disabledDocs.uninstall -> } else {
app.db.migrate() docs = this.docPicker.getSelectedDocs();
app.reload() app.settings.setDocs(docs);
return }
onChange: => this.saveBtn.textContent = 'Saving\u2026';
@addClass('_dirty') const disabledDocs = new app.collections.Docs((() => {
return const result = [];
for (var doc of Array.from(app.docs.all())) { if (docs.indexOf(doc.slug) === -1) {
onEnter: => result.push(doc);
@save() }
return }
return result;
onSubmit: (event) => })());
event.preventDefault() disabledDocs.uninstall(function() {
@save() app.db.migrate();
return return app.reload();
});
onImport: => }
@addClass('_dirty') }
@save(import: true)
return onChange() {
this.addClass('_dirty');
onClick: (event) => }
return if event.which isnt 1
if event.target is @backBtn onEnter() {
$.stopEvent(event) this.save();
app.router.show '/' }
return
onSubmit(event) {
event.preventDefault();
this.save();
}
onImport() {
this.addClass('_dirty');
this.save({import: true});
}
onClick(event) {
if (event.which !== 1) { return; }
if (event.target === this.backBtn) {
$.stopEvent(event);
app.router.show('/');
}
}
});
Cls.initClass();
return Cls;
})();

@ -1,124 +1,177 @@
class app.views.ListFocus extends app.View /*
@activeClass: 'focus' * decaffeinate suggestions:
* DS002: Fix invalid constructor
@events: * DS102: Remove unnecessary code created because of implicit returns
click: 'onClick' * DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
@shortcuts: * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
up: 'onUp' */
down: 'onDown' const Cls = (app.views.ListFocus = class ListFocus extends app.View {
left: 'onLeft' static initClass() {
enter: 'onEnter' this.activeClass = 'focus';
superEnter: 'onSuperEnter'
this.events =
{click: 'onClick'};
this.shortcuts = {
up: 'onUp',
down: 'onDown',
left: 'onLeft',
enter: 'onEnter',
superEnter: 'onSuperEnter',
escape: 'blur' escape: 'blur'
};
constructor: (@el) -> }
super
@focusOnNextFrame = $.framify(@focus, @) constructor(el) {
this.blur = this.blur.bind(this);
focus: (el, options = {}) -> this.onDown = this.onDown.bind(this);
if el and not el.classList.contains @constructor.activeClass this.onUp = this.onUp.bind(this);
@blur() this.onLeft = this.onLeft.bind(this);
el.classList.add @constructor.activeClass this.onEnter = this.onEnter.bind(this);
$.trigger el, 'focus' unless options.silent is true this.onSuperEnter = this.onSuperEnter.bind(this);
return this.onClick = this.onClick.bind(this);
this.el = el;
blur: => super(...arguments);
if cursor = @getCursor() this.focusOnNextFrame = $.framify(this.focus, this);
cursor.classList.remove @constructor.activeClass }
$.trigger cursor, 'blur'
return focus(el, options) {
if (options == null) { options = {}; }
getCursor: -> if (el && !el.classList.contains(this.constructor.activeClass)) {
@findByClass(@constructor.activeClass) or @findByClass(app.views.ListSelect.activeClass) this.blur();
el.classList.add(this.constructor.activeClass);
findNext: (cursor) -> if (options.silent !== true) { $.trigger(el, 'focus'); }
if next = cursor.nextSibling }
if next.tagName is 'A' }
next
else if next.tagName is 'SPAN' # pagination link blur() {
$.click(next) let cursor;
@findNext cursor if (cursor = this.getCursor()) {
else if next.tagName is 'DIV' # sub-list cursor.classList.remove(this.constructor.activeClass);
if cursor.className.indexOf(' open') >= 0 $.trigger(cursor, 'blur');
@findFirst(next) or @findNext(next) }
else }
@findNext(next)
else if next.tagName is 'H6' # title getCursor() {
@findNext(next) return this.findByClass(this.constructor.activeClass) || this.findByClass(app.views.ListSelect.activeClass);
else if cursor.parentNode isnt @el }
@findNext cursor.parentNode
findNext(cursor) {
findFirst: (cursor) -> let next;
return unless first = cursor.firstChild if (next = cursor.nextSibling) {
if (next.tagName === 'A') {
if first.tagName is 'A' return next;
first } else if (next.tagName === 'SPAN') { // pagination link
else if first.tagName is 'SPAN' # pagination link $.click(next);
$.click(first) return this.findNext(cursor);
@findFirst cursor } else if (next.tagName === 'DIV') { // sub-list
if (cursor.className.indexOf(' open') >= 0) {
findPrev: (cursor) -> return this.findFirst(next) || this.findNext(next);
if prev = cursor.previousSibling } else {
if prev.tagName is 'A' return this.findNext(next);
prev }
else if prev.tagName is 'SPAN' # pagination link } else if (next.tagName === 'H6') { // title
$.click(prev) return this.findNext(next);
@findPrev cursor }
else if prev.tagName is 'DIV' # sub-list } else if (cursor.parentNode !== this.el) {
if prev.previousSibling.className.indexOf('open') >= 0 return this.findNext(cursor.parentNode);
@findLast(prev) or @findPrev(prev) }
else }
@findPrev(prev)
else if prev.tagName is 'H6' # title findFirst(cursor) {
@findPrev(prev) let first;
else if cursor.parentNode isnt @el if (!(first = cursor.firstChild)) { return; }
@findPrev cursor.parentNode
if (first.tagName === 'A') {
findLast: (cursor) -> return first;
return unless last = cursor.lastChild } else if (first.tagName === 'SPAN') { // pagination link
$.click(first);
if last.tagName is 'A' return this.findFirst(cursor);
last }
else if last.tagName is 'SPAN' or last.tagName is 'H6' # pagination link or title }
@findPrev last
else if last.tagName is 'DIV' # sub-list findPrev(cursor) {
@findLast last let prev;
if (prev = cursor.previousSibling) {
onDown: => if (prev.tagName === 'A') {
if cursor = @getCursor() return prev;
@focusOnNextFrame @findNext(cursor) } else if (prev.tagName === 'SPAN') { // pagination link
else $.click(prev);
@focusOnNextFrame @findByTag('a') return this.findPrev(cursor);
return } else if (prev.tagName === 'DIV') { // sub-list
if (prev.previousSibling.className.indexOf('open') >= 0) {
onUp: => return this.findLast(prev) || this.findPrev(prev);
if cursor = @getCursor() } else {
@focusOnNextFrame @findPrev(cursor) return this.findPrev(prev);
else }
@focusOnNextFrame @findLastByTag('a') } else if (prev.tagName === 'H6') { // title
return return this.findPrev(prev);
}
onLeft: => } else if (cursor.parentNode !== this.el) {
cursor = @getCursor() return this.findPrev(cursor.parentNode);
if cursor and not cursor.classList.contains(app.views.ListFold.activeClass) and cursor.parentNode isnt @el }
prev = cursor.parentNode.previousSibling }
@focusOnNextFrame cursor.parentNode.previousSibling if prev and prev.classList.contains(app.views.ListFold.targetClass)
return findLast(cursor) {
let last;
onEnter: => if (!(last = cursor.lastChild)) { return; }
if cursor = @getCursor()
$.click(cursor) if (last.tagName === 'A') {
return return last;
} else if ((last.tagName === 'SPAN') || (last.tagName === 'H6')) { // pagination link or title
onSuperEnter: => return this.findPrev(last);
if cursor = @getCursor() } else if (last.tagName === 'DIV') { // sub-list
$.popup(cursor) return this.findLast(last);
return }
}
onClick: (event) =>
return if event.which isnt 1 or event.metaKey or event.ctrlKey onDown() {
target = $.eventTarget(event) let cursor;
if target.tagName is 'A' if ((cursor = this.getCursor())) {
@focus target, silent: true this.focusOnNextFrame(this.findNext(cursor));
return } else {
this.focusOnNextFrame(this.findByTag('a'));
}
}
onUp() {
let cursor;
if ((cursor = this.getCursor())) {
this.focusOnNextFrame(this.findPrev(cursor));
} else {
this.focusOnNextFrame(this.findLastByTag('a'));
}
}
onLeft() {
const cursor = this.getCursor();
if (cursor && !cursor.classList.contains(app.views.ListFold.activeClass) && (cursor.parentNode !== this.el)) {
const prev = cursor.parentNode.previousSibling;
if (prev && prev.classList.contains(app.views.ListFold.targetClass)) { this.focusOnNextFrame(cursor.parentNode.previousSibling); }
}
}
onEnter() {
let cursor;
if (cursor = this.getCursor()) {
$.click(cursor);
}
}
onSuperEnter() {
let cursor;
if (cursor = this.getCursor()) {
$.popup(cursor);
}
}
onClick(event) {
if ((event.which !== 1) || event.metaKey || event.ctrlKey) { return; }
const target = $.eventTarget(event);
if (target.tagName === 'A') {
this.focus(target, {silent: true});
}
}
});
Cls.initClass();

@ -1,71 +1,95 @@
class app.views.ListFold extends app.View /*
@targetClass: '_list-dir' * decaffeinate suggestions:
@handleClass: '_list-arrow' * DS002: Fix invalid constructor
@activeClass: 'open' * DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.ListFold = class ListFold extends app.View {
static initClass() {
this.targetClass = '_list-dir';
this.handleClass = '_list-arrow';
this.activeClass = 'open';
@events: this.events =
click: 'onClick' {click: 'onClick'};
@shortcuts: this.shortcuts = {
left: 'onLeft' left: 'onLeft',
right: 'onRight' right: 'onRight'
};
}
constructor: (@el) -> super constructor(el) { this.onLeft = this.onLeft.bind(this); this.onRight = this.onRight.bind(this); this.onClick = this.onClick.bind(this); this.el = el; super(...arguments); }
open: (el) -> open(el) {
if el and not el.classList.contains @constructor.activeClass if (el && !el.classList.contains(this.constructor.activeClass)) {
el.classList.add @constructor.activeClass el.classList.add(this.constructor.activeClass);
$.trigger el, 'open' $.trigger(el, 'open');
return }
}
close: (el) -> close(el) {
if el and el.classList.contains @constructor.activeClass if (el && el.classList.contains(this.constructor.activeClass)) {
el.classList.remove @constructor.activeClass el.classList.remove(this.constructor.activeClass);
$.trigger el, 'close' $.trigger(el, 'close');
return }
}
toggle: (el) -> toggle(el) {
if el.classList.contains @constructor.activeClass if (el.classList.contains(this.constructor.activeClass)) {
@close el this.close(el);
else } else {
@open el this.open(el);
return }
}
reset: -> reset() {
while el = @findByClass @constructor.activeClass let el;
@close el while ((el = this.findByClass(this.constructor.activeClass))) {
return this.close(el);
}
}
getCursor: -> getCursor() {
@findByClass(app.views.ListFocus.activeClass) or @findByClass(app.views.ListSelect.activeClass) return this.findByClass(app.views.ListFocus.activeClass) || this.findByClass(app.views.ListSelect.activeClass);
}
onLeft: => onLeft() {
cursor = @getCursor() const cursor = this.getCursor();
if cursor?.classList.contains @constructor.activeClass if (cursor != null ? cursor.classList.contains(this.constructor.activeClass) : undefined) {
@close cursor this.close(cursor);
return }
}
onRight: => onRight() {
cursor = @getCursor() const cursor = this.getCursor();
if cursor?.classList.contains @constructor.targetClass if (cursor != null ? cursor.classList.contains(this.constructor.targetClass) : undefined) {
@open cursor this.open(cursor);
return }
}
onClick: (event) => onClick(event) {
return if event.which isnt 1 or event.metaKey or event.ctrlKey if ((event.which !== 1) || event.metaKey || event.ctrlKey) { return; }
return unless event.pageY # ignore fabricated clicks if (!event.pageY) { return; } // ignore fabricated clicks
el = $.eventTarget(event) let el = $.eventTarget(event);
el = el.parentNode if el.parentNode.tagName.toUpperCase() is 'SVG' if (el.parentNode.tagName.toUpperCase() === 'SVG') { el = el.parentNode; }
if el.classList.contains @constructor.handleClass if (el.classList.contains(this.constructor.handleClass)) {
$.stopEvent(event) $.stopEvent(event);
@toggle el.parentNode this.toggle(el.parentNode);
else if el.classList.contains @constructor.targetClass } else if (el.classList.contains(this.constructor.targetClass)) {
if el.hasAttribute('href') if (el.hasAttribute('href')) {
if el.classList.contains(@constructor.activeClass) if (el.classList.contains(this.constructor.activeClass)) {
@close(el) if el.classList.contains(app.views.ListSelect.activeClass) if (el.classList.contains(app.views.ListSelect.activeClass)) { this.close(el); }
else } else {
@open(el) this.open(el);
else }
@toggle(el) } else {
return this.toggle(el);
}
}
}
});
Cls.initClass();

@ -1,43 +1,65 @@
class app.views.ListSelect extends app.View /*
@activeClass: 'active' * decaffeinate suggestions:
* DS002: Fix invalid constructor
@events: * DS102: Remove unnecessary code created because of implicit returns
click: 'onClick' * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
* DS206: Consider reworking classes to avoid initClass
constructor: (@el) -> super * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
deactivate: -> const Cls = (app.views.ListSelect = class ListSelect extends app.View {
@deselect() if super static initClass() {
return this.activeClass = 'active';
select: (el) -> this.events =
@deselect() {click: 'onClick'};
if el }
el.classList.add @constructor.activeClass
$.trigger el, 'select' constructor(el) { this.onClick = this.onClick.bind(this); this.el = el; super(...arguments); }
return
deactivate() {
deselect: -> if (super.deactivate(...arguments)) { this.deselect(); }
if selection = @getSelection() }
selection.classList.remove @constructor.activeClass
$.trigger selection, 'deselect' select(el) {
return this.deselect();
if (el) {
selectByHref: (href) -> el.classList.add(this.constructor.activeClass);
unless @getSelection()?.getAttribute('href') is href $.trigger(el, 'select');
@select @find("a[href='#{href}']") }
return }
selectCurrent: -> deselect() {
@selectByHref location.pathname + location.hash let selection;
return if (selection = this.getSelection()) {
selection.classList.remove(this.constructor.activeClass);
getSelection: -> $.trigger(selection, 'deselect');
@findByClass @constructor.activeClass }
}
onClick: (event) =>
return if event.which isnt 1 or event.metaKey or event.ctrlKey selectByHref(href) {
target = $.eventTarget(event) if (__guard__(this.getSelection(), x => x.getAttribute('href')) !== href) {
if target.tagName is 'A' this.select(this.find(`a[href='${href}']`));
@select target }
return }
selectCurrent() {
this.selectByHref(location.pathname + location.hash);
}
getSelection() {
return this.findByClass(this.constructor.activeClass);
}
onClick(event) {
if ((event.which !== 1) || event.metaKey || event.ctrlKey) { return; }
const target = $.eventTarget(event);
if (target.tagName === 'A') {
this.select(target);
}
}
});
Cls.initClass();
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,90 +1,121 @@
class app.views.PaginatedList extends app.View /*
PER_PAGE = app.config.max_results * decaffeinate suggestions:
* DS002: Fix invalid constructor
constructor: (@data) -> * DS102: Remove unnecessary code created because of implicit returns
(@constructor.events or= {}).click ?= 'onClick' * DS104: Avoid inline assignments
super * DS202: Simplify dynamic range loops
* DS206: Consider reworking classes to avoid initClass
renderPaginated: -> * DS207: Consider shorter variations of null checks
@page = 0 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
if @totalPages() > 1 (function() {
@paginateNext() let PER_PAGE = undefined;
else const Cls = (app.views.PaginatedList = class PaginatedList extends app.View {
@html @renderAll() static initClass() {
return PER_PAGE = app.config.max_results;
}
# render: (dataSlice) -> implemented by subclass
constructor(data) {
renderAll: -> let base;
@render @data this.onClick = this.onClick.bind(this);
this.data = data;
renderPage: (page) -> if (((base = this.constructor.events || (this.constructor.events = {}))).click == null) { base.click = 'onClick'; }
@render @data[((page - 1) * PER_PAGE)...(page * PER_PAGE)] super(...arguments);
}
renderPageLink: (count) ->
@tmpl 'sidebarPageLink', count renderPaginated() {
this.page = 0;
renderPrevLink: (page) ->
@renderPageLink (page - 1) * PER_PAGE if (this.totalPages() > 1) {
this.paginateNext();
renderNextLink: (page) -> } else {
@renderPageLink @data.length - page * PER_PAGE this.html(this.renderAll());
}
totalPages: -> }
Math.ceil @data.length / PER_PAGE
// render: (dataSlice) -> implemented by subclass
paginate: (link) ->
$.lockScroll link.nextSibling or link.previousSibling, => renderAll() {
$.batchUpdate @el, => return this.render(this.data);
if link.nextSibling then @paginatePrev link else @paginateNext link }
return
return renderPage(page) {
return return this.render(this.data.slice(((page - 1) * PER_PAGE), (page * PER_PAGE)));
}
paginateNext: ->
@remove @el.lastChild if @el.lastChild # remove link renderPageLink(count) {
@hideTopPage() if @page >= 2 # keep previous page into view return this.tmpl('sidebarPageLink', count);
@page++ }
@append @renderPage(@page)
@append @renderNextLink(@page) if @page < @totalPages() renderPrevLink(page) {
return return this.renderPageLink((page - 1) * PER_PAGE);
}
paginatePrev: ->
@remove @el.firstChild # remove link renderNextLink(page) {
@hideBottomPage() return this.renderPageLink(this.data.length - (page * PER_PAGE));
@page-- }
@prepend @renderPage(@page - 1) # previous page is offset by one
@prepend @renderPrevLink(@page - 1) if @page >= 3 totalPages() {
return return Math.ceil(this.data.length / PER_PAGE);
}
paginateTo: (object) ->
index = @data.indexOf(object) paginate(link) {
if index >= PER_PAGE $.lockScroll(link.nextSibling || link.previousSibling, () => {
@paginateNext() for [0...(index // PER_PAGE)] $.batchUpdate(this.el, () => {
return if (link.nextSibling) { this.paginatePrev(link); } else { this.paginateNext(link); }
});
hideTopPage: -> });
n = if @page <= 2 }
paginateNext() {
if (this.el.lastChild) { this.remove(this.el.lastChild); } // remove link
if (this.page >= 2) { this.hideTopPage(); } // keep previous page into view
this.page++;
this.append(this.renderPage(this.page));
if (this.page < this.totalPages()) { this.append(this.renderNextLink(this.page)); }
}
paginatePrev() {
this.remove(this.el.firstChild); // remove link
this.hideBottomPage();
this.page--;
this.prepend(this.renderPage(this.page - 1)); // previous page is offset by one
if (this.page >= 3) { this.prepend(this.renderPrevLink(this.page - 1)); }
}
paginateTo(object) {
const index = this.data.indexOf(object);
if (index >= PER_PAGE) {
for (let i = 0, end = Math.floor(index / PER_PAGE), asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { this.paginateNext(); }
}
}
hideTopPage() {
const n = this.page <= 2 ?
PER_PAGE PER_PAGE
else :
PER_PAGE + 1 # remove link PER_PAGE + 1; // remove link
@remove @el.firstChild for [0...n] for (let i = 0, end = n, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { this.remove(this.el.firstChild); }
@prepend @renderPrevLink(@page) this.prepend(this.renderPrevLink(this.page));
return }
hideBottomPage: -> hideBottomPage() {
n = if @page is @totalPages() const n = this.page === this.totalPages() ?
@data.length % PER_PAGE or PER_PAGE (this.data.length % PER_PAGE) || PER_PAGE
else :
PER_PAGE + 1 # remove link PER_PAGE + 1; // remove link
@remove @el.lastChild for [0...n] for (let i = 0, end = n, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { this.remove(this.el.lastChild); }
@append @renderNextLink(@page - 1) this.append(this.renderNextLink(this.page - 1));
return }
onClick: (event) => onClick(event) {
target = $.eventTarget(event) const target = $.eventTarget(event);
if target.tagName is 'SPAN' # link if (target.tagName === 'SPAN') { // link
$.stopEvent(event) $.stopEvent(event);
@paginate target this.paginate(target);
return }
}
});
Cls.initClass();
return Cls;
})();

@ -1,34 +1,55 @@
#= require views/misc/notif /*
* decaffeinate suggestions:
class app.views.News extends app.views.Notif * DS101: Remove unnecessary use of Array.from
@className += ' _notif-news' * DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
@defautOptions: * DS206: Consider reworking classes to avoid initClass
autoHide: 30000 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
init: -> //= require views/misc/notif
@unreadNews = @getUnreadNews()
@show() if @unreadNews.length const Cls = (app.views.News = class News extends app.views.Notif {
@markAllAsRead() static initClass() {
return this.className += ' _notif-news';
render: -> this.defautOptions =
@html app.templates.notifNews(@unreadNews) {autoHide: 30000};
return }
getUnreadNews: -> init() {
return [] unless time = @getLastReadTime() this.unreadNews = this.getUnreadNews();
if (this.unreadNews.length) { this.show(); }
for news in app.news this.markAllAsRead();
break if new Date(news[0]).getTime() <= time }
news
render() {
getLastNewsTime: -> this.html(app.templates.notifNews(this.unreadNews));
new Date(app.news[0][0]).getTime() }
getLastReadTime: -> getUnreadNews() {
app.settings.get 'news' let time;
if (!(time = this.getLastReadTime())) { return []; }
markAllAsRead: ->
app.settings.set 'news', @getLastNewsTime() return (() => {
return const result = [];
for (var news of Array.from(app.news)) {
if (new Date(news[0]).getTime() <= time) { break; }
result.push(news);
}
return result;
})();
}
getLastNewsTime() {
return new Date(app.news[0][0]).getTime();
}
getLastReadTime() {
return app.settings.get('news');
}
markAllAsRead() {
app.settings.set('news', this.getLastNewsTime());
}
});
Cls.initClass();

@ -1,27 +1,38 @@
class app.views.Notice extends app.View /*
@className: '_notice' * decaffeinate suggestions:
@attributes: * DS002: Fix invalid constructor
role: 'alert' * DS101: Remove unnecessary use of Array.from
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.Notice = class Notice extends app.View {
static initClass() {
this.className = '_notice';
this.attributes =
{role: 'alert'};
}
constructor: (@type, @args...) -> super constructor(type, ...rest) { this.type = type; [...this.args] = Array.from(rest); super(...arguments); }
init: -> init() {
@activate() this.activate();
return }
activate: -> activate() {
@show() if super if (super.activate(...arguments)) { this.show(); }
return }
deactivate: -> deactivate() {
@hide() if super if (super.deactivate(...arguments)) { this.hide(); }
return }
show: -> show() {
@html @tmpl("#{@type}Notice", @args...) this.html(this.tmpl(`${this.type}Notice`, ...Array.from(this.args)));
@prependTo app.el this.prependTo(app.el);
return }
hide: -> hide() {
$.remove @el $.remove(this.el);
return }
});
Cls.initClass();

@ -1,59 +1,78 @@
class app.views.Notif extends app.View /*
@className: '_notif' * decaffeinate suggestions:
@activeClass: '_in' * DS002: Fix invalid constructor
@attributes: * DS206: Consider reworking classes to avoid initClass
role: 'alert' * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
@defautOptions: */
autoHide: 15000 const Cls = (app.views.Notif = class Notif extends app.View {
static initClass() {
@events: this.className = '_notif';
click: 'onClick' this.activeClass = '_in';
this.attributes =
constructor: (@type, @options = {}) -> {role: 'alert'};
@options = $.extend {}, @constructor.defautOptions, @options
super this.defautOptions =
{autoHide: 15000};
init: ->
@show() this.events =
return {click: 'onClick'};
}
show: ->
if @timeout constructor(type, options) {
clearTimeout @timeout this.onClick = this.onClick.bind(this);
@timeout = @delay @hide, @options.autoHide this.type = type;
else if (options == null) { options = {}; }
@render() this.options = options;
@position() this.options = $.extend({}, this.constructor.defautOptions, this.options);
@activate() super(...arguments);
@appendTo document.body }
@el.offsetWidth # force reflow
@addClass @constructor.activeClass init() {
@timeout = @delay @hide, @options.autoHide if @options.autoHide this.show();
return }
hide: -> show() {
clearTimeout @timeout if (this.timeout) {
@timeout = null clearTimeout(this.timeout);
@detach() this.timeout = this.delay(this.hide, this.options.autoHide);
return } else {
this.render();
render: -> this.position();
@html @tmpl("notif#{@type}") this.activate();
return this.appendTo(document.body);
this.el.offsetWidth; // force reflow
position: -> this.addClass(this.constructor.activeClass);
notifications = $$ ".#{app.views.Notif.className}" if (this.options.autoHide) { this.timeout = this.delay(this.hide, this.options.autoHide); }
if notifications.length }
lastNotif = notifications[notifications.length - 1] }
@el.style.top = lastNotif.offsetTop + lastNotif.offsetHeight + 16 + 'px'
return hide() {
clearTimeout(this.timeout);
onClick: (event) => this.timeout = null;
return if event.which isnt 1 this.detach();
target = $.eventTarget(event) }
return if target.hasAttribute('data-behavior')
if target.tagName isnt 'A' or target.classList.contains('_notif-close') render() {
$.stopEvent(event) this.html(this.tmpl(`notif${this.type}`));
@hide() }
return
position() {
const notifications = $$(`.${app.views.Notif.className}`);
if (notifications.length) {
const lastNotif = notifications[notifications.length - 1];
this.el.style.top = lastNotif.offsetTop + lastNotif.offsetHeight + 16 + 'px';
}
}
onClick(event) {
if (event.which !== 1) { return; }
const target = $.eventTarget(event);
if (target.hasAttribute('data-behavior')) { return; }
if ((target.tagName !== 'A') || target.classList.contains('_notif-close')) {
$.stopEvent(event);
this.hide();
}
}
});
Cls.initClass();

@ -1,11 +1,20 @@
#= require views/misc/notif /*
* decaffeinate suggestions:
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
//= require views/misc/notif
class app.views.Tip extends app.views.Notif const Cls = (app.views.Tip = class Tip extends app.views.Notif {
@className: '_notif _notif-tip' static initClass() {
this.className = '_notif _notif-tip';
@defautOptions: this.defautOptions =
autoHide: false {autoHide: false};
}
render: -> render() {
@html @tmpl("tip#{@type}") this.html(this.tmpl(`tip${this.type}`));
return }
});
Cls.initClass();

@ -1,34 +1,56 @@
#= require views/misc/notif /*
* decaffeinate suggestions:
class app.views.Updates extends app.views.Notif * DS101: Remove unnecessary use of Array.from
@className += ' _notif-news' * DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
@defautOptions: * DS206: Consider reworking classes to avoid initClass
autoHide: 30000 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
init: -> //= require views/misc/notif
@lastUpdateTime = @getLastUpdateTime()
@updatedDocs = @getUpdatedDocs() const Cls = (app.views.Updates = class Updates extends app.views.Notif {
@updatedDisabledDocs = @getUpdatedDisabledDocs() static initClass() {
@show() if @updatedDocs.length > 0 or @updatedDisabledDocs.length > 0 this.className += ' _notif-news';
@markAllAsRead()
return this.defautOptions =
{autoHide: 30000};
render: -> }
@html app.templates.notifUpdates(@updatedDocs, @updatedDisabledDocs)
return init() {
this.lastUpdateTime = this.getLastUpdateTime();
getUpdatedDocs: -> this.updatedDocs = this.getUpdatedDocs();
return [] unless @lastUpdateTime this.updatedDisabledDocs = this.getUpdatedDisabledDocs();
doc for doc in app.docs.all() when doc.mtime > @lastUpdateTime if ((this.updatedDocs.length > 0) || (this.updatedDisabledDocs.length > 0)) { this.show(); }
this.markAllAsRead();
getUpdatedDisabledDocs: -> }
return [] unless @lastUpdateTime
doc for doc in app.disabledDocs.all() when doc.mtime > @lastUpdateTime and app.docs.findBy('slug_without_version', doc.slug_without_version) render() {
this.html(app.templates.notifUpdates(this.updatedDocs, this.updatedDisabledDocs));
getLastUpdateTime: -> }
app.settings.get 'version'
getUpdatedDocs() {
markAllAsRead: -> if (!this.lastUpdateTime) { return []; }
app.settings.set 'version', if app.config.env is 'production' then app.config.version else Math.floor(Date.now() / 1000) return Array.from(app.docs.all()).filter((doc) => doc.mtime > this.lastUpdateTime);
return }
getUpdatedDisabledDocs() {
if (!this.lastUpdateTime) { return []; }
return (() => {
const result = [];
for (var doc of Array.from(app.disabledDocs.all())) { if ((doc.mtime > this.lastUpdateTime) && app.docs.findBy('slug_without_version', doc.slug_without_version)) {
result.push(doc);
}
}
return result;
})();
}
getLastUpdateTime() {
return app.settings.get('version');
}
markAllAsRead() {
app.settings.set('version', app.config.env === 'production' ? app.config.version : Math.floor(Date.now() / 1000));
}
});
Cls.initClass();

@ -1,43 +1,61 @@
class app.views.BasePage extends app.View /*
constructor: (@el, @entry) -> super * decaffeinate suggestions:
* DS002: Fix invalid constructor
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
app.views.BasePage = class BasePage extends app.View {
constructor(el, entry) { this.paintCode = this.paintCode.bind(this); this.el = el; this.entry = entry; super(...arguments); }
deactivate: -> deactivate() {
if super if (super.deactivate(...arguments)) {
@highlightNodes = [] return this.highlightNodes = [];
}
}
render: (content, fromCache = false) -> render(content, fromCache) {
@highlightNodes = [] if (fromCache == null) { fromCache = false; }
@previousTiming = null this.highlightNodes = [];
@addClass "_#{@entry.doc.type}" unless @constructor.className this.previousTiming = null;
@html content if (!this.constructor.className) { this.addClass(`_${this.entry.doc.type}`); }
@highlightCode() unless fromCache this.html(content);
@activate() if (!fromCache) { this.highlightCode(); }
@delay @afterRender if @afterRender this.activate();
if @highlightNodes.length > 0 if (this.afterRender) { this.delay(this.afterRender); }
$.requestAnimationFrame => $.requestAnimationFrame(@paintCode) if (this.highlightNodes.length > 0) {
return $.requestAnimationFrame(() => $.requestAnimationFrame(this.paintCode));
}
}
highlightCode: -> highlightCode() {
for el in @findAll('pre[data-language]') for (var el of Array.from(this.findAll('pre[data-language]'))) {
language = el.getAttribute('data-language') var language = el.getAttribute('data-language');
el.classList.add("language-#{language}") el.classList.add(`language-${language}`);
@highlightNodes.push(el) this.highlightNodes.push(el);
return }
}
paintCode: (timing) => paintCode(timing) {
if @previousTiming if (this.previousTiming) {
if Math.round(1000 / (timing - @previousTiming)) > 50 # fps if (Math.round(1000 / (timing - this.previousTiming)) > 50) { // fps
@nodesPerFrame = Math.round(Math.min(@nodesPerFrame * 1.25, 50)) this.nodesPerFrame = Math.round(Math.min(this.nodesPerFrame * 1.25, 50));
else } else {
@nodesPerFrame = Math.round(Math.max(@nodesPerFrame * .8, 10)) this.nodesPerFrame = Math.round(Math.max(this.nodesPerFrame * .8, 10));
else }
@nodesPerFrame = 10 } else {
this.nodesPerFrame = 10;
}
for el in @highlightNodes.splice(0, @nodesPerFrame) for (var el of Array.from(this.highlightNodes.splice(0, this.nodesPerFrame))) {
$.remove(clipEl) if clipEl = el.lastElementChild var clipEl;
Prism.highlightElement(el) if (clipEl = el.lastElementChild) { $.remove(clipEl); }
$.append(el, clipEl) if clipEl Prism.highlightElement(el);
if (clipEl) { $.append(el, clipEl); }
}
$.requestAnimationFrame(@paintCode) if @highlightNodes.length > 0 if (this.highlightNodes.length > 0) { $.requestAnimationFrame(this.paintCode); }
@previousTiming = timing this.previousTiming = timing;
return }
};

@ -1,16 +1,28 @@
class app.views.HiddenPage extends app.View /*
@events: * decaffeinate suggestions:
click: 'onClick' * DS002: Fix invalid constructor
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.HiddenPage = class HiddenPage extends app.View {
static initClass() {
this.events =
{click: 'onClick'};
}
constructor: (@el, @entry) -> super constructor(el, entry) { this.onClick = this.onClick.bind(this); this.el = el; this.entry = entry; super(...arguments); }
init: -> init() {
@addSubview @notice = new app.views.Notice 'disabledDoc' this.addSubview(this.notice = new app.views.Notice('disabledDoc'));
@activate() this.activate();
return }
onClick: (event) => onClick(event) {
if link = $.closestLink(event.target, @el) let link;
$.stopEvent(event) if (link = $.closestLink(event.target, this.el)) {
$.popup(link) $.stopEvent(event);
return $.popup(link);
}
}
});
Cls.initClass();

@ -1,45 +1,65 @@
#= require views/pages/base /*
* decaffeinate suggestions:
class app.views.JqueryPage extends app.views.BasePage * DS002: Fix invalid constructor
@demoClassName: '_jquery-demo' * DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
afterRender: -> * DS206: Consider reworking classes to avoid initClass
# Prevent jQuery Mobile's demo iframes from scrolling the page * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
for iframe in @findAllByTag 'iframe' */
iframe.style.display = 'none' //= require views/pages/base
$.on iframe, 'load', @onIframeLoaded
const Cls = (app.views.JqueryPage = class JqueryPage extends app.views.BasePage {
@runExamples() constructor(...args) {
this.onIframeLoaded = this.onIframeLoaded.bind(this);
onIframeLoaded: (event) => super(...args);
event.target.style.display = '' }
$.off event.target, 'load', @onIframeLoaded
return static initClass() {
this.demoClassName = '_jquery-demo';
runExamples: -> }
for el in @findAllByClass 'entry-example'
try @runExample el catch afterRender() {
return // Prevent jQuery Mobile's demo iframes from scrolling the page
for (var iframe of Array.from(this.findAllByTag('iframe'))) {
runExample: (el) -> iframe.style.display = 'none';
source = el.getElementsByClassName('syntaxhighlighter')[0] $.on(iframe, 'load', this.onIframeLoaded);
return unless source and source.innerHTML.indexOf('!doctype') isnt -1 }
unless iframe = el.getElementsByClassName(@constructor.demoClassName)[0] return this.runExamples();
iframe = document.createElement 'iframe' }
iframe.className = @constructor.demoClassName
iframe.width = '100%' onIframeLoaded(event) {
iframe.height = 200 event.target.style.display = '';
el.appendChild(iframe) $.off(event.target, 'load', this.onIframeLoaded);
}
doc = iframe.contentDocument
doc.write @fixIframeSource(source.textContent) runExamples() {
doc.close() for (var el of Array.from(this.findAllByClass('entry-example'))) {
return try { this.runExample(el); } catch (error) {}
}
fixIframeSource: (source) -> }
source = source.replace '"/resources/', '"https://api.jquery.com/resources/' # attr(), keydown()
source = source.replace '</head>', """ runExample(el) {
let iframe;
const source = el.getElementsByClassName('syntaxhighlighter')[0];
if (!source || (source.innerHTML.indexOf('!doctype') === -1)) { return; }
if (!(iframe = el.getElementsByClassName(this.constructor.demoClassName)[0])) {
iframe = document.createElement('iframe');
iframe.className = this.constructor.demoClassName;
iframe.width = '100%';
iframe.height = 200;
el.appendChild(iframe);
}
const doc = iframe.contentDocument;
doc.write(this.fixIframeSource(source.textContent));
doc.close();
}
fixIframeSource(source) {
source = source.replace('"/resources/', '"https://api.jquery.com/resources/'); // attr(), keydown()
source = source.replace('</head>', `\
<style> <style>
html, body { border: 0; margin: 0; padding: 0; } html, body { border: 0; margin: 0; padding: 0; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; }
@ -52,6 +72,10 @@ class app.views.JqueryPage extends app.views.BasePage
} }
}); });
</script> </script>
</head> </head>\
""" `
source.replace /<script>/gi, '<script nonce="devdocs">' );
return source.replace(/<script>/gi, '<script nonce="devdocs">');
}
});
Cls.initClass();

@ -1,15 +1,26 @@
#= require views/pages/base /*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
//= require views/pages/base
class app.views.RdocPage extends app.views.BasePage const Cls = (app.views.RdocPage = class RdocPage extends app.views.BasePage {
@events: static initClass() {
click: 'onClick' this.events =
{click: 'onClick'};
}
onClick: (event) -> onClick(event) {
return unless event.target.classList.contains 'method-click-advice' if (!event.target.classList.contains('method-click-advice')) { return; }
$.stopEvent(event) $.stopEvent(event);
source = $ '.method-source-code', event.target.closest('.method-detail') const source = $('.method-source-code', event.target.closest('.method-detail'));
isShown = source.style.display is 'block' const isShown = source.style.display === 'block';
source.style.display = if isShown then 'none' else 'block' source.style.display = isShown ? 'none' : 'block';
event.target.textContent = if isShown then 'Show source' else 'Hide source' return event.target.textContent = isShown ? 'Show source' : 'Hide source';
}
});
Cls.initClass();

@ -1,17 +1,34 @@
#= require views/pages/base /*
* decaffeinate suggestions:
* DS002: Fix invalid constructor
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
//= require views/pages/base
class app.views.SqlitePage extends app.views.BasePage const Cls = (app.views.SqlitePage = class SqlitePage extends app.views.BasePage {
@events: constructor(...args) {
click: 'onClick' this.onClick = this.onClick.bind(this);
super(...args);
}
onClick: (event) => static initClass() {
return unless id = event.target.getAttribute('data-toggle') this.events =
return unless el = @find("##{id}") {click: 'onClick'};
$.stopEvent(event) }
if el.style.display == 'none'
el.style.display = 'block' onClick(event) {
event.target.textContent = 'hide' let el, id;
else if (!(id = event.target.getAttribute('data-toggle'))) { return; }
el.style.display = 'none' if (!(el = this.find(`#${id}`))) { return; }
event.target.textContent = 'show' $.stopEvent(event);
return if (el.style.display === 'none') {
el.style.display = 'block';
event.target.textContent = 'hide';
} else {
el.style.display = 'none';
event.target.textContent = 'show';
}
}
});
Cls.initClass();

@ -1,14 +1,23 @@
#= require views/pages/base /*
* decaffeinate suggestions:
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
//= require views/pages/base
class app.views.SupportTablesPage extends app.views.BasePage const Cls = (app.views.SupportTablesPage = class SupportTablesPage extends app.views.BasePage {
@events: static initClass() {
click: 'onClick' this.events =
{click: 'onClick'};
}
onClick: (event) -> onClick(event) {
return unless event.target.classList.contains 'show-all' if (!event.target.classList.contains('show-all')) { return; }
$.stopEvent(event) $.stopEvent(event);
el = event.target let el = event.target;
el = el.parentNode until el.tagName is 'TABLE' while (el.tagName !== 'TABLE') { el = el.parentNode; }
el.classList.add 'show-all' el.classList.add('show-all');
return }
});
Cls.initClass();

@ -1,168 +1,225 @@
class app.views.Search extends app.View /*
SEARCH_PARAM = app.config.search_param * decaffeinate suggestions:
* DS002: Fix invalid constructor
@el: '._search' * DS102: Remove unnecessary code created because of implicit returns
@activeClass: '_search-active' * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
* DS206: Consider reworking classes to avoid initClass
@elements: * DS207: Consider shorter variations of null checks
input: '._search-input' * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
(function() {
let SEARCH_PARAM = undefined;
let HASH_RGX = undefined;
const Cls = (app.views.Search = class Search extends app.View {
constructor(...args) {
this.focus = this.focus.bind(this);
this.autoFocus = this.autoFocus.bind(this);
this.onWindowFocus = this.onWindowFocus.bind(this);
this.onReady = this.onReady.bind(this);
this.onInput = this.onInput.bind(this);
this.searchUrl = this.searchUrl.bind(this);
this.google = this.google.bind(this);
this.stackoverflow = this.stackoverflow.bind(this);
this.duckduckgo = this.duckduckgo.bind(this);
this.onResults = this.onResults.bind(this);
this.onEnd = this.onEnd.bind(this);
this.onClick = this.onClick.bind(this);
this.onScopeChange = this.onScopeChange.bind(this);
this.afterRoute = this.afterRoute.bind(this);
super(...args);
}
static initClass() {
SEARCH_PARAM = app.config.search_param;
this.el = '._search';
this.activeClass = '_search-active';
this.elements = {
input: '._search-input',
resetLink: '._search-clear' resetLink: '._search-clear'
};
@events: this.events = {
input: 'onInput' input: 'onInput',
click: 'onClick' click: 'onClick',
submit: 'onSubmit' submit: 'onSubmit'
};
@shortcuts: this.shortcuts = {
typing: 'focus' typing: 'focus',
altG: 'google' altG: 'google',
altS: 'stackoverflow' altS: 'stackoverflow',
altD: 'duckduckgo' altD: 'duckduckgo'
};
@routes:
after: 'afterRoute' this.routes =
{after: 'afterRoute'};
init: ->
@addSubview @scope = new app.views.SearchScope @el HASH_RGX = new RegExp(`^#${SEARCH_PARAM}=(.*)`);
}
@searcher = new app.Searcher
@searcher init() {
.on 'results', @onResults this.addSubview(this.scope = new app.views.SearchScope(this.el));
.on 'end', @onEnd
this.searcher = new app.Searcher;
@scope this.searcher
.on 'change', @onScopeChange .on('results', this.onResults)
.on('end', this.onEnd);
app.on 'ready', @onReady
$.on window, 'hashchange', @searchUrl this.scope
$.on window, 'focus', @onWindowFocus .on('change', this.onScopeChange);
return
app.on('ready', this.onReady);
focus: => $.on(window, 'hashchange', this.searchUrl);
return if document.activeElement is @input $.on(window, 'focus', this.onWindowFocus);
return if app.settings.get('noAutofocus') }
@input.focus()
return focus() {
if (document.activeElement === this.input) { return; }
autoFocus: => if (app.settings.get('noAutofocus')) { return; }
return if app.isMobile() or $.isAndroid() or $.isIOS() this.input.focus();
return if document.activeElement?.tagName is 'INPUT' }
return if app.settings.get('noAutofocus')
@input.focus() autoFocus() {
return if (app.isMobile() || $.isAndroid() || $.isIOS()) { return; }
if ((document.activeElement != null ? document.activeElement.tagName : undefined) === 'INPUT') { return; }
onWindowFocus: (event) => if (app.settings.get('noAutofocus')) { return; }
@autoFocus() if event.target is window this.input.focus();
}
getScopeDoc: ->
@scope.getScope() if @scope.isActive() onWindowFocus(event) {
if (event.target === window) { return this.autoFocus(); }
reset: (force) -> }
@scope.reset() if force or not @input.value
@el.reset() getScopeDoc() {
@onInput() if (this.scope.isActive()) { return this.scope.getScope(); }
@autoFocus() }
return
reset(force) {
onReady: => if (force || !this.input.value) { this.scope.reset(); }
@value = '' this.el.reset();
@delay @onInput this.onInput();
return this.autoFocus();
}
onInput: =>
return if not @value? or # ignore events pre-"ready" onReady() {
@value is @input.value this.value = '';
@value = @input.value this.delay(this.onInput);
}
if @value.length
@search() onInput() {
else if ((this.value == null) || // ignore events pre-"ready"
@clear() (this.value === this.input.value)) { return; }
return this.value = this.input.value;
search: (url = false) -> if (this.value.length) {
@addClass @constructor.activeClass this.search();
@trigger 'searching' } else {
this.clear();
@hasResults = null }
@flags = urlSearch: url, initialResults: true }
@searcher.find @scope.getScope().entries.all(), 'text', @value
return search(url) {
if (url == null) { url = false; }
searchUrl: => this.addClass(this.constructor.activeClass);
if location.pathname is '/' this.trigger('searching');
@scope.searchUrl()
else if not app.router.isIndex() this.hasResults = null;
return this.flags = {urlSearch: url, initialResults: true};
this.searcher.find(this.scope.getScope().entries.all(), 'text', this.value);
return unless value = @extractHashValue() }
@input.value = @value = value
@input.setSelectionRange(value.length, value.length) searchUrl() {
@search true let value;
true if (location.pathname === '/') {
this.scope.searchUrl();
clear: -> } else if (!app.router.isIndex()) {
@removeClass @constructor.activeClass return;
@trigger 'clear' }
return
if (!(value = this.extractHashValue())) { return; }
externalSearch: (url) -> this.input.value = (this.value = value);
if value = @value this.input.setSelectionRange(value.length, value.length);
value = "#{@scope.name()} #{value}" if @scope.name() this.search(true);
$.popup "#{url}#{encodeURIComponent value}" return true;
@reset() }
return
clear() {
google: => this.removeClass(this.constructor.activeClass);
@externalSearch "https://www.google.com/search?q=" this.trigger('clear');
return }
stackoverflow: => externalSearch(url) {
@externalSearch "https://stackoverflow.com/search?q=" let value;
return if (value = this.value) {
if (this.scope.name()) { value = `${this.scope.name()} ${value}`; }
duckduckgo: => $.popup(`${url}${encodeURIComponent(value)}`);
@externalSearch "https://duckduckgo.com/?t=devdocs&q=" this.reset();
return }
}
onResults: (results) =>
@hasResults = true if results.length google() {
@trigger 'results', results, @flags this.externalSearch("https://www.google.com/search?q=");
@flags.initialResults = false }
return
stackoverflow() {
onEnd: => this.externalSearch("https://stackoverflow.com/search?q=");
@trigger 'noresults' unless @hasResults }
return
duckduckgo() {
onClick: (event) => this.externalSearch("https://duckduckgo.com/?t=devdocs&q=");
if event.target is @resetLink }
$.stopEvent(event)
@reset() onResults(results) {
return if (results.length) { this.hasResults = true; }
this.trigger('results', results, this.flags);
onSubmit: (event) -> this.flags.initialResults = false;
$.stopEvent(event) }
return
onEnd() {
onScopeChange: => if (!this.hasResults) { this.trigger('noresults'); }
@value = '' }
@onInput()
return onClick(event) {
if (event.target === this.resetLink) {
afterRoute: (name, context) => $.stopEvent(event);
return if app.shortcuts.eventInProgress?.name is 'escape' this.reset();
@reset(true) if not context.init and app.router.isIndex() }
@delay @searchUrl if context.hash }
$.requestAnimationFrame @autoFocus
return onSubmit(event) {
$.stopEvent(event);
extractHashValue: -> }
if (value = @getHashValue())?
app.router.replaceHash() onScopeChange() {
value this.value = '';
this.onInput();
HASH_RGX = new RegExp "^##{SEARCH_PARAM}=(.*)" }
getHashValue: -> afterRoute(name, context) {
try HASH_RGX.exec($.urlDecode location.hash)?[1] catch if ((app.shortcuts.eventInProgress != null ? app.shortcuts.eventInProgress.name : undefined) === 'escape') { return; }
if (!context.init && app.router.isIndex()) { this.reset(true); }
if (context.hash) { this.delay(this.searchUrl); }
$.requestAnimationFrame(this.autoFocus);
}
extractHashValue() {
let value;
if ((value = this.getHashValue()) != null) {
app.router.replaceHash();
return value;
}
}
getHashValue() {
try { return __guard__(HASH_RGX.exec($.urlDecode(location.hash)), x => x[1]); } catch (error) {}
}
});
Cls.initClass();
return Cls;
})();
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,135 +1,180 @@
class app.views.SearchScope extends app.View /*
SEARCH_PARAM = app.config.search_param * decaffeinate suggestions:
* DS002: Fix invalid constructor
@elements: * DS102: Remove unnecessary code created because of implicit returns
input: '._search-input' * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
(function() {
let SEARCH_PARAM = undefined;
let HASH_RGX = undefined;
const Cls = (app.views.SearchScope = class SearchScope extends app.View {
static initClass() {
SEARCH_PARAM = app.config.search_param;
this.elements = {
input: '._search-input',
tag: '._search-tag' tag: '._search-tag'
};
@events: this.events = {
click: 'onClick' click: 'onClick',
keydown: 'onKeydown' keydown: 'onKeydown',
textInput: 'onTextInput' textInput: 'onTextInput'
};
this.routes =
{after: 'afterRoute'};
@routes: HASH_RGX = new RegExp(`^#${SEARCH_PARAM}=(.+?) .`);
after: 'afterRoute' }
constructor: (@el) -> super constructor(el) { this.onResults = this.onResults.bind(this); this.reset = this.reset.bind(this); this.doScopeSearch = this.doScopeSearch.bind(this); this.onClick = this.onClick.bind(this); this.onKeydown = this.onKeydown.bind(this); this.onTextInput = this.onTextInput.bind(this); this.afterRoute = this.afterRoute.bind(this); this.el = el; super(...arguments); }
init: -> init() {
@placeholder = @input.getAttribute 'placeholder' this.placeholder = this.input.getAttribute('placeholder');
@searcher = new app.SynchronousSearcher this.searcher = new app.SynchronousSearcher({
fuzzy_min_length: 2 fuzzy_min_length: 2,
max_results: 1 max_results: 1
@searcher.on 'results', @onResults });
this.searcher.on('results', this.onResults);
return
}
getScope: ->
@doc or app getScope() {
return this.doc || app;
isActive: -> }
!!@doc
isActive() {
name: -> return !!this.doc;
@doc?.name }
search: (value, searchDisabled = false) -> name() {
return if @doc return (this.doc != null ? this.doc.name : undefined);
@searcher.find app.docs.all(), 'text', value }
@searcher.find app.disabledDocs.all(), 'text', value if not @doc and searchDisabled
return search(value, searchDisabled) {
if (searchDisabled == null) { searchDisabled = false; }
searchUrl: -> if (this.doc) { return; }
if value = @extractHashValue() this.searcher.find(app.docs.all(), 'text', value);
@search value, true if (!this.doc && searchDisabled) { this.searcher.find(app.disabledDocs.all(), 'text', value); }
return }
onResults: (results) => searchUrl() {
return unless doc = results[0] let value;
if app.docs.contains(doc) if (value = this.extractHashValue()) {
@selectDoc(doc) this.search(value, true);
else }
@redirectToDoc(doc) }
return
onResults(results) {
selectDoc: (doc) -> let doc;
previousDoc = @doc if (!(doc = results[0])) { return; }
return if doc is previousDoc if (app.docs.contains(doc)) {
@doc = doc this.selectDoc(doc);
} else {
@tag.textContent = doc.fullName this.redirectToDoc(doc);
@tag.style.display = 'block' }
}
@input.removeAttribute 'placeholder'
@input.value = @input.value[@input.selectionStart..] selectDoc(doc) {
@input.style.paddingLeft = @tag.offsetWidth + 10 + 'px' const previousDoc = this.doc;
if (doc === previousDoc) { return; }
$.trigger @input, 'input' this.doc = doc;
@trigger 'change', @doc, previousDoc
return this.tag.textContent = doc.fullName;
this.tag.style.display = 'block';
redirectToDoc: (doc) ->
hash = location.hash this.input.removeAttribute('placeholder');
app.router.replaceHash('') this.input.value = this.input.value.slice(this.input.selectionStart);
location.assign doc.fullPath() + hash this.input.style.paddingLeft = this.tag.offsetWidth + 10 + 'px';
return
$.trigger(this.input, 'input');
reset: => this.trigger('change', this.doc, previousDoc);
return unless @doc }
previousDoc = @doc
@doc = null redirectToDoc(doc) {
const {
@tag.textContent = '' hash
@tag.style.display = 'none' } = location;
app.router.replaceHash('');
@input.setAttribute 'placeholder', @placeholder location.assign(doc.fullPath() + hash);
@input.style.paddingLeft = '' }
@trigger 'change', null, previousDoc reset() {
return if (!this.doc) { return; }
const previousDoc = this.doc;
doScopeSearch: (event) => this.doc = null;
@search @input.value[0...@input.selectionStart]
$.stopEvent(event) if @doc this.tag.textContent = '';
return this.tag.style.display = 'none';
onClick: (event) => this.input.setAttribute('placeholder', this.placeholder);
if event.target is @tag this.input.style.paddingLeft = '';
@reset()
$.stopEvent(event) this.trigger('change', null, previousDoc);
return }
onKeydown: (event) => doScopeSearch(event) {
if event.which is 8 # backspace this.search(this.input.value.slice(0, this.input.selectionStart));
if @doc and @input.selectionEnd is 0 if (this.doc) { $.stopEvent(event); }
@reset() }
$.stopEvent(event)
else if not @doc and @input.value and not $.isChromeForAndroid() onClick(event) {
return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey if (event.target === this.tag) {
if event.which is 9 or # tab this.reset();
(event.which is 32 and app.isMobile()) # space $.stopEvent(event);
@doScopeSearch(event) }
return }
onTextInput: (event) => onKeydown(event) {
return unless $.isChromeForAndroid() if (event.which === 8) { // backspace
if not @doc and @input.value and event.data == ' ' if (this.doc && (this.input.selectionEnd === 0)) {
@doScopeSearch(event) this.reset();
return $.stopEvent(event);
}
extractHashValue: -> } else if (!this.doc && this.input.value && !$.isChromeForAndroid()) {
if value = @getHashValue() if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) { return; }
newHash = $.urlDecode(location.hash).replace "##{SEARCH_PARAM}=#{value} ", "##{SEARCH_PARAM}=" if ((event.which === 9) || // tab
app.router.replaceHash(newHash) ((event.which === 32) && app.isMobile())) { // space
value this.doScopeSearch(event);
}
HASH_RGX = new RegExp "^##{SEARCH_PARAM}=(.+?) ." }
}
getHashValue: ->
try HASH_RGX.exec($.urlDecode location.hash)?[1] catch onTextInput(event) {
if (!$.isChromeForAndroid()) { return; }
afterRoute: (name, context) => if (!this.doc && this.input.value && (event.data === ' ')) {
if !app.isSingleDoc() and context.init and context.doc this.doScopeSearch(event);
@selectDoc(context.doc) }
return }
extractHashValue() {
let value;
if (value = this.getHashValue()) {
const newHash = $.urlDecode(location.hash).replace(`#${SEARCH_PARAM}=${value} `, `#${SEARCH_PARAM}=`);
app.router.replaceHash(newHash);
return value;
}
}
getHashValue() {
try { return __guard__(HASH_RGX.exec($.urlDecode(location.hash)), x => x[1]); } catch (error) {}
}
afterRoute(name, context) {
if (!app.isSingleDoc() && context.init && context.doc) {
this.selectDoc(context.doc);
}
}
});
Cls.initClass();
return Cls;
})();
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,187 +1,234 @@
class app.views.DocList extends app.View /*
@className: '_list' * decaffeinate suggestions:
@attributes: * DS002: Fix invalid constructor
role: 'navigation' * DS101: Remove unnecessary use of Array.from
* DS206: Consider reworking classes to avoid initClass
@events: * DS207: Consider shorter variations of null checks
open: 'onOpen' * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
close: 'onClose' */
const Cls = (app.views.DocList = class DocList extends app.View {
constructor(...args) {
this.render = this.render.bind(this);
this.onOpen = this.onOpen.bind(this);
this.onClose = this.onClose.bind(this);
this.onClick = this.onClick.bind(this);
this.onEnabled = this.onEnabled.bind(this);
this.afterRoute = this.afterRoute.bind(this);
super(...args);
}
static initClass() {
this.className = '_list';
this.attributes =
{role: 'navigation'};
this.events = {
open: 'onOpen',
close: 'onClose',
click: 'onClick' click: 'onClick'
};
@routes: this.routes =
after: 'afterRoute' {after: 'afterRoute'};
@elements: this.elements = {
disabledTitle: '._list-title' disabledTitle: '._list-title',
disabledList: '._disabled-list' disabledList: '._disabled-list'
};
init: -> }
@lists = {}
init() {
@addSubview @listFocus = new app.views.ListFocus @el this.lists = {};
@addSubview @listFold = new app.views.ListFold @el
@addSubview @listSelect = new app.views.ListSelect @el this.addSubview(this.listFocus = new app.views.ListFocus(this.el));
this.addSubview(this.listFold = new app.views.ListFold(this.el));
app.on 'ready', @render this.addSubview(this.listSelect = new app.views.ListSelect(this.el));
return
app.on('ready', this.render);
activate: -> }
if super
list.activate() for slug, list of @lists activate() {
@listSelect.selectCurrent() if (super.activate(...arguments)) {
return for (var slug in this.lists) { var list = this.lists[slug]; list.activate(); }
this.listSelect.selectCurrent();
deactivate: -> }
if super }
list.deactivate() for slug, list of @lists
return deactivate() {
if (super.deactivate(...arguments)) {
render: => for (var slug in this.lists) { var list = this.lists[slug]; list.deactivate(); }
html = '' }
for doc in app.docs.all() }
html += @tmpl('sidebarDoc', doc, fullName: app.docs.countAllBy('name', doc.name) > 1)
@html html render() {
@renderDisabled() unless app.isSingleDoc() or app.disabledDocs.size() is 0 let html = '';
return for (var doc of Array.from(app.docs.all())) {
html += this.tmpl('sidebarDoc', doc, {fullName: app.docs.countAllBy('name', doc.name) > 1});
renderDisabled: -> }
@append @tmpl('sidebarDisabled', count: app.disabledDocs.size()) this.html(html);
@refreshElements() if (!app.isSingleDoc() && (app.disabledDocs.size() !== 0)) { this.renderDisabled(); }
@renderDisabledList() }
return
renderDisabled() {
renderDisabledList: -> this.append(this.tmpl('sidebarDisabled', {count: app.disabledDocs.size()}));
if app.settings.get('hideDisabled') this.refreshElements();
@removeDisabledList() this.renderDisabledList();
else }
@appendDisabledList()
return renderDisabledList() {
if (app.settings.get('hideDisabled')) {
appendDisabledList: -> this.removeDisabledList();
html = '' } else {
docs = [].concat(app.disabledDocs.all()...) this.appendDisabledList();
}
while doc = docs.shift() }
if doc.version?
versions = '' appendDisabledList() {
loop let doc;
versions += @tmpl('sidebarDoc', doc, disabled: true) let html = '';
break if docs[0]?.name isnt doc.name const docs = [].concat(...Array.from(app.disabledDocs.all() || []));
doc = docs.shift()
html += @tmpl('sidebarDisabledVersionedDoc', doc, versions) while ((doc = docs.shift())) {
else if (doc.version != null) {
html += @tmpl('sidebarDoc', doc, disabled: true) var versions = '';
while (true) {
@append @tmpl('sidebarDisabledList', html) versions += this.tmpl('sidebarDoc', doc, {disabled: true});
@disabledTitle.classList.add('open-title') if ((docs[0] != null ? docs[0].name : undefined) !== doc.name) { break; }
@refreshElements() doc = docs.shift();
return }
html += this.tmpl('sidebarDisabledVersionedDoc', doc, versions);
removeDisabledList: -> } else {
$.remove @disabledList if @disabledList html += this.tmpl('sidebarDoc', doc, {disabled: true});
@disabledTitle.classList.remove('open-title') }
@refreshElements() }
return
this.append(this.tmpl('sidebarDisabledList', html));
reset: (options = {}) -> this.disabledTitle.classList.add('open-title');
@listSelect.deselect() this.refreshElements();
@listFocus?.blur() }
@listFold.reset()
@revealCurrent() if options.revealCurrent || app.isSingleDoc() removeDisabledList() {
return if (this.disabledList) { $.remove(this.disabledList); }
this.disabledTitle.classList.remove('open-title');
onOpen: (event) => this.refreshElements();
$.stopEvent(event) }
doc = app.docs.findBy 'slug', event.target.getAttribute('data-slug')
reset(options) {
if doc and not @lists[doc.slug] if (options == null) { options = {}; }
@lists[doc.slug] = if doc.types.isEmpty() this.listSelect.deselect();
new app.views.EntryList doc.entries.all() if (this.listFocus != null) {
else this.listFocus.blur();
new app.views.TypeList doc }
$.after event.target, @lists[doc.slug].el this.listFold.reset();
return if (options.revealCurrent || app.isSingleDoc()) { this.revealCurrent(); }
}
onClose: (event) =>
$.stopEvent(event) onOpen(event) {
doc = app.docs.findBy 'slug', event.target.getAttribute('data-slug') $.stopEvent(event);
const doc = app.docs.findBy('slug', event.target.getAttribute('data-slug'));
if doc and @lists[doc.slug]
@lists[doc.slug].detach() if (doc && !this.lists[doc.slug]) {
delete @lists[doc.slug] this.lists[doc.slug] = doc.types.isEmpty() ?
return new app.views.EntryList(doc.entries.all())
:
select: (model) -> new app.views.TypeList(doc);
@listSelect.selectByHref model?.fullPath() $.after(event.target, this.lists[doc.slug].el);
return }
}
reveal: (model) ->
@openDoc model.doc onClose(event) {
@openType model.getType() if model.type $.stopEvent(event);
@focus model const doc = app.docs.findBy('slug', event.target.getAttribute('data-slug'));
@paginateTo model
@scrollTo model if (doc && this.lists[doc.slug]) {
return this.lists[doc.slug].detach();
delete this.lists[doc.slug];
focus: (model) -> }
@listFocus?.focus @find("a[href='#{model.fullPath()}']") }
return
select(model) {
revealCurrent: -> this.listSelect.selectByHref(model != null ? model.fullPath() : undefined);
if model = app.router.context.type or app.router.context.entry }
@reveal model
@select model reveal(model) {
return this.openDoc(model.doc);
if (model.type) { this.openType(model.getType()); }
openDoc: (doc) -> this.focus(model);
@listFold.open @find("[data-slug='#{doc.slug_without_version}']") if app.disabledDocs.contains(doc) and doc.version this.paginateTo(model);
@listFold.open @find("[data-slug='#{doc.slug}']") this.scrollTo(model);
return }
closeDoc: (doc) -> focus(model) {
@listFold.close @find("[data-slug='#{doc.slug}']") if (this.listFocus != null) {
return this.listFocus.focus(this.find(`a[href='${model.fullPath()}']`));
}
openType: (type) -> }
@listFold.open @lists[type.doc.slug].find("[data-slug='#{type.slug}']")
return revealCurrent() {
let model;
paginateTo: (model) -> if (model = app.router.context.type || app.router.context.entry) {
@lists[model.doc.slug]?.paginateTo(model) this.reveal(model);
return this.select(model);
}
scrollTo: (model) -> }
$.scrollTo @find("a[href='#{model.fullPath()}']"), null, 'top', margin: if app.isMobile() then 48 else 0
return openDoc(doc) {
if (app.disabledDocs.contains(doc) && doc.version) { this.listFold.open(this.find(`[data-slug='${doc.slug_without_version}']`)); }
toggleDisabled: -> this.listFold.open(this.find(`[data-slug='${doc.slug}']`));
if @disabledTitle.classList.contains('open-title') }
@removeDisabledList()
app.settings.set 'hideDisabled', true closeDoc(doc) {
else this.listFold.close(this.find(`[data-slug='${doc.slug}']`));
@appendDisabledList() }
app.settings.set 'hideDisabled', false
return openType(type) {
this.listFold.open(this.lists[type.doc.slug].find(`[data-slug='${type.slug}']`));
onClick: (event) => }
target = $.eventTarget(event)
if @disabledTitle and $.hasChild(@disabledTitle, target) and target.tagName isnt 'A' paginateTo(model) {
$.stopEvent(event) if (this.lists[model.doc.slug] != null) {
@toggleDisabled() this.lists[model.doc.slug].paginateTo(model);
else if slug = target.getAttribute('data-enable') }
$.stopEvent(event) }
doc = app.disabledDocs.findBy('slug', slug)
app.enableDoc(doc, @onEnabled, @onEnabled) if doc scrollTo(model) {
return $.scrollTo(this.find(`a[href='${model.fullPath()}']`), null, 'top', {margin: app.isMobile() ? 48 : 0});
}
onEnabled: =>
@reset() toggleDisabled() {
@render() if (this.disabledTitle.classList.contains('open-title')) {
return this.removeDisabledList();
app.settings.set('hideDisabled', true);
afterRoute: (route, context) => } else {
if context.init this.appendDisabledList();
@reset revealCurrent: true if @activated app.settings.set('hideDisabled', false);
else }
@select context.type or context.entry }
return
onClick(event) {
let slug;
const target = $.eventTarget(event);
if (this.disabledTitle && $.hasChild(this.disabledTitle, target) && (target.tagName !== 'A')) {
$.stopEvent(event);
this.toggleDisabled();
} else if (slug = target.getAttribute('data-enable')) {
$.stopEvent(event);
const doc = app.disabledDocs.findBy('slug', slug);
if (doc) { app.enableDoc(doc, this.onEnabled, this.onEnabled); }
}
}
onEnabled() {
this.reset();
this.render();
}
afterRoute(route, context) {
if (context.init) {
if (this.activated) { this.reset({revealCurrent: true}); }
} else {
this.select(context.type || context.entry);
}
}
});
Cls.initClass();

@ -1,88 +1,130 @@
class app.views.DocPicker extends app.View /*
@className: '_list _list-picker' * decaffeinate suggestions:
* DS002: Fix invalid constructor
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.DocPicker = class DocPicker extends app.View {
constructor(...args) {
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.onDOMFocus = this.onDOMFocus.bind(this);
super(...args);
}
@events: static initClass() {
mousedown: 'onMouseDown' this.className = '_list _list-picker';
this.events = {
mousedown: 'onMouseDown',
mouseup: 'onMouseUp' mouseup: 'onMouseUp'
};
}
init() {
this.addSubview(this.listFold = new app.views.ListFold(this.el));
}
activate() {
if (super.activate(...arguments)) {
this.render();
$.on(this.el, 'focus', this.onDOMFocus, true);
}
}
deactivate() {
if (super.deactivate(...arguments)) {
this.empty();
$.off(this.el, 'focus', this.onDOMFocus, true);
this.focusEl = null;
}
}
render() {
let doc;
let html = this.tmpl('docPickerHeader');
let docs = app.docs.all().concat(...Array.from(app.disabledDocs.all() || []));
while ((doc = docs.shift())) {
if (doc.version != null) {
var versions;
[docs, versions] = Array.from(this.extractVersions(docs, doc));
html += this.tmpl('sidebarVersionedDoc', doc, this.renderVersions(versions), {open: app.docs.contains(doc)});
} else {
html += this.tmpl('sidebarLabel', doc, {checked: app.docs.contains(doc)});
}
}
this.html(html + this.tmpl('docPickerNote'));
$.requestAnimationFrame(() => __guard__(this.findByTag('input'), x => x.focus()));
}
renderVersions(docs) {
let html = '';
for (var doc of Array.from(docs)) { html += this.tmpl('sidebarLabel', doc, {checked: app.docs.contains(doc)}); }
return html;
}
extractVersions(originalDocs, version) {
const docs = [];
const versions = [version];
for (var doc of Array.from(originalDocs)) {
(doc.name === version.name ? versions : docs).push(doc);
}
return [docs, versions];
}
empty() {
this.resetClass();
super.empty(...arguments);
}
getSelectedDocs() {
return Array.from(this.findAllByTag('input')).filter((input) => (input != null ? input.checked : undefined)).map((input) =>
input.name);
}
onMouseDown() {
this.mouseDown = Date.now();
}
onMouseUp() {
this.mouseUp = Date.now();
}
onDOMFocus(event) {
const {
target
} = event;
if (target.tagName === 'INPUT') {
if ((!this.mouseDown || !(Date.now() < (this.mouseDown + 100))) && (!this.mouseUp || !(Date.now() < (this.mouseUp + 100)))) {
$.scrollTo(target.parentNode, null, 'continuous');
}
} else if (target.classList.contains(app.views.ListFold.targetClass)) {
target.blur();
if (!this.mouseDown || !(Date.now() < (this.mouseDown + 100))) {
if (this.focusEl === $('input', target.nextElementSibling)) {
if (target.classList.contains(app.views.ListFold.activeClass)) { this.listFold.close(target); }
let prev = target.previousElementSibling;
while ((prev.tagName !== 'LABEL') && !prev.classList.contains(app.views.ListFold.targetClass)) { prev = prev.previousElementSibling; }
if (prev.classList.contains(app.views.ListFold.activeClass)) { prev = $.makeArray($$('input', prev.nextElementSibling)).pop(); }
this.delay(() => prev.focus());
} else {
if (!target.classList.contains(app.views.ListFold.activeClass)) { this.listFold.open(target); }
this.delay(() => $('input', target.nextElementSibling).focus());
}
}
}
this.focusEl = target;
}
});
Cls.initClass();
init: -> function __guard__(value, transform) {
@addSubview @listFold = new app.views.ListFold(@el) return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
return }
activate: ->
if super
@render()
$.on @el, 'focus', @onDOMFocus, true
return
deactivate: ->
if super
@empty()
$.off @el, 'focus', @onDOMFocus, true
@focusEl = null
return
render: ->
html = @tmpl('docPickerHeader')
docs = app.docs.all().concat(app.disabledDocs.all()...)
while doc = docs.shift()
if doc.version?
[docs, versions] = @extractVersions(docs, doc)
html += @tmpl('sidebarVersionedDoc', doc, @renderVersions(versions), open: app.docs.contains(doc))
else
html += @tmpl('sidebarLabel', doc, checked: app.docs.contains(doc))
@html html + @tmpl('docPickerNote')
$.requestAnimationFrame => @findByTag('input')?.focus()
return
renderVersions: (docs) ->
html = ''
html += @tmpl('sidebarLabel', doc, checked: app.docs.contains(doc)) for doc in docs
html
extractVersions: (originalDocs, version) ->
docs = []
versions = [version]
for doc in originalDocs
(if doc.name is version.name then versions else docs).push(doc)
[docs, versions]
empty: ->
@resetClass()
super
return
getSelectedDocs: ->
for input in @findAllByTag 'input' when input?.checked
input.name
onMouseDown: =>
@mouseDown = Date.now()
return
onMouseUp: =>
@mouseUp = Date.now()
return
onDOMFocus: (event) =>
target = event.target
if target.tagName is 'INPUT'
unless (@mouseDown and Date.now() < @mouseDown + 100) or (@mouseUp and Date.now() < @mouseUp + 100)
$.scrollTo target.parentNode, null, 'continuous'
else if target.classList.contains(app.views.ListFold.targetClass)
target.blur()
unless @mouseDown and Date.now() < @mouseDown + 100
if @focusEl is $('input', target.nextElementSibling)
@listFold.close(target) if target.classList.contains(app.views.ListFold.activeClass)
prev = target.previousElementSibling
prev = prev.previousElementSibling until prev.tagName is 'LABEL' or prev.classList.contains(app.views.ListFold.targetClass)
prev = $.makeArray($$('input', prev.nextElementSibling)).pop() if prev.classList.contains(app.views.ListFold.activeClass)
@delay -> prev.focus()
else
@listFold.open(target) unless target.classList.contains(app.views.ListFold.activeClass)
@delay -> $('input', target.nextElementSibling).focus()
@focusEl = target
return

@ -1,15 +1,27 @@
#= require views/list/paginated_list /*
* decaffeinate suggestions:
* DS002: Fix invalid constructor
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
//= require views/list/paginated_list
class app.views.EntryList extends app.views.PaginatedList const Cls = (app.views.EntryList = class EntryList extends app.views.PaginatedList {
@tagName: 'div' static initClass() {
@className: '_list _list-sub' this.tagName = 'div';
this.className = '_list _list-sub';
}
constructor: (@entries) -> super constructor(entries) { this.entries = entries; super(...arguments); }
init: -> init() {
@renderPaginated() this.renderPaginated();
@activate() this.activate();
return }
render: (entries) -> render(entries) {
@tmpl 'sidebarEntry', entries return this.tmpl('sidebarEntry', entries);
}
});
Cls.initClass();

@ -1,68 +1,93 @@
class app.views.Results extends app.View /*
@className: '_list' * decaffeinate suggestions:
* DS002: Fix invalid constructor
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.Results = class Results extends app.View {
static initClass() {
this.className = '_list';
@events: this.events =
click: 'onClick' {click: 'onClick'};
@routes: this.routes =
after: 'afterRoute' {after: 'afterRoute'};
}
constructor: (@sidebar, @search) -> super constructor(sidebar, search) { this.onResults = this.onResults.bind(this); this.onNoResults = this.onNoResults.bind(this); this.onClear = this.onClear.bind(this); this.afterRoute = this.afterRoute.bind(this); this.onClick = this.onClick.bind(this); this.sidebar = sidebar; this.search = search; super(...arguments); }
deactivate: -> deactivate() {
if super if (super.deactivate(...arguments)) {
@empty() this.empty();
return }
}
init: -> init() {
@addSubview @listFocus = new app.views.ListFocus @el this.addSubview(this.listFocus = new app.views.ListFocus(this.el));
@addSubview @listSelect = new app.views.ListSelect @el this.addSubview(this.listSelect = new app.views.ListSelect(this.el));
@search this.search
.on 'results', @onResults .on('results', this.onResults)
.on 'noresults', @onNoResults .on('noresults', this.onNoResults)
.on 'clear', @onClear .on('clear', this.onClear);
return }
onResults: (entries, flags) => onResults(entries, flags) {
@listFocus?.blur() if flags.initialResults if (flags.initialResults) { if (this.listFocus != null) {
@empty() if flags.initialResults this.listFocus.blur();
@append @tmpl('sidebarResult', entries) } }
if (flags.initialResults) { this.empty(); }
this.append(this.tmpl('sidebarResult', entries));
if flags.initialResults if (flags.initialResults) {
if flags.urlSearch then @openFirst() else @focusFirst() if (flags.urlSearch) { this.openFirst(); } else { this.focusFirst(); }
return }
}
onNoResults: => onNoResults() {
@html @tmpl('sidebarNoResults') this.html(this.tmpl('sidebarNoResults'));
return }
onClear: => onClear() {
@empty() this.empty();
return }
focusFirst: -> focusFirst() {
@listFocus?.focusOnNextFrame @el.firstElementChild unless app.isMobile() if (!app.isMobile()) { if (this.listFocus != null) {
return this.listFocus.focusOnNextFrame(this.el.firstElementChild);
} }
}
openFirst: -> openFirst() {
@el.firstElementChild?.click() if (this.el.firstElementChild != null) {
return this.el.firstElementChild.click();
}
}
onDocEnabled: (doc) -> onDocEnabled(doc) {
app.router.show(doc.fullPath()) app.router.show(doc.fullPath());
@sidebar.onDocEnabled() return this.sidebar.onDocEnabled();
}
afterRoute: (route, context) => afterRoute(route, context) {
if route is 'entry' if (route === 'entry') {
@listSelect.selectByHref context.entry.fullPath() this.listSelect.selectByHref(context.entry.fullPath());
else } else {
@listSelect.deselect() this.listSelect.deselect();
return }
}
onClick: (event) => onClick(event) {
return if event.which isnt 1 let slug;
if slug = $.eventTarget(event).getAttribute('data-enable') if (event.which !== 1) { return; }
$.stopEvent(event) if (slug = $.eventTarget(event).getAttribute('data-enable')) {
doc = app.disabledDocs.findBy('slug', slug) $.stopEvent(event);
app.enableDoc(doc, @onDocEnabled.bind(@, doc), $.noop) if doc const doc = app.disabledDocs.findBy('slug', slug);
if (doc) { return app.enableDoc(doc, this.onDocEnabled.bind(this, doc), $.noop); }
}
}
});
Cls.initClass();

@ -1,164 +1,217 @@
class app.views.Sidebar extends app.View /*
@el: '._sidebar' * decaffeinate suggestions:
* DS002: Fix invalid constructor
@events: * DS102: Remove unnecessary code created because of implicit returns
focus: 'onFocus' * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
select: 'onSelect' * DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.Sidebar = class Sidebar extends app.View {
constructor(...args) {
this.resetHoverOnMouseMove = this.resetHoverOnMouseMove.bind(this);
this.resetHover = this.resetHover.bind(this);
this.showResults = this.showResults.bind(this);
this.onReady = this.onReady.bind(this);
this.onScopeChange = this.onScopeChange.bind(this);
this.onSearching = this.onSearching.bind(this);
this.onSearchClear = this.onSearchClear.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onSelect = this.onSelect.bind(this);
this.onClick = this.onClick.bind(this);
this.onAltR = this.onAltR.bind(this);
this.onEscape = this.onEscape.bind(this);
this.afterRoute = this.afterRoute.bind(this);
super(...args);
}
static initClass() {
this.el = '._sidebar';
this.events = {
focus: 'onFocus',
select: 'onSelect',
click: 'onClick' click: 'onClick'
};
@routes: this.routes =
after: 'afterRoute' {after: 'afterRoute'};
@shortcuts: this.shortcuts = {
altR: 'onAltR' altR: 'onAltR',
escape: 'onEscape' escape: 'onEscape'
};
}
init: -> init() {
@addSubview @hover = new app.views.SidebarHover @el unless app.isMobile() if (!app.isMobile()) { this.addSubview(this.hover = new app.views.SidebarHover(this.el)); }
@addSubview @search = new app.views.Search this.addSubview(this.search = new app.views.Search);
@search this.search
.on 'searching', @onSearching .on('searching', this.onSearching)
.on 'clear', @onSearchClear .on('clear', this.onSearchClear)
.scope .scope
.on 'change', @onScopeChange .on('change', this.onScopeChange);
@results = new app.views.Results @, @search this.results = new app.views.Results(this, this.search);
@docList = new app.views.DocList this.docList = new app.views.DocList;
app.on 'ready', @onReady app.on('ready', this.onReady);
$.on document.documentElement, 'mouseleave', => @hide() $.on(document.documentElement, 'mouseleave', () => this.hide());
$.on document.documentElement, 'mouseenter', => @resetDisplay(forceNoHover: false) $.on(document.documentElement, 'mouseenter', () => this.resetDisplay({forceNoHover: false}));
return }
hide: -> hide() {
@removeClass 'show' this.removeClass('show');
return }
display: -> display() {
@addClass 'show' this.addClass('show');
return }
resetDisplay: (options = {}) -> resetDisplay(options) {
return unless @hasClass 'show' if (options == null) { options = {}; }
@removeClass 'show' if (!this.hasClass('show')) { return; }
this.removeClass('show');
unless options.forceNoHover is false or @hasClass 'no-hover'
@addClass 'no-hover' if ((options.forceNoHover !== false) && !this.hasClass('no-hover')) {
$.on window, 'mousemove', @resetHoverOnMouseMove this.addClass('no-hover');
return $.on(window, 'mousemove', this.resetHoverOnMouseMove);
}
resetHoverOnMouseMove: => }
$.off window, 'mousemove', @resetHoverOnMouseMove
$.requestAnimationFrame @resetHover resetHoverOnMouseMove() {
$.off(window, 'mousemove', this.resetHoverOnMouseMove);
resetHover: => return $.requestAnimationFrame(this.resetHover);
@removeClass 'no-hover' }
showView: (view) -> resetHover() {
unless @view is view return this.removeClass('no-hover');
@hover?.hide() }
@saveScrollPosition()
@view?.deactivate() showView(view) {
@view = view if (this.view !== view) {
@render() if (this.hover != null) {
@view.activate() this.hover.hide();
@restoreScrollPosition() }
return this.saveScrollPosition();
if (this.view != null) {
render: -> this.view.deactivate();
@html @view }
return this.view = view;
this.render();
showDocList: -> this.view.activate();
@showView @docList this.restoreScrollPosition();
return }
}
showResults: =>
@display() render() {
@showView @results this.html(this.view);
return }
reset: -> showDocList() {
@display() this.showView(this.docList);
@showDocList() }
@docList.reset()
@search.reset() showResults() {
return this.display();
this.showView(this.results);
onReady: => }
@view = @docList
@render() reset() {
@view.activate() this.display();
return this.showDocList();
this.docList.reset();
onScopeChange: (newDoc, previousDoc) => this.search.reset();
@docList.closeDoc(previousDoc) if previousDoc }
if newDoc then @docList.reveal(newDoc.toEntry()) else @scrollToTop()
return onReady() {
this.view = this.docList;
saveScrollPosition: -> this.render();
if @view is @docList this.view.activate();
@scrollTop = @el.scrollTop }
return
onScopeChange(newDoc, previousDoc) {
restoreScrollPosition: -> if (previousDoc) { this.docList.closeDoc(previousDoc); }
if @view is @docList and @scrollTop if (newDoc) { this.docList.reveal(newDoc.toEntry()); } else { this.scrollToTop(); }
@el.scrollTop = @scrollTop }
@scrollTop = null
else saveScrollPosition() {
@scrollToTop() if (this.view === this.docList) {
return this.scrollTop = this.el.scrollTop;
}
scrollToTop: -> }
@el.scrollTop = 0
return restoreScrollPosition() {
if ((this.view === this.docList) && this.scrollTop) {
onSearching: => this.el.scrollTop = this.scrollTop;
@showResults() this.scrollTop = null;
return } else {
this.scrollToTop();
onSearchClear: => }
@resetDisplay() }
@showDocList()
return scrollToTop() {
this.el.scrollTop = 0;
onFocus: (event) => }
@display()
$.scrollTo event.target, @el, 'continuous', bottomGap: 2 unless event.target is @el onSearching() {
return this.showResults();
}
onSelect: =>
@resetDisplay() onSearchClear() {
return this.resetDisplay();
this.showDocList();
onClick: (event) => }
return if event.which isnt 1
if $.eventTarget(event).hasAttribute? 'data-reset-list' onFocus(event) {
$.stopEvent(event) this.display();
@onAltR() if (event.target !== this.el) { $.scrollTo(event.target, this.el, 'continuous', {bottomGap: 2}); }
return }
onAltR: => onSelect() {
@reset() this.resetDisplay();
@docList.reset(revealCurrent: true) }
@display()
return onClick(event) {
if (event.which !== 1) { return; }
onEscape: => if (__guardMethod__($.eventTarget(event), 'hasAttribute', o => o.hasAttribute('data-reset-list'))) {
@reset() $.stopEvent(event);
@resetDisplay() this.onAltR();
if doc = @search.getScopeDoc() then @docList.reveal(doc.toEntry()) else @scrollToTop() }
return }
onDocEnabled: -> onAltR() {
@docList.onEnabled() this.reset();
@reset() this.docList.reset({revealCurrent: true});
return this.display();
}
afterRoute: (name, context) =>
return if app.shortcuts.eventInProgress?.name is 'escape' onEscape() {
@reset() if not context.init and app.router.isIndex() let doc;
@resetDisplay() this.reset();
return this.resetDisplay();
if ((doc = this.search.getScopeDoc())) { this.docList.reveal(doc.toEntry()); } else { this.scrollToTop(); }
}
onDocEnabled() {
this.docList.onEnabled();
this.reset();
}
afterRoute(name, context) {
if ((app.shortcuts.eventInProgress != null ? app.shortcuts.eventInProgress.name : undefined) === 'escape') { return; }
if (!context.init && app.router.isIndex()) { this.reset(); }
this.resetDisplay();
}
});
Cls.initClass();
function __guardMethod__(obj, methodName, transform) {
if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') {
return transform(obj, methodName);
} else {
return undefined;
}
}

@ -1,100 +1,143 @@
class app.views.SidebarHover extends app.View /*
@itemClass: '_list-hover' * decaffeinate suggestions:
* DS002: Fix invalid constructor
@events: * DS102: Remove unnecessary code created because of implicit returns
focus: 'onFocus' * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
blur: 'onBlur' * DS206: Consider reworking classes to avoid initClass
mouseover: 'onMouseover' * DS207: Consider shorter variations of null checks
mouseout: 'onMouseout' * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
scroll: 'onScroll' */
const Cls = (app.views.SidebarHover = class SidebarHover extends app.View {
static initClass() {
this.itemClass = '_list-hover';
this.events = {
focus: 'onFocus',
blur: 'onBlur',
mouseover: 'onMouseover',
mouseout: 'onMouseout',
scroll: 'onScroll',
click: 'onClick' click: 'onClick'
};
@routes:
after: 'onRoute' this.routes =
{after: 'onRoute'};
constructor: (@el) -> }
unless isPointerEventsSupported()
delete @constructor.events.mouseover constructor(el) {
super this.position = this.position.bind(this);
this.onFocus = this.onFocus.bind(this);
show: (el) -> this.onBlur = this.onBlur.bind(this);
unless el is @cursor this.onMouseover = this.onMouseover.bind(this);
@hide() this.onMouseout = this.onMouseout.bind(this);
if @isTarget(el) and @isTruncated(el.lastElementChild or el) this.onScroll = this.onScroll.bind(this);
@cursor = el this.onClick = this.onClick.bind(this);
@clone = @makeClone @cursor this.onRoute = this.onRoute.bind(this);
$.append document.body, @clone this.el = el;
@offsetTop ?= @el.offsetTop if (!isPointerEventsSupported()) {
@position() delete this.constructor.events.mouseover;
return }
super(...arguments);
hide: -> }
if @cursor
$.remove @clone show(el) {
@cursor = @clone = null if (el !== this.cursor) {
return this.hide();
if (this.isTarget(el) && this.isTruncated(el.lastElementChild || el)) {
position: => this.cursor = el;
if @cursor this.clone = this.makeClone(this.cursor);
rect = $.rect(@cursor) $.append(document.body, this.clone);
if rect.top >= @offsetTop if (this.offsetTop == null) { this.offsetTop = this.el.offsetTop; }
@clone.style.top = rect.top + 'px' this.position();
@clone.style.left = rect.left + 'px' }
else }
@hide() }
return
hide() {
makeClone: (el) -> if (this.cursor) {
clone = el.cloneNode(true) $.remove(this.clone);
clone.classList.add 'clone' this.cursor = (this.clone = null);
clone }
}
isTarget: (el) ->
el?.classList?.contains @constructor.itemClass position() {
if (this.cursor) {
isSelected: (el) -> const rect = $.rect(this.cursor);
el.classList.contains 'active' if (rect.top >= this.offsetTop) {
this.clone.style.top = rect.top + 'px';
isTruncated: (el) -> this.clone.style.left = rect.left + 'px';
el.scrollWidth > el.offsetWidth } else {
this.hide();
onFocus: (event) => }
@focusTime = Date.now() }
@show event.target }
return
makeClone(el) {
onBlur: => const clone = el.cloneNode(true);
@hide() clone.classList.add('clone');
return return clone;
}
onMouseover: (event) =>
if @isTarget(event.target) and not @isSelected(event.target) and @mouseActivated() isTarget(el) {
@show event.target return __guard__(el != null ? el.classList : undefined, x => x.contains(this.constructor.itemClass));
return }
onMouseout: (event) => isSelected(el) {
if @isTarget(event.target) and @mouseActivated() return el.classList.contains('active');
@hide() }
return
isTruncated(el) {
mouseActivated: -> return el.scrollWidth > el.offsetWidth;
# Skip mouse events caused by focus events scrolling the sidebar. }
not @focusTime or Date.now() - @focusTime > 500
onFocus(event) {
onScroll: => this.focusTime = Date.now();
@position() this.show(event.target);
return }
onClick: (event) => onBlur() {
if event.target is @clone this.hide();
$.click @cursor }
return
onMouseover(event) {
onRoute: => if (this.isTarget(event.target) && !this.isSelected(event.target) && this.mouseActivated()) {
@hide() this.show(event.target);
return }
}
isPointerEventsSupported = ->
el = document.createElement 'div' onMouseout(event) {
el.style.cssText = 'pointer-events: auto' if (this.isTarget(event.target) && this.mouseActivated()) {
el.style.pointerEvents is 'auto' this.hide();
}
}
mouseActivated() {
// Skip mouse events caused by focus events scrolling the sidebar.
return !this.focusTime || ((Date.now() - this.focusTime) > 500);
}
onScroll() {
this.position();
}
onClick(event) {
if (event.target === this.clone) {
$.click(this.cursor);
}
}
onRoute() {
this.hide();
}
});
Cls.initClass();
var isPointerEventsSupported = function() {
const el = document.createElement('div');
el.style.cssText = 'pointer-events: auto';
return el.style.pointerEvents === 'auto';
};
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,53 +1,77 @@
class app.views.TypeList extends app.View /*
@tagName: 'div' * decaffeinate suggestions:
@className: '_list _list-sub' * DS002: Fix invalid constructor
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const Cls = (app.views.TypeList = class TypeList extends app.View {
static initClass() {
this.tagName = 'div';
this.className = '_list _list-sub';
@events: this.events = {
open: 'onOpen' open: 'onOpen',
close: 'onClose' close: 'onClose'
};
}
constructor: (@doc) -> super constructor(doc) { this.onOpen = this.onOpen.bind(this); this.onClose = this.onClose.bind(this); this.doc = doc; super(...arguments); }
init: -> init() {
@lists = {} this.lists = {};
@render() this.render();
@activate() this.activate();
return }
activate: -> activate() {
if super if (super.activate(...arguments)) {
list.activate() for slug, list of @lists for (var slug in this.lists) { var list = this.lists[slug]; list.activate(); }
return }
}
deactivate: ->
if super deactivate() {
list.deactivate() for slug, list of @lists if (super.deactivate(...arguments)) {
return for (var slug in this.lists) { var list = this.lists[slug]; list.deactivate(); }
}
render: -> }
html = ''
html += @tmpl('sidebarType', group) for group in @doc.types.groups() render() {
@html(html) let html = '';
for (var group of Array.from(this.doc.types.groups())) { html += this.tmpl('sidebarType', group); }
onOpen: (event) => return this.html(html);
$.stopEvent(event) }
type = @doc.types.findBy 'slug', event.target.getAttribute('data-slug')
onOpen(event) {
if type and not @lists[type.slug] $.stopEvent(event);
@lists[type.slug] = new app.views.EntryList(type.entries()) const type = this.doc.types.findBy('slug', event.target.getAttribute('data-slug'));
$.after event.target, @lists[type.slug].el
return if (type && !this.lists[type.slug]) {
this.lists[type.slug] = new app.views.EntryList(type.entries());
onClose: (event) => $.after(event.target, this.lists[type.slug].el);
$.stopEvent(event) }
type = @doc.types.findBy 'slug', event.target.getAttribute('data-slug') }
if type and @lists[type.slug] onClose(event) {
@lists[type.slug].detach() $.stopEvent(event);
delete @lists[type.slug] const type = this.doc.types.findBy('slug', event.target.getAttribute('data-slug'));
return
if (type && this.lists[type.slug]) {
paginateTo: (model) -> this.lists[type.slug].detach();
if model.type delete this.lists[type.slug];
@lists[model.getType().slug]?.paginateTo(model) }
return }
paginateTo(model) {
if (model.type) {
__guard__(this.lists[model.getType().slug], x => x.paginateTo(model));
}
}
});
Cls.initClass();
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

@ -1,172 +1,214 @@
class app.View /*
$.extend @prototype, Events * decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
constructor: -> * DS102: Remove unnecessary code created because of implicit returns
@setupElement() * DS206: Consider reworking classes to avoid initClass
@originalClassName = @el.className if @el.className * DS207: Consider shorter variations of null checks
@resetClass() if @constructor.className * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
@refreshElements() */
@init?() const Cls = (app.View = class View {
@refreshElements() static initClass() {
$.extend(this.prototype, Events);
setupElement: -> }
@el ?= if typeof @constructor.el is 'string'
$ @constructor.el constructor() {
else if @constructor.el this.setupElement();
@constructor.el if (this.el.className) { this.originalClassName = this.el.className; }
else if (this.constructor.className) { this.resetClass(); }
document.createElement @constructor.tagName or 'div' this.refreshElements();
if (typeof this.init === 'function') {
if @constructor.attributes this.init();
for key, value of @constructor.attributes }
@el.setAttribute(key, value) this.refreshElements();
return }
refreshElements: -> setupElement() {
if @constructor.elements if (this.el == null) { this.el = typeof this.constructor.el === 'string' ?
@[name] = @find selector for name, selector of @constructor.elements $(this.constructor.el)
return : this.constructor.el ?
this.constructor.el
addClass: (name) -> :
@el.classList.add(name) document.createElement(this.constructor.tagName || 'div'); }
return
if (this.constructor.attributes) {
removeClass: (name) -> for (var key in this.constructor.attributes) {
@el.classList.remove(name) var value = this.constructor.attributes[key];
return this.el.setAttribute(key, value);
}
toggleClass: (name) -> }
@el.classList.toggle(name) }
return
refreshElements() {
hasClass: (name) -> if (this.constructor.elements) {
@el.classList.contains(name) for (var name in this.constructor.elements) { var selector = this.constructor.elements[name]; this[name] = this.find(selector); }
}
resetClass: -> }
@el.className = @originalClassName or ''
if @constructor.className addClass(name) {
@addClass name for name in @constructor.className.split ' ' this.el.classList.add(name);
return }
find: (selector) -> removeClass(name) {
$ selector, @el this.el.classList.remove(name);
}
findAll: (selector) ->
$$ selector, @el toggleClass(name) {
this.el.classList.toggle(name);
findByClass: (name) -> }
@findAllByClass(name)[0]
hasClass(name) {
findLastByClass: (name) -> return this.el.classList.contains(name);
all = @findAllByClass(name)[0] }
all[all.length - 1]
resetClass() {
findAllByClass: (name) -> this.el.className = this.originalClassName || '';
@el.getElementsByClassName(name) if (this.constructor.className) {
for (var name of Array.from(this.constructor.className.split(' '))) { this.addClass(name); }
findByTag: (tag) -> }
@findAllByTag(tag)[0] }
findLastByTag: (tag) -> find(selector) {
all = @findAllByTag(tag) return $(selector, this.el);
all[all.length - 1] }
findAllByTag: (tag) -> findAll(selector) {
@el.getElementsByTagName(tag) return $$(selector, this.el);
}
append: (value) ->
$.append @el, value.el or value findByClass(name) {
return return this.findAllByClass(name)[0];
}
appendTo: (value) ->
$.append value.el or value, @el findLastByClass(name) {
return const all = this.findAllByClass(name)[0];
return all[all.length - 1];
prepend: (value) -> }
$.prepend @el, value.el or value
return findAllByClass(name) {
return this.el.getElementsByClassName(name);
prependTo: (value) -> }
$.prepend value.el or value, @el
return findByTag(tag) {
return this.findAllByTag(tag)[0];
before: (value) -> }
$.before @el, value.el or value
return findLastByTag(tag) {
const all = this.findAllByTag(tag);
after: (value) -> return all[all.length - 1];
$.after @el, value.el or value }
return
findAllByTag(tag) {
remove: (value) -> return this.el.getElementsByTagName(tag);
$.remove value.el or value }
return
append(value) {
empty: -> $.append(this.el, value.el || value);
$.empty @el }
@refreshElements()
return appendTo(value) {
$.append(value.el || value, this.el);
html: (value) -> }
@empty()
@append value prepend(value) {
return $.prepend(this.el, value.el || value);
}
tmpl: (args...) ->
app.templates.render(args...) prependTo(value) {
$.prepend(value.el || value, this.el);
delay: (fn, args...) -> }
delay = if typeof args[args.length - 1] is 'number' then args.pop() else 0
setTimeout fn.bind(@, args...), delay before(value) {
$.before(this.el, value.el || value);
onDOM: (event, callback) -> }
$.on @el, event, callback
return after(value) {
$.after(this.el, value.el || value);
offDOM: (event, callback) -> }
$.off @el, event, callback
return remove(value) {
$.remove(value.el || value);
bindEvents: -> }
if @constructor.events
@onDOM name, @[method] for name, method of @constructor.events empty() {
$.empty(this.el);
if @constructor.routes this.refreshElements();
app.router.on name, @[method] for name, method of @constructor.routes }
if @constructor.shortcuts html(value) {
app.shortcuts.on name, @[method] for name, method of @constructor.shortcuts this.empty();
return this.append(value);
}
unbindEvents: ->
if @constructor.events tmpl(...args) {
@offDOM name, @[method] for name, method of @constructor.events return app.templates.render(...Array.from(args || []));
}
if @constructor.routes
app.router.off name, @[method] for name, method of @constructor.routes delay(fn, ...args) {
const delay = typeof args[args.length - 1] === 'number' ? args.pop() : 0;
if @constructor.shortcuts return setTimeout(fn.bind(this, ...Array.from(args)), delay);
app.shortcuts.off name, @[method] for name, method of @constructor.shortcuts }
return
onDOM(event, callback) {
addSubview: (view) -> $.on(this.el, event, callback);
(@subviews or= []).push(view) }
activate: -> offDOM(event, callback) {
return if @activated $.off(this.el, event, callback);
@bindEvents() }
view.activate() for view in @subviews if @subviews
@activated = true bindEvents() {
true let method, name;
if (this.constructor.events) {
deactivate: -> for (name in this.constructor.events) { method = this.constructor.events[name]; this.onDOM(name, this[method]); }
return unless @activated }
@unbindEvents()
view.deactivate() for view in @subviews if @subviews if (this.constructor.routes) {
@activated = false for (name in this.constructor.routes) { method = this.constructor.routes[name]; app.router.on(name, this[method]); }
true }
detach: -> if (this.constructor.shortcuts) {
@deactivate() for (name in this.constructor.shortcuts) { method = this.constructor.shortcuts[name]; app.shortcuts.on(name, this[method]); }
$.remove @el }
return }
unbindEvents() {
let method, name;
if (this.constructor.events) {
for (name in this.constructor.events) { method = this.constructor.events[name]; this.offDOM(name, this[method]); }
}
if (this.constructor.routes) {
for (name in this.constructor.routes) { method = this.constructor.routes[name]; app.router.off(name, this[method]); }
}
if (this.constructor.shortcuts) {
for (name in this.constructor.shortcuts) { method = this.constructor.shortcuts[name]; app.shortcuts.off(name, this[method]); }
}
}
addSubview(view) {
return (this.subviews || (this.subviews = [])).push(view);
}
activate() {
if (this.activated) { return; }
this.bindEvents();
if (this.subviews) { for (var view of Array.from(this.subviews)) { view.activate(); } }
this.activated = true;
return true;
}
deactivate() {
if (!this.activated) { return; }
this.unbindEvents();
if (this.subviews) { for (var view of Array.from(this.subviews)) { view.deactivate(); } }
this.activated = false;
return true;
}
detach() {
this.deactivate();
$.remove(this.el);
}
});
Cls.initClass();

Loading…
Cancel
Save