Last active
March 4, 2018 06:14
-
-
Save jonchang/3368437 to your computer and use it in GitHub Desktop.
ETI Keyboard Shortcuts
This file contains 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 ETI Keyboard Shortcuts | |
// @namespace shoecream@endoftheinter.net | |
// @description Adds keyboard shortcuts to streamline your shitposting | |
// @match *://boards.endoftheinter.net/* | |
// @match *://endoftheinter.net/* | |
// @grant GM.openInTab | |
// @version 0.17 | |
// ==/UserScript== | |
/* | |
* Acunote Shortcuts. | |
* Javascript keyboard shortcuts mini-framework. | |
* | |
* Copyright (c) 2007-2011 Pluron, Inc. | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining | |
* a copy of this software and associated documentation files (the | |
* "Software"), to deal in the Software without restriction, including | |
* without limitation the rights to use, copy, modify, merge, publish, | |
* distribute, sublicense, and/or sell copies of the Software, and to | |
* permit persons to whom the Software is furnished to do so, subject to | |
* the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be | |
* included in all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
shortcutListener = { | |
listen: true, | |
shortcut: null, | |
combination: '', | |
lastKeypress: 0, | |
clearTimeout: 2000, | |
// Keys we don't listen | |
keys: { | |
KEY_BACKSPACE: 8, | |
KEY_TAB: 9, | |
KEY_ENTER: 13, | |
KEY_SHIFT: 16, | |
KEY_CTRL: 17, | |
KEY_ALT: 18, | |
KEY_SPACE: 32, | |
KEY_LEFT: 37, | |
KEY_UP: 38, | |
KEY_RIGHT: 39, | |
KEY_DOWN: 40, | |
KEY_DELETE: 46, | |
KEY_HOME: 36, | |
KEY_END: 35, | |
KEY_PAGEUP: 33, | |
KEY_PAGEDOWN: 34 | |
}, | |
// adapted map from keycode.js | |
map : { | |
186: 59, // ;: in IE | |
187: 61, // =+ in IE | |
188: 44, // ,< | |
189: 95, // -_ in IE | |
190: 62, // .> | |
191: 47, // /? | |
192: 126, // `~ | |
219: 91, // {[ | |
220: 92, // \| | |
221: 93 // }] | |
}, | |
// adapted map from keycode.js | |
// revesed key-value, since we need to shift char, and they need to un-shift | |
shifted : { | |
59 : 58, // ; -> : | |
61 : 43, // + -> = | |
44 : 60, // , -> < | |
45 : 95, // - -> _ | |
46 : 62, // . -> > | |
47 : 63, // / -> ? | |
192 : 96, // ~ -> ` | |
92 : 124, // \ -> | | |
222 : 39, // 222 -> ' | |
222 : 34, // 222 -> " | |
49 : 33, // 1 -> ! | |
50 : 64, // 2 -> @ | |
51 : 35, // 3 -> # | |
52 : 36, // 4 -> $ | |
53 : 37, // 5 -> % | |
54 : 94, // 6 -> ^ | |
55 : 38, // 7 -> & | |
56 : 42, // 8 -> * | |
57 : 40, // 9 -> ( | |
58 : 41, // 0 -> ) | |
91 : 123, // [ -> { | |
93 : 125 // ] -> } | |
}, | |
UA : { | |
gecko : navigator.userAgent.indexOf('Gecko') != -1, // chrome fits here | |
ie : navigator.userAgent.indexOf('MSIE') != -1, | |
opera : window.opera, | |
webkit : (/Safari|Chrome/.test(navigator.userAgent)), | |
konq : (/Konqueror/.test(navigator.userAgent)) | |
}, | |
init: function() { | |
if (!SHORTCUTS) return false; | |
this.createStatusArea(); | |
// opera and webkit needs special keymap for certain symbols | |
// it produced from shifted map | |
// adapted code from keymap.js | |
if (this.UA.opera) { | |
this.map = {}, reverse = {} | |
for (var key in this.shifted) { | |
reverse[this.shifted[key]] = key; | |
} | |
var unshift = [33, 64, 35, 36, 37, 94, 38, 42, 40, 41, 58, 43, 60, 95, 62, 63, 124, 34]; | |
for (var i = 0; i < unshift.length; ++i) { | |
this.map[unshift[i]] = reverse[unshift[i]]; | |
} | |
} | |
if (this.UA.konq) { | |
this.map[0] = 45; | |
this.map[127] = 46; | |
this.map[45] = 95; | |
} | |
this.setObserver(); | |
}, | |
isInputTarget: function(e) { | |
var target = e.target || e.srcElement, | |
targetNodeName = target.nodeName.toLowerCase(); | |
if (targetNodeName == 'textarea' || targetNodeName == 'select' || | |
(targetNodeName == 'input' && target.type && | |
(target.type.toLowerCase() == 'text' || target.type.toLowerCase() == 'password'))) { | |
return true; | |
} | |
return false; | |
}, | |
stopEvent: function(event) { | |
if (event.preventDefault) { | |
event.preventDefault(); | |
event.stopPropagation(); | |
} else { | |
event.returnValue = false; | |
event.cancelBubble = true; | |
} | |
return false; | |
}, | |
// shortcut notification/status area | |
createStatusArea: function() { | |
this.statusNode = document.createElement('div'); | |
this.statusNode.setAttribute('id', 'shortcut_status'); | |
this.statusNode.style.display = 'none'; | |
document.body.appendChild(this.statusNode); | |
}, | |
showStatus: function() { | |
this.statusNode.style.display = ''; | |
}, | |
hideStatus: function() { | |
this.statusNode.style.display = 'none'; | |
}, | |
showCombination: function() { | |
this.statusNode.innerHTML = this.combination; | |
this.showStatus(); | |
}, | |
// use keypress for Gecko and Opera and keydown for Safari, Chrome and K | |
eventType : function() { | |
return (this.UA.ie || this.UA.webkit || this.UA.konq) ? 'keydown' : 'keypress'; | |
}, | |
setObserver: function() { | |
var listener = function(e) {shortcutListener.keyCollector(e)}; | |
if (document.addEventListener) { | |
document.addEventListener(this.eventType(), listener, false); | |
} else if (document.attachEvent) { | |
document.attachEvent('on' + this.eventType(), listener); | |
} | |
}, | |
// Key press collector. Collects all keypresses into combination | |
// and checks it we have action for it | |
keyCollector: function(e) { | |
// do not listen if listener was explicitly turned off | |
if (!shortcutListener.listen) return true; | |
// do not listen for functional keys | |
var isFunctional = (function(e) { | |
// IE doesn't stop default F-keys actions, so it doesn't need this | |
if (!this.UA.ie) { | |
if (!e.key) { | |
// when only keyCode filled and keyCode is in a range - key is functional | |
return (!e.which && !e.charCode && e.keyCode >= 112 && e.keyCode <= 123) | |
} else { | |
// in modern browsers KeyboardEvent.key is filled with FNN string | |
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key | |
return (/^F\d+$/.test(e.key)) | |
} | |
} | |
}).bind(this) | |
if (isFunctional(e)) return true; | |
// get pressed key code | |
var code = e.which ? e.which : e.keyCode; | |
code = this.map[code] || code; | |
// shifting char manually for desired browsers | |
if (e.shiftKey && (this.UA.ie || this.UA.opera || this.UA.webkit)) { | |
code = this.shifted[code] || code; | |
} | |
// do not listen for Ctrl, Alt, Tab, Space and others | |
for (var key in this.keys) { | |
if (this.UA.gecko) { | |
if ((e.keyCode && e.keyCode == this.keys[key])) return true; | |
} else { | |
if (code == this.keys[key]) return true; | |
} | |
} | |
var letter = null; | |
// adds ability to add shortcut to Esc key | |
if (code != 27) { | |
letter = String.fromCharCode(code).toLowerCase(); | |
} else { | |
letter = 'Esc' | |
} | |
// do not listen in input/select/textarea fields | |
// unless we pressed Esc or the modifier | |
if (this.isInputTarget(e) && !(letter == 'Esc' || e.altKey || e.ctrlKey || e.metaKey)) return true; | |
if (e.shiftKey) { | |
letter = letter.toUpperCase() | |
} | |
if (e.altKey || e.ctrlKey || e.metaKey) { | |
letter = '-' + letter; | |
} | |
if (e.altKey) { | |
letter = 'A' + letter; | |
} | |
if (e.ctrlKey) { | |
letter = 'C' + letter; | |
} | |
if (e.metaKey) { | |
letter = 'M' + letter; | |
} | |
if (shortcutListener.process(letter, e)) { | |
shortcutListener.stopEvent(e); | |
} | |
}, | |
// process keys | |
process: function(letter, e) { | |
// if no combination then start from the begining | |
if (!shortcutListener.shortcut) { shortcutListener.shortcut = SHORTCUTS; } | |
// if unknown letter then say goodbye | |
if (!shortcutListener.shortcut[letter]) return false; | |
if (typeof(shortcutListener.shortcut[letter]) == "function") { | |
shortcutListener.shortcut[letter](e, letter); | |
shortcutListener.clearCombination(); | |
} else { | |
shortcutListener.shortcut = shortcutListener.shortcut[letter]; | |
// append combination | |
shortcutListener.combination = shortcutListener.combination + letter; | |
if (shortcutListener.combination.length > 0) { | |
shortcutListener.showCombination(); | |
// save last keypress timestamp (for autoclear) | |
var d = new Date; | |
shortcutListener.lastKeypress = d.getTime(); | |
// autoclear combination in 2 seconds | |
setTimeout(shortcutListener.clearCombinationOnTimeout, shortcutListener.clearTimeout); | |
}; | |
} | |
return true; | |
}, | |
// clear combination | |
clearCombination: function() { | |
shortcutListener.shortcut = null; | |
shortcutListener.combination = ''; | |
this.hideStatus(); | |
}, | |
clearCombinationOnTimeout: function() { | |
var d = new Date; | |
// check if last keypress was earlier than (now - clearTimeout) | |
// 100ms here is used just to be sure that this will work in superfast browsers :) | |
if ((d.getTime() - shortcutListener.lastKeypress) >= (shortcutListener.clearTimeout - 100)) { | |
shortcutListener.clearCombination(); | |
} | |
} | |
} | |
// scoped querySelector, querySelectorAll | |
function $ (q, scope) { | |
if (!scope) scope = document; | |
return scope.querySelector(q); | |
} | |
function $$ (q, scope) { | |
if (!scope) scope = document; | |
return scope.querySelectorAll(q); | |
} | |
// convert to Array method | |
function $A (c) { | |
if (c instanceof Array) { | |
return c.slice(); | |
} else { | |
var a=[]; | |
for (var b = 0; b < c.length; b++) { | |
a.push(c[b]); | |
} | |
return a; | |
} | |
} | |
// ghetto element creation | |
function $E (tag, attrs, inner) { | |
let dom = document.createElement(tag); | |
if (attrs) { | |
let props = Object.getOwnPropertyNames(attrs); | |
[].forEach.call(props, function(key) dom.setAttribute(key, attrs[key])); | |
} | |
if (inner) dom.innerHTML = inner; | |
return dom; | |
} | |
// various utility methods and information about the current page | |
let DOM = (function () { | |
return { | |
// wrapper around `getBoundingClientRect` that gives absolute positions | |
get_bbox: el => { | |
let bb = el.getBoundingClientRect(); | |
let sy = window.scrollY; | |
let sx = window.scrollX; | |
return { | |
top: bb.top + sy, | |
bottom: bb.bottom + sy, | |
left: bb.left + sx, | |
right: bb.right + sx, | |
height: bb.height, | |
width: bb.width | |
} | |
}, | |
get_valid_rows: function () { | |
switch (DOM.page_type()) { | |
case "topic_list": | |
return [].filter.call($$(".grid tr"), row => !$("th", row)); | |
case "message_list": | |
return $A($$(".message-container")); | |
default: | |
return []; | |
} | |
}, | |
// check for Anonymous -- probably fragile (oh well) | |
get_username: function () { | |
if ($("h2 a[href*='/topics/Anonymous']")) return "Human"; | |
let profile = $(".userbar a[href*='profile.php']"); | |
let userpieces = profile.textContent.split(/\s+/); | |
let username = userpieces.slice(0, -1).join(" "); | |
return username; | |
}, | |
page_type: () => { | |
if ($$('.grid tr').length) return "topic_list"; | |
if ($$('.message-container').length) return "message_list"; | |
return ""; | |
} | |
} | |
})(); | |
// Scroll(elem, [elem2]) => | |
// Scrolls to a specific element `elem`. If `elem2` is provided it | |
// tries to scroll to that instead, unless `elem` would go out of view. | |
let Scroll = (function () { | |
let timers = []; | |
let current_elem = null; | |
let time = .3; // 300 ms | |
let divisions = time * 50; | |
let de = document.documentElement; | |
// returns a signed number that represents the direction and magnitude | |
// of scrolling necessary to put `obj` completely into view. | |
let findDiff = obj => { | |
let elem = DOM.get_bbox(obj); | |
let top = elem.top - de.scrollTop; | |
let bot = elem.bottom - (de.scrollTop + de.clientHeight); | |
return (top ^ bot) < 0 ? 0 : top < 0 ? top : bot; | |
} | |
let ease = p => -1 * (p * p) + 2 * p; | |
return (elem, elem2) => { | |
// don't do anything if we're already scrolling to `elem` | |
// otherwise, clear all the timers to prevent jittery scrolls. | |
if (elem === current_elem) { | |
return; | |
} else { | |
timers.forEach(clearTimeout); | |
timers = []; | |
current_elem = elem; | |
} | |
let diff = findDiff(elem); | |
if (elem2) { | |
// check to see that we're within bounds | |
let test = findDiff(elem2); | |
if (Math.abs(test) <= de.clientHeight) diff = test; | |
} | |
let scrollerFac = distance => () => window.scrollBy(0, distance); | |
let prev = 0; | |
for (let i = 0; i < time; i += time / divisions) { | |
let scrollby = (Math.round(ease(i / time) * diff)); | |
timers.push(setTimeout(scrollerFac(scrollby - prev), i * 1000)); | |
prev = scrollby; | |
} | |
}; | |
})(); | |
function insert_at_caret(text, target) { | |
let s = target.selectionStart; | |
let e = target.selectionEnd; | |
let o = target.value; | |
target.value = o.substring(0, s) + text + o.substring(s); | |
target.setSelectionRange(s + text.length, e + text.length); | |
} | |
function insert_like(target) { | |
let me = DOM.get_username(); | |
let prof = $("a[href*='profile.php']", target); | |
let them = prof && prof.textContent || "Human"; | |
let textarea = $(".quickpost-body textarea"); | |
let img = '<img src="http://i4.endoftheinter.net/i/n/f818de60196ad15c888b7f2140a77744/like.png" />'; | |
insert_at_caret(img + me + " likes " + them + "'s post.", textarea); | |
let submit = $("input[name='post']", textarea.parentNode); | |
//submit.click(); | |
} | |
function onlike(target) { | |
let observer = new MutationObserver(mutations => { | |
mutations.forEach(mutation => { | |
if (mutation.target.classList.contains("quickpost-expanded")) { | |
insert_like(target); | |
observer.disconnect(); | |
} | |
}); | |
}); | |
let config = {attributes: true, attributeFilter: ["class"]} | |
observer.observe($("body"), config); | |
$("a[href*='quote']", target).click(); | |
} | |
// things involving the movement caret | |
let Caret = function () { | |
let rows = DOM.get_valid_rows(); | |
if (rows == false) return; | |
// create the caret's DOM object | |
this.caret_dom = $E('div', {style: "left:0px;top:1px;position:absolute;width:8px"}); | |
this.caret_dom.style.backgroundColor = getComputedStyle($(".menubar")).getPropertyValue("background-color"); | |
document.body.appendChild(this.caret_dom); | |
this.set_initial_position(); | |
} | |
Caret.prototype.set_initial_position = function () { | |
switch (DOM.page_type()) { | |
case "topic_list": | |
[].some.call(DOM.get_valid_rows(), (row, idx) => { | |
if (!$('b', row)) { | |
this.set_caret(idx); | |
return true; | |
} | |
}, this); | |
break; | |
case "message_list": | |
if (location.hash && $(location.hash)) { | |
DOM.get_valid_rows().forEach((row, idx) => { | |
if (row.id == location.hash.substr(1)) { | |
this.set_caret(idx); | |
} | |
}); | |
} else { | |
this.set_caret(0); | |
} | |
break; | |
} | |
} | |
Caret.prototype.set_caret = function (idx) { | |
idx = +idx; | |
let rows = DOM.get_valid_rows(); | |
if (rows == false) return; | |
if (idx < 0 || isNaN(idx)) idx = 0; | |
if (idx > rows.length - 1) idx = rows.length - 1; | |
this.caret_row = rows[idx]; | |
let pos = DOM.get_bbox(this.caret_row); | |
this.caret_dom.style.left = pos.left - 8 + "px"; | |
this.caret_dom.style.top = pos.top + "px"; | |
this.caret_dom.style.height = pos.height + "px"; | |
this.idx = idx; | |
if (idx == 0) { | |
Scroll(this.caret_row, $(".menubar")); // scroll to top | |
} else if (idx == rows.length - 1) { | |
Scroll(this.caret_row, $A($$("small")).pop()); // scroll to bottom | |
} else { | |
Scroll(this.caret_row); | |
} | |
}; | |
Caret.prototype.make_mover = function (distance) { | |
distance = +distance; | |
return () => this.set_caret(this.idx + distance); | |
} | |
// opens the topic under the caret with the function `fn` | |
Caret.prototype.open_with = function (fn) { | |
return () => { | |
// find a span that links to showmessages.php since moneybags uses a span | |
let bookmark = $A($$('td a[href*="showmessages"]', this.caret_row)).pop(); | |
fn((bookmark ? bookmark : $("a", this.caret_row)).href); | |
} | |
} | |
Caret.prototype.quote_post = function() { | |
return () => { // bind to this scope | |
let quoter = $('a[href*="postmsg.php"]', this.caret_row); | |
if (quoter) { quoter.click(); } | |
} | |
} | |
Caret.prototype.like_post = function() { | |
return () => onlike(this.caret_row) | |
} | |
Caret.prototype.maybe_next_page = function() { | |
return () => { | |
let rows = DOM.get_valid_rows(); | |
if (this.idx == rows.length - 1) { | |
let next_page = $("#nextpage"); | |
if (window.getComputedStyle(next_page).display !== "none") { | |
window.location.assign(next_page.href); | |
} | |
} else { | |
Caret.make_mover(10)(); | |
} | |
} | |
} | |
let make_focus_div = title => $E('div', {"class": "focus-div", hidden: true, style: "position:fixed;background-color:black;border-radius:1em;opacity:.8;top:15%;left:15%;width:70%;color:white;padding:1em;text-align:center;z-index:9;font-size:120%;"}, '<h2 style="font-size:200%;color:white">' + title + '</h2>'); | |
let help = (function () { | |
let help_div; | |
function create_help () { | |
let div = make_focus_div('Keyboard shortcuts'); | |
function add(key, text) { | |
let line = $E('div'); | |
line.appendChild($E("span", {style: "width:40%;font-family:monospace;text-align:right;display:inline-block;margin:1px 1%"}, key)); | |
line.appendChild($E("span", {style: "display:inline-block;text-align:left;width:50%;margin:1px 1%"}, "—" + text)); | |
div.appendChild(line); | |
} | |
function header(name) { | |
let line = $E('h3', false, name); | |
div.appendChild(line); | |
} | |
add('?', 'show or hide this help text'); | |
add('Esc', 'close open dialogs (like this one)'); | |
add('j', 'move cursor down'); | |
add('k', 'move cursor up'); | |
add('n', 'move cursor down x10, or next page if at end of message list'); | |
add('p', 'move cursor up x10'); | |
add('o', 'open topic (at topic bookmark, if present)'); | |
add('shift-o', 'open topic in new tab (at topic bookmark)'); | |
add('c', 'create topic'); | |
add('s', 'search topics'); | |
add('r', 'reply to / quote post (message list only)'); | |
add('l', 'like post (message list only)'); | |
add('g then h', 'go to the home page'); | |
add('g then i', 'go to private messages'); | |
add('g then b', 'go to LUE'); | |
add('g then p', 'go to subscribed messages'); | |
add('g then 1, 2, ..., 8', 'go to 1st, 2nd, ..., 8th tag bookmark'); | |
document.body.appendChild(div); | |
return(div); | |
} | |
return function () { | |
if (!help_div) help_div = create_help(); | |
help_div.hidden = !help_div.hidden; | |
} | |
})(); | |
let search = (function () { | |
let search_div; | |
function create_search () { | |
let search = $('div.userbar > a[href*="search.php"]'); | |
if (!search) return; | |
let search_target = search.href.split('=').pop(); | |
let div = make_focus_div('Search ' + $('h1').textContent); | |
let frm = $E('form', {method: "get", action: "/topics/" + search_target}); | |
frm.appendChild($E("input", {type: "text", autofocus: true, name: "q", style: "font-size:24px", id: "huge-search-box-input"})); | |
frm.appendChild($E("input", {type: "submit", value: "Submit", hidden: true})); | |
div.appendChild(frm); | |
document.body.appendChild(div); | |
return div; | |
} | |
return function () { | |
if (!search_div) search_div = create_search(); | |
search_div.hidden = false; | |
$('input', search_div).select(); | |
} | |
})(); | |
let g_shortcuts = (function () { | |
let locs = { | |
'h': '/main.php', | |
'i': '/inbox.php', | |
'b': '/topics/LUE', | |
'p': '/topics/Posted+Starred' | |
}; | |
function jump(ll) { | |
return function () { | |
location.assign(ll) | |
} | |
} | |
function bookmark_jump(num) { | |
// the bookmarks need to be read dynamically because they can change | |
// between page load and when the keyboard shortcut is called. | |
return function () { | |
num = +num - 1; // bookmark 1 is index 0 | |
let bookmarks = $$('#bookmarks > span > a'); | |
if (num in bookmarks) { | |
location.assign(bookmarks[num].href); | |
} | |
} | |
} | |
for (let ii in locs) { | |
if (locs.hasOwnProperty(ii)) locs[ii] = jump(locs[ii]); | |
} | |
for (let ii = 1; ii <= 8; ii++) { | |
locs[ii] = bookmark_jump(ii); | |
} | |
return locs; | |
})(); | |
var SHORTCUTS; | |
function init() { | |
Caret = new Caret(); | |
SHORTCUTS = { | |
'?': help, | |
'j': Caret.make_mover(1), | |
'k': Caret.make_mover(-1), | |
'n': Caret.maybe_next_page(), | |
'p': Caret.make_mover(-10), | |
'o': Caret.open_with(url => location.assign(url)), // can't pass native functions as function parameters | |
'O': Caret.open_with(GM.openInTab), | |
'r': Caret.quote_post(), | |
'l': Caret.like_post(), | |
's': search, | |
'g': g_shortcuts, | |
'c': function () { location.assign($("a[href*='postmsg.php']")) }, | |
'Esc': function () { [].forEach.call($$(".focus-div"), function (el) el.hidden = true)} | |
} | |
shortcutListener.init(); | |
} | |
// start up the keyboard shortcut helper | |
// if it's the home page wait for TOTM to load, otherwise load immediately | |
if ($("#totm")) { | |
let observer = new MutationObserver(mutations => { | |
mutations.forEach(function (mutation) { | |
// melonwolf is an <img> element, not <table> | |
if (mutation.target.id == "totm" && $("table", mutation.target)) { | |
init(); | |
observer.disconnect(); | |
} | |
}); | |
}); | |
let config = {childList: true}; | |
observer.observe($("#totm"), config); | |
} else { | |
init() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment