Created
March 19, 2017 14:08
-
-
Save jmerle/0c1aaeeaf60d8f85e5b6d3b11f74278b to your computer and use it in GitHub Desktop.
Adds some functionality to Edabit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Edabit Toolbox | |
// @namespace EdabitToolbox | |
// @version 1.0.0 | |
// @description Adds some functionality to Edabit. | |
// @author Jasperr | |
// @match https://edabit.com/* | |
// @grant none | |
// ==/UserScript== | |
class Config { | |
static set(key, value) { | |
localStorage.setItem('edabit-toolbox-' + key, JSON.stringify(value)); | |
} | |
static get(key) { | |
const item = localStorage.getItem('edabit-toolbox-' + key); | |
return item !== null ? JSON.parse(item) : null; | |
} | |
static remove(key) { | |
localStorage.removeItem('edabit-toolbox-' + key); | |
} | |
static has(key) { | |
return Config.get(key) !== null; | |
} | |
} | |
class Utils { | |
static getChallengeID() { | |
const path = window.location.pathname; | |
return path.substr(path.lastIndexOf('/') + 1); | |
} | |
// http://stackoverflow.com/a/12475270/5841273 | |
static timeAgo(time) { | |
switch (typeof time) { | |
case 'number': | |
break; | |
case 'string': | |
time = +new Date(time); | |
break; | |
case 'object': | |
if (time.constructor === Date) | |
time = time.getTime(); | |
break; | |
default: | |
time = +new Date(); | |
} | |
const timeFormats = [ | |
[60, 'seconds', 1], | |
[120, '1 minute ago', '1 minute from now'], | |
[3600, 'minutes', 60], | |
[7200, '1 hour ago', '1 hour from now'], | |
[86400, 'hours', 3600], | |
[172800, 'Yesterday', 'Tomorrow'], | |
[604800, 'days', 86400], | |
[1209600, 'Last week', 'Next week'], | |
[2419200, 'weeks', 604800], | |
[4838400, 'Last month', 'Next month'], | |
[29030400, 'months', 2419200], | |
[58060800, 'Last year', 'Next year'], | |
[2903040000, 'years', 29030400], | |
[5806080000, 'Last century', 'Next century'], | |
[58060800000, 'centuries', 2903040000] | |
]; | |
let seconds = (+new Date() - time) / 1000; | |
let token = 'ago'; | |
let listChoice = 1; | |
if (seconds === 0) return 'Just now'; | |
if (seconds < 0) { | |
seconds = Math.abs(seconds); | |
token = 'from now'; | |
listChoice = 2; | |
} | |
let i = 0; | |
let format; | |
while (format = timeFormats[i++]) { | |
if (seconds < format[0]) { | |
if (typeof format[2] === 'string') { | |
return format[listChoice]; | |
} else { | |
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token; | |
} | |
} | |
} | |
return time; | |
} | |
} | |
class CodeEditor { | |
constructor() { | |
this.defaults = { | |
'indentUnit': 2, | |
'autoSave': true, | |
'autoSaveFrequency': 30 | |
}; | |
this.editor = null; | |
this.autoSaveTimeout = null; | |
} | |
run() { | |
this.editor = $('.CodeMirror')[0].CodeMirror; | |
window.CodeMirror = this.editor; | |
this.setLoading(true); | |
this.initDefaults(); | |
this.applySettings(); | |
this.restoreLastSave(); | |
this.injectSettings(); | |
this.injectSaved(); | |
} | |
initDefaults() { | |
for (let key in this.defaults) { | |
if (this.defaults.hasOwnProperty(key)) { | |
if (!Config.has('editor.' + key)) { | |
Config.set('editor.' + key, this.defaults[key]); | |
} | |
} | |
} | |
} | |
applySettings() { | |
this.editor.setOption('indentUnit', Config.get('editor.indentUnit')); | |
this.editor.setOption('tabSize', Config.get('editor.indentUnit')); | |
} | |
restoreLastSave() { | |
if (Config.has('editor.saved.' + Utils.getChallengeID())) { | |
const lastSave = Config.get('editor.saved.' + Utils.getChallengeID()); | |
setTimeout(() => { | |
this.editor.setValue(lastSave); | |
this.editor.execCommand('undo'); | |
this.editor.execCommand('redo'); | |
if (Config.get('editor.autoSave')) { | |
this.save(); | |
} | |
this.setLoading(false); | |
}, 1000); | |
} else { | |
if (Config.get('editor.autoSave')) { | |
this.save(); | |
} | |
this.setLoading(false); | |
} | |
} | |
save() { | |
$('#last-saved-text').text('Saving...'); | |
Config.set('editor.saved.' + Utils.getChallengeID(), this.editor.getValue()); | |
Config.set('editor.lastSaved.' + Utils.getChallengeID(), Date.now()); | |
$('#last-saved-text').text(Utils.timeAgo(Config.get('editor.lastSaved.' + Utils.getChallengeID()))); | |
if (Config.get('editor.autoSave')) { | |
clearTimeout(this.autoSaveTimeout); | |
this.autoSaveTimeout = setTimeout(() => { this.save(); }, Config.get('editor.autoSaveFrequency') * 1000); | |
} | |
} | |
updateLastSaved() { | |
if (Config.has('editor.lastSaved.' + Utils.getChallengeID())) { | |
$('#last-saved-text').text(Utils.timeAgo(Config.get('editor.lastSaved.' + Utils.getChallengeID()))); | |
} else { | |
$('#last-saved-text').text('Never'); | |
} | |
setTimeout(() => { this.updateLastSaved(); }, 1000); | |
} | |
injectSettings() { | |
if ($('#editor-settings-modal').length === 0) { | |
$('body').append(`<div class="ui small modal" id="editor-settings-modal"> | |
<div class="header">Editor Settings</div> | |
<div class="content"> | |
<div class="ui form"> | |
<div class="field"> | |
<label>Spaces per tab</label> | |
<input type="text" id="editor-settings-indentUnit"> | |
</div> | |
<div class="field"> | |
<div class="ui toggle checkbox"> | |
<input type="checkbox" tabindex="0" id="editor-settings-autosave"> | |
<label>Auto-save</label> | |
</div> | |
</div> | |
<div class="field"> | |
<label>Auto-save frequency (seconds)</label> | |
<input type="text" id="editor-settings-autosave-frequency"> | |
</div> | |
</div> | |
</div> | |
<div class="actions"> | |
<button class="ui cancel button">Close</button> | |
<button class="ui green button" id="editor-settings-save">Save</button> | |
</div> | |
</div>`); | |
$('#editor-settings-indentUnit').val(Config.get('editor.indentUnit')); | |
$('#editor-settings-autosave-frequency').val(Config.get('editor.autoSaveFrequency')); | |
$('#editor-settings-autosave').checkbox(); | |
$('#editor-settings-autosave').prop('checked', Config.get('editor.autoSave')); | |
const self = this; | |
$('#editor-settings-save').on('click', function() { | |
$(this).addClass('loading'); | |
Config.set('editor.indentUnit', parseInt($('#editor-settings-indentUnit').val())); | |
Config.set('editor.autoSave', $('#editor-settings-autosave').is(':checked')); | |
Config.set('editor.autoSaveFrequency', parseInt($('#editor-settings-autosave-frequency').val())); | |
if (Config.get('editor.autoSave')) { | |
this.save(); | |
} else { | |
clearTimeout(this.autoSaveTimeout); | |
} | |
self.applySettings(); | |
$(this).removeClass('loading'); | |
$('#editor-settings-modal').modal('hide'); | |
}); | |
} | |
$('#Code').append('<button class="ui left floated button" id="editor-settings-button">Editor Settings</button>'); | |
$('#editor-settings-button').on('click', function() { | |
$('#editor-settings-modal').modal('show'); | |
}); | |
this.editor.addKeyMap({ | |
Tab: function(cm) { | |
cm.replaceSelection(' '.repeat(cm.getOption('indentUnit'))); | |
} | |
}); | |
} | |
injectSaved() { | |
$('.ReactCodeMirror').append('<a class="ui bottom right attached label" id="last-saved-link">Last saved: <span id="last-saved-text">Loading</span></a>'); | |
this.updateLastSaved(); | |
const self = this; | |
$('#last-saved-link').on('click', function() { | |
self.save(); | |
}); | |
$(document).on('keydown', function(e) { | |
if (e.ctrlKey && e.which === 83) { | |
e.preventDefault(); | |
self.save(); | |
return false; | |
} | |
}); | |
} | |
isInjected() { | |
return $('#editor-settings-button').length === 1; | |
} | |
setLoading(state) { | |
if (state) { | |
if ($('#editor-loader').length === 0) { | |
$('.ReactCodeMirror').append('<div class="ui active inverted dimmer" id="editor-loader"><div class="ui loader"></div></div>'); | |
} | |
$('#editor-loader').addClass('active'); | |
} else { | |
$('#editor-loader').removeClass('active'); | |
} | |
} | |
} | |
class EdabitToolbox { | |
constructor() { | |
this.editor = new CodeEditor(); | |
} | |
run() { | |
this.initEvents(); | |
} | |
initEvents() { | |
const self = this; | |
const editorObserver = new MutationObserver(function(mutations) { | |
mutations.forEach(function(mutation) { | |
if (mutation.addedNodes.length !== 0 && $(mutation.addedNodes[0]).hasClass('CodeMirror') && !self.editor.isInjected()) { | |
self.editor.run(); | |
} | |
}); | |
}); | |
editorObserver.observe(document.body, { | |
attributes: true, | |
childList: true, | |
characterData: true, | |
subtree: true | |
}); | |
} | |
} | |
$(function() { | |
const toolbox = new EdabitToolbox(); | |
toolbox.run(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment