Skip to content

Instantly share code, notes, and snippets.

@jmerle
Created March 19, 2017 14:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmerle/0c1aaeeaf60d8f85e5b6d3b11f74278b to your computer and use it in GitHub Desktop.
Save jmerle/0c1aaeeaf60d8f85e5b6d3b11f74278b to your computer and use it in GitHub Desktop.
Adds some functionality to Edabit.
// ==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