// ==UserScript==
// @name 2ch Thread Viewer
// @namespace https://greasyfork.org/users/1009-kengo321
// @version 4
// @description 2ちゃんねるのスレッドビューワ
// @grant GM_getValue
// @grant GM_setValue
// @match http://*.2ch.net/test/read.cgi/*
// @match http://*.bbspink.com/test/read.cgi/*
// @license MIT
// @noframes
// ==/UserScript==
;(function() {
'use strict'
var find = function(predicate, array) {
for (var i = 0; i < array.length; i++) {
var e = array[i]
if (predicate(e)) return e
}
}
var pushIfAbsent = function(array, value) {
if (array.indexOf(value) === -1) array.push(value)
return array
}
var not = function(fn) {
return function() { return !fn.apply(this, arguments) }
}
var array = Function.prototype.call.bind([].slice)
var curry = (function() {
var applyOrRebind = function(func, arity, args) {
var passed = args.concat(array(arguments, 3)).slice(0, arity)
return arity === passed.length
? func.apply(this, passed)
: applyOrRebind.bind(this, func, arity, passed)
}
return function(func) {
return applyOrRebind.bind(this, func, func.length, [])
}
})()
var invoke = curry(function(methodName, args, obj) {
return obj[methodName].apply(obj, args)
})
var equalObj = curry(function(o1, o2) {
return Object.keys(o1)
.concat(Object.keys(o2))
.reduce(pushIfAbsent, [])
.every(function(key) { return o1[key] === o2[key] })
})
var prop = curry(function(propName, obj) {
return obj[propName]
})
var listeners = {
set: function(eventTypes, observer) {
eventTypes.forEach(function(t) {
observer[`_${t}Listener`] = observer[`_${t}`].bind(observer)
})
},
add: function(eventTypes, observer, observable) {
eventTypes.forEach(function(t) {
observable.addEventListener(t, observer[`_${t}Listener`])
})
},
remove: function(eventTypes, observer, observable) {
eventTypes.forEach(function(t) {
observable.removeEventListener(t, observer[`_${t}Listener`])
})
},
addWithoutSet: function(eventTypes, observer, observable) {
eventTypes.forEach(function(t) {
observable.addEventListener(t, observer[`_${t}`].bind(observer))
})
},
}
var Observable = (function() {
var Observable = function() {
this._eventTypeToListeners = Object.create(null)
}
Observable.prototype.addEventListener = function(eventType, listener) {
var m = this._eventTypeToListeners
var v = m[eventType]
if (v) v.push(listener); else m[eventType] = [listener]
}
Observable.prototype.removeEventListener = function(eventType, listener) {
var v = this._eventTypeToListeners[eventType]
if (!v) return
var i = v.indexOf(listener)
if (i >= 0) v.splice(i, 1)
}
Observable.prototype.getEventListeners = function(eventType) {
return this._eventTypeToListeners[eventType] || []
}
Observable.prototype.fireEvent = function(eventType/* , ...args */) {
var v = this._eventTypeToListeners[eventType]
;(v || []).forEach(invoke('apply', [null, array(arguments, 1)]))
}
return Observable
})()
var Response = (function(_super) {
var padZero = function(n) {
return (n <= 9 ? '0' : '') + n
}
var Response = function(objParam) {
_super.call(this)
this.number = objParam.number
this.name = objParam.name
this.mail = objParam.mail
this.jstTime = objParam.jstTime
this.id = objParam.id
this.content = objParam.content
this.anchors = objParam.anchors
this.children = []
this.sameIdResponses = []
this.ngId = false
this.ngParent = false
this.ngWord = false
}
Response.prototype = Object.create(_super.prototype)
Response.prototype.getDateTimeString = function() {
var d = new Date(this.jstTime)
var y = d.getUTCFullYear()
var mon = padZero(d.getUTCMonth() + 1)
var date = padZero(d.getUTCDate())
var h = padZero(d.getUTCHours())
var min = padZero(d.getUTCMinutes())
var s = padZero(d.getUTCSeconds())
return `${y}-${mon}-${date} ${h}:${min}:${s}`
}
Response.prototype.getIndexOfSameIdResponses = function() {
return this.sameIdResponses.indexOf(this)
}
Response.prototype.addChildren = function(children) {
if (children.length === 0) return
;[].push.apply(this.children, children)
children.forEach(invoke('setParent', [this]))
this.fireEvent('childrenAdded', children)
}
Response.prototype.addSameIdResponses = function(sameIdResponses) {
if (sameIdResponses.length === 0) return
;[].push.apply(this.sameIdResponses, sameIdResponses)
this.fireEvent('sameIdResponsesAdded', sameIdResponses)
}
Response.prototype.getNoNgChildren = function() {
return this.children.filter(not(invoke('isNg', [])))
}
Response.prototype.isNg = function() {
return this.ngId || this.ngParent || this.ngWord
}
Response.prototype._setNg = function(propName, getNewVal) {
var preNg = this.isNg()
this[propName] = getNewVal.call(this)
if (preNg !== this.isNg()) this.fireEvent('ngChanged', this.isNg())
}
Response.prototype.setNgIdIfMatchAny = function(ngIds) {
this._setNg('ngId', function() {
return ngIds.some(invoke('match', [this]))
})
}
Response.prototype.setNgIdByAddedNgId = function(addedNgId) {
if (!this.ngId) this.setNgIdIfMatchAny([addedNgId])
}
Response.prototype.setNgIdByRemovedNgId = function(removedNgId) {
if (!this.ngId) return
this._setNg('ngId', function() {
return !removedNgId.match(this)
})
}
Response.prototype.setNgParent = function(ngParent) {
this._setNg('ngParent', function() { return ngParent })
}
Response.prototype.setNgWordIfInclude = function(ngWords) {
this._setNg('ngWord', function() {
return ngWords.some(function(ngWord) {
return this.content.indexOf(ngWord) >= 0
}, this)
})
}
Response.prototype.setNgWordByAddedNgWord = function(addedNgWord) {
if (!this.ngWord) this.setNgWordIfInclude([addedNgWord])
}
Response.prototype.setParent = function(parent) {
this.setNgParent(parent.isNg())
parent.addEventListener('ngChanged', this.setNgParent.bind(this))
}
Response.prototype.hasAsciiArt = function() {
return this.content.includes('\u3000\x20')
}
return Response
})(Observable)
var Parser = (function() {
var number = function(dt) {
return parseInt(dt.firstChild.textContent.split(' ')[0])
}
var name = function(dt) {
return dt.childNodes[1].textContent
}
var mail = function(dt) {
var e = dt.childNodes[1]
return e.tagName === 'FONT'
? ''
: decodeURI(e.href.slice('mailto:'.length))
}
var jstTime = function(dt) {
var t = dt.childNodes[2].textContent
var datetime = /(\d{4})\/(\d{2})\/(\d{2})\(.\)/.exec(t)
if (!datetime) return NaN
var year = datetime[1]
var month = datetime[2] - 1
var date = datetime[3]
var time = /(\d{2}):(\d{2}):(\d{2})/.exec(t)
var hour = time ? time[1] : 0
var minute = time ? time[2] : 0
var seconds = time ? time[3] : 0
return Date.UTC(year, month, date, hour, minute, seconds)
}
var id = function(dt) {
var r = /ID:([\w+/]+)/.exec(dt.childNodes[2].textContent)
return r ? r[1] : ''
}
var content = function(dd) {
return [].map.call(dd.childNodes, function(n) {
return n.tagName === 'BR' ? '\n' : n.textContent
}).join('').replace(/\s+$/, '')
}
var anchors = function(dd, responseNumber) {
return [].filter.call(dd.childNodes, function(n) {
return n.tagName === 'A' && n.textContent.startsWith('>>')
}).map(function(n) {
return parseInt(n.textContent.slice('>>'.length))
}).filter(function(num) {
return num < responseNumber
}).reduce(pushIfAbsent, [])
}
var createResponse = function(dt, dd) {
var num = number(dt)
return new Response({
number: num,
name: name(dt),
mail: mail(dt),
jstTime: jstTime(dt),
id: id(dt),
content: content(dd),
anchors: anchors(dd, num),
})
}
var responses = function(document) {
var dl = document.querySelector('.thread')
var dt = dl.getElementsByTagName('dt')
var dd = dl.getElementsByTagName('dd')
var result = []
for (var i = 0; i < dt.length; i++) {
result.push(createResponse(dt[i], dd[i]))
}
return result
}
var postedResShowElem = function(document) {
return find(function(e) {
return e.textContent === '新着レスの表示'
}, document.getElementsByTagName('center'))
}
var hasThreadClosed = function(document) {
return !postedResShowElem(document)
}
var ads = function(document) {
return [
'.ad--right',
'.js--ad--top',
'.js--ad--bottom',
].reduce(function(a, s) {
;[].push.apply(a, document.querySelectorAll(s))
return a
}, [])
}
var hrAbove = function(e) {
var p = e.previousSibling
if (!p) return null
var hr = p.previousSibling
return hr && hr.tagName === 'HR' ? hr : null
}
var pageSizeElem = function(document) {
return find(function(e) {
return e.textContent.endsWith('KB')
&& e.getAttribute('color') === 'red'
&& e.getAttribute('face') === 'Arial'
}, document.getElementsByTagName('font'))
}
var pushIfTruthy = function(array, value) {
if (value) array.push(value)
}
var elementsToRemove = function(document) {
var result = []
var e = postedResShowElem(document)
if (e) {
result.push(e)
pushIfTruthy(result, hrAbove(e))
}
pushIfTruthy(result, pageSizeElem(document))
return result
}
var postForm = function(document) {
return find(function(f) {
return f.getAttribute('action').startsWith('../test/bbs.cgi')
&& f.method.toUpperCase() === 'POST'
}, document.querySelectorAll('form'))
}
var boardId = function(document) {
var l = document.location
if (!l) return ''
var r = /\/test\/read.cgi\/([^/]+)/.exec(l.pathname)
return r ? r[1] : ''
}
var threadNumber = function(document) {
var l = document.location
if (!l) return 0
var r = /\/test\/read.cgi\/[^/]+\/(\d+)/.exec(l.pathname)
return r ? parseInt(r[1]) : 0
}
var Parser = function() {}
Parser.prototype.parse = function(document) {
return {
responses: responses(document),
threadClosed: hasThreadClosed(document),
ads: ads(document),
elementsToRemove: elementsToRemove(document),
threadRootElement: document.querySelector('.thread'),
postForm: postForm(document),
boardId: boardId(document),
threadNumber: threadNumber(document),
floatedSpan: document.querySelector('body > div > span'),
}
}
return Parser
})()
var ResponseRequest = (function() {
var HTTP_OK = 200
var parseResponseText = function(responseText) {
var d = new DOMParser().parseFromString(responseText, 'text/html')
var r = new Parser().parse(d)
return {
responses: r.responses.slice(1),
threadClosed: r.threadClosed,
}
}
var onload = function(xhr, resolve, reject) {
return function() {
if (xhr.status === HTTP_OK) {
try {
resolve(parseResponseText(xhr.responseText))
} catch (e) {
reject(e)
}
} else {
reject(new Error(xhr.status + ' ' + xhr.statusText))
}
}
}
var ResponseRequest = function() {}
ResponseRequest.prototype.send = function(basePath, startResponseNumber) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.timeout = 10000
xhr.onload = onload(xhr, resolve, reject)
xhr.onerror = function() { reject(new Error('エラー')) }
xhr.ontimeout = function() { reject(new Error('時間切れ')) }
xhr.open('GET', `${basePath}${startResponseNumber - 1}n-`)
xhr.overrideMimeType('text/html; charset=shift_jis')
xhr.send()
})
}
return ResponseRequest
})()
var NgId = (function() {
var msPerDay = 86400000
var truncTime = function(dateTime) {
return dateTime - dateTime % msPerDay
}
var NgId = function(boardId, jstTime, id) {
this.boardId = boardId
this.activeDate = truncTime(jstTime)
this.id = id
}
NgId.prototype.match = function(response) {
return this.id === response.id
&& this.activeDate === truncTime(response.jstTime)
}
NgId.prototype.getActiveDateString = function() {
var d = new Date(this.activeDate)
return `${d.getUTCFullYear()}-${d.getUTCMonth() + 1}-${d.getUTCDate()}`
}
return NgId
})()
var Config = (function(_super) {
var ngWordObj = function(ngWord, boardId, threadNumber) {
var result = {ngWord: ngWord}
if (boardId) result.boardId = boardId
if (threadNumber) result.threadNumber = threadNumber
return result
}
var Config = function(getValue, setValue) {
_super.call(this)
this._getValue = getValue
this._setValue = setValue
}
Config.prototype = Object.create(_super.prototype)
Config.prototype.addNgWord = function(ngWord, boardId, threadNumber) {
var o = ngWordObj(ngWord, boardId, threadNumber)
this._setNgWords(this.getNgWords().concat(o))
this.fireEvent('ngWordAdded', o)
}
Config.prototype.removeNgWord = function(ngWord, boardId, threadNumber) {
if (threadNumber) threadNumber = Math.trunc(threadNumber)
var o = ngWordObj(ngWord, boardId, threadNumber)
this._setNgWords(this.getNgWords().filter(not(equalObj(o))))
this.fireEvent('ngWordRemoved', o)
}
Config.prototype.removeAllNgWords = function() {
this._setNgWords([])
this.fireEvent('allNgWordsRemoved')
}
Config.prototype.getNgWords = function() {
return JSON.parse(this._getValue('ngWords', '[]'))
}
Config.prototype._setNgWords = function(ngWords) {
this._setValue('ngWords', JSON.stringify(ngWords))
}
Config.prototype.addNgId = function(ngId) {
this._setNgIds(this.getNgIds().concat(ngId))
this.fireEvent('ngIdAdded', ngId)
}
Config.prototype.removeNgId = function(ngId) {
this._setNgIds(this.getNgIds().filter(not(equalObj(ngId))))
this.fireEvent('ngIdRemoved', ngId)
}
Config.prototype.removeAllNgIds = function() {
this._setNgIds([])
this.fireEvent('allNgIdsRemoved')
}
Config.prototype.getNgIds = function() {
return JSON.parse(this._getValue('ngIds', '[]')).map(function(o) {
return new NgId(o.boardId, o.activeDate, o.id)
})
}
Config.prototype._setNgIds = function(ngIds) {
this._setValue('ngIds', JSON.stringify(ngIds))
}
Config.prototype.isPageCentering = function() {
return this._getValue('pageCentering', true)
}
Config.prototype.setPageCentering = function(pageCentering) {
this._setValue('pageCentering', pageCentering)
this.fireEvent('pageCenteringChanged', pageCentering)
}
Config.prototype.getPageMaxWidth = function() {
return this._getValue('pageMaxWidth', 600)
}
Config.prototype.setPageMaxWidth = function(pageMaxWidth) {
this._setValue('pageMaxWidth', pageMaxWidth)
this.fireEvent('pageMaxWidthChanged', pageMaxWidth)
}
return Config
})(Observable)
var Thread = (function(_super) {
var putAsArray = function(obj, key, value) {
var array = obj[key]
if (array) array.push(value); else obj[key] = [value]
return obj
}
var putResById = function(obj, res) {
return res.id ? putAsArray(obj, res.id, res) : obj
}
var putResByPassedAnchor = curry(function(res, obj, anchor) {
return putAsArray(obj, anchor, res)
})
var putResByAnchor = function(obj, res) {
return res.anchors.reduce(putResByPassedAnchor(res), obj)
}
var putResByNumber = function(obj, res) {
obj[res.number] = res
return obj
}
var addNewChild = function(responses, addedResponses) {
var all = responses.concat(addedResponses)
var resNumToRes = all.reduce(putResByNumber, {})
var addedAnchors = addedResponses.reduce(putResByAnchor, {})
Object.keys(addedAnchors).forEach(function(anchor) {
var r = resNumToRes[anchor]
if (r) r.addChildren(addedAnchors[anchor])
})
}
var addSameId = curry(function(idToRes, response) {
var sameId = idToRes[response.id]
if (sameId) response.addSameIdResponses(sameId)
})
var addNewSameId = function(responses, addedResponses) {
responses.forEach(addSameId(addedResponses.reduce(putResById, {})))
addedResponses.forEach(
addSameId(responses.concat(addedResponses).reduce(putResById, {})))
}
var eventTypes = [
'ngIdAdded',
'ngIdRemoved',
'allNgIdsRemoved',
'ngWordAdded',
'ngWordRemoved',
'allNgWordsRemoved',
]
var Thread = function(config, boardId, threadNumber) {
_super.call(this)
this._responses = []
this._boardId = boardId
this._threadNumber = threadNumber
this.config = config
listeners.addWithoutSet(eventTypes, this, config)
}
Thread.prototype = Object.create(_super.prototype)
Thread.prototype.addResponses = function(responses) {
responses.forEach(invoke('setNgIdIfMatchAny', [this._getNgIds()]))
responses.forEach(invoke('setNgWordIfInclude', [this._getNgWords()]))
addNewChild(this._responses, responses)
addNewSameId(this._responses, responses)
;[].push.apply(this._responses, responses)
this.fireEvent('responsesAdded', responses)
}
Thread.prototype._testNgWordForValid = function(ngWord) {
var w = ngWord
return (!w.boardId || w.boardId === this._boardId)
&& (!w.threadNumber || w.threadNumber === this._threadNumber)
}
Thread.prototype._getNgWords = function() {
return this.config.getNgWords()
.filter(this._testNgWordForValid.bind(this))
.map(prop('ngWord'))
}
Thread.prototype._getNgIds = function() {
return this.config.getNgIds().filter(function(ngId) {
return this._boardId === ngId.boardId
}, this)
}
Thread.prototype.getLastResponseNumber = function() {
var r = this._responses
var last = r[r.length - 1]
return last ? last.number : -1
}
Thread.prototype.addNgId = function(jstTime, ngId) {
this.config.addNgId(new NgId(this._boardId, jstTime, ngId))
}
Thread.prototype._ngIdAdded = function(addedNgId) {
this._responses.forEach(invoke('setNgIdByAddedNgId', [addedNgId]))
}
Thread.prototype._ngIdRemoved = function(removedNgId) {
if (this._boardId === removedNgId.boardId) {
this._responses.forEach(invoke('setNgIdByRemovedNgId', [removedNgId]))
}
}
Thread.prototype._allNgIdsRemoved = function() {
this._responses.forEach(invoke('setNgIdIfMatchAny', [[]]))
}
Thread.prototype.addNgWordForBoard = function(ngWord) {
this.config.addNgWord(ngWord, this._boardId)
}
Thread.prototype.addNgWordForThread = function(ngWord) {
this.config.addNgWord(ngWord, this._boardId, this._threadNumber)
}
Thread.prototype._ngWordAdded = function(addedNgWord) {
this._responses
.forEach(invoke('setNgWordByAddedNgWord', [addedNgWord.ngWord]))
}
Thread.prototype._ngWordRemoved = function() {
this._responses
.forEach(invoke('setNgWordIfInclude', [this._getNgWords()]))
}
Thread.prototype._allNgWordsRemoved = function() {
this._responses.forEach(invoke('setNgWordIfInclude', [[]]))
}
return Thread
})(Observable)
var ResponseView = (function() {
var eventTypes = ['childrenAdded', 'sameIdResponsesAdded', 'ngChanged']
var ResponseView = function(document, response, root) {
this._doc = document
this._response = response
this._factory = new ResponseView.Factory(document, response, root)
this.rootElement = this._factory.createResponseElement()
this._childResponseViews = []
this._sameIdResponseViews = []
listeners.set(eventTypes, this)
listeners.add(eventTypes, this, this._response)
this._childNgChangedListener = this._childNgChanged.bind(this)
this._addListenersToChildren(response.children)
}
ResponseView.new = curry(function(document, response) {
return new ResponseView(document, response)
})
ResponseView.prototype._childrenAdded = function(addedChildren) {
this._addListenersToChildren(addedChildren)
this._updateResNumElem()
this._appendAddedChildren(addedChildren)
}
ResponseView.prototype._sameIdResponsesAdded = function(addedSameId) {
this._updateIdElem()
this._appendAddedSameId(addedSameId)
}
ResponseView.prototype._ngChanged = function(ng) {
if (ng) this._destroyAllResponseViews()
this._replaceRootWithNew()
}
ResponseView.prototype._isChildrenVisibleAndAllNg = function() {
return Boolean(this.rootElement.querySelector('.children'))
&& this._response.getNoNgChildren().length === 0
}
ResponseView.prototype._childNgChanged = function() {
this._updateResNumElem()
if (this._isChildrenVisibleAndAllNg()) this._destroyChildren()
}
ResponseView.prototype._addListenersToChildren = function(children) {
children.forEach(invoke('addEventListener'
, ['ngChanged', this._childNgChangedListener]))
}
ResponseView.prototype._removeListenersFromChildren = function() {
this._response.children
.forEach(invoke('removeEventListener'
, ['ngChanged', this._childNgChangedListener]))
}
ResponseView.prototype._removeListenersFromResponse = function() {
listeners.remove(eventTypes, this, this._response)
}
ResponseView.prototype._updateResNumElem = function() {
if (this._response.isNg()) return
var numElem = this.rootElement.querySelector('header .number')
if (numElem) this._factory.updateHeaderNumClass(numElem)
}
ResponseView.prototype._appendAdded = function(added, propName, selector) {
var views = added.map(ResponseView.new(this._doc))
;[].push.apply(this[propName], views)
var toggled = this.rootElement.querySelector(selector)
views.map(prop('rootElement')).forEach(toggled.appendChild.bind(toggled))
}
ResponseView.prototype._appendAddedChildren = function(addedChildren) {
if (this._childResponseViews.length) {
this._appendAdded(addedChildren, '_childResponseViews', '.children')
}
}
ResponseView.prototype._appendAddedSameId = function(addedSameId) {
if (this._sameIdResponseViews.length) {
this._appendAdded(addedSameId, '_sameIdResponseViews', '.sameId')
}
}
ResponseView.prototype._getIdValElem = function() {
return this.rootElement.querySelector('header .id .value')
}
ResponseView.prototype._updateIdValueElem = function() {
this._factory.updateIdValClass(this._getIdValElem())
}
ResponseView.prototype._hasIdCountElem = function() {
return Boolean(this.rootElement.querySelector('header .id .count'))
}
ResponseView.prototype._insertIdCountElem = function() {
var e = this._getIdValElem()
e.parentNode.insertBefore(this._factory.createIdCount(), e.nextSibling)
}
ResponseView.prototype._updateIdTotalElem = function() {
var e = this.rootElement.querySelector('header .id .count .total')
e.textContent = this._response.sameIdResponses.length
}
ResponseView.prototype._updateIdElem = function() {
if (this._response.isNg()) return
this._updateIdValueElem()
if (this._hasIdCountElem()) {
this._updateIdTotalElem()
} else {
this._insertIdCountElem()
}
}
ResponseView.prototype._replaceRootWithNew = function() {
var old = this.rootElement
this.rootElement = this._factory.createResponseElement()
var p = old.parentNode
if (p) p.replaceChild(this.rootElement, old)
}
ResponseView.prototype._destroyResponseViews = function(propName) {
this[propName].forEach(function(v) {
v._removeListenersFromResponse()
v._removeListenersFromChildren()
v._destroyAllResponseViews()
})
this[propName] = []
}
ResponseView.prototype._destroyAllResponseViews = function() {
this._destroyResponseViews('_childResponseViews')
this._destroyResponseViews('_sameIdResponseViews')
}
ResponseView.prototype._newSubResponseViews = function(propName) {
return this._response[propName].map(ResponseView.new(this._doc))
}
ResponseView.prototype._insertAfterContent = function(views, methodName) {
var responseElems = views.map(prop('rootElement'))
var toggledElem = this._factory[methodName](responseElems)
var contentElem = this.rootElement.querySelector('.content')
this.rootElement.insertBefore(toggledElem, contentElem.nextSibling)
}
ResponseView.prototype._destroyChildren = function() {
this.rootElement.querySelector('.children').remove()
this._destroyResponseViews('_childResponseViews')
}
ResponseView.prototype.toggleChildren = function() {
if (this._response.children.length === 0) return
var e = this.rootElement.querySelector('.children')
if (e) {
this._destroyChildren()
} else {
var views = this._newSubResponseViews('children')
this._childResponseViews = views
this._insertAfterContent(views, 'createChildrenElement')
}
}
ResponseView.prototype.toggleSameId = function() {
if (this._response.sameIdResponses.length < 2) return
var e = this.rootElement.querySelector('.sameId')
if (e) {
e.remove()
this._destroyResponseViews('_sameIdResponseViews')
} else {
var views = this._newSubResponseViews('sameIdResponses')
this._sameIdResponseViews = views
this._insertAfterContent(views, 'createSameIdElement')
}
}
ResponseView.prototype._getResponseViewByChild = function(elem, select) {
var resViews = this._childResponseViews.concat(this._sameIdResponseViews)
for (var i = 0; i < resViews.length; i++) {
var v = resViews[i]._getResponseViewBy(select, elem)
if (v) return v
}
return null
}
ResponseView.prototype._getResponseViewBy = function(elem, select) {
return select(this.rootElement) === elem
? this
: this._getResponseViewByChild(select, elem)
}
ResponseView.prototype.getResponseViewByNumElem = function(numElem) {
return this._getResponseViewBy(numElem, function(rootElem) {
return rootElem.querySelector('header .number')
})
}
ResponseView.prototype.getResponseViewByIdValElem = function(idValElem) {
return this._getResponseViewBy(idValElem, function(rootElem) {
var h = rootElem.querySelector('header')
return h ? h.querySelector('.id .value') : null
})
}
return ResponseView
})()
ResponseView.Factory = (function() {
var replaceMatchedByCreatedElem = function(textNode, regExp, createElem) {
var document = textNode.ownerDocument
var result = document.createDocumentFragment()
var begin = 0
var text = textNode.nodeValue
for (var r; r = regExp.exec(text);) {
result.appendChild(document.createTextNode(text.slice(begin, r.index)))
result.appendChild(createElem(r[0]))
begin = regExp.lastIndex
}
result.appendChild(document.createTextNode(text.slice(begin)))
result.normalize()
return result
}
var replaceTextNodeIfMatched = function(node, regExp, createElem) {
;[].filter.call(node.childNodes, function(child) {
return child.nodeType === Node.TEXT_NODE
}).forEach(function(textNode) {
var newNode = replaceMatchedByCreatedElem(textNode, regExp, createElem)
node.replaceChild(newNode, textNode)
}, this)
return node
}
var replaceHash = function(location, hash) {
return '//' + location.host + location.pathname + location.search + hash
}
var createBR = function(document) {
return function() {
return document.createElement('br')
}
}
var createAnchor = curry(function(document, responseNumber, matchedText) {
var matchedNumber = parseInt(matchedText.slice(2))
if (matchedNumber >= responseNumber) {
return document.createTextNode(matchedText)
}
var result = document.createElement('a')
result.href = replaceHash(document.location, '#res' + matchedNumber)
result.textContent = matchedText
return result
})
var createLink = curry(function(document, matchedText) {
var result = document.createElement('a')
result.href = matchedText[0] === 'h' ? matchedText : 'h' + matchedText
result.target = '_blank'
result.textContent = matchedText
return result
})
var Factory = function(document, response, root) {
this._doc = document
this._response = response
this._root = root
}
Factory.prototype._createIdTotal = function() {
var result = this._doc.createElement('span')
result.className = 'total'
result.textContent = this._response.sameIdResponses.length
return result
}
Factory.prototype._createIdIndex = function() {
var i = this._response.getIndexOfSameIdResponses() + 1
return this._doc.createTextNode('(' + i + '/')
}
Factory.prototype.updateIdValClass = function(idValElem) {
var n = this._response.sameIdResponses.length
var l = idValElem.classList
if (n >= 2) l.add('sameIdExist')
if (n >= 5) l.add('sameIdExist5')
}
Factory.prototype._createIdVal = function() {
var result = this._doc.createElement('span')
result.className = 'value'
result.textContent = this._response.id
this.updateIdValClass(result)
return result
}
Factory.prototype._createIdNgButton = function() {
var result = this._doc.createElement('span')
result.className = 'ngButton'
result.title = 'NGID'
result.textContent = '[×]'
result.dataset.id = this._response.id
result.dataset.jstTime = this._response.jstTime
return result
}
Factory.prototype.createIdCount = function() {
var result = this._doc.createDocumentFragment()
if (this._response.sameIdResponses.length >= 2) {
var count = this._doc.createElement('span')
count.className = 'count'
count.appendChild(this._createIdIndex())
count.appendChild(this._createIdTotal())
count.appendChild(this._doc.createTextNode(')'))
result.appendChild(count)
}
return result
}
Factory.prototype._createId = function() {
var result = this._doc.createDocumentFragment()
if (this._response.id) {
var id = this._doc.createElement('span')
id.className = 'id'
id.appendChild(this._createIdVal())
id.appendChild(this.createIdCount())
id.appendChild(this._createIdNgButton())
result.appendChild(id)
}
return result
}
Factory.prototype.updateHeaderNumClass = function(numElem) {
var childNum = this._response.getNoNgChildren().length
numElem.classList[childNum >= 1 ? 'add' : 'remove']('hasChild')
numElem.classList[childNum >= 3 ? 'add' : 'remove']('hasChild3')
}
Factory.prototype._createHeaderNum = function() {
var result = this._doc.createElement('span')
result.className = 'number'
this.updateHeaderNumClass(result)
result.textContent = this._response.number
return result
}
Factory.prototype._getHeaderNameText = function() {
var n = this._response.name
var i = n.indexOf('@')
return i >= 0 ? n.slice(0, i) : n
}
Factory.prototype._createHeaderName = function() {
var result = this._doc.createElement('span')
result.className = 'name'
result.textContent = this._getHeaderNameText()
return result
}
Factory.prototype._getHeaderMailText = function() {
var m = this._response.mail
return `[${m === 'sage' ? '↓' : m}]`
}
Factory.prototype._createHeaderMail = function() {
var result = this._doc.createDocumentFragment()
if (this._response.mail) {
var e = this._doc.createElement('span')
e.className = 'mail'
e.textContent = this._getHeaderMailText()
result.appendChild(e)
}
return result
}
Factory.prototype._createHeaderTime = function() {
var result = this._doc.createDocumentFragment()
if (!Number.isNaN(this._response.jstTime)) {
var datetime = this._doc.createElement('time')
datetime.textContent = this._response.getDateTimeString()
result.appendChild(datetime)
}
return result
}
Factory.prototype._createHeader = function() {
var result = this._doc.createElement('header')
result.appendChild(this._createHeaderNum())
result.appendChild(this._createHeaderName())
result.appendChild(this._createHeaderMail())
result.appendChild(this._createHeaderTime())
result.appendChild(this._createId())
return result
}
Factory.prototype._createContent = function() {
var f = this._doc.createDocumentFragment()
f.appendChild(this._doc.createTextNode(this._response.content))
replaceTextNodeIfMatched(f, /\n/g, createBR(this._doc))
replaceTextNodeIfMatched(f
, />>\d+/g
, createAnchor(this._doc, this._response.number))
replaceTextNodeIfMatched(f, /h?ttps?:\/\/\S+/g, createLink(this._doc))
var result = this._doc.createElement('div')
result.className = 'content'
if (this._response.hasAsciiArt()) result.classList.add('asciiArt')
result.appendChild(f)
return result
}
Factory.prototype.createResponseElement = function() {
var result = this._doc.createElement('article')
if (this._response.isNg()) {
result.classList.add('ng')
result.appendChild(this._createNgResponse())
} else {
result.appendChild(this._createHeader())
result.appendChild(this._createContent())
}
if (this._root) result.id = 'res' + this._response.number
return result
}
Factory.prototype._createNgResponse = function() {
var text = this._response.number + ' あぼーん'
return this._doc.createTextNode(text)
}
Factory.prototype.createChildrenElement = function(responseElements) {
var result = this._doc.createElement('div')
result.className = 'children'
responseElements.forEach(result.appendChild.bind(result))
return result
}
Factory.prototype.createSameIdElement = function(responseElements) {
var result = this._doc.createElement('div')
result.className = 'sameId'
responseElements.forEach(result.appendChild.bind(result))
return result
}
return Factory
})()
var ConfigView = (function() {
var addTH = curry(function(row, text) {
var doc = row.ownerDocument
var th = doc.createElement('th')
th.textContent = text
row.appendChild(th)
})
var setTHead = function(tHead, texts) {
texts.forEach(addTH(tHead.insertRow()))
}
var addCell = function(row, text) {
var result = row.insertCell()
result.textContent = text
return result
}
var addDelCell = function(row) {
var result = addCell(row, '削除')
result.className = 'removeButton'
return result
}
var getDelCell = function(row) {
return row.cells[3]
}
var ConfigView = function(document, config) {
this._doc = document
this._config = config
this.rootElement = this._createRootElem()
listeners.set(this._eventTypes, this)
listeners.add(this._eventTypes, this, config)
}
ConfigView.prototype.destroy = function() {
this.rootElement.remove()
listeners.remove(this._eventTypes, this, this._config)
}
ConfigView.prototype._createRootElem = function() {
var result = this._doc.createElement('div')
result.className = 'config'
result.appendChild(this._createRootChild())
return result
}
ConfigView.View = (function(_super) {
var View = function(document, config) {
_super.call(this, document, config)
}
View.toggleText = '表示'
View.toggleClass = 'viewToggle'
View.prototype = Object.create(_super.prototype)
View.prototype.constructor = View
View.prototype._eventTypes = []
View.prototype._createCenteringP = function() {
var checkbox = this._doc.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = this._config.isPageCentering()
var label = this._doc.createElement('label')
label.appendChild(checkbox)
label.appendChild(this._doc.createTextNode('ページ中央に配置'))
var result = this._doc.createElement('p')
result.className = 'centering'
result.appendChild(label)
return result
}
View.prototype._createMaxWidthP = function() {
var input = this._doc.createElement('input')
input.type = 'number'
input.valueAsNumber = this._config.getPageMaxWidth()
var label = this._doc.createElement('label')
label.appendChild(this._doc.createTextNode('最大幅'))
label.appendChild(input)
label.appendChild(this._doc.createTextNode('px'))
var result = this._doc.createElement('p')
result.className = 'maxWidth'
result.appendChild(label)
return result
}
View.prototype._createRootChild = function() {
var h2 = this._doc.createElement('h2')
h2.textContent = '表示'
var result = this._doc.createElement('section')
result.className = 'viewSection'
result.appendChild(h2)
result.appendChild(this._createCenteringP())
result.appendChild(this._createMaxWidthP())
return result
}
View.prototype.isPageCenteringChecked = function() {
return this.rootElement
.querySelector('.viewSection .centering label input')
.checked
}
View.prototype.getPageMaxWidthValue = function() {
return this.rootElement
.querySelector('.viewSection .maxWidth label input')
.valueAsNumber
}
return View
})(ConfigView)
ConfigView.NgWord = (function(_super) {
var newNgWordByDataset = function(dataset) {
return {
boardId: dataset.boardId,
threadNumber: dataset.threadNumber
? parseInt(dataset.threadNumber)
: undefined,
ngWord: dataset.ngWord,
}
}
var addNgWordDelCell = function(row, ngWord) {
var c = addDelCell(row)
var s = c.dataset
s.ngWord = ngWord.ngWord
if (ngWord.boardId) s.boardId = ngWord.boardId
if (ngWord.threadNumber) s.threadNumber = ngWord.threadNumber
}
var setNgWordRow = function(row, ngWord) {
addCell(row, ngWord.boardId)
addCell(row, ngWord.threadNumber)
addCell(row, ngWord.ngWord)
addNgWordDelCell(row, ngWord)
}
var setNgWordTBody = function(tBody, ngWords) {
ngWords.slice().reverse().forEach(function(ngWord) {
setNgWordRow(tBody.insertRow(), ngWord)
})
}
var NgWord = function(document, config) {
_super.call(this, document, config)
}
NgWord.toggleText = 'NGワード'
NgWord.toggleClass = 'ngWordToggle'
NgWord.prototype = Object.create(_super.prototype)
NgWord.prototype.constructor = NgWord
NgWord.prototype._eventTypes = [
'ngWordAdded',
'ngWordRemoved',
'allNgWordsRemoved',
]
NgWord.prototype._createOption = function(value, text) {
var result = this._doc.createElement('option')
result.value = value
result.textContent = text
return result
}
NgWord.prototype._createTargetSelect = function() {
var result = this._doc.createElement('select')
result.appendChild(this._createOption('thread', 'このスレッド'))
result.appendChild(this._createOption('board', 'この板'))
result.appendChild(this._createOption('all', '全体'))
return result
}
NgWord.prototype._createNgWordInput = function() {
var result = this._doc.createElement('input')
result.className = 'ngWordInput'
return result
}
NgWord.prototype._createNgWordAddButton = function() {
var result = this._doc.createElement('input')
result.className = 'addButton'
result.type = 'button'
result.value = '追加'
return result
}
NgWord.prototype._createNgWordAddP = function() {
var result = this._doc.createElement('p')
result.className = 'add'
result.appendChild(this._createTargetSelect())
result.appendChild(this._createNgWordInput())
result.appendChild(this._createNgWordAddButton())
return result
}
NgWord.prototype._createNgWordTable = function() {
var result = this._doc.createElement('table')
setTHead(result.createTHead(), ['板', 'スレッド', 'NGワード', ''])
setNgWordTBody(result.createTBody(), this._config.getNgWords())
return result
}
NgWord.prototype._createRemoveAllButton = function() {
var result = this._doc.createElement('span')
result.className = 'removeAllButton'
result.textContent = '[すべて削除]'
return result
}
NgWord.prototype._createSectionHeader = function(text) {
var result = this._doc.createElement('h2')
result.appendChild(this._doc.createTextNode(text))
result.appendChild(this._createRemoveAllButton())
return result
}
NgWord.prototype._createRootChild = function() {
var result = this._doc.createElement('section')
result.className = 'ngWordSection'
result.appendChild(this._createSectionHeader('NGワード'))
result.appendChild(this._createNgWordAddP())
result.appendChild(this._createNgWordTable())
return result
}
NgWord.prototype._getNgWordTable = function() {
return this.rootElement.querySelector('.ngWordSection table')
}
NgWord.prototype._ngWordAdded = function(addedNgWord) {
var r = this._getNgWordTable().tBodies[0].insertRow(0)
setNgWordRow(r, addedNgWord)
}
NgWord.prototype._ngWordRemoved = function(removedNgWord) {
var rows = this._getNgWordTable().tBodies[0].rows
;[].filter.call(rows, function(row) {
var ngWord = newNgWordByDataset(getDelCell(row).dataset)
return equalObj(ngWord, removedNgWord)
}).forEach(invoke('remove', []))
}
NgWord.prototype._allNgWordsRemoved = function() {
var newTable = this._createNgWordTable()
var oldTable = this._getNgWordTable()
oldTable.parentNode.replaceChild(newTable, oldTable)
}
NgWord.prototype._getNgWordInput = function() {
return this.rootElement.querySelector('.ngWordSection .add .ngWordInput')
}
NgWord.prototype.getNgWordInputValue = function() {
return this._getNgWordInput().value.trim()
}
NgWord.prototype.clearNgWordInputValue = function() {
this._getNgWordInput().value = ''
}
NgWord.prototype.getNgWordAddTarget = function() {
return this.rootElement.querySelector('.ngWordSection .add select').value
}
return NgWord
})(ConfigView)
ConfigView.NgId = (function(_super) {
var addNgIdDelCell = function(row, ngId) {
var c = addDelCell(row)
var s = c.dataset
s.boardId = ngId.boardId
s.activeDate = ngId.activeDate
s.id = ngId.id
}
var newNgIdByDataset = function(dataset) {
return new NgId(dataset.boardId, dataset.activeDate, dataset.id)
}
var setNgIdRow = function(row, ngId) {
addCell(row, ngId.boardId)
addCell(row, ngId.getActiveDateString())
addCell(row, ngId.id)
addNgIdDelCell(row, ngId)
}
var setNgIdTBody = function(tBody, ngIds) {
ngIds.slice().reverse().forEach(function(ngId) {
setNgIdRow(tBody.insertRow(), ngId)
})
}
var NgIdView = function(document, config) {
_super.call(this, document, config)
}
NgIdView.toggleText = 'NGID'
NgIdView.toggleClass = 'ngIdToggle'
NgIdView.prototype = Object.create(_super.prototype)
NgIdView.prototype.constructor = NgIdView
NgIdView.prototype._eventTypes = [
'ngIdAdded',
'ngIdRemoved',
'allNgIdsRemoved',
]
NgIdView.prototype._createRemoveAllButton = function() {
var result = this._doc.createElement('span')
result.className = 'removeAllButton'
result.textContent = '[すべて削除]'
return result
}
NgIdView.prototype._createSectionHeader = function(text) {
var result = this._doc.createElement('h2')
result.appendChild(this._doc.createTextNode(text))
result.appendChild(this._createRemoveAllButton())
return result
}
NgIdView.prototype._createNgIdTable = function() {
var result = this._doc.createElement('table')
setTHead(result.createTHead(), ['板', '有効日', 'ID', ''])
setNgIdTBody(result.createTBody(), this._config.getNgIds())
return result
}
NgIdView.prototype._createRootChild = function() {
var result = this._doc.createElement('section')
result.className = 'ngIdSection'
result.appendChild(this._createSectionHeader('NGID'))
result.appendChild(this._createNgIdTable())
return result
}
NgIdView.prototype._getNgIdTable = function() {
return this.rootElement.querySelector('.ngIdSection table')
}
NgIdView.prototype._ngIdAdded = function(addedNgId) {
var r = this._getNgIdTable().tBodies[0].insertRow(0)
setNgIdRow(r, addedNgId)
}
NgIdView.prototype._ngIdRemoved = function(removedNgId) {
var rows = this._getNgIdTable().tBodies[0].rows
;[].filter.call(rows, function(row) {
var ngId = newNgIdByDataset(getDelCell(row).dataset)
return equalObj(ngId, removedNgId)
}).forEach(invoke('remove', []))
}
NgIdView.prototype._allNgIdsRemoved = function() {
var newTable = this._createNgIdTable()
var oldTable = this._getNgIdTable()
oldTable.parentNode.replaceChild(newTable, oldTable)
}
return NgIdView
})(ConfigView)
return ConfigView
})()
var ThreadView = (function() {
var createTopBar = function(document) {
var ngWordToggle = document.createElement('span')
ngWordToggle.className = ConfigView.NgWord.toggleClass
ngWordToggle.textContent = `${ConfigView.NgWord.toggleText}▼`
var ngIdToggle = document.createElement('span')
ngIdToggle.className = ConfigView.NgId.toggleClass
ngIdToggle.textContent = `${ConfigView.NgId.toggleText}▼`
var viewToggle = document.createElement('span')
viewToggle.className = ConfigView.View.toggleClass
viewToggle.textContent = `${ConfigView.View.toggleText}▼`
var result = document.createElement('div')
result.className = 'topBar'
result.appendChild(ngWordToggle)
result.appendChild(ngIdToggle)
result.appendChild(viewToggle)
return result
}
var createBottomBar = function(document) {
var reloadButton = document.createElement('input')
reloadButton.type = 'button'
reloadButton.value = '新着レスの取得'
reloadButton.className = 'reloadButton'
var reloadMessage = document.createElement('span')
reloadMessage.className = 'reloadMessage'
var result = document.createElement('div')
result.className = 'bottomBar'
result.appendChild(reloadButton)
result.appendChild(reloadMessage)
return result
}
var createRoot = function(document) {
var main = document.createElement('div')
main.className = 'main'
var result = document.createElement('div')
result.className = 'threadView'
result.appendChild(createTopBar(document))
result.appendChild(main)
result.appendChild(createBottomBar(document))
return result
}
var ThreadView = function(document, thread) {
this.doc = document
this._thread = thread
this.rootElement = createRoot(document)
this._responseViews = []
this.configView = null
this.responsePostForm = null
this._setPageCentering(thread.config.isPageCentering())
thread.addEventListener('responsesAdded', this._responsesAdded.bind(this))
thread.config.addEventListener('pageCenteringChanged'
, this._setPageCentering.bind(this))
thread.config.addEventListener('pageMaxWidthChanged'
, this._setPageMaxWidth.bind(this))
}
ThreadView.prototype.getReloadButton = function() {
return this.rootElement.querySelector('.reloadButton')
}
ThreadView.prototype.getReloadMessageElement = function() {
return this.rootElement.querySelector('.reloadMessage')
}
ThreadView.prototype._getTopBar = function() {
return this.rootElement.querySelector('.topBar')
}
ThreadView.prototype.replace = function(threadRootElement) {
var p = threadRootElement.parentNode
p.replaceChild(this.rootElement, threadRootElement)
}
ThreadView.prototype.disableReload = function() {
this.rootElement.querySelector('.bottomBar').remove()
}
ThreadView.prototype._createResponseViews = function(responses) {
return responses.map(function(r) {
return new ResponseView(this.doc, r, true)
}, this)
}
ThreadView.prototype._getMainElement = function() {
return this.rootElement.querySelector('.main')
}
ThreadView.prototype._addResponseViewsToMainElement = function(views) {
var main = this._getMainElement()
views.map(prop('rootElement')).forEach(main.appendChild.bind(main))
}
ThreadView.prototype._getNewResponseBar = function() {
return this.rootElement.querySelector('#new')
}
ThreadView.prototype._removeNewResponseBar = function() {
var e = this._getNewResponseBar()
if (e) e.remove()
}
ThreadView.prototype._addNewResponseBarIfRequired = function(newResNum) {
if (newResNum === 0) return
var main = this._getMainElement()
if (!main.hasChildNodes()) return
var newResBar = this.doc.createElement('p')
newResBar.id = 'new'
newResBar.textContent = `${newResNum} 件の新着レス`
main.appendChild(newResBar)
}
ThreadView.prototype._scrollToNewResponseBar = function() {
if (!this._getNewResponseBar()) return
this.doc.location.hash = ''
this.doc.location.hash = '#new'
}
ThreadView.prototype._responsesAdded = function(addedResponses) {
this._removeNewResponseBar()
this._addNewResponseBarIfRequired(addedResponses.length)
var views = this._createResponseViews(addedResponses)
;[].push.apply(this._responseViews, views)
this._addResponseViewsToMainElement(views)
this._scrollToNewResponseBar()
}
ThreadView.prototype._toggleSubView = function(getView, toggle) {
for (var i = 0; i < this._responseViews.length; i++) {
var v = getView(this._responseViews[i])
if (v) {
toggle(v)
break
}
}
}
ThreadView.prototype.toggleResponseChildren = function(numElem) {
this._toggleSubView(invoke('getResponseViewByNumElem', [numElem])
, invoke('toggleChildren', []))
}
ThreadView.prototype.toggleSameIdResponses = function(idValElem) {
this._toggleSubView(invoke('getResponseViewByIdValElem', [idValElem])
, invoke('toggleSameId', []))
}
ThreadView.prototype._removeConfigView = function(constructor) {
var v = this.configView
if (!v) return true
this.configView = null
v.destroy()
this.rootElement
.querySelector(`.topBar .${v.constructor.toggleClass}`)
.textContent = `${v.constructor.toggleText}▼`
return !(v instanceof constructor)
}
ThreadView.prototype._addConfigView = function(constructor) {
this.configView = new constructor(this.doc, this._thread.config)
this._getTopBar().appendChild(this.configView.rootElement)
this.rootElement
.querySelector(`.topBar .${constructor.toggleClass}`)
.textContent = `${constructor.toggleText}▲`
}
ThreadView.prototype.toggleViewConfig = function() {
if (this._removeConfigView(ConfigView.View)) {
this._addConfigView(ConfigView.View)
}
}
ThreadView.prototype.toggleNgWordConfig = function() {
if (this._removeConfigView(ConfigView.NgWord)) {
this._addConfigView(ConfigView.NgWord)
}
}
ThreadView.prototype.toggleNgIdConfig = function() {
if (this._removeConfigView(ConfigView.NgId)) {
this._addConfigView(ConfigView.NgId)
}
}
ThreadView.prototype.close = function() {
if (this.responsePostForm) this.responsePostForm.remove()
this.responsePostForm = null
this.disableReload()
}
ThreadView.prototype._setPageCentering = function(pageCentering) {
var methodName = pageCentering ? 'add' : 'remove'
this.doc.documentElement.classList[methodName]('centering')
}
ThreadView.prototype.addStyle = function() {
var e = this.doc.createElement('style')
e.id = 'threadViewerStyle'
e.textContent = this._getStyleText()
this.doc.head.appendChild(e)
}
ThreadView.prototype._setPageMaxWidth = function() {
this.doc.getElementById('threadViewerStyle')
.textContent = this._getStyleText()
}
ThreadView.prototype._getStyleText = function() {
return `
html.centering {
max-width: ${this._thread.config.getPageMaxWidth()}px;
margin: 0 auto;
}
.threadView {
line-height: 1.5em;
}
.threadView .main article header .name {
color: green;
font-weight: bold;
}
.threadView .main article header time,
.threadView .main article header .id {
white-space: nowrap;
}
.threadView .main article header .name,
.threadView .main article header time,
.threadView .main article header .id,
.threadView .topBar .viewToggle,
.threadView .topBar .ngIdToggle {
margin-left: 0.5em;
}
.threadView .main article header .number.hasChild,
.threadView .main article header .id .value.sameIdExist,
.threadView .main article header .id .ngButton,
.threadView .topBar .viewToggle,
.threadView .topBar .ngIdToggle,
.threadView .topBar .ngWordToggle,
.threadView .topBar .config h2 .removeAllButton,
.threadView .topBar .config table .removeButton {
cursor: pointer;
text-decoration: underline;
}
.threadView .main article header .number.hasChild3,
.threadView .main article header .id .value.sameIdExist5 {
font-weight: bold;
color: red;
}
.threadView .main article .content {
margin: 0 0 1em 1em;
}
.threadView .main article .content.asciiArt {
white-space: nowrap;
/* https://ja.wikipedia.org/wiki/アスキーアート */
font-family: IPAMonaPGothic, "IPA モナー Pゴシック", Monapo, Mona, "MS PGothic", "MS Pゴシック", sans-serif;
font-size: 16px;
line-height: 18px;
}
.threadView .main article .sameId,
.threadView .main article .children {
border-top: solid black thin;
border-left: solid black thin;
padding: 5px 0 0 5px;
}
.threadView .main article .sameId > article > header .id .value {
color: black;
background-color: yellow;
}
.threadView .main article.ng,
.threadView .main article header .name,
.threadView .main article header .mail,
.threadView .main article header time,
.threadView .main article header .id,
.threadView .topBar .config h2 .removeAllButton {
font-size: smaller;
}
.threadView .topBar .config {
border: solid black thin;
padding: 0 0.5em;
}
.threadView .topBar .config h2 {
font-size: medium;
}
.threadView .topBar .config table {
border-collapse: collapse;
}
.threadView .topBar .config table th,
.threadView .topBar .config table td {
border: solid thin black;
line-height: 1.5em;
padding: 0 0.5em;
}
.threadView .topBar .config .viewSection .maxWidth {
margin-left: 2em;
}
.postTarget {
width: 100%;
}
.postTarget.loading {
display: none;
}
#new {
background-color: lightblue;
padding-left: 0.5em;
}
`
}
return ThreadView
})()
var ResponsePostForm = (function(_super) {
var ResponsePostForm = function(form) {
_super.call(this)
this._form = this._initForm(form)
this._progress = this._createProgress()
this._target = null
}
ResponsePostForm.prototype = Object.create(_super.prototype)
ResponsePostForm.prototype._initForm = function(form) {
form.target = 'postTarget'
form.addEventListener('submit', this._formSubmitted.bind(this))
return form
}
ResponsePostForm.prototype._getDoc = function() {
return this._form.ownerDocument
}
ResponsePostForm.prototype._createProgress = function() {
var result = this._getDoc().createElement('p')
result.textContent = '書き込み中...'
return result
}
ResponsePostForm.prototype._insertProgress = function() {
var f = this._form
f.parentNode.insertBefore(this._progress, f.nextSibling)
}
ResponsePostForm.prototype._createTarget = function() {
var result = this._getDoc().createElement('iframe')
result.name = this._form.target
result.className = 'postTarget loading'
result.addEventListener('load', this._targetLoaded.bind(this))
return result
}
ResponsePostForm.prototype._hideOrCreateTarget = function() {
if (this._target) {
this._target.classList.add('loading')
} else {
this._target = this._createTarget()
var p = this._progress
p.parentNode.insertBefore(this._target, p.nextSibling)
}
}
ResponsePostForm.prototype._formSubmitted = function() {
this._form.submit.disabled = true
this._insertProgress()
this._hideOrCreateTarget()
}
ResponsePostForm.prototype._getTargetLocation = function() {
return this._target.contentDocument.location.toString()
}
ResponsePostForm.prototype._isPostDone = function() {
return this._target.contentDocument.title.indexOf('書きこみました') >= 0
}
ResponsePostForm.prototype._targetLoaded = function() {
if (this._getTargetLocation() === 'about:blank') return
this._form.submit.disabled = false
this._progress.remove()
if (this._isPostDone()) {
this._target.remove()
this._target = null
this._form.MESSAGE.value = ''
this.fireEvent('postDone')
} else {
this._target.classList.remove('loading')
}
}
ResponsePostForm.prototype.remove = function() {
;[this._form, this._progress, this._target]
.filter(Boolean)
.forEach(invoke('remove', []))
}
return ResponsePostForm
})(Observable)
var ThreadController = (function() {
var ThreadController = function(thread, threadView) {
this.thread = thread
this.threadView = threadView
}
ThreadController.prototype.addCallback = function() {
var r = this.threadView.rootElement
r.addEventListener('click', this.callback.bind(this))
r.addEventListener('keydown', this.keydownCallback.bind(this))
r.addEventListener('change', this.changeCallback.bind(this))
}
ThreadController.prototype.requestNewResponses = function() {
this.threadView.getReloadButton().disabled = true
this.threadView.getReloadMessageElement().textContent = ''
var path = this.threadView.doc.location.pathname
var _this = this
new ResponseRequest()
.send(path.slice(0, path.lastIndexOf('/') + 1)
, this.thread.getLastResponseNumber() + 1)
.then(function(result) {
_this.thread.addResponses(result.responses)
if (result.threadClosed) _this.threadView.close()
})
.catch(function(error) {
_this.threadView.getReloadMessageElement().textContent = error
})
.then(function() {
_this.threadView.getReloadButton().disabled = false
})
}
ThreadController.prototype._addNgWord = function() {
var view = this.threadView.configView
var val = view.getNgWordInputValue()
if (!val) return
switch (view.getNgWordAddTarget()) {
case 'all':
this.thread.config.addNgWord(val)
break
case 'board':
this.thread.addNgWordForBoard(val)
break
case 'thread':
this.thread.addNgWordForThread(val)
break
default:
throw new Error(view.getNgWordAddTarget())
}
view.clearNgWordInputValue()
}
ThreadController.prototype._removeNgWord = function(target) {
var s = target.dataset
this.thread.config.removeNgWord(s.ngWord, s.boardId, s.threadNumber)
}
ThreadController.prototype._addNgId = function(target) {
this.thread.addNgId(target.dataset.jstTime, target.dataset.id)
}
ThreadController.prototype._removeNgId = function(target) {
var s = target.dataset
this.thread.config.removeNgId(new NgId(s.boardId, s.activeDate, s.id))
}
ThreadController.prototype._actionMap = function() {
var view = this.threadView
var cfg = this.thread.config
var header = '.threadView .main article header'
var topBar = '.threadView .topBar'
var config = `${topBar} .config`
var ngIdSection = `${config} .ngIdSection`
var ngWordSection = `${config} .ngWordSection`
return {
[`${header} .number`]: view.toggleResponseChildren.bind(view),
[`${header} .id .value`]: view.toggleSameIdResponses.bind(view),
[`${header} .id .ngButton`]: this._addNgId.bind(this),
'.threadView .bottomBar .reloadButton': this.requestNewResponses.bind(this),
[`${topBar} .viewToggle`]: view.toggleViewConfig.bind(view),
[`${topBar} .ngWordToggle`]: view.toggleNgWordConfig.bind(view),
[`${topBar} .ngIdToggle`]: view.toggleNgIdConfig.bind(view),
[`${ngIdSection} table .removeButton`]: this._removeNgId.bind(this),
[`${ngIdSection} h2 .removeAllButton`]: cfg.removeAllNgIds.bind(cfg),
[`${ngWordSection} .add .addButton`]: this._addNgWord.bind(this),
[`${ngWordSection} table .removeButton`]: this._removeNgWord.bind(this),
[`${ngWordSection} h2 .removeAllButton`]: cfg.removeAllNgWords.bind(cfg),
}
}
ThreadController.prototype._getAction = function(map, target) {
var selectors = Object.keys(map)
for (var i = 0; i < selectors.length; i++) {
var s = selectors[i]
if (target.matches(s)) return map[s]
}
}
ThreadController.prototype.callback = function(event) {
var action = this._getAction(this._actionMap(), event.target)
if (action) action(event.target)
}
ThreadController.prototype.keydownCallback = function(event) {
var enterKeyCode = 13
if (event.keyCode !== enterKeyCode) return
var s = '.threadView .topBar .config .ngWordSection .add .ngWordInput'
if (event.target.matches(s)) this._addNgWord()
}
ThreadController.prototype._setPageCentering = function() {
this.thread.config.setPageCentering(
this.threadView.configView.isPageCenteringChecked())
}
ThreadController.prototype._setPageMaxWidth = function() {
this.thread.config.setPageMaxWidth(
this.threadView.configView.getPageMaxWidthValue())
}
ThreadController.prototype._changeActionMap = function() {
var viewSection = '.threadView .topBar .config .viewSection'
return {
[`${viewSection} .centering label input`]: this._setPageCentering.bind(this),
[`${viewSection} .maxWidth label input`]: this._setPageMaxWidth.bind(this),
}
}
ThreadController.prototype.changeCallback = function(event) {
var action = this._getAction(this._changeActionMap(), event.target)
if (action) action(event.target)
}
return ThreadController
})()
var main = function() {
var parsed = new Parser().parse(document)
parsed.ads.forEach(function(e) { e.style.display = 'none' })
parsed.elementsToRemove.forEach(invoke('remove', []))
if (parsed.floatedSpan) parsed.floatedSpan.style.cssFloat = ''
var config = new Config(GM_getValue, GM_setValue)
var thread = new Thread(config, parsed.boardId, parsed.threadNumber)
var threadView = new ThreadView(document, thread)
threadView.addStyle()
if (parsed.threadClosed) threadView.disableReload()
thread.addResponses(parsed.responses)
threadView.replace(parsed.threadRootElement)
var ctrl = new ThreadController(thread, threadView)
ctrl.addCallback()
if (parsed.postForm) {
var postForm = new ResponsePostForm(parsed.postForm)
postForm.addEventListener('postDone'
, ctrl.requestNewResponses.bind(ctrl))
threadView.responsePostForm = postForm
}
}
main()
})()