// ==UserScript== // @name Tabun fixes // @version 30.11 // @description Несколько улучшений для табуна // // @updateURL https://raw.githubusercontent.com/lxyd/tabun-fixes/master/dist/tabun-fixes.meta.js // @downloadURL https://raw.githubusercontent.com/lxyd/tabun-fixes/master/dist/tabun-fixes.user.js // // @grant none // // @include http://tabun.everypony.ru/* // @match http://tabun.everypony.ru/* // @include http://tabun.everypony.info/* // @match http://tabun.everypony.info/* // @include https://tabun.everypony.ru/* // @match https://tabun.everypony.ru/* // @include https://tabun.everypony.info/* // @match https://tabun.everypony.info/* // @author eeyup // ==/UserScript== (function(document, fn) { var script = document.createElement('script') script.setAttribute("type", "text/javascript") script.textContent = '(' + fn + ')(window, window.document, jQuery)' document.body.appendChild(script) // run the script document.body.removeChild(script) // clean up })(document, function(window, document, $) { // a bit of mimic require.js's way to define a module, but // - no dynamic file loading (!) // - no module hierarchy supported: module name is interpreted as a plain string // - no requirejs plugins, no shim etc var define = (function() { var awaiting = {} , unresolved = {} , modules = {} , nextName = null /** * Define a module with a given name in a way syntactically close to require.js * * Ways to call: * * 1) define('module-name', ['deps', 'list'], module) * 2) define('module-name', module) * * 3) define('next-module-name', []) - speciall call with name and no module is * meant to be used by a build system to * set next-module-name for future calls * 4) define(['deps', 'list'], module) - like #1 but using next-module-name as name * 5) define(module) - like #2 but using next-module-name as name * */ function define(name, deps, module) { if (typeof name == 'string' && Array.isArray(deps) && !module) { nextName = name return } if (typeof name != 'string') { if (!nextName) { throw new Error("Module name no set. Be sure to pass module name or call define('next-name', []) before defining a module") } module = deps deps = name name = nextName } if (typeof module == 'undefined') { module = deps deps = null } if (defined(name) || awaiting[name]) { throw new Error("Module '" + name + "' already defined") } nextName = null // consume next-module-name deps = deps || [] awaiting[name] = { deps: deps, module: module, } deps.forEach(function(d) { if (!defined(d)) { unresolved[d] = unresolved[d] || [] unresolved[d].push(name) } }) tryToDefineAwaiting(name) } function tryToDefineAwaiting(name) { var m = awaiting[name] if (m && m.deps.every(defined)) { modules[name] = createModule(m.module, m.deps) delete awaiting[name] ;(unresolved[name] || []).forEach(tryToDefineAwaiting) delete unresolved[name] } } function createModule(module, deps) { if (typeof module == 'function') { return module.apply(null, deps.map(require)) } else { return module } } function require(name) { if (!defined(name)) { throw new Error("Module '" + name + "' not defined") } return modules[name] } function defined(name) { return typeof modules[name] != 'undefined' } modules['require'] = require return define })() define('app', []) define(['module', 'deep', 'require'], function(Module, deep, require) { function App(id) { this._id = id // для отделения хранимых настроек разных приложений на случай, если их будет несколько this._moduleIds = [] // id модулей в порядке их добавления в приложение /** * _modules содержит словать id -> объект вида { * id: 'module-id', // уникальный идентификатор модуля в приложении * module: {}, // объект модуля, либо добавленный в приложение явно, либо созданный с помощью * // конструктора, полученного через require(id) * installParams: [], // дополнительные параметры, привязанные к установленному модулю, например * // служебная информация для gui-config'а * initArgs: [], // аргументы, которые будут переданы модулю при инициализации (метод init) * } */ this._modules = {} this._started = false // эти данные будут загружены позже this._configs = null // сохранённые конфиги модулей this._enabled = null // сохранённые данные о состоянии модулей (enabled/disabled) this._dirty = {} // id модулей, которые стали dirty: они не делают update или remove до перезагрузки страницы } /** * Добавить модуль в приложение * * @moduleOrId - экземпляр модуля или строковый id в таблице конструкторов модулей * @installParams - параметры для установки модуля: * { * id: 'module-id', // строковый идентификатор модуля. Optional, если в moduleOrId был передан ключ * defaultEnabled: true/false, // включён ли модуль по умолчанию, когда настроек ещё нет. Optional: по умолчанию false * ... // прочие необязательные параметры установки для дальнейшего использования * } * @initArgs... - аргументы для module.init() (передаются после параметра config) */ App.prototype.add = function app_addModule(moduleOrId, installParams/*, initArgs...*/) { this._checkStarted(false) installParams = installParams || {} var module, id if (typeof moduleOrId == "string") { id = installParams.id || moduleOrId module = new (require(moduleOrId))() } else { if (!installParams.id) { throw new Error("Install parameters must contain 'id' field") } id = installParams.id module = moduleOrId } this._moduleIds.push(id) this._modules[id] = { id: id, module: module, installParams: installParams, initArgs: [].slice.call(arguments, 2), } module.onAdded(this, id) return this // chain } /** * Запустить приложение с добавленными модулями */ App.prototype.start = function app_start() { this._checkStarted(false) this._started = true // больше нельзя вызвать add и start, зато можно делать всё остальное var data = this._loadData() this._configs = data.configs this._enabled = data.enabled for (var id in this._modules) { var moduleInfo = this._modules[id] if (this._enabled[id] == null && moduleInfo.installParams.defaultEnabled) { this._enabled[id] = true } var args = [this._configs[id] || null].concat(moduleInfo.initArgs) var cfg = moduleInfo.module.init.apply(moduleInfo.module, args) this._configs[id] = deep.clone(cfg) } this._saveData() for (var id in this._modules) { var moduleInfo = this._modules[id] if (this._enabled[id]) { moduleInfo.module.attach(deep.clone(this._configs[id])) } } return this // chain } App.prototype.getModuleIds = function app_getModuleIds() { return this._moduleIds } App.prototype.getModule = function app_getModule(id) { return this._modules[id].module } App.prototype.getModuleInstallParams = function app_getModuleInstallParams(moduleOrId) { var id = this._getModuleId(moduleOrId) return this._modules[id].installParams } App.prototype.isModuleEnabled = function app_isModuleEnabled(moduleOrId) { this._checkStarted(true) return this._enabled[this._getModuleId(moduleOrId)] } App.prototype.isModuleDirty = function app_isModuleDirty(moduleOrId) { this._checkStarted(true) return this._dirty[this._getModuleId(moduleOrId)] } App.prototype.setModuleEnabled = function app_setModuleEnabled(moduleOrId, enabled) { this._checkStarted(true) enabled = !!enabled var id = this._getModuleId(moduleOrId) if (this.isModuleEnabled(id) == enabled) { return } this._enabled[id] = enabled this._postSaveData() if (this._dirty[id]) { return } try { if (enabled) { this._modules[id].module.attach(deep.clone(this._configs[id])) } else { if (this._modules[id].module.detach() == false) { this._dirty[id] = true } } } catch (err) { this.log("Error " + (enabled ? "enabling" : "disabling") + " module " + id, err) this._dirty[id] = true this._enabled[id] = false } } App.prototype.getModuleConfig = function app_getModuleConfig(moduleOrId) { this._checkStarted(true) var id = this._getModuleId(moduleOrId) return deep.clone(this._configs[id]) } App.prototype.setModuleConfig = function app_setModuleConfig(moduleOrId, config, omitModuleUpdate) { this._checkStarted(true) var id = this._getModuleId(moduleOrId) if (deep.equals(this._configs[id], config)) { return } this._configs[id] = deep.clone(config) this._postSaveData() if (omitModuleUpdate || this._dirty[id] || !this._enabled[id]) { return } try { if (this._modules[id].module.update(config) == false) { this._dirty[id] = true } } catch (err) { this.log("Error updating module " + id, err) this._dirty[id] = true this._enabled[id] = false } } App.prototype.reconfigure = function app_reconfigure() { this._checkStarted(true) data = this._loadData() for (id in data.enabled) { if (this._modules[id]) { this.setModuleEnabled(id, data.enabled[id]) } } for (id in data.configs) { if (this._modules[id]) { this.setModuleConfig(id, data.configs[id]) } } } App.prototype.getId = function app_getId() { return this._id } App.prototype.log = function app_log(/*args*/) { if (console && console.log) { console.log.apply(console, arguments) } } App.prototype._checkStarted = function app_checkStarted(started) { if (this._started != started) { throw new Error(this._started ? "App is already started" : "App is not started yet") } } App.prototype._getModuleId = function app_getModuleId(moduleOrId) { if (typeof moduleOrId == "string") { return moduleOrId } else { return moduleOrId.getId() } } App.prototype._loadData = function app_loadData() { var data try { data = JSON.parse(localStorage.getItem(this._id + '-data') || "{}") || {} data.configs = data.configs || {} data.enabled = data.enabled || {} } catch (err) { this.log(err) data = { configs: {}, enabled: {}, } } return data } App.prototype._saveData = function app_saveData() { delete this._saveDataTimeout var data = { configs: this._configs, enabled: this._enabled, } localStorage.setItem(this._id + '-data', JSON.stringify(data)) } App.prototype._postSaveData = function app_postSaveData() { if (!this._saveDataTimeout) { this._saveDataTimeout = setTimeout(this._saveData.bind(this), 0) } } return App }) define('deep', []) define({ clone: function deepClone(o) { var r, i, keys if (o && typeof o == 'object') { r = Array.isArray(o) ? [] : {} keys = Object.keys(o) for (i = 0; i < keys.length; i++) { r[keys[i]] = deepClone(o[keys[i]]) } } return r || o }, equals: function deepEquals(a, b) { if (!a || typeof a != 'object') { return a === b } if (!b || typeof b != 'object' || Array.isArray(a) != Array.isArray(b)) { return false } var ka = Object.keys(a) if (ka.length != Object.keys(b).length) { return false } for (var i = 0; i < ka.length; i++) { if (!(ka[i] in b)) { return false } if (!deepEquals(a[ka[i]], b[ka[i]])) { return false } } return true }, }) define('hook', []) define(function() { // configuration values before, after, orig, proxies look like: // [{ // "key": obj, // "vals": { // "someFieldName": fieldValueOrArrayOfValues, // ... // }, // }] var cfg = { before: [], after: [], orig: [], proxies: [], } var global = this /** * Add a function to be run before (after) execution some other * function available at root.path * * @param root - optional, global object if ommited * @param path - path to root's field * @param fn - function to execute before (after) root.path * if fn returns false, execution is terminated * @param options { * after : boolean - execute fn after function (default false) * force : boolean - force re-writing a proxy even if already written * (for the case it has been removed somewhere else) * } */ function addHook(root, path, fn, options) { var args = parseArgs.apply(this, arguments) root = args.root path = args.path fn = args.fn options = args.options var cur = getPath(root, path) if (typeof cur != 'function') { throw new Error("Only functions might be hooked") } addCfgElement(options.after ? cfg.after : cfg.before, root, path, fn) var proxy = getCfg(cfg.proxies, root, path) , orig = getCfg(cfg.orig, root, path) var write = false if (!proxy) { // proxy was not written yet orig = cur proxy = createProxy(root, path, cur) write = true } else if (cur !== proxy && options.force) { // proxy was written, but currently there is another function at root[path] // according to the force option, consider proxy to be thrown away orig = cur proxy = createProxy(root, path, cur) write = true } // otherwise consider proxy written and functional (but possibly hidden behind another proxy) if (write) { setCfg(cfg.orig, root, path, orig) setCfg(cfg.proxies, root, path, proxy) setPath(root, path, proxy) } } /** * Remove previously added hook * * @param root - optional, global object if ommited * @param path - path to root's field * @param fn - function to execute before (after) root.path * if fn returns false, execution is terminated * @param options { * after : boolean - execute fn after function (default false) * } */ function removeHook(root, path, fn, options) { var args = parseArgs.apply(this, arguments) root = args.root path = args.path fn = args.fn options = args.options removeCfgElement(options.after ? cfg.after : cfg.before, root, path, fn) if ((getCfg(cfg.after, root, path)||[]).length + (getCfg(cfg.before, root, path)||[]).length == 0) { removeAllHooks(root, path) } } function removeAllHooks(root, path) { var args = parseArgs.apply(this, arguments) root = args.root path = args.path removeCfg(cfg.before, root, path) removeCfg(cfg.after, root, path) var orig = getCfg(cfg.orig, root, path) , proxy = getCfg(cfg.proxies, root, path) , cur = getPath(root, path) // replace function with orig if we are currently on top of proxy chain if (cur === proxy) { setPath(root, path, orig) } // if cur !== proxy, leak a bit of memory by leaving and forgetting // our proxy in root.path proxy chain removeCfg(cfg.proxies, root, path) removeCfg(cfg.orig, root, path) } function parseArgs(root, path, fn, options) { if (typeof root == 'string') { options = fn fn = path path = root root = global } if (!root) { root = global } if (!path) { throw new Error("Path must not be empty") } if (typeof options == 'boolean') { options = { after: options } } else { options = options || {} } var p = path.split('.') for (var i = 0; i < p.length-1; i++) { root = root[p[i]] path = p[i+1] if (!root) { throw new Error("Path not found: " + p[i]) } } return { root: root, path: path, fn: fn, options: options, } } function createProxy(root, path, orig) { return function() { return doProxy(root, path, orig, arguments) } } function doProxy(root, path, orig, args) { var hooks , res , origRes hooks = getCfg(cfg.before, root, path) || [] // if any before-hook returns a value, stop // execution and return that value for (var i = 0; i < hooks.length; i++) { try { res = hooks[i].call(root, args) } catch (err) { return } if (typeof res != 'undefined') { return res } } origRes = orig.apply(root, args) hooks = getCfg(cfg.after, root, path) || [] for (var i = 0; i < hooks.length; i++) { try { res = hooks[i].call(root, origRes, args) } catch (err) { return } if (typeof res != 'undefined') { return res } } return origRes } function getPath(root, path) { return root[path] } function setPath(root, path, val) { root[path] = val } function getCfg(cfg, key, path) { return (findCfgForKey(cfg, key)||{})[path] } function setCfg(cfg, key, path, val) { var vals = findCfgForKey(cfg, key) if (!vals) { vals = {} cfg.push({ key: key, vals: vals, }) } vals[path] = val } function removeCfg(cfg, key, path) { var vals = findCfgForKey(cfg, key) if (!vals) { return } delete vals[path] if (Object.keys(vals).length == 0) { removeCfgForKey(cfg, key) } } function addCfgElement(cfg, key, path, val) { removeCfgElement(cfg, key, path, val) var vals = findCfgForKey(cfg, key) if (!vals) { vals = {} cfg.push({ key: key, vals: vals, }) } vals[path] = vals[path] || [] if (!Array.isArray(vals[path])) { throw new Error("Cannot add to non-array element") } vals[path].push(val) } function removeCfgElement(cfg, key, path, val) { var vals = findCfgForKey(cfg, key) if (!vals || !(path in vals)) { return } var idx = -1 for (var i = 0; i < vals[path].length; i++) { if (vals[path][i] === val) { idx = i break } } if (idx >= 0) { vals[path].splice(idx, 1) } if (vals[path].length == 0) { delete vals[path] } if (Object.keys(vals).length == 0) { removeCfgForKey(cfg, key) } } function findCfgForKey(cfg, key) { for (var i = 0; i < cfg.length; i++) { var o = cfg[i] if (key == o.key) { return o.vals } } } function removeCfgForKey(cfg, key) { var idx = -1 for (var i = 0; i < cfg.length; i++) { var o = cfg[i] if (key == o.key) { idx = i break } } if (idx >= 0) { cfg.splice(idx, 1) } } return { add: addHook, remove: removeHook, removeAll: removeAllHooks, } }) define('module', []) define(function() { function Module() { } Module.prototype.onAdded = function module_onAdded(app, id) { this._app = app this._id = id } Module.prototype.isEnabled = function module_isEnabled() { return this._app.isModuleEnabled(this._id) } Module.prototype.isDirty = function module_isDirty() { return this._app.isModuleDirty(this._id) } Module.prototype.getId = function module_getId() { return this._id } Module.prototype.getApp = function module_getApp() { return this._app } Module.prototype.getInstallParams = function module_getInstallParams() { return this._app.getModuleInstallParams(this._id) } Module.prototype.getConfig = function module_getConfig() { return this._app.getModuleConfig(this._id) } Module.prototype.saveConfig = function module_saveConfig(config) { this._app.setModuleConfig(this._id, config, true) } // Реализации по умолчанию для методов жизненного цикла Module.prototype.getLabel = function module_getLabel() { return this._id } Module.prototype.init = function module_init(config/*, initArgs...*/) { return config } Module.prototype.attach = function module_attach(config) { throw new Error("Not implemented") } Module.prototype.update = function module_update(config) { if (this.detach() == false) { return false } this.attach(config) return true } Module.prototype.detach = function module_detach() { return false } return Module }) define('add-onclick-to-spoilers', []) /** * Этот модуль добавляет пустой атрибут onclik к заголовкам спойлеров, * чтобы VimFx воспринимал спойлеры как кликабельный объект и давал * кликать по ним без мышки, с помощью клавиатуры */ define(['jquery', 'module', 'ls-hook'], function($, Module, lsHook) { function AddOnClickToSpoilersModule() { } AddOnClickToSpoilersModule.prototype = new Module() AddOnClickToSpoilersModule.prototype.attach = function addOnClickToSpoilers_attach(config) { this._hook = this.onDataLoaded.bind(this) lsHook.add('ls_comments_load_after', this._hook) lsHook.add('ls_userfeed_get_more_after', this._hook) this.addAttr() } AddOnClickToSpoilersModule.prototype.detach = function addOnClickToSpoilers_detach() { lsHook.remove('ls_comments_load_after', this._hook) lsHook.remove('ls_userfeed_get_more_after', this._hook) this._hook = null this.removeAttr() } AddOnClickToSpoilersModule.prototype.onDataLoaded = function addOnClickToSpoilers_onDataLoaded() { this.addAttr() } AddOnClickToSpoilersModule.prototype.addAttr = function addOnClickToSpoilers_addAttr() { $('.spoiler-title').each(function() { if (!this.hasAttribute('onclick')) { this.setAttribute('onclick', '') } }) } AddOnClickToSpoilersModule.prototype.removeAttr = function addOnClickToSpoilers_removeAttr() { // в старой версии табуна спойлеры создавались с onclick="return true;" // соответственно, из этих спойлеров удалять атрибут не будем: удаляем только // пустые атрибуты, созданные этим плагином $('.spoiler-title').each(function() { if (this.getAttribute('onclick') == '') { this.removeAttribute('onclick') } }) } return AddOnClickToSpoilersModule }) define('alter-links-to-mirrors', []) define(['module'], function(Module) { function AlterLinksToMirrorsModule() { } AlterLinksToMirrorsModule.prototype = new Module() AlterLinksToMirrorsModule.prototype.init = function alterLinksToMirrors_init(config) { this.host = window.location.host this.mirrors = [ 'tabun.everypony.ru', 'tabun.everypony.info', 'табун.всепони.рф', ].filter(function(h) { return h != this.host }) return config } AlterLinksToMirrorsModule.prototype.getLabel = function alterLinksToMirrors_getLabel() { return "Открывать ссылки на другие зеркала (" + this.mirrors.join(', ') + ") на текущем зеркале (" + this.host + ")" } AlterLinksToMirrorsModule.prototype.attach = function alterLinksToMirrors_attach(config) { this.handler = (function(ev) { this.changeAnchorHrefForClick(closestAnchor(ev.target)) }).bind(this) document.addEventListener('click', this.handler, true) } AlterLinksToMirrorsModule.prototype.detach = function alterLinksToMirrors_detach() { document.removeEventListener('click', this.handler, true) this.handler = null } AlterLinksToMirrorsModule.prototype.changeAnchorHrefForClick = function alterLinksToMirrors_changeAnchorHrefForClick(a) { if (!isAnchorWithHref(a) || this.mirrors.indexOf(a.hostname) < 0 || isSiteRootAnchor(a)) { // Ничего не трогаем, если нам дали не элемент , // либо элемент без атрибута href, // либо ссылку на левый сайт, не являющийся зеркалом табуна // Также ссылки на корни зеркал трогать не будем: // они, вероятно, ведут туда намеренно return } var backup = a.href // на время клика подменим hostname a.hostname = this.host // сразу после клика вернём всё как было setTimeout(function() { a.href = backup }, 0) } function closestAnchor(el) { while (el instanceof HTMLElement) { if (el.nodeName.toUpperCase() == 'A') { return el } else { el = el.parentNode } } return null } function isAnchorWithHref(el) { return el instanceof HTMLElement && el.nodeName.toUpperCase() == 'A' && el.href } function isSiteRootAnchor(a) { return !a.pathname || a.pathname == '/' } return AlterLinksToMirrorsModule }) define('alter-same-page-links', []) define(['module'], function(Module) { var mirrors = [ 'tabun.everypony.ru', 'tabun.everypony.info', 'табун.всепони.рф', ] function AlterSamePageLinksModule() { } AlterSamePageLinksModule.prototype = new Module() AlterSamePageLinksModule.prototype.init = function alterSamePageLinks_init(config) { this.cssClass = this.getApp().getId() + '-same-page-anchor' return config } AlterSamePageLinksModule.prototype.getLabel = function alterSamePageLinks_getLabel() { return "При клике на ссылку на коммент, находящийся на текущей странице, сразу скроллить на него (такие ссылки будут зеленеть при наведении)" } AlterSamePageLinksModule.prototype.attach = function alterSamePageLinks_attach(config) { this.clickHandler = this.onClick.bind(this) this.mouseOverHandler = this.onMouseOver.bind(this) this.mouseOutHandler = this.onMouseOut.bind(this) document.addEventListener('click', this.clickHandler) document.addEventListener('mouseover', this.mouseOverHandler) document.addEventListener('mouseout', this.mouseOutHandler) this.style = $('