mirror of https://github.com/freeCodeCamp/devdocs
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 """
|
|
||||||
JavaScript code has been injected in the page which prevents DevDocs from running correctly.
|
indexHost() {
|
||||||
Please check your browser extensions/addons. """
|
// Can't load the index files from the host/CDN when service worker is
|
||||||
Raven.captureMessage 'injection error', level: 'info'
|
// enabled because it doesn't support caching URLs that use CORS.
|
||||||
return
|
return this.config[this.serviceWorker && this.settings.hasDocs() ? 'index_path' : 'docs_origin'];
|
||||||
|
},
|
||||||
isInjectionError: ->
|
|
||||||
# Some browser extensions expect the entire web to use jQuery.
|
onBootError(...args) {
|
||||||
# I gave up trying to fight back.
|
this.trigger('bootError');
|
||||||
window.$ isnt app._$ or window.$$ isnt app._$$ or window.page isnt app._page or typeof $.empty isnt 'function' or typeof page.show isnt 'function'
|
this.hideLoadingScreen();
|
||||||
|
},
|
||||||
isAppError: (error, file) ->
|
|
||||||
# Ignore errors from external scripts.
|
onQuotaExceeded() {
|
||||||
file and file.indexOf('devdocs') isnt -1 and file.indexOf('.js') is file.length - 3
|
if (this.quotaExceeded) { return; }
|
||||||
|
this.quotaExceeded = true;
|
||||||
isSupportedBrowser: ->
|
new app.views.Notif('QuotaExceeded', {autoHide: null});
|
||||||
try
|
},
|
||||||
features =
|
|
||||||
bind: !!Function::bind
|
onCookieBlocked(key, value, actual) {
|
||||||
pushState: !!history.pushState
|
if (this.cookieBlocked) { return; }
|
||||||
matchMedia: !!window.matchMedia
|
this.cookieBlocked = true;
|
||||||
insertAdjacentHTML: !!document.body.insertAdjacentHTML
|
new app.views.Notif('CookieBlocked', {autoHide: null});
|
||||||
defaultPrevented: document.createEvent('CustomEvent').defaultPrevented is false
|
Raven.captureMessage(`CookieBlocked/${key}`, {level: 'warning', extra: {value, actual}});
|
||||||
cssVariables: !!CSS?.supports?('(--t: 0)')
|
},
|
||||||
|
|
||||||
for key, value of features when not value
|
onWindowError(...args) {
|
||||||
Raven.captureMessage "unsupported/#{key}", level: 'info'
|
if (this.cookieBlocked) { return; }
|
||||||
return false
|
if (this.isInjectionError(...Array.from(args || []))) {
|
||||||
|
this.onInjectionError();
|
||||||
true
|
} else if (this.isAppError(...Array.from(args || []))) {
|
||||||
catch error
|
if (typeof this.previousErrorHandler === 'function') {
|
||||||
Raven.captureMessage 'unsupported/exception', level: 'info', extra: { error: error }
|
this.previousErrorHandler(...Array.from(args || []));
|
||||||
false
|
}
|
||||||
|
this.hideLoadingScreen();
|
||||||
isSingleDoc: ->
|
if (!this.errorNotif) { this.errorNotif = new app.views.Notif('Error'); }
|
||||||
document.body.hasAttribute('data-doc')
|
this.errorNotif.show();
|
||||||
|
}
|
||||||
isMobile: ->
|
},
|
||||||
@_isMobile ?= app.views.Mobile.detect()
|
|
||||||
|
onInjectionError() {
|
||||||
isAndroidWebview: ->
|
if (!this.injectionError) {
|
||||||
@_isAndroidWebview ?= app.views.Mobile.detectAndroidWebview()
|
this.injectionError = true;
|
||||||
|
alert(`\
|
||||||
isInvalidLocation: ->
|
JavaScript code has been injected in the page which prevents DevDocs from running correctly.
|
||||||
@config.env is 'production' and location.host.indexOf(app.config.production_host) isnt 0
|
Please check your browser extensions/addons. `
|
||||||
|
);
|
||||||
$.extend app, Events
|
Raven.captureMessage('injection error', {level: 'info'});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isInjectionError() {
|
||||||
|
// Some browser extensions expect the entire web to use jQuery.
|
||||||
|
// I gave up trying to fight back.
|
||||||
|
return (window.$ !== app._$) || (window.$$ !== app._$$) || (window.page !== app._page) || (typeof $.empty !== 'function') || (typeof page.show !== 'function');
|
||||||
|
},
|
||||||
|
|
||||||
|
isAppError(error, file) {
|
||||||
|
// Ignore errors from external scripts.
|
||||||
|
return file && (file.indexOf('devdocs') !== -1) && (file.indexOf('.js') === (file.length - 3));
|
||||||
|
},
|
||||||
|
|
||||||
|
isSupportedBrowser() {
|
||||||
|
try {
|
||||||
|
const features = {
|
||||||
|
bind: !!Function.prototype.bind,
|
||||||
|
pushState: !!history.pushState,
|
||||||
|
matchMedia: !!window.matchMedia,
|
||||||
|
insertAdjacentHTML: !!document.body.insertAdjacentHTML,
|
||||||
|
defaultPrevented: document.createEvent('CustomEvent').defaultPrevented === false,
|
||||||
|
cssVariables: !!__guardMethod__(CSS, 'supports', o => o.supports('(--t: 0)'))
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var key in features) {
|
||||||
|
var value = features[key];
|
||||||
|
if (!value) {
|
||||||
|
Raven.captureMessage(`unsupported/${key}`, {level: 'info'});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
Raven.captureMessage('unsupported/exception', {level: 'info', extra: { error }});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isSingleDoc() {
|
||||||
|
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);
|
||||||
'&': '&'
|
if (![document.body, document.documentElement].includes(parent)) { top -= $.rect(parent).top; }
|
||||||
'<': '<'
|
fn();
|
||||||
'>': '>'
|
parent.scrollTop = $.offset(el, parent).top - top;
|
||||||
'"': '"'
|
} else {
|
||||||
"'": '''
|
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 = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
'/': '/'
|
'/': '/'
|
||||||
|
};
|
||||||
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,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>→</code> #{doc.release}" if doc.release
|
if (doc.release) { html += ` <code>→</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>→</code> #{doc.release}" if doc.release
|
if (doc.release) { html += ` <code>→</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,81 +1,86 @@
|
|||||||
themeOption = ({ label, value }, settings) -> """
|
/*
|
||||||
<label class="_settings-label _theme-label">
|
* decaffeinate suggestions:
|
||||||
<input type="radio" name="theme" value="#{value}"#{if settings.theme == value then ' checked' else ''}>
|
* DS102: Remove unnecessary code created because of implicit returns
|
||||||
#{label}
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||||
</label>
|
*/
|
||||||
"""
|
const themeOption = ({ label, value }, settings) => `\
|
||||||
|
<label class="_settings-label _theme-label">
|
||||||
|
<input type="radio" name="theme" value="${value}"${settings.theme === value ? ' checked' : ''}>
|
||||||
|
${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>
|
||||||
|
|
||||||
<div class="_settings-fieldset">
|
<div class="_settings-fieldset">
|
||||||
<h2 class="_settings-legend">General:</h2>
|
<h2 class="_settings-legend">General:</h2>
|
||||||
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="_settings-fieldset _hide-on-mobile">
|
<div class="_settings-fieldset _hide-on-mobile">
|
||||||
<h2 class="_settings-legend">Scrolling:</h2>
|
<h2 class="_settings-legend">Scrolling:</h2>
|
||||||
|
|
||||||
<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">↑</code><code class="_label">↓</code><code class="_label">←</code><code class="_label">→</code> to navigate the sidebar.</small>
|
<small>With this checked, use <code class="_label">shift</code> + <code class="_label">↑</code><code class="_label">↓</code><code class="_label">←</code><code class="_label">→</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="_hide-on-mobile">
|
<p class="_hide-on-mobile">
|
||||||
<button type="button" class="_btn" data-action="export">Export</button>
|
<button type="button" class="_btn" data-action="export">Export</button>
|
||||||
<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 = () -> """
|
/*
|
||||||
<p class="_notif-text">
|
* 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">
|
||||||
<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">↓</code> <code class="_label">↑</code> <code class="_label">←</code> <code class="_label">→</code> to navigate the sidebar.<br>
|
Hit ${app.settings.get('arrowScroll') ? '<code class="_label">shift</code> +' : ''} <code class="_label">↓</code> <code class="_label">↑</code> <code class="_label">←</code> <code class="_label">→</code> to navigate the sidebar.<br>
|
||||||
Hit <code class="_label">space / shift space</code>#{if app.settings.get('arrowScroll') then ' or <code class="_label">↓/↑</code>' else ', <code class="_label">alt ↓/↑</code> or <code class="_label">shift ↓/↑</code>'} to scroll the page.
|
Hit <code class="_label">space / shift space</code>${app.settings.get('arrowScroll') ? ' or <code class="_label">↓/↑</code>' : ', <code class="_label">alt ↓/↑</code> or <code class="_label">shift ↓/↑</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,57 +1,81 @@
|
|||||||
#= 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) {
|
||||||
<style>
|
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>
|
||||||
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; }
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
$.ajaxPrefilter(function(opt, opt2, xhr) {
|
$.ajaxPrefilter(function(opt, opt2, xhr) {
|
||||||
if (opt.url.indexOf('http') !== 0) {
|
if (opt.url.indexOf('http') !== 0) {
|
||||||
xhr.abort();
|
xhr.abort();
|
||||||
document.body.innerHTML = "<p><strong>This demo cannot run inside DevDocs.</strong></p>";
|
document.body.innerHTML = "<p><strong>This demo cannot run inside DevDocs.</strong></p>";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</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…
Reference in new issue