// ==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 = $('