6152 lines
206 KiB
JavaScript
6152 lines
206 KiB
JavaScript
'use strict';
|
||
|
||
var style_cookie;
|
||
var style_cookie_txt;
|
||
var style_cookie_site;
|
||
var kumod_set=false;
|
||
var ispage;
|
||
var is_entering=false;
|
||
|
||
var _messages = {
|
||
en: {
|
||
noLocalStorage: "Your browser does not support LocalStorage",
|
||
oops: "Something went wrong...",
|
||
blankResponse: "blank response",
|
||
expandingThread: "Expanding thread...",
|
||
newThread: "new thread",
|
||
NewThread: "New thread",
|
||
replyTo: "reply to",
|
||
cancel: "Cancel",
|
||
update: "Update",
|
||
updatingCounts: "Updating...",
|
||
couldntFetch: "Could not fetch this post",
|
||
noNewPosts: "No new posts",
|
||
replies: "Replies",
|
||
settings_fxEnabled: "Animation effects",
|
||
settings_showReplies: "Show replies inside posts",
|
||
settings_sfwMode: "NSFW mode",
|
||
settings_expandImgFull: "Expand images to full size",
|
||
settings_constrainWidth: "Constrain content width",
|
||
settings_newMediaViewer: "Experimental media viewer",
|
||
settings_hidePostBtn: 'Post hide buttons',
|
||
settings_endlessScroll: 'Endless scrolling',
|
||
enterCaptcha: "Please enter captcha.",
|
||
selectText: "Select some text",
|
||
dcls: "Double click to show source",
|
||
watchOn: "Watch on",
|
||
captcharot: "Captcha has rotted",
|
||
threadUpdationAutomatically: "Tread is being updated automatically.",
|
||
stopFuckingDolls: "<b>Отключите AJAX-отправку постов и AJAX-обновление треда.</b><br />(Кликните, чтобы закрыть)",
|
||
del: "Delete",
|
||
delandban: "Delete and ban",
|
||
ban: "Ban",
|
||
stickthread: "Stick thread",
|
||
unstickthread: "Unstick thread",
|
||
lockthread: "Lock thread",
|
||
unlockthread: "Unlock thread",
|
||
returnDesktop: "Switch to desktop interface",
|
||
returnTouch: "Switch to touch interface",
|
||
forceDesktop: "Force Desktop interface",
|
||
okay: "Okay",
|
||
captchalang: "Captcha language",
|
||
reply: "Reply",
|
||
imageDownscaledBy: "Image was downscaled by",
|
||
videoDownscaledBy: "Video was downscaled by",
|
||
toFit: "to feet screen",
|
||
newReplies: "New replies",
|
||
newThreadsAvailable: "New threads available.",
|
||
loading: "Loading",
|
||
anonymous: "Anonymous",
|
||
sortBy: "Sort by",
|
||
bumpOrder: "Bump order",
|
||
lastReply: "Last reply",
|
||
creationDate: "Creation date",
|
||
replyCount: "Reply count",
|
||
doStick: "Respect stickied",
|
||
showHidden: "Show hidden",
|
||
doNotStick: "Ignore stickied",
|
||
hideHidden: "Hide hidden",
|
||
search: "Search",
|
||
threadOnPage: "Thread is on page",
|
||
goToThread: "Go to thread",
|
||
smallPics: "Small pictures",
|
||
largePics: "Large pictures",
|
||
legacyMode: "Legacy mode",
|
||
threads: "Threads",
|
||
comments: "Replies",
|
||
active_since: "Active since",
|
||
last_active: "Last seen",
|
||
active_on: "Active on",
|
||
tryAgain: "Try again",
|
||
xhrError: "XHR error",
|
||
details: "details",
|
||
thread: "Thread",
|
||
post: 'Post',
|
||
posts: "Posts",
|
||
deleted:'has been deleted',
|
||
deletedMulti: 'have been deleted',
|
||
report: 'Report',
|
||
reported:'has been reported',
|
||
reportedMulti: 'have been reported',
|
||
fileRemoved: 'File<br>removed.',
|
||
file: 'File',
|
||
files: 'Files',
|
||
collapse: 'Collapse',
|
||
asMod: 'as Mod',
|
||
asOP: 'as OP',
|
||
historyEmpty: 'History is empty',
|
||
noDataLoaded: 'No data was loaded',
|
||
unable_load_20: 'Unable to load 2.0 boards list',
|
||
captchaLangChanged: 'Captcha laguage changed',
|
||
found: 'Found',
|
||
links: 'Links',
|
||
selectMultiple: 'Select multiple',
|
||
selected: 'Selected',
|
||
directLink: 'Direct link',
|
||
quoteLink: 'Quote link',
|
||
maxAttNumReached: 'Attachments number limit reached.',
|
||
unsupportedFileType: 'unsupported file type',
|
||
hideName: 'Hide file name',
|
||
removeFile: 'Remove file',
|
||
spoilerOnOff: 'Spoier On/Off',
|
||
hidePost: 'Hide post',
|
||
unhidePost: 'Unhide post',
|
||
settings_hideCompletely: 'Hide items completely',
|
||
pinUnpin: 'Pin/Unpin',
|
||
hideForm: 'Hide form',
|
||
collapseExpand: 'Collapse/Expand',
|
||
notEmpty: 'Not empty',
|
||
showName: 'Show/hide name/trip field',
|
||
showSubject: 'Show/hide subject field',
|
||
showEmbeds: 'Show/hide embeds',
|
||
showPassword: 'Show/hide password field',
|
||
showTTL: 'Show/hide time-to-live field',
|
||
attachFile: 'Attach file...',
|
||
classicSimplifiedForm: 'Classic/simplified form',
|
||
showMarkup: 'Show/hide markup',
|
||
refreshCatalog: 'Refresh catalog',
|
||
cancelTimer: 'Cancel timer',
|
||
saved:'saved from deletion',
|
||
savedMulti: 'saved from deletion',
|
||
password: 'Password',
|
||
captcha: 'Captcha',
|
||
captchaImage: 'Captcha image',
|
||
refreshCaptcha: 'Refresh captcha',
|
||
showCaptcha: 'Show captcha',
|
||
captchaExpired: 'Captcha has expired.',
|
||
mod_: 'Mod.'
|
||
},
|
||
ru: {
|
||
noLocalStorage: "localStorage не поддерживается браузером",
|
||
oops: "Что-то пошло не так...",
|
||
blankResponse: "пустой ответ",
|
||
expandingThread: "Разворачиваем тред...",
|
||
newThread: "новый тред",
|
||
NewThread: "Создать тред",
|
||
replyTo: "ответ на",
|
||
cancel: "Отмена",
|
||
update: "Обновить",
|
||
updatingCounts: "Ищем новые посты...",
|
||
couldntFetch: "Не удалось загрузить этот пост",
|
||
noNewPosts: "Нет новых постов",
|
||
replies: "Ответы",
|
||
settings_fxEnabled: "Анимированные эффекты",
|
||
settings_showReplies: "Показывать ответы внутри поста",
|
||
settings_sfwMode: "Мамка в комнате",
|
||
settings_expandImgFull: "Разворачивать картинки до исходного размера",
|
||
settings_constrainWidth: "Ограничивать ширину контента",
|
||
settings_newMediaViewer: "Экспериментальный просмотрщик медиа",
|
||
settings_hidePostBtn: 'Кнопки скрытия постов',
|
||
settings_endlessScroll: 'Бесконечная прокрутка',
|
||
enterCaptcha: "Пожалуйста, введите капчу.",
|
||
selectText: "Текст не выделен",
|
||
dcls: "Double click to show source",
|
||
watchOn: "Смотреть на",
|
||
odc: "javascript:LatexIT.replaceWithSrc(this);",
|
||
captcharot: "Капча протухла",
|
||
threadUpdationAutomatically: "Тред обновляется автоматически",
|
||
stopFuckingDolls: "<b>Отключите AJAX-отправку постов и AJAX-обновление треда.</b><br />(Кликните, чтобы закрыть)",
|
||
del: "Удалить",
|
||
delandban: "Удалить и забанить",
|
||
ban: "Забанить",
|
||
stickthread: "Прикрепить тред",
|
||
unstickthread: "Отлепить тред",
|
||
lockthread: "Закрыть тред",
|
||
unlockthread: "Открыть тред",
|
||
returnDesktop: "Переключиться на десктопный интерфейс",
|
||
returnTouch: "Переключиться на тач-интерфейс",
|
||
forceDesktop: "Force Desktop interface",
|
||
okay: "Ясно",
|
||
captchalang: "Язык капчи",
|
||
reply: "Ответить",
|
||
imageDownscaledBy: "Картинка ужата на",
|
||
videoDownscaledBy: "Видео ужато на",
|
||
toFit: "по размеру окна",
|
||
newReplies: "Новых ответов",
|
||
newThreadsAvailable: "Доступны новые треды.",
|
||
loading: "Загружаем",
|
||
anonymous: "Аноним",
|
||
sortBy: "Сортировать по",
|
||
bumpOrder: "бампам",
|
||
lastReply: "дате ответов",
|
||
creationDate: "дате создания",
|
||
replyCount: "числу ответов",
|
||
doStick: "Прикреплять",
|
||
showHidden: "Показывать скрытые",
|
||
doNotStick: "Не прикреплять",
|
||
hideHidden: "Скрывать скрытые",
|
||
search: "Поиск",
|
||
threadOnPage: "Тред располагается на странице",
|
||
goToThread: "Перейти в тред",
|
||
smallPics: "Мелкие картинки",
|
||
largePics: "Большие картинки",
|
||
legacyMode: "Олдфажный режим",
|
||
threads: "Тредов",
|
||
comments: "Ответов",
|
||
active_since: "Присоединился",
|
||
last_active: "Был активен",
|
||
active_on: "Активен на",
|
||
tryAgain: "Попробовать снова",
|
||
xhrError: "Ошибка XHR",
|
||
details: "подробности",
|
||
thread: "Тред",
|
||
post: 'Пост',
|
||
posts: 'Посты',
|
||
deleted:'удален',
|
||
deletedMulti: 'удалены',
|
||
report: 'Пожаловаться',
|
||
reported:': жалоба отправлена',
|
||
reportedMulti: ': жалобы отправлены',
|
||
fileRemoved: 'Файл<br>удален.',
|
||
file: 'Файл',
|
||
files: 'Файлы',
|
||
collapse: 'Свернуть',
|
||
asMod: 'от лица модератора',
|
||
asOP: 'от лица ОПа',
|
||
historyEmpty: 'История пуста',
|
||
noDataLoaded: 'Данные не загружены',
|
||
unable_load_20: 'Unable to load 2.0 boards list',
|
||
captchaLangChanged: 'Язык капчи изменен',
|
||
found: 'Найдено',
|
||
links: 'Ссылки',
|
||
selectMultiple: 'Выбрать несколько',
|
||
selected: 'Выбрано',
|
||
directLink: 'Прямая ссылка на пост',
|
||
quoteLink: 'Ссылка для цитирования',
|
||
maxAttNumReached: 'Превышено допустимое число вложений.',
|
||
unsupportedFileType: 'недопустимый тип файла',
|
||
hideName: 'Скрыть имя файла',
|
||
removeFile: 'Удалить файл',
|
||
spoilerOnOff: 'Спойлер Вкл/Выкл',
|
||
hidePost: 'Скрыть пост',
|
||
unhidePost: 'Раскрыть пост',
|
||
settings_hideCompletely: 'Скрывать треды и посты полностью',
|
||
pinUnpin: 'Прикрепить/Открепить',
|
||
hideForm: 'Скрыть форму',
|
||
collapseExpand: 'Свернуть/Развернуть',
|
||
notEmpty: 'Содержит данные',
|
||
showName: 'Показать/скрыть поле имени',
|
||
showSubject: 'Показать/скрыть поле темы',
|
||
showEmbeds: 'Показать/скрыть вложения',
|
||
showPassword: 'Показать/скрыть поле ввода пароля',
|
||
showTTL: 'Показать/скрыть время жизни',
|
||
attachFile: 'Прикрепить файл...',
|
||
classicSimplifiedForm: 'Упрощенная/классическая форма',
|
||
showMarkup: 'Показать/скрыть разметку',
|
||
refreshCatalog: 'Обновить каталог',
|
||
cancelTimer: 'Отменить таймер',
|
||
saved:'спасен от удаления',
|
||
savedMulti: 'спасены от удаления',
|
||
password: 'Пароль',
|
||
captcha: 'Captcha',
|
||
captchaImage: 'Captcha image',
|
||
refreshCaptcha: 'Обновить капчу',
|
||
showCaptcha: 'Показать капчу',
|
||
captchaExpired: 'Капча протухла.',
|
||
mod_: 'Мод.'
|
||
}
|
||
}
|
||
var _l = (typeof locale !== 'undefined' && _messages.hasOwnProperty(locale)) ? _messages[locale] : _messages.ru;
|
||
|
||
var onReady = {
|
||
pushTask: function(fn) {
|
||
if (this.isReady)
|
||
fn()
|
||
else
|
||
this.tasks.push(fn)
|
||
},
|
||
tasks: [],
|
||
ready: function() {
|
||
this.isReady = true
|
||
this.tasks.forEach(fn=>fn())
|
||
}
|
||
}
|
||
|
||
/* IE/Opera fix, because they need to go learn a book on how to use indexOf with arrays */ // Is this still relevant tho?
|
||
if (!Array.prototype.indexOf) {
|
||
Array.prototype.indexOf = function(elt /*, from*/) {
|
||
var len = this.length;
|
||
|
||
var from = Number(arguments[1]) || 0;
|
||
from = (from < 0)
|
||
? Math.ceil(from)
|
||
: Math.floor(from);
|
||
if (from < 0)
|
||
from += len;
|
||
|
||
for (; from < len; from++) {
|
||
if (from in this &&
|
||
this[from] === elt)
|
||
return from;
|
||
}
|
||
return -1;
|
||
};
|
||
}
|
||
|
||
/* Utf8 strings de-/encoder */
|
||
var Utf8 = {
|
||
// public method for url encoding
|
||
encode : function (string) {
|
||
string = string.replace(/\r\n/g,"\n");
|
||
var utftext = "";
|
||
for (var n = 0; n < string.length; n++) {
|
||
var c = string.charCodeAt(n);
|
||
if (c < 128) {
|
||
utftext += String.fromCharCode(c);
|
||
}
|
||
else if((c > 127) && (c < 2048)) {
|
||
utftext += String.fromCharCode((c >> 6) | 192);
|
||
utftext += String.fromCharCode((c & 63) | 128);
|
||
}
|
||
else {
|
||
utftext += String.fromCharCode((c >> 12) | 224);
|
||
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
|
||
utftext += String.fromCharCode((c & 63) | 128);
|
||
}
|
||
}
|
||
return utftext;
|
||
},
|
||
// public method for url decoding
|
||
decode : function (utftext) {
|
||
var string = "", i = 0, c = 0, c1 = 0, c2 = 0, c3 = 0
|
||
while ( i < utftext.length ) {
|
||
c = utftext.charCodeAt(i);
|
||
if (c < 128) {
|
||
string += String.fromCharCode(c);
|
||
i++;
|
||
}
|
||
else if((c > 191) && (c < 224)) {
|
||
c2 = utftext.charCodeAt(i+1);
|
||
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
|
||
i += 2;
|
||
}
|
||
else {
|
||
c2 = utftext.charCodeAt(i+1);
|
||
c3 = utftext.charCodeAt(i+2);
|
||
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
|
||
i += 3;
|
||
}
|
||
}
|
||
return string;
|
||
}
|
||
}
|
||
|
||
function Cookie(name) {
|
||
if (arguments.length == 1) {
|
||
// with(document.cookie) {
|
||
var regexp=new RegExp("(^|;\\s+)"+name+"=(.*?)(;|$)");
|
||
var hit=regexp.exec(document.cookie);
|
||
if(hit&&hit.length>2) return Utf8.decode(unescape(replaceAll(hit[2],'+','%20')));
|
||
else return '';
|
||
// }
|
||
} else {
|
||
var value = arguments[1]
|
||
, days = arguments[2]
|
||
, expires=""
|
||
if(days) {
|
||
var date=new Date();
|
||
date.setTime(date.getTime()+(days*24*60*60*1000));
|
||
expires="; expires="+date.toGMTString();
|
||
}
|
||
document.cookie=name+"="+value+expires+"; path=/";
|
||
}
|
||
}
|
||
|
||
function replaceAll(str, from, to) {
|
||
var idx = str.indexOf( from );
|
||
while ( idx > -1 ) {
|
||
str = str.replace( from, to );
|
||
idx = str.indexOf( from );
|
||
}
|
||
return str;
|
||
}
|
||
|
||
var _lastActiveForm = false
|
||
Object.defineProperty(window, 'lastActiveForm', {
|
||
get: function() {
|
||
if (!_lastActiveForm || !_lastActiveForm.getClientRects().length) {
|
||
updateActiveForm()
|
||
}
|
||
return _lastActiveForm
|
||
},
|
||
set: function(el) {
|
||
document.querySelectorAll('.qrf-active').forEach(a => a.classList.remove('qrf-active'))
|
||
if (el) {
|
||
let form = el._findParent('form') // _findParent retuns element itself if in matches the selector
|
||
form.classList.add('qrf-active')
|
||
if (form.classList.contains('collapsed')) {
|
||
toggleFormCollapse(form, false/*← uncollapse*/, false/*← no focus*/)
|
||
}
|
||
_lastActiveForm = form
|
||
}
|
||
else
|
||
_lastActiveForm = false
|
||
}
|
||
})
|
||
function updateActiveForm() {
|
||
let floating = document.querySelector('.qrf-floating:not(.collapsed)')
|
||
if (floating) {
|
||
lastActiveForm = floating
|
||
return
|
||
}
|
||
lastActiveForm = findClosest('.qrf-floating.collapsed, .quick-reply-form textarea')
|
||
return
|
||
}
|
||
|
||
function findClosest(selector) {
|
||
let height = window.innerHeight
|
||
, hh = height / 2
|
||
, list = document.querySelectorAll(selector)
|
||
if (!list) return false
|
||
let closest = [].map.call(list, el => {
|
||
let rect = el.getClientRects()
|
||
, offset = rect.length
|
||
? Math.abs(hh - (rect[0].y + rect[0].height/2))
|
||
: null
|
||
return { el: el, offset: offset }
|
||
})
|
||
.filter(a => a.offset !== null)
|
||
.sort((e1, e2) => e1.offset < e2.offset ? -1 : 1)
|
||
return closest.length ? closest[0].el : false
|
||
}
|
||
|
||
function insert(text, breakBefore=true, breakAfter=true) {
|
||
let form = lastActiveForm || document.querySelector('#postform')
|
||
, textarea = form && form.querySelector('textarea')
|
||
if(textarea) {
|
||
if (form.classList.contains('collapsed')) {
|
||
toggleFormCollapse(form, false/*← uncollapse*/, false/*← no focus*/)
|
||
}
|
||
let start = textarea.selectionStart
|
||
, end = textarea.selectionEnd
|
||
, pre = textarea.value.substr(0,start)
|
||
, post = textarea.value.substr(end)
|
||
, charBefore = pre[pre.length-1]
|
||
, charAfter = post[0]
|
||
if (breakBefore && text[0] !== "\n" && charBefore && charBefore !== "\n")
|
||
text = "\n"+text
|
||
if (breakAfter && text[text.length-1] !== "\n" && charAfter !== "\n")
|
||
text += "\n"
|
||
let selectionRange = start + text.length
|
||
textarea.value = pre + text + post
|
||
textarea.setSelectionRange(selectionRange, selectionRange)
|
||
textarea.focus()
|
||
}
|
||
}
|
||
|
||
function markup($target, start, end, istart, iend) {
|
||
let element = $target.find('textarea').get(0);
|
||
if (element.selectionStart || element.selectionStart == '0') {
|
||
element.focus();
|
||
let startPos = element.selectionStart
|
||
, endPos = element.selectionEnd
|
||
, selected = element.value.substring(startPos, endPos);
|
||
if(selected.indexOf('\n') === (-1) && typeof istart !== "undefined" && typeof iend !== "undefined") {
|
||
start = istart; end = iend;
|
||
}
|
||
element.value = element.value.substring(0, startPos) + start + element.value.substring(startPos, endPos) + end + element.value.substring(endPos, element.value.length);
|
||
} else {
|
||
element.value += start + end;
|
||
}
|
||
}
|
||
|
||
function bullets($target, bullet, istart, iend) {
|
||
let $area = $target.find('textarea')
|
||
, element = $area[0]
|
||
, startPos = element.selectionStart
|
||
, endPos = element.selectionEnd
|
||
, selected = $area.val().substring(startPos, endPos)
|
||
if(!~selected.indexOf('\n') && typeof istart !== "undefined" && typeof iend !== "undefined") {
|
||
element.value = element.value.substring(0, startPos) + istart + element.value.substring(startPos, endPos) + iend + element.value.substring(endPos, element.value.length);
|
||
}
|
||
else {
|
||
selected = $area.val().substring(startPos, endPos).split('\n');
|
||
var newtxt = "";
|
||
for(var i=0; i<selected.length; i++) {
|
||
newtxt += (bullet + selected[i]);
|
||
if(i < (selected.length - 1)) newtxt += '\n';
|
||
}
|
||
$area.val(
|
||
$area.val().substring(0, startPos)
|
||
+ newtxt +
|
||
$area.val().substring(endPos)
|
||
);
|
||
}
|
||
}
|
||
|
||
function quotes($target, type) {
|
||
let $area = $target.find('textarea')
|
||
, areaVal = $area.val()
|
||
, element = $area[0]
|
||
, startPos = element.selectionStart
|
||
, endPos = element.selectionEnd
|
||
, selected = areaVal.substring(startPos, endPos)
|
||
, newtxt = ""
|
||
if (~selected.indexOf('\n')) { // multiline quoting
|
||
selected = areaVal.substring(startPos, endPos).split('\n')
|
||
for(var i=0; i<selected.length; i++) {
|
||
let bullet = (type=='g') ? '>'
|
||
: (type=='r') ? '<'
|
||
: (i%2) ? '<'
|
||
: '>'
|
||
newtxt += (bullet + ' ' + selected[i])
|
||
if(i < (selected.length - 1)) newtxt += '\n'
|
||
}
|
||
}
|
||
else { // inline quoting
|
||
let tag = type=='r' ? 'rq' : 'q'
|
||
newtxt = `[${tag}]${selected}[/${tag}]`
|
||
}
|
||
element.value =
|
||
areaVal.substring(0, startPos)
|
||
+ newtxt +
|
||
areaVal.substring(endPos)
|
||
}
|
||
|
||
function quote(b, a) {
|
||
var v = eval("document." + a + ".message");
|
||
v.value += ">>" + b + "\n";
|
||
v.focus();
|
||
}
|
||
|
||
function checkhighlight() {
|
||
var match;
|
||
if(match=/#i([0-9]+)/.exec(document.location.toString()))
|
||
if(!$('#postform textarea').val())
|
||
insert(">>"+match[1]);
|
||
if(match=/#([0-9]+)/.exec(document.location.toString()))
|
||
highlight(match[1]);
|
||
}
|
||
|
||
function highlight(id, offTimeout=5000) {
|
||
$('.highlight').removeClass('highlight');
|
||
if (!(''+id).split('-')[1])
|
||
id += '-'+this_board_dir
|
||
let $post = $(`#postbody${id}`).parents('.reply')
|
||
if (!$post.length) return false;
|
||
let post = $post[0]
|
||
$post.addClass('highlight');
|
||
if (offTimeout) {
|
||
setTimeout(() => $post.removeClass('highlight'), offTimeout)
|
||
}
|
||
// Scroll to post
|
||
let bcr = post.getBoundingClientRect()
|
||
, docHeight = document.documentElement.clientHeight
|
||
if (bcr.bottom > docHeight || bcr.top < 0) {
|
||
let postX = bcr.top + document.documentElement.scrollTop
|
||
, spaceAround = docHeight - bcr.height
|
||
window.scrollTo(0, postX - (spaceAround ? Math.ceil(spaceAround/2) : 0))
|
||
}
|
||
return true
|
||
}
|
||
|
||
const password_length = 20;
|
||
|
||
function get_password(name) {
|
||
let pass = getCookie(name);
|
||
if(pass) return pass;
|
||
pass = randomString(password_length)
|
||
Cookie(name, pass, 365);
|
||
return(pass);
|
||
}
|
||
|
||
function togglePassword() {
|
||
var passwordbox_html = $("#passwordbox").html().toLowerCase();
|
||
var newhtml = '<td></td><td></td>';
|
||
if (passwordbox_html == newhtml) {
|
||
newhtml = '<td class="postblock">Mod</td><td><input type="text" name="modpassword" size="28" maxlength="75"> <acronym title="Display staff status (Mod/Admin)">D</acronym>: <input type="checkbox" name="displaystaffstatus" checked> <acronym title="Lock">L</acronym>: <input type="checkbox" name="lockonpost"> <acronym title="Sticky">S</acronym>: <input type="checkbox" name="stickyonpost"> <acronym title="Raw HTML">RH</acronym>: <input type="checkbox" name="rawhtml"> <acronym title="Name">N</acronym>: <input type="checkbox" name="usestaffname"></td>';
|
||
}
|
||
$("#passwordbox").html(newhtml).show();
|
||
return false;
|
||
}
|
||
|
||
// proxied functions
|
||
function getCookie(name) { return Cookie(name) }
|
||
function set_cookie(name,value,days) { return Cookie(name,value,days) }
|
||
|
||
var Styles = {
|
||
all: [], titles: [],
|
||
init: function() {
|
||
_.each(document.getElementsByTagName("link"), function(link) {
|
||
if(link.getAttribute("rel").indexOf("style")!=-1 && link.getAttribute("title")) {
|
||
this.all.push(link);
|
||
this.titles.push(link.getAttribute("title"));
|
||
if(link.getAttribute("rel").indexOf("alternate")===-1) {
|
||
this._default = link.getAttribute("title");
|
||
}
|
||
if(link.hasAttribute("data-custom")) {
|
||
this.custom = link.getAttribute("title");
|
||
}
|
||
}
|
||
}, this);
|
||
this.current = this._default;
|
||
var customBypass = getCookie('bypasscustom');
|
||
this.customBypass = (customBypass.length && typeof this_board_dir !== 'undefined' && in_array(this_board_dir, customBypass.split('|'))) ? true : false;
|
||
this.initiated = true
|
||
},
|
||
decide: function() {
|
||
this.init();
|
||
var testingCSS = LSfetchJSON('testing-css')
|
||
if(testingCSS) {
|
||
let title = this.addStyle(testingCSS.url, testingCSS.title)
|
||
this.setStyle(title)
|
||
var $clink = $('<a href="#">Отключить тестирование '+title+'.css</a>')
|
||
$clink.click(function(ev) {
|
||
ev.preventDefault()
|
||
Styles.quitTest()
|
||
$(this).parent().slideUp()
|
||
})
|
||
this.$cancelLink = $('<div style="font-weight: bold"></div>').append($clink)
|
||
return
|
||
}
|
||
if(this.hasOwnProperty('custom') && !this.customBypass)
|
||
return this.setCustom();
|
||
var sc = getCookie(style_cookie);
|
||
if(sc && in_array(sc, this.titles))
|
||
this.setStyle(sc);
|
||
else {
|
||
this.setDefault();
|
||
set_cookie("kustyle_site",this._default,365);
|
||
set_cookie("kustyle",this._default,365);
|
||
}
|
||
},
|
||
change: function(stylename) {
|
||
if(!in_array(stylename, this.titles) || this.current === stylename) return;
|
||
this.setStyle(stylename);
|
||
if(this.hasOwnProperty('custom') && this.custom === stylename) {
|
||
this.removeBypass();
|
||
}
|
||
else {
|
||
if(this.hasOwnProperty('custom'))
|
||
this.addBypass();
|
||
set_cookie("kustyle_site",stylename,365);
|
||
set_cookie("kustyle",stylename,365);
|
||
}
|
||
},
|
||
removeBypass: function() {
|
||
if(!this.customBypass || typeof this_board_dir === 'undefined') return;
|
||
this.customBypass = false;
|
||
var oldcookie = getCookie('bypasscustom').split('|'), newcookie = [];
|
||
_.each(oldcookie, function(brd) {
|
||
if(brd !== this_board_dir) newcookie.push(brd);
|
||
});
|
||
newcookie = newcookie.length ? newcookie.join('|') : '';
|
||
set_cookie("bypasscustom",newcookie,365);
|
||
},
|
||
addBypass: function() {
|
||
if(this.customBypass || typeof this_board_dir === 'undefined') return;
|
||
this.customBypass = true;
|
||
var cookie = getCookie('bypasscustom').split('|');
|
||
if(!in_array(this_board_dir, cookie)) {
|
||
cookie.push(this_board_dir);
|
||
set_cookie("bypasscustom",cookie.join('|'),365);
|
||
}
|
||
},
|
||
setDefault: function() {
|
||
if(this.hasOwnProperty('_default') && this.current !== this._default)
|
||
this.setStyle(this._default);
|
||
},
|
||
setCustom: function() {
|
||
if(this.hasOwnProperty('custom'))
|
||
this.setStyle(this.custom);
|
||
},
|
||
setStyle: function(stylename) {
|
||
if(!in_array(stylename, this.titles)) return;
|
||
if (scrollAnchor && scrollAnchor.save)
|
||
scrollAnchor.save('setstyle', '.postnode')
|
||
_.each(this.all, function(sheet) {
|
||
sheet.disabled=true; // Hello IE
|
||
if(sheet.getAttribute("title") === stylename)
|
||
sheet.disabled=false;
|
||
});
|
||
if (scrollAnchor && scrollAnchor.restore)
|
||
scrollAnchor.restore('setstyle')
|
||
this.current = stylename;
|
||
},
|
||
onTest: null,
|
||
addStyle: function(url, title='') {
|
||
if (! title) {
|
||
let m = /(?:.+\/)?(.+)\.css/i.exec(url)
|
||
if(!m) return;
|
||
title = m[1]
|
||
}
|
||
title = _.capitalize(_.escape(title))
|
||
if(!in_array(title, this.titles)) {
|
||
var $link = $('<link rel="stylesheet" type="text/css" href="'+url+'" title="'+title+'" disabled>')
|
||
$('head').append($link);
|
||
this.titles.push(title)
|
||
this.all.push($link[0])
|
||
}
|
||
return title
|
||
},
|
||
testStyle: function(url, title) {
|
||
title = this.addStyle(url, title)
|
||
this.setStyle(title)
|
||
this.onTest = {
|
||
url: url,
|
||
title: title
|
||
}
|
||
pups.succ(`Установлен стиль ${title}.
|
||
<div class="styletest-form">
|
||
<button onclick="Styles.confirmLongTermTest()">OK</button>
|
||
<button onclick="Styles.quitTest()">${_l.cancel}</button>
|
||
</div>`, {time: 0, save: true})
|
||
},
|
||
confirmLongTermTest: function() {
|
||
if(this.onTest)
|
||
localStorage.setItem('testing-css', JSON.stringify(this.onTest))
|
||
},
|
||
quitTest: function() {
|
||
localStorage.removeItem('testing-css')
|
||
this.decide()
|
||
}
|
||
}
|
||
if(style_cookie) Styles.decide();
|
||
|
||
var HiddenItems = {
|
||
init: function() {
|
||
this.hideCompletely = localStorage['hideCompletely.'+this_board_dir]=='true'
|
||
;['posts', 'threads'].forEach(kind => {
|
||
let ls = localStorage[`hidden${_.capitalize(kind)}`]
|
||
, list = ls ? ls.split(',').filter(a => a) : []
|
||
this.lists[kind] = list
|
||
})
|
||
document.write(`<style id="injector:hideitems">${this.buildCSS()}</style>`)
|
||
},
|
||
buildCSS: function() {
|
||
let unhide = [], hide = [], hiddenPosts = []
|
||
;['posts', 'threads'].forEach(kind => {
|
||
this.lists[kind].forEach(id => {
|
||
if (kind=="posts") {
|
||
let node = `#postnode${id}`
|
||
hiddenPosts.push(node)
|
||
if (this.hideCompletely)
|
||
hide.push(node)
|
||
else
|
||
hide.push(`#postbody${id}`)
|
||
}
|
||
else if(ispage) {
|
||
hide.push(`#thread${id}`)
|
||
if (this.hideCompletely) {
|
||
hide.push(`#unhidethread${id}`)
|
||
hide.push(`#thread${id} + br`)
|
||
hide.push(`#thread${id} + br + hr`)
|
||
}
|
||
else {
|
||
unhide.push(`#unhidethread${id}`)
|
||
}
|
||
}
|
||
})
|
||
})
|
||
if (hiddenPosts.length) {
|
||
onReady.pushTask(() => {
|
||
$(hiddenPosts.join(',')).addClass('post-hidden')
|
||
})
|
||
}
|
||
return (hide.length ? hide.join(',') + '{ display: none }' : '') +
|
||
(unhide.length ? unhide.join(',') + '{ display: inline-block }' : '')
|
||
},
|
||
updateCSS: function() {
|
||
injector.inject('hideitems', this.buildCSS())
|
||
},
|
||
hideCompletely: false,
|
||
lists: {},
|
||
isHidden: function(kind, id) {
|
||
return this.lists[kind+'s'].indexOf(id.toString()) != -1
|
||
},
|
||
hideItem: function(kind, id) {
|
||
if (this.isHidden(kind, id)) return;
|
||
this.lists[kind+'s'].push(id)
|
||
this.saveList(kind)
|
||
},
|
||
unhideItem: function(kind, id) {
|
||
if (!this.isHidden(kind, id)) return;
|
||
this.lists[kind+'s'] = this.lists[kind+'s'].filter(a => a!=id)
|
||
this.saveList(kind)
|
||
},
|
||
hideThread: function(id, inCatalog=false) {
|
||
this.hideItem('thread', id)
|
||
$('#unhidethread' + id).slideDown()
|
||
let $thread = $('#thread' + id)
|
||
, $form = $thread.find('.qrf-floating')
|
||
$thread.slideUp().promise().done(updateActiveForm)
|
||
if ($form.length)
|
||
toggleFormFloating($form[0], false /* ← float=off*/, false /* ← no focus*/)
|
||
},
|
||
unhideThread: function(id, inCatalog=false) {
|
||
this.unhideItem('thread', id)
|
||
$('#thread' + id).slideDown()
|
||
$('#unhidethread' + id).slideUp()
|
||
},
|
||
hidePost: function(id) {
|
||
this.hideItem('post', id)
|
||
$('#postbody' + id).slideUp('fast')
|
||
},
|
||
unhidePost: function(id) {
|
||
this.unhideItem('post', id)
|
||
$('#postbody' + id).slideDown('fast')
|
||
},
|
||
togglePost: function(id) {
|
||
let $body = $('#postbody' + id)
|
||
, $btn = $body.parent().find('.hide-post-btn')
|
||
if ($body.css('display') == 'none') {
|
||
this.unhidePost(id)
|
||
$btn.attr('title', _l.hidePost)
|
||
.find('use').attr('xlink:href', '#i-hide')
|
||
}
|
||
else {
|
||
this.hidePost(id)
|
||
$btn.attr('title', _l.unhidePost)
|
||
.find('use').attr('xlink:href', '#i-unhide')
|
||
}
|
||
},
|
||
saveList: function(kind) {
|
||
localStorage[`hidden${_.capitalize(kind)}s`] = this.lists[kind+'s'].join(',')
|
||
}
|
||
}
|
||
HiddenItems.init()
|
||
|
||
function expandthread(threadid, board, ev) {
|
||
ev.preventDefault()
|
||
let $thread = $(`#thread${threadid}-${board}`)
|
||
if ($thread.length) {
|
||
let expandFrom = + $thread.find('.postnode.op').data('id')
|
||
, expandTo = + $thread.find('.postnode:not(.op):first').data('id')
|
||
, $omitted = $thread.find('.omittedposts')
|
||
$omitted.find('a').hide()
|
||
if (! $omitted.find('span').length) {
|
||
$(`<div class="spinner"></div><span>${_l.loading}...</span>`).prependTo($omitted)
|
||
}
|
||
else {
|
||
$omitted.find('.spinner').show()
|
||
}
|
||
HTMLoader.getThread(board, +threadid, [expandFrom, expandTo], (err, posts) => {
|
||
if (posts) {
|
||
$thread.find('.omittedposts').replaceWith(posts)
|
||
replyMap.showReplies()
|
||
}
|
||
else {
|
||
setTimeout(() => $omitted.find('.spinner').hide(), 1000)
|
||
$omitted.find('span').text(_l.oops + (err !== false ? ` (${err})` : ''))
|
||
$omitted.find('a').text(_l.tryAgain).show()
|
||
}
|
||
})
|
||
}
|
||
return false;
|
||
}
|
||
|
||
var newposts = {
|
||
busy: false,
|
||
get: function(options={}) {
|
||
options = _.defaults(options, {
|
||
threadid: null,
|
||
expectedPost: null,
|
||
silent: false,
|
||
onError: err => pups.err(`Error geting new posts (${err})`),
|
||
onSuccess: null,
|
||
timestamp: null
|
||
})
|
||
if (!options.threadid) {
|
||
if (ispage)
|
||
return options.onError()
|
||
options.threadid = +$('input[name=replythread]').val()
|
||
}
|
||
this.pushTask(options)
|
||
return false
|
||
},
|
||
execute: function(task) {
|
||
this.busy = true
|
||
let threadid = ''+task.threadid
|
||
, expectedPost = task.expectedPost
|
||
, onError = err => {
|
||
this.next()
|
||
task.onError(err)
|
||
}
|
||
if (!threadid.split('-')[1])
|
||
threadid += '-'+this_board_dir
|
||
let replies_container, $newposts_get
|
||
if (!ispage) {
|
||
$newposts_get = $('#newposts_get')
|
||
$newposts_get.removeClass('upd-counting').addClass('upd-updating')
|
||
replies_container = document.querySelector('.replies')
|
||
}
|
||
else {
|
||
replies_container = document.querySelector(`#thread${threadid} .replies`)
|
||
if (!replies_container) return onError('No replies container!');
|
||
}
|
||
// If post is already there, skip the entire thing
|
||
if (expectedPost && highlight(expectedPost)) {
|
||
$newposts_get.removeClass('upd-updating')
|
||
this.next()
|
||
return
|
||
}
|
||
let $lastQrl = $(`.qrl[data-parent="${threadid}"]`).last()
|
||
, lastpost = ($lastQrl.data('postnum') || $lastQrl.data('parent'))+''
|
||
if (! lastpost) return onError();
|
||
let idb = threadid.split('-')
|
||
, id = idb[0], bd = idb[1]
|
||
HTMLoader.getThread(bd, id, [+(lastpost.split('-')[0]),Infinity], (err, posts) => {
|
||
if (!ispage) {
|
||
$newposts_get.removeClass('upd-updating')
|
||
}
|
||
if (posts) {
|
||
replies_container.insertAdjacentHTML('beforeend', posts)
|
||
replyMap.showReplies()
|
||
unreadCounter.refreshTimestamp(task.timestamp || null, bd)
|
||
if (task.onSuccess)
|
||
task.onSuccess()
|
||
}
|
||
else {
|
||
if (!task.silent) {
|
||
pups.info(posts === '' ? _l.noNewPosts : _l.oops + (err !== false ? ` (${err})` : ''), {time: 1})
|
||
}
|
||
}
|
||
if (expectedPost && !highlight(expectedPost)) {
|
||
return onError('No target reply!');
|
||
}
|
||
this.next()
|
||
}, false, true)
|
||
},
|
||
pushTask: function(task) {
|
||
if (this.busy)
|
||
this.stack.push(task)
|
||
else
|
||
this.execute(task)
|
||
},
|
||
next: function() {
|
||
let next = this.stack.shift()
|
||
if (next)
|
||
this.execute(next)
|
||
else
|
||
this.busy = false
|
||
},
|
||
stack: []
|
||
}
|
||
|
||
// Hiding animation
|
||
function flyTo(el, dest) {
|
||
let destBox = dest.getBoundingClientRect()
|
||
, destX = destBox.x + (destBox.width / 2)
|
||
, destY = destBox.y + (destBox.height / 2)
|
||
, elBox = el.getBoundingClientRect()
|
||
el.style.transformOrigin = `${(destX - elBox.x) + 'px'} ${(destY - elBox.y) + 'px'}`
|
||
el.classList.add('fly-to-zero')
|
||
return new Promise((resolve, reject) => {
|
||
setTimeout(() => {
|
||
el.classList.remove('fly-to-zero')
|
||
el.style.transformOrigin = null
|
||
resolve(el)
|
||
}, (+getComputedStyle(el).transition.match(/transform ([0-9\.]+)/)[1] * 1000) + 100 )
|
||
})
|
||
}
|
||
|
||
function toggleFormCollapse(form, collapse = !form.classList.contains('collapsed'), focus = !collapse) {
|
||
let oldBox = form.getBoundingClientRect()
|
||
, offRight = oldBox.right - (window.innerWidth - 32)
|
||
form.classList.toggle('collapsed', collapse)
|
||
let box = form.getBoundingClientRect()
|
||
, newWidth = box.width
|
||
form.style.left =
|
||
box.left
|
||
+ (oldBox.width - newWidth)
|
||
- (offRight > 0 ? offRight : 0)
|
||
+ 'px'
|
||
form.querySelector('.collapser').classList.toggle('collapsed', collapse)
|
||
if (focus) {
|
||
form.querySelector('textarea').focus()
|
||
}
|
||
}
|
||
|
||
function toggleFormRow(form, btn) {
|
||
let id = btn.dataset.toggle
|
||
, row = form.querySelector(`.${btn.dataset.toggle}-row`)
|
||
if (! row) return;
|
||
let show = row.classList.toggle('row-shown')
|
||
btn.querySelector('svg').classList.toggle('pressed', show)
|
||
let rem = show ? 0 : isRowEmpty(row, id)
|
||
btn.classList.toggle('button-with-reminder', rem)
|
||
if (rem) {
|
||
if (! btn._originalTitle)
|
||
btn._originalTitle = btn.title
|
||
btn.title = `${btn._originalTitle} (${_l.notEmpty})`
|
||
}
|
||
else if (btn._originalTitle)
|
||
btn.title = btn._originalTitle
|
||
if (show) { // focus on field
|
||
let focal = null
|
||
if (id == 'subject-send') {
|
||
let i = form.querySelector('input[name=subject]')
|
||
if (!i.value)
|
||
focal = i
|
||
}
|
||
if (id == 'name') {
|
||
let i = form.querySelector('input[name=name]')
|
||
if (!i.value && !i.disabled)
|
||
focal = i
|
||
}
|
||
if (id == 'embed') {
|
||
let inputs = [].filter.call(form.querySelectorAll('input[type=text][name^=embed]'),
|
||
el => el.getClientRects().length && !el.value)
|
||
if (inputs.length)
|
||
focal = inputs[0]
|
||
}
|
||
if (focal)
|
||
focal.focus()
|
||
}
|
||
}
|
||
|
||
function isRowEmpty(row, id) {
|
||
return !!(
|
||
(id == 'name' &&
|
||
row.querySelector('input[name=name]').value != ''
|
||
&&
|
||
!row.querySelector('input[name=disable_name]').checked
|
||
) ||
|
||
(id == 'subject-send' &&
|
||
row.querySelector('input[name=subject]').value != ''
|
||
) ||
|
||
(id == 'embed' &&
|
||
[].find.call(row.querySelectorAll('input[type=text]'), i => i.value.trim())
|
||
) ||
|
||
(id == 'ttl' &&
|
||
row.querySelector('input[name=ttl-enable]').checked
|
||
)
|
||
)
|
||
}
|
||
|
||
function updateReminders(form) {
|
||
['name', 'subject-send', 'embed', 'ttl'].forEach(id => {
|
||
let row = form.querySelector(`.${id}-row:not(.row-shown)`)
|
||
, btn = row && form.querySelector(`.simplified-toggle[data-toggle="${id}"]`)
|
||
if (btn)
|
||
btn.classList.toggle('button-with-reminder', isRowEmpty(row, id))
|
||
})
|
||
}
|
||
|
||
function toggleFormFloating(form, float, focus=true) {
|
||
let pinner = form.querySelector('.pinner')
|
||
pinner.classList.toggle('pinned', !float)
|
||
pinner.classList.toggle('unpinned', float)
|
||
if (float) { // Make the form floating
|
||
// Pin other forms
|
||
scrollAnchor.save('toggleFormFloating', form.getParentPost())
|
||
document.querySelectorAll('.qrf-floating').forEach(f => toggleFormFloating(f, false))
|
||
scrollAnchor.restore('toggleFormFloating')
|
||
.then(() => { // When all scrolling is settled
|
||
let box = form.getBoundingClientRect()
|
||
form.classList.add('qrf-floating')
|
||
form.classList.remove('qrf-builtin', 'qrf-builtin-back')
|
||
form.style.top = box.top + 'px'
|
||
form.style.left = box.left + 'px'
|
||
$(form).drags()
|
||
.find('input, textarea, select, label, .fe-sort-wrapper, button, .b-icon').mousedown(function(e) {
|
||
e.stopPropagation()
|
||
})
|
||
})
|
||
}
|
||
else { // Build the form back in
|
||
form.classList.remove('qrf-floating', 'collapsed')
|
||
form.querySelector('.collapser').classList.remove('collapsed')
|
||
form.classList.add('qrf-builtin', 'qrf-builtin-back')
|
||
$(form).dragsOff()
|
||
;['top', 'left', 'zIndex'].forEach(prop => form.style[prop] = null)
|
||
}
|
||
if (focus)
|
||
form.querySelector('textarea').focus()
|
||
}
|
||
|
||
function hidePostForm(form) {
|
||
form.classList.add('hidden')
|
||
toggleFormFloating(form, false)
|
||
toggleFormCollapse(form, false)
|
||
setTimeout(() => {
|
||
form.classList.remove('qrf-builtin-back')
|
||
}, 200)
|
||
if (form._preview) {
|
||
let pinner = form._preview.querySelector('.pinner')
|
||
if (pinner) {
|
||
pinner.classList.remove('pinned')
|
||
pinner.classList.add('unpinned')
|
||
}
|
||
form._preview._pinned = false
|
||
}
|
||
updateActiveForm()
|
||
}
|
||
|
||
function toggleFormSimplify(form, simplify) {
|
||
let simplified = form.classList.toggle('postform-simplified', simplify)
|
||
if (simplified)
|
||
updateReminders(form)
|
||
}
|
||
|
||
function clonePostForm(anchorID, preview, boardName=false) {
|
||
return new Promise((resolve, reject) => {
|
||
HTMLoader.getPostbox(boardName).then(oldForm => {
|
||
if (Captcha.enabled)
|
||
Captcha.state = 'init' // Destroy old captcha because it will be inevitably reloaded at form cloning
|
||
let newForm = oldForm.cloneNode(true)
|
||
newForm.id = anchorID + '_qrf'
|
||
newForm.dataset.anchorID = anchorID
|
||
newForm.classList.remove('main-reply-form')
|
||
newForm.querySelector('.simplified-send-row .primary span').innerText = _l.reply
|
||
newForm.querySelector('input[name="postpassword"]').value = get_password('postpassword')
|
||
newForm.querySelector('input.primary').value = _l.reply
|
||
newForm.querySelector('.noko-row').remove()
|
||
let blotter = newForm.querySelector('.blotter-row')
|
||
if (preview)
|
||
newForm._preview = preview
|
||
blotter.insertAdjacentHTML('afterend', `
|
||
<span class="extrabtns simplified-extras">
|
||
${newForm.querySelector('.name-row') ? `
|
||
<a href="#" class="simplified-toggle" data-toggle="name" title="${_l.showName}">
|
||
<svg class="icon b-icon"><use xlink:href="#i-name"></use></svg>
|
||
</a>
|
||
` : ''}
|
||
<a href="#" class="simplified-toggle" data-toggle="subject-send" title="${_l.showSubject}">
|
||
<svg class="icon b-icon"><use xlink:href="#i-subject"></use></svg>
|
||
</a>
|
||
<a href="#" class="s-markup" data-toggle="password" title="${_l.showMarkup}">
|
||
<svg class="icon b-icon"><use xlink:href="#i-markup"></use></svg>
|
||
</a>
|
||
${newForm.querySelector('.embed-row') ? `
|
||
<a href="#" class="simplified-toggle" data-toggle="embed" title="${_l.showEmbeds}">
|
||
<svg class="icon b-icon"><use xlink:href="#i-embed"></use></svg>
|
||
</a>
|
||
` : ''}
|
||
<a href="#" class="simplified-toggle" data-toggle="password" title="${_l.showPassword}">
|
||
<svg class="icon b-icon"><use xlink:href="#i-password"></use></svg>
|
||
</a>
|
||
${newForm.querySelector('.ttl-row') ? `
|
||
<a href="#" class="simplified-toggle" data-toggle="ttl" title="${_l.showTTL}">
|
||
<svg class="icon b-icon"><use xlink:href="#i-timer"></use></svg>
|
||
</a>
|
||
` : ''}
|
||
</span>`)
|
||
// Name reminder
|
||
updateReminders(newForm)
|
||
// Enabling extra inputs in simplified form
|
||
newForm.querySelectorAll('.simplified-toggle').forEach(a => {
|
||
a.onclick = ev => {
|
||
ev.preventDefault()
|
||
toggleFormRow(newForm, a)
|
||
}
|
||
})
|
||
newForm.querySelector('.s-markup').onclick = function(ev) {
|
||
ev.preventDefault()
|
||
this.querySelector('.icon').classList.toggle('pressed')
|
||
newForm.querySelector('.message-row').classList.toggle('markup-enabled')
|
||
}
|
||
blotter.remove()
|
||
// Rename label[for]s and corresponding inputs
|
||
let add = '_'+_.uniqueId()
|
||
// newForm.id = 'postform'+add
|
||
newForm.querySelectorAll('label').forEach(l => {
|
||
let id = l.getAttribute('for')
|
||
, newid = id + add
|
||
, input = newForm.querySelector('#'+id)
|
||
if (input)
|
||
input.id = newid
|
||
l.setAttribute('for', newid)
|
||
})
|
||
newForm.classList.add('quick-reply-form') // New general class to tell qr form apart from the main form
|
||
newForm.classList.add('postform-simplified')
|
||
// Clear fields
|
||
resetForm(newForm, true /* ← isClone */)
|
||
// Init rich file input
|
||
richFileInput.initForm(newForm)
|
||
// Add extra buttons
|
||
newForm.insertAdjacentHTML('beforeend', `
|
||
<span class="extrabtns postboxcontrol">
|
||
<svg class="icon form-handle-icon"><use xlink:href="#i-handle"></use></svg>
|
||
<a href="#" title="${_l.classicSimplifiedForm}" class="simplify-qr-form">
|
||
<svg class="icon b-icon">
|
||
<use class="use-simplify" xlink:href="#i-simplify"></use>
|
||
<use class="use-complicate" xlink:href="#i-complicate"></use>
|
||
</svg>
|
||
</a>
|
||
<a href="#" title="${_l.collapseExpand}" class="collapse-qr-form collapser">
|
||
<svg class="icon b-icon">
|
||
<use class="use-collapse" xlink:href="#i-collapse"></use>
|
||
<use class="use-uncollapse" xlink:href="#i-uncollapse"></use>
|
||
</svg>
|
||
</a>
|
||
<a href="#" class="pinner" title="${_l.pinUnpin}">
|
||
<svg class="icon b-icon">
|
||
<use class="use-pin" xlink:href="#i-float"></use>
|
||
<use class="use-unpin" xlink:href="#i-unfloat"></use>
|
||
</svg>
|
||
</a>
|
||
|
||
<a href="#" title="${_l.hideForm}" class="hide-qr-form">
|
||
<svg class="icon b-icon"><use xlink:href="#i-x"></use></svg>
|
||
</a>
|
||
</span>`)
|
||
// Form button actions
|
||
newForm.getParentPost = () => {
|
||
let maybePost = newForm._findPrevious('*:not(.i0svcel)')
|
||
return maybePost.matches('.postnode') ? maybePost : null
|
||
}
|
||
newForm.querySelector('.hide-qr-form').onclick = ev => {
|
||
ev.preventDefault()
|
||
let skip = false
|
||
if (newForm.classList.contains('qrf-floating')) {
|
||
let anchor = newForm.getParentPost()
|
||
if (anchor) {
|
||
flyTo(newForm, anchor.querySelector('a.qrl .b-icon')).then(hidePostForm)
|
||
skip = true
|
||
}
|
||
}
|
||
if (!skip) {
|
||
hidePostForm(newForm)
|
||
}
|
||
}
|
||
newForm.querySelector('.simplify-qr-form').onclick = ev => {
|
||
ev.preventDefault()
|
||
toggleFormSimplify(newForm)
|
||
}
|
||
newForm.querySelector('.pinner').onclick = function(ev) {
|
||
ev.preventDefault()
|
||
let pinned = newForm.classList.contains('qrf-builtin')
|
||
toggleFormFloating(newForm, pinned)
|
||
}
|
||
newForm.querySelector('.collapse-qr-form').onclick = ev => {
|
||
ev.preventDefault()
|
||
toggleFormCollapse(newForm)
|
||
}
|
||
// Prevent dragging of links
|
||
newForm.querySelectorAll('a').forEach(a => {
|
||
a.ondragstart = ev => false
|
||
a.ondrop = ev => false
|
||
})
|
||
// Extra hooks for the new form
|
||
initForm(newForm)
|
||
|
||
resolve(newForm)
|
||
})
|
||
})
|
||
}
|
||
|
||
function quickreply() {
|
||
let $this = $(this)
|
||
, threadID = $this.data('parent')
|
||
, postID = $(this).data('postnum') || threadID.split('-')[0]
|
||
, $post = $this.parents('.postnode')
|
||
, boardName = $post.data('board')
|
||
, fromExternalBoard = (!is_overboard && boardName != this_board_dir)
|
||
, post = $post[0]
|
||
, $preview = $post.parents('.reflinkpreview')
|
||
, isPreview = !!$preview
|
||
, needInsert = true
|
||
, form = post._findNext('*:not(.i0svcel)')
|
||
, formExists = form && form.getAttribute('name') == 'postform'
|
||
, formHidden = formExists && form.classList.contains('hidden')
|
||
, formFloating = formExists && form.classList.contains('qrf-floating')
|
||
, isOpen = true
|
||
, preview = post._findParent('.reflinkpreview')
|
||
, quotation = needInsert ? getPostQuotation(post.querySelector('.postbody')) : ''
|
||
|
||
if (formExists) {
|
||
if (formFloating) {
|
||
toggleFormCollapse(form, false)
|
||
toggleFormFloating(form, false)
|
||
}
|
||
else {
|
||
if (!formHidden) {
|
||
hidePostForm(form)
|
||
formHidden = true
|
||
}
|
||
else {
|
||
form.classList.remove('hidden')
|
||
}
|
||
formHidden = !formHidden
|
||
}
|
||
needInsert = false
|
||
}
|
||
else {
|
||
$this.addClass('spin-around')
|
||
clonePostForm(post.id || post.parentElement.id, preview, (is_overboard ? boardName : false)).then(form => {
|
||
$this.removeClass('spin-around')
|
||
form.classList.add('qrf-builtin')
|
||
form.querySelector('.pinner').classList.toggle('pinned')
|
||
form.querySelector('input[name=replythread]').value = threadID
|
||
post.insertAdjacentElement('afterend', form)
|
||
document.querySelectorAll('.qrf-floating:not(.collapsed)')
|
||
.forEach(existingForm => toggleFormCollapse(existingForm))
|
||
form.querySelector('textarea').focus()
|
||
if (needInsert) {
|
||
insert(`>>${fromExternalBoard ? `/${boardName}/` : ''}${postID}\n${quotation}`)
|
||
}
|
||
// Pin the post if it is in preview
|
||
if (preview && !preview._pinned) {
|
||
post.querySelector('.pinner').click()
|
||
}
|
||
})
|
||
}
|
||
// post.classList.toggle('reply-with-form-open', isOpen)
|
||
return false
|
||
}
|
||
|
||
function _getSelection() {
|
||
if (! window.getSelection) return null
|
||
let selection = window.getSelection()
|
||
if (selection.type !== "Range") return null
|
||
let selectedText = selection.toString()
|
||
if (! selectedText) return null
|
||
return {
|
||
text: selectedText,
|
||
node: selection.anchorNode
|
||
}
|
||
}
|
||
|
||
// get the selected text within the given element
|
||
function getLocalSelection(parent) {
|
||
let sel = _getSelection()
|
||
return sel && sel.node._isChildOf(parent)
|
||
? sel.text
|
||
: null
|
||
}
|
||
|
||
function getPostQuotation(post) {
|
||
let text = getLocalSelection(post)
|
||
if (!text) return ''
|
||
text = text
|
||
.replace(/^\s/, '').replace(/\s$/, '') // remove leading and trailing whitespaces
|
||
.replace(/^>/gm, ' >').replace(/^/gm, '> ') // add quotation marks
|
||
return text/*+'\n'*/ || ''
|
||
}
|
||
|
||
function popupMessage(content, delay=1000) {
|
||
pups.info(content, {time: delay/1000})
|
||
console.warn('popupMessage() is deprecated. Please use YOBA alerts instead.')
|
||
}
|
||
|
||
var Captcha = {
|
||
init: function(forceEnable=false) {
|
||
let captchaImage = document.querySelector('.captchaimage')
|
||
this.enabled = forceEnable || !!captchaImage
|
||
if (!this.enabled) return;
|
||
injector.inject('captcha-rotting',
|
||
`.cw-running .rotting-indicator {
|
||
-webkit-animation-duration: ${captchaTimeout}s;
|
||
animation-duration: ${captchaTimeout}s;
|
||
}
|
||
.cw-running .captchaimage,
|
||
.cw-running .rotten-msg {
|
||
-webkit-animation-delay: ${captchaTimeout}s;
|
||
animation-delay: ${captchaTimeout}s;}`)
|
||
if (captchaImage) {
|
||
this.addImgLoadListener(captchaImage)
|
||
}
|
||
},
|
||
initForm: function(form) {
|
||
let cw = form.querySelector('.captchawrap')
|
||
if (!cw) return;
|
||
cw.onclick = this.onClick.bind(this)
|
||
;['click', 'focus'].forEach(evt => {
|
||
form.querySelector('input[name=captcha]').addEventListener(evt, this.onFieldClick.bind(this))
|
||
})
|
||
let captchaImage = form.querySelector('.captchaimage')
|
||
if (captchaImage) {
|
||
this.addImgLoadListener(captchaImage)
|
||
}
|
||
},
|
||
addImgLoadListener: function(captchaImage) {
|
||
captchaImage.onload = this.onImageLoad.bind(this)
|
||
;['animationend', 'webkitAnimationEnd', 'msAnimationEnd'].forEach(evType => {
|
||
captchaImage.addEventListener(evType, this.onAnimationEnd.bind(this))
|
||
})
|
||
},
|
||
_state: 'init',
|
||
get state() {
|
||
return this._state
|
||
},
|
||
set state(s) { // Capthca state machine
|
||
document.querySelectorAll('.captchawrap').forEach(c => {
|
||
if (s == 'init') {
|
||
c.classList.remove('cw-running')
|
||
c.classList.add('cw-initial')
|
||
}
|
||
if (s == 'init' || s == 'load') {
|
||
c.classList.add('captchaimage-invisible')
|
||
}
|
||
if (s == 'load') {
|
||
c.classList.remove('cw-initial', 'cw-running')
|
||
void c.offsetWidth //trigger a reflow
|
||
c.classList.add('cw-running', 'cw-loading')
|
||
}
|
||
else {
|
||
c.classList.remove('cw-loading')
|
||
}
|
||
if (s == 'run') {
|
||
c.classList.remove('captchaimage-invisible')
|
||
}
|
||
})
|
||
if (s !== 'run')
|
||
this.clear()
|
||
this._state = s
|
||
},
|
||
onClick: function(ev) {
|
||
this.refresh(ev.ctrlKey || ev.altKey)
|
||
ev.target._findParent('form').querySelector('input[name=captcha]').focus()
|
||
},
|
||
onFieldClick: function() {
|
||
if (this.state == 'init' || this.state == 'expired')
|
||
this.refresh()
|
||
},
|
||
onImageLoad: function() {
|
||
if (this.state == 'load')
|
||
this.state = 'run'
|
||
},
|
||
onAnimationEnd: function() {
|
||
this.state = 'expired'
|
||
},
|
||
clear: function() {
|
||
document.querySelectorAll('input[name="captcha"]').forEach(i => i.value = '')
|
||
},
|
||
refresh: function(switch_lang=false) {
|
||
let cColor = $('.captchawrap').css('color').match(/([0-9]+(?:\.[0-9]+)?)/g)
|
||
cColor = (cColor.length >= 3) ? '&color='+cColor.slice(0, 3).join(',') : ''
|
||
let url = ku_boardspath+'/captcha.php?'+Math.random()+cColor+(switch_lang ? '&switch' : '')
|
||
document.querySelectorAll('.captchaimage').forEach(img => img.src = url)
|
||
this.state = 'load'
|
||
}
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes < 1000) {
|
||
return `${bytes}B`
|
||
}
|
||
else if (bytes < 1000000) {
|
||
return `${Math.round(bytes/1000)}KB`
|
||
}
|
||
else {
|
||
return `${Math.round(bytes/100000)/10}MB`
|
||
}
|
||
}
|
||
|
||
const richFileInput = {
|
||
handleFiles: function(files, form, fromClipboard=false) {
|
||
let maxfiles = +form.dataset.maxfiles
|
||
, slotsLeft = maxfiles - $(form).find('.file-entry').length
|
||
, filesLeft = files.length
|
||
, i = 0
|
||
for (; slotsLeft && filesLeft; i++) {
|
||
let fileAttached = this.handleFile(files[i], form, fromClipboard)
|
||
slotsLeft -= fileAttached
|
||
filesLeft--
|
||
}
|
||
if (slotsLeft <= 0 && filesLeft)
|
||
pups.warn(_l.maxAttNumReached)
|
||
},
|
||
handleFile: function(file, form, fromClipboard=false) {
|
||
let fx = {}
|
||
// Determine the file extension
|
||
let ns = file.name.split('.')
|
||
fx.ext = (ns.length) > 1 ? ns.pop().toLowerCase() : ""
|
||
if (fx.ext == 'jpeg')
|
||
fx.ext = 'jpg'
|
||
|
||
if (
|
||
form.dataset.allowedAllFiletypes != '*'
|
||
&&
|
||
!_.contains(form.dataset.allowedFiletypes.split(','), fx.ext)
|
||
) {
|
||
pups.err(`${file.name}: ${_l.unsupportedFileType}.`)
|
||
return 0
|
||
}
|
||
fx.fname = ns.join('.')
|
||
|
||
fx.sizeFormatted = formatFileSize(file.size)
|
||
|
||
if (file.type.indexOf("image") != -1) {
|
||
let reader = new FileReader()
|
||
reader.onload = ev => {
|
||
fx.img = ev.target.result
|
||
this.addFile(file, fx, form, fromClipboard)
|
||
}
|
||
reader.readAsDataURL(file, fx, form)
|
||
}
|
||
else {
|
||
this.addFile(file, fx, form, fromClipboard)
|
||
}
|
||
return 1
|
||
},
|
||
addFile: function(file, extra, form, fromClipboard=false) {
|
||
let index = this.store.push(file)-1
|
||
form.querySelector('.fe-sort-wrapper').insertAdjacentHTML('beforeEnd', `
|
||
<div class="file-entry${fromClipboard ? ' fe-name-hidden' : ''}" data-index="${index}" title="${file.name}">
|
||
<div class="fe-thumb">
|
||
${extra.img ? `<img src=${extra.img}>` : makeGenericFileIcon(extra.ext)}
|
||
</div>
|
||
<input type="text" value="${extra.fname}" class="fe-fn" ${fromClipboard ? ' disabled' : ''}>
|
||
<button class="clear-filename fe-pop-button icon-wraping-button" title="${_l.hideName}"><svg class="icon b-icon">
|
||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#i-hide-name"></use>
|
||
</svg></button>
|
||
<button class="remove-file fe-pop-button icon-wraping-button" title="${_l.removeFile}"><svg class="icon b-icon">
|
||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#i-x"></use>
|
||
</svg></button>
|
||
<button class="spoiler-file fe-pop-button icon-wraping-button" title="${_l.spoilerOnOff}"><svg class="icon b-icon">
|
||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#i-spoiler"></use>
|
||
</svg></button>
|
||
<div class="fe-info"></div>
|
||
</div>
|
||
`)
|
||
this.recalcForm(form)
|
||
let thisEl = form.querySelector(`.file-entry[data-index="${index}"]`)
|
||
, img = false
|
||
, updateInfo = () => thisEl.querySelector(`.fe-info`).innerText =
|
||
`${extra.ext.toUpperCase()}${(img ? `, ${img.naturalWidth}×${img.naturalHeight}`: '')}, ${extra.sizeFormatted}`
|
||
if (extra.img) {
|
||
img = thisEl.querySelector('img')
|
||
img.onload = updateInfo
|
||
img.onerror = updateInfo
|
||
}
|
||
else updateInfo()
|
||
thisEl._ext = extra.ext
|
||
this.updateTitle($(thisEl))
|
||
},
|
||
recalcForm: function(form) {
|
||
let count = form.querySelectorAll('.file-entry').length
|
||
, maxfiles = +form.dataset.maxfiles
|
||
form.classList.toggle('fda-non-empty', count > 0)
|
||
form.classList.toggle('fda-form-full', count >= maxfiles)
|
||
form.querySelector('.file-count').innerHTML = count > 0
|
||
? `${_l.files}<br>(${count}/${maxfiles})`
|
||
: `${_l.file}`
|
||
},
|
||
updateTitle: function($el) {
|
||
let v = $el.find('.fe-fn').val()
|
||
, fname = !$el.hasClass('fe-name-hidden') && v ? v : '*****'
|
||
$el[0].title = `${fname}.${$el[0]._ext}`
|
||
},
|
||
serialize: function(form, fd) {
|
||
form.querySelectorAll('.file-entry').forEach((fe, i) => {
|
||
fd.append('imagefile[]', this.store[+fe.dataset.index])
|
||
if (fe.classList.contains('fe-spoiler'))
|
||
fd.append(`spoiler-${i}`, 1)
|
||
let fname = fe.querySelector('.fe-fn').value
|
||
if (fe.classList.contains('fe-name-hidden') || fname==='')
|
||
fd.append(`hidename-${i}`, 1)
|
||
else
|
||
fd.append(`filename-${i}`, fname)
|
||
} )
|
||
},
|
||
store: [],
|
||
initiated: false,
|
||
init: function(form) {
|
||
this.enabled = form.querySelector('.file-row') && 'FileReader' in window
|
||
if (this.enabled) {
|
||
let _this = this
|
||
$('body')
|
||
.on('click', '.remove-file', function(ev) {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
let $entry = $(this).parents('.file-entry')
|
||
_this.store[+$entry.data('index')] = null
|
||
let $form = $entry.parents('form[name=postform]')
|
||
$entry.remove()
|
||
_this.recalcForm($form[0])
|
||
})
|
||
.on('click', '.file-entry', function(ev) {
|
||
$(this).toggleClass('fe-expanded')
|
||
setTimeout(() => $(this).find('input').select(), 450)
|
||
})
|
||
.on('click', '.file-entry input', function(ev) {
|
||
ev.stopPropagation()
|
||
})
|
||
.on('click', '.spoiler-file', function(ev) {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
$(this).parents('.file-entry').toggleClass('fe-spoiler')
|
||
})
|
||
.on('click', '.clear-filename', function(ev) {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
let $entry = $(this).parents('.file-entry')
|
||
$entry.find('.fe-fn').prop('disabled', !$entry.hasClass('fe-name-hidden'))
|
||
$entry.toggleClass('fe-name-hidden')
|
||
_this.updateTitle($entry)
|
||
})
|
||
.on('change', '.fe-fn', function(ev) {
|
||
_this.updateTitle($(this).parents('.file-entry'))
|
||
})
|
||
.on('change', 'input[type=file]', function(ev) {
|
||
let form = $(this).parents('form[name=postform]')[0]
|
||
_this.handleFiles(ev.target.files, form)
|
||
this.value = null
|
||
})
|
||
.on('click', '.add-files', function(ev) {
|
||
ev.preventDefault()
|
||
$(this).parents('form[name=postform]').find('input[type=file]')[0].click()
|
||
})
|
||
.on('paste', 'form[name=postform]', function(ev) {
|
||
let items = (ev.clipboardData || ev.originalEvent.clipboardData).items
|
||
, arr = [];
|
||
for (var i = 0 ; i < items.length ; i++) {
|
||
var item = items[i];
|
||
if (item.kind == 'file') {
|
||
arr.push(item.getAsFile())
|
||
}
|
||
}
|
||
_this.handleFiles(arr, this, true)
|
||
})
|
||
}
|
||
else {
|
||
$('body')
|
||
.on('click', '.remove-file', function(ev) {
|
||
ev.preventDefault()
|
||
$(this).parent().find('input[type=file]')
|
||
.val(null)
|
||
.removeClass('non-empty-file-input')
|
||
})
|
||
.on('change', 'input[type=file]', function() {
|
||
$(this).toggleClass('non-empty-file-input', this.value)
|
||
})
|
||
}
|
||
this.initiated = true
|
||
/*if (!is_overboard)
|
||
this.initForm(postform)*/
|
||
},
|
||
initForm: function(form) {
|
||
if (!this.initiated)
|
||
this.init(form)
|
||
let _this = this
|
||
if (this.enabled) {
|
||
// Drag and drop events
|
||
$(form) // I tried using plain JS, doesn't work, sorry
|
||
.on('dragenter dragend dragover drop dragleave', function(ev) {
|
||
ev.originalEvent.stopPropagation()
|
||
if (!(ev.target.nodeName == 'TEXTAREA' || ev.target.nodeName == 'INPUT'))
|
||
ev.preventDefault()
|
||
})
|
||
.on('drop', function(ev) {
|
||
ev.originalEvent.stopPropagation()
|
||
if (ev.originalEvent.dataTransfer && ev.originalEvent.dataTransfer.files.length) {
|
||
ev.originalEvent.preventDefault()
|
||
_this.handleFiles(ev.originalEvent.dataTransfer.files, this)
|
||
}
|
||
})
|
||
// File entry sorting
|
||
if(form.querySelector('.fe-sort-wrapper')) {
|
||
Sortable.create(form.querySelector('.fe-sort-wrapper'), {
|
||
animation: 150,
|
||
handle: '.fe-thumb'
|
||
})
|
||
}
|
||
// Prevent legacy file inputs from being serialized
|
||
form.querySelectorAll('input[name^=imagefile]').forEach(i => i.removeAttribute('name'))
|
||
}
|
||
else {
|
||
let area = form.querySelector('.drop-area')
|
||
if (!area) return;
|
||
area.previousElementSibling.classList.remove('noscript')
|
||
area.remove()
|
||
}
|
||
},
|
||
clear: function(form, clearStore=false) {
|
||
if (this.enabled) {
|
||
form.querySelectorAll('.file-entry').forEach(fe => {
|
||
if (clearStore)
|
||
this.store[fe.dataset.index] = null
|
||
fe.remove()
|
||
})
|
||
this.recalcForm(form)
|
||
}
|
||
}
|
||
}
|
||
|
||
function makeGenericFileIcon(ext) {
|
||
let color = ext.toLowerCase().split('').map(char=>char.charCodeAt(0)).reverse().join('') % 360
|
||
return `<div class="generic-file-icon" style="background-color: hsl(${color}, 66%, 50%)"><div class="gfi-extension">${ext}</div></div>`
|
||
}
|
||
|
||
function handleCtrlEnter(ev) {
|
||
ev.preventDefault()
|
||
let area = document.activeElement
|
||
, form = area._findParent('form[name=postform]')
|
||
if (form) {
|
||
Ajax.submitPost(form)
|
||
}
|
||
}
|
||
|
||
function redirectTo(href) {
|
||
// console.warn('About to redirect to:', href)
|
||
document.location.href = href
|
||
}
|
||
|
||
const Ajax = {
|
||
submitPost: function(form, sage) {
|
||
if (form.querySelector('script[src^="https://js.hcaptcha.com"]')) {
|
||
hcaptcha.execute({ async: true })
|
||
.then(({ response, key }) => {
|
||
this.actuallySubmitPost(form, sage, response)
|
||
})
|
||
.catch(err => {
|
||
console.error('hcapthca:', err)
|
||
if (err == 'challenge-expired')
|
||
this.shownErrors.push(pups.warn(_l.captchaExpired))
|
||
});
|
||
}
|
||
else {
|
||
this.actuallySubmitPost(form, sage)
|
||
}
|
||
},
|
||
actuallySubmitPost: function(form, sage, hCaptchaResponse=false) {
|
||
let alertsToRemove = _.clone(this.shownErrors)
|
||
setTimeout(() => {
|
||
alertsToRemove.forEach(a => {
|
||
pups.closeByID(a)
|
||
})
|
||
}, 300)
|
||
this.shownErrors = []
|
||
let showError = (a, b={}) => this.shownErrors.push(pups.err(a, b))
|
||
let showWarning = (a, b={}) => this.shownErrors.push(pups.warn(a, b))
|
||
|
||
let captchafield = form.captcha
|
||
if (captchafield && !captchafield.value.length) {
|
||
captchafield.focus()
|
||
return showWarning(_l.enterCaptcha)
|
||
}
|
||
|
||
form.classList.add('form-sending')
|
||
let fd = new FormData(form)
|
||
|
||
if (typeof sage !== 'undefined')
|
||
fd.append('sagebtn', sage)
|
||
|
||
if (hCaptchaResponse) {
|
||
fd.append('h-captcha-response', hCaptchaResponse)
|
||
}
|
||
|
||
if (richFileInput.enabled)
|
||
richFileInput.serialize(form, fd)
|
||
let xr = new XMLHttpRequest()
|
||
fd.append('AJAX', 1)
|
||
|
||
// Token for live updates
|
||
this.postToken = randomString()
|
||
fd.append('token', this.postToken)
|
||
|
||
xr.open('POST', form.action)
|
||
xr.onload = e => {
|
||
form.classList.remove('form-sending')
|
||
if (xr.status !== 200) {
|
||
showError(_l.xhrError)
|
||
return
|
||
}
|
||
;[].forEach.call(form.querySelectorAll('.error-in-attachment'), el => el.classList.remove('error-in-attachment'))
|
||
if (Captcha.enabled)
|
||
Captcha.state = 'init'
|
||
let res = null
|
||
try {
|
||
res = JSON.parse(xr.response)
|
||
}
|
||
catch(e) {
|
||
showError(_l.xhrError)
|
||
console.error('Malformed response (JSON expected):', xr.response)
|
||
return
|
||
}
|
||
if (res.error) {
|
||
if (res.error_verbose)
|
||
res.error = `${res.error}<br>${res.error_verbose}`
|
||
if (res.error_type) {
|
||
if (res.error_type == 'ban') {
|
||
showError(`${res.error} (<a href="${ku_cgipath}/banned.php" target="_blank">${_l.details}</a>)`, {time: 0, save: true})
|
||
}
|
||
if (res.error_type == 'duplicate_file') {
|
||
showError(res.error, {time: 0})
|
||
form.querySelector('input[name="imagefile"]').value = null
|
||
}
|
||
if (res.error_type == 'upload_error') {
|
||
showError(res.error, {time: 0})
|
||
let badatt = form.querySelector(`.multiembedwrap[data-pos="${res.error_data.attachmenttype}-${res.error_data.position+1}"]`)
|
||
if (badatt) {
|
||
badatt.classList.add('error-in-attachment')
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
showError(res.error, {time: 0})
|
||
}
|
||
return
|
||
}
|
||
else {
|
||
if (res.timings) {
|
||
console.log('timings:', res.timings)
|
||
}
|
||
resetForm(form)
|
||
if (form.classList.contains('quick-reply-form')) {
|
||
hidePostForm(form) // TODO: tumbach pinning?
|
||
}
|
||
/*else {
|
||
}*/
|
||
let nokoInput = form.querySelector('input[name="redirecttothread"]')
|
||
, noko = nokoInput && nokoInput.checked
|
||
if (res.thread_replyto != 0) {
|
||
newposts.get({
|
||
threadid: ispage ? res.thread_replyto+'-'+res.board : null,
|
||
expectedPost: res.post_id+'-'+res.board,
|
||
onError: (err) => {
|
||
console.error(err)
|
||
redirectTo(`${ku_boardspath}/${res.board}/res/${res.thread_replyto}.html#${res.post_id}`)
|
||
if (! ispage)
|
||
document.location.reload()
|
||
},
|
||
onSuccess: () => $(`#delform div[id^=thread${res.thread_replyto}] .fresh-replies`).remove()
|
||
})
|
||
}
|
||
else {
|
||
if (noko) {
|
||
redirectTo(`${ku_boardspath}/${res.board}/res/${res.post_id}.html`)
|
||
}
|
||
else {
|
||
redirectTo(`${ku_boardspath}/${res.board}`)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
xr.send(fd)
|
||
},
|
||
shownErrors: [],
|
||
reportPost: function(fd, callback=null) {
|
||
let xr = new XMLHttpRequest()
|
||
fd.append('AJAX', 1)
|
||
fd.append('reportpost', 1)
|
||
xr.open('POST', document.forms.delform.action)
|
||
xr.onload = e => {
|
||
if (xr.status !== 200) {
|
||
pups.err(_l.xhrError)
|
||
return callback(false)
|
||
}
|
||
let res = null
|
||
try {
|
||
res = JSON.parse(xr.response)
|
||
}
|
||
catch(e) {
|
||
pups.err(_l.xhrError)
|
||
console.error('Malformed response (JSON expected):', xr.response)
|
||
return callback(false)
|
||
}
|
||
if (callback)
|
||
callback(res.data)
|
||
if (res.error) {
|
||
if (res.error_verbose)
|
||
res.error = `${res.error}<br>${res.error_verbose}`
|
||
pups.err(res.error, {time: 0})
|
||
return
|
||
}
|
||
else {
|
||
$('body').removeClass('select-multiple')
|
||
$('.userdelete').removeClass('ud-active')
|
||
let postsReported = []
|
||
, postErrors = []
|
||
res.data.forEach(item => {
|
||
if (!item.success) {
|
||
postErrors.push(item)
|
||
return
|
||
}
|
||
postsReported.push(item.id)
|
||
if (! $('input[name="post[]"]:checked').length)
|
||
$('.userdelete').removeClass('ud-active');
|
||
})
|
||
let msg = ''
|
||
if (postsReported.length) {
|
||
postsReported = postsReported.map(post => '#'+post)
|
||
msg += postsReported.length > 1 ?
|
||
`${_l.posts} ${postsReported.join(', ')} ${_l.reportedMulti}.`
|
||
:`${_l.post} ${postsReported[0]} ${_l.reported}.`
|
||
pups.succ(msg, {time: 2 + postsReported.length})
|
||
}
|
||
postErrors.forEach(err => {
|
||
pups.err(`${_l.post} #${err.id}: ${err.message}`)
|
||
})
|
||
}
|
||
}
|
||
xr.send(fd)
|
||
},
|
||
cancelTimer: function(fd, callback=null) {
|
||
let xr = new XMLHttpRequest()
|
||
fd.append('AJAX', 1)
|
||
fd.append('cancel_timer', 1)
|
||
|
||
// Token for live updates
|
||
this.timerToken = randomString()
|
||
fd.append('token', this.timerToken)
|
||
|
||
xr.open('POST', document.forms.delform.action)
|
||
xr.onload = e => {
|
||
if (xr.status !== 200) {
|
||
pups.err(_l.xhrError)
|
||
return callback(false)
|
||
}
|
||
let res = null
|
||
try {
|
||
res = JSON.parse(xr.response)
|
||
}
|
||
catch(e) {
|
||
pups.err(_l.xhrError)
|
||
console.error('Malformed response (JSON expected):', xr.response)
|
||
return callback(false)
|
||
}
|
||
if (res.error) {
|
||
if (res.error_verbose)
|
||
res.error = `${res.error}<br>${res.error_verbose}`
|
||
pups.err(res.error, {time: 0})
|
||
return callback(false)
|
||
}
|
||
else {
|
||
$('body').removeClass('select-multiple')
|
||
$('.userdelete').removeClass('ud-active')
|
||
let postsSaved = []
|
||
, postErrors = []
|
||
res.data.forEach(item => {
|
||
if (!item.success) {
|
||
postErrors.push(item)
|
||
return
|
||
}
|
||
postsSaved.push(item.id)
|
||
if (! $('input[name="post[]"]:checked').length)
|
||
$('.userdelete').removeClass('ud-active');
|
||
})
|
||
let msg = ''
|
||
if (postsSaved.length) {
|
||
postsSaved.forEach(id => {
|
||
let $reply = $(`.postnode[data-id=${id}]`)
|
||
$reply.find('.post-ttl').remove()
|
||
$reply.find('.post-menu').hide()
|
||
} )
|
||
msg += postsSaved.length > 1 ?
|
||
`${_l.posts} #${postsSaved.join(', #')} ${_l.savedMulti}.`
|
||
:`${_l.post} #${postsSaved[0]} ${_l.saved}.`
|
||
pups.succ(msg, {time: 2 + postsSaved.length})
|
||
}
|
||
postErrors.forEach(err => {
|
||
pups.err(`${_l.post} #${err.id}: ${err.message}`)
|
||
})
|
||
callback(postErrors)
|
||
}
|
||
}
|
||
xr.send(fd)
|
||
},
|
||
modThread: function(a, action) {
|
||
let $a = $(a)
|
||
, fd = new FormData()
|
||
, xr = new XMLHttpRequest()
|
||
, $li = $a.find('li')
|
||
$li.addClass('spin-around')
|
||
fd.append('AJAX', 1)
|
||
xr.open('POST', a.getAttribute('href'))
|
||
xr.onload = e => {
|
||
$li.removeClass('spin-around')
|
||
if (xr.status !== 200) {
|
||
pups.err(_l.xhrError)
|
||
return
|
||
}
|
||
let res = null
|
||
try {
|
||
res = JSON.parse(xr.response)
|
||
}
|
||
catch(e) {
|
||
pups.err(_l.xhrError)
|
||
console.error('Malformed response (JSON expected):', xr.response)
|
||
return
|
||
}
|
||
if (res.error) {
|
||
if (res.error_verbose)
|
||
res.error = `${res.error}<br>${res.error_verbose}`
|
||
pups.err(res.error, {time: 0})
|
||
return
|
||
}
|
||
else {
|
||
let $posthead = $a.parents('.posthead')
|
||
, $extrabtns = $posthead.find('.extrabtns')
|
||
if (action == 'stickypost') {
|
||
$extrabtns.prepend(makeIcon('pin', 'i-icon i-pin'));
|
||
$posthead.addClass('thread-stickied')
|
||
}
|
||
if (action == 'unstickypost') {
|
||
$extrabtns.find('svg.i-icon.i-pin').remove()
|
||
$posthead.removeClass('thread-stickied')
|
||
}
|
||
if (action == 'lockpost') {
|
||
$extrabtns.prepend(makeIcon('lock', 'i-icon i-lock'));
|
||
$posthead.addClass('thread-locked')
|
||
}
|
||
if (action == 'unlockpost') {
|
||
$extrabtns.find('svg.i-icon.i-lock').remove()
|
||
$posthead.removeClass('thread-locked')
|
||
}
|
||
pups.succ(res.message)
|
||
}
|
||
}
|
||
xr.send(fd)
|
||
},
|
||
deleteItems: function(fd, callback=null, asMod=false) {
|
||
let kind = 'Items'
|
||
if (asMod) {
|
||
console.log('yes')
|
||
fd.append('moddelete', 1)
|
||
}
|
||
if (fd.has) {
|
||
if (fd.has('post[]')) {
|
||
if (!fd.has('delete-file[]'))
|
||
kind = 'Posts'
|
||
fd.append('deletepost', 1)
|
||
}
|
||
else if (fd.has('delete-file[]'))
|
||
kind = 'Files'
|
||
else return;
|
||
}
|
||
else { // FormData.has() is not supported
|
||
fd.append('deletepost', 1)
|
||
}
|
||
|
||
let xr = new XMLHttpRequest()
|
||
fd.append('AJAX', 1)
|
||
|
||
// Token for live updates
|
||
this.delToken = randomString()
|
||
fd.append('token', this.delToken)
|
||
|
||
xr.open('POST', document.forms.delform.action)
|
||
xr.onload = e => {
|
||
if (xr.status !== 200) {
|
||
pups.err(_l.xhrError)
|
||
return callback ? callback(null) : null
|
||
}
|
||
let res = null
|
||
try {
|
||
res = JSON.parse(xr.response)
|
||
}
|
||
catch(e) {
|
||
pups.err(_l.xhrError)
|
||
console.error('Malformed response (JSON expected):', xr.response)
|
||
return callback(null)
|
||
}
|
||
if (callback)
|
||
callback(res.data)
|
||
if (res.error) {
|
||
if (res.error_verbose)
|
||
res.error = `${res.error}<br>${res.error_verbose}`
|
||
pups.err(res.error, {time: 0})
|
||
return
|
||
}
|
||
else {
|
||
deleteItems(res.data)
|
||
}
|
||
}
|
||
xr.send(fd)
|
||
}
|
||
}
|
||
|
||
function inspectFormData(formData) {
|
||
for (var pair of formData.entries()) {
|
||
console.log(pair[0]+ ', ' + pair[1]);
|
||
}
|
||
}
|
||
|
||
function makeIcon(i, classes="", bare=false) {
|
||
return `${bare ? '' : `<svg class="icon ${classes}">`}
|
||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#i-${i}"></use>
|
||
${bare ? '' : '</svg>'}`
|
||
}
|
||
|
||
function addCaptchaToMenu($menu) {
|
||
if ($menu.find('.captchawrap').length) {
|
||
Captcha.state = 'init';
|
||
return;
|
||
}
|
||
if (!Captcha.enabled) {
|
||
Captcha.init('only-access')
|
||
}
|
||
$menu.prepend(`<li class="menu-captcha">
|
||
<div class="captchawrap cw-initial captchaimage-invisible" title="${_l.refreshCaptcha}">
|
||
<div class="captcha-show msg">${_l.showCaptcha}</div>
|
||
<img class="captchaimage" valign="middle" border="0" alt="${_l.captchaImage}">
|
||
<div class="rotting-indicator"></div>
|
||
<div class="rotten-msg msg">${_l.captchaExpired}</div>
|
||
</div>
|
||
<input type="text" name="captcha" placeholder="${_l.captcha}" autocomplete="off">
|
||
</li>`)
|
||
Captcha.initForm($menu[0])
|
||
$menu.find('input[name=captcha]').keydown(ev => {
|
||
if (ev.key == 'Enter') {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
}
|
||
})
|
||
}
|
||
|
||
// YOBA media previews
|
||
class MediaViewer {
|
||
static parseVideo(link) {
|
||
let postnum = link.dataset.id
|
||
, imgurl = link.href
|
||
, thumburl = link.dataset.thumb
|
||
, imgw = link.dataset.width
|
||
, imgh = link.dataset.height
|
||
, thumb = link.querySelector('img')
|
||
, thumbw = thumb.width
|
||
, thumbh = thumb.height
|
||
return [postnum, imgurl, thumburl, imgw, imgh, thumbw, thumbh]
|
||
}
|
||
static parseImg(link) {
|
||
return link.getAttribute('onclick')
|
||
.match(/expandimg\('(.+?)', ?'(.+?)', ?'(.+?)', ?'(.+?)', ?'(.+?)', ?'(.+?)', ?'(.+?)'/)
|
||
.slice(1)
|
||
}
|
||
constructor(postnum, imgurl, thumburl, imgw, imgh, thumbw, thumbh, isVideo=false) {
|
||
let thumb = document.querySelector(`#thumb${postnum} img`)
|
||
if (thumb == null) return false;
|
||
this.post = thumb._findParent('.postbody')
|
||
this.initZoom()
|
||
this.createContainer()
|
||
this.resetTransform()
|
||
let pre = thumb._findParent('.reflinkpreview')
|
||
if (pre)
|
||
PostPreviews.pinPreview(pre, true, true)
|
||
let thBCR = thumb.getBoundingClientRect()
|
||
, thumb_x = thBCR.x, thumb_y = thBCR.y, thumb_w = thBCR.width, thumb_h = thBCR.height
|
||
, win_w = this.viewer.clientWidth, win_h = this.viewer.clientHeight
|
||
, scaleDownFactor = Math.min(win_w / imgw, win_h / imgh, 1)
|
||
, pre_w = imgw * scaleDownFactor, pre_h = imgh * scaleDownFactor
|
||
, visibleMarginX = (win_w-pre_w)/2, visibleMarginY = (win_h-pre_h)/2
|
||
, mi_w = Math.min(win_w, imgw), mi_h = Math.min(win_h, imgh)
|
||
, screenScaleDownFactor = thumb_w/pre_w
|
||
, transX = (thumb_x + thumb_w/2) - win_w/2, transY = (thumb_y + thumb_h/2) - win_h/2
|
||
, trans = `style="transform: translate(${transX}px, ${transY}px) scale(${screenScaleDownFactor})"`
|
||
, me = this.viewer._ins('afterBegin', this.createMediaElement(thumb.src, imgw, imgh, imgurl, trans, isVideo))
|
||
this.refreshList()
|
||
this.currentThumb = thumb
|
||
thumb.style.visibility = 'hidden'
|
||
this.updateCounter()
|
||
this.showPercentage(scaleDownFactor)
|
||
me._isVideo = isVideo
|
||
this.setVolume(me)
|
||
me._scaleDownFactor = scaleDownFactor
|
||
this.currentMediaItem = me
|
||
me._realWidth = imgw
|
||
this.imgw = imgw
|
||
this.imgh = imgh
|
||
document.body.style.overflow = 'hidden'
|
||
requestAnimationFrame(() => {
|
||
this.container.classList.remove('mvc-collapsed')
|
||
me.style.transform = null
|
||
})
|
||
}
|
||
refreshList() {
|
||
this.list = document.querySelectorAll('img.thumb')
|
||
}
|
||
findIndex(thumb) {
|
||
return Array.prototype.findIndex.call(this.list, e => e==thumb)
|
||
}
|
||
// Container
|
||
createContainer() {
|
||
this.container = document.body._ins('beforeend', `<div class="mv-container mvc-collapsed">
|
||
<div class="media-viewer">
|
||
<div class="mv-label">
|
||
<span class="item-counter"></span>
|
||
<span class="scale-indicator"></span>
|
||
</div>
|
||
<div class="mv-button mv-prev-next mv-prev"></div>
|
||
<div class="mv-button mv-prev-next mv-next"></div>
|
||
<div class="mv-button mv-restore"></div>
|
||
<div class="mv-button mv-close"></div>
|
||
</div></div>`)
|
||
this.viewer = this.container.querySelector('.media-viewer')
|
||
this.viewer.addEventListener('wheel', this.handleZoom.bind(this))
|
||
this.viewer.addEventListener('mousemove', ev => {
|
||
this.mouseX = ev.clientX
|
||
this.mouseY = ev.clientY
|
||
if (this.isMouseDown) {
|
||
this.handleDrag()
|
||
}
|
||
})
|
||
this.viewer.addEventListener('mousedown', ev => {
|
||
if (ev.button==2 && this.currentMediaItem._isVideo) {
|
||
let ov = this.currentMediaItem.querySelector('.video-event-overlay')
|
||
ov.style.pointerEvents='none'
|
||
setTimeout(() => ov.style.pointerEvents='all', 100)
|
||
}
|
||
this.gripX = ev.clientX
|
||
this.gripY = ev.clientY
|
||
this.isMouseDown = true
|
||
if (ev.target.classList.contains('media-viewer') || ev.target.classList.contains('mv-under'))
|
||
this.emptyClick = true
|
||
})
|
||
this.viewer.addEventListener('contextmenu', ev => {
|
||
if (this.currentMediaItem._isVideo) {
|
||
let ov = this.currentMediaItem.querySelector('.video-event-overlay')
|
||
ov.style.pointerEvents='none'
|
||
setTimeout(() => ov.style.pointerEvents='all', 100)
|
||
}
|
||
})
|
||
this.viewer.addEventListener('click', ev=> {
|
||
if (this.isDragged) {
|
||
this.isDragged = false
|
||
ev.preventDefault()
|
||
}
|
||
})
|
||
this.viewer.addEventListener('mouseup', ev => {
|
||
this.isMouseDown = false
|
||
this.currentMediaItem.classList.remove('no-transition')
|
||
if (this.isDragged) {
|
||
ev.preventDefault()
|
||
}
|
||
else if (ev.button == 0) {
|
||
this.collapse()
|
||
}
|
||
this.emptyClick = false
|
||
})
|
||
this.viewer.addEventListener('dragstart', ev => {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
})
|
||
this.viewer.querySelectorAll('.mv-button').forEach(pnb => {
|
||
pnb.addEventListener('click', ev => {
|
||
ev.stopPropagation()
|
||
this.refreshList()
|
||
let index = this.findIndex(this.currentThumb)
|
||
if (pnb.classList.contains('mv-prev') || pnb.classList.contains('mv-next')) {
|
||
if (pnb.classList.contains('mv-prev')) {
|
||
index -= 1
|
||
if (index < 0)
|
||
index = this.list.length-1
|
||
}
|
||
else {
|
||
index += 1
|
||
if (index >= this.list.length)
|
||
index = 0
|
||
}
|
||
this.switchItem(this.list[index])
|
||
}
|
||
else if (pnb.classList.contains('mv-close')) {
|
||
this.collapse()
|
||
}
|
||
else if (pnb.classList.contains('mv-restore')) {
|
||
this.toggleFullSize(0)
|
||
}
|
||
})
|
||
;['mouseup', 'mousedown'].forEach(evt => pnb.addEventListener(evt, ev => ev.stopPropagation()))
|
||
})
|
||
this.viewer.addEventListener('mouseleave', ev => {
|
||
this.isMouseDown = false
|
||
this.isDragged = false
|
||
})
|
||
}
|
||
// Managing transforms
|
||
applyTransform() {
|
||
let me = this.currentMediaItem
|
||
if (!me) return;
|
||
me.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`
|
||
}
|
||
resetTransform() {
|
||
this.translateX = 0
|
||
this.translateY = 0
|
||
this.scale = 1
|
||
}
|
||
// Zooming
|
||
initZoom() {
|
||
this.zoomAmount = 0.5
|
||
this.minScale = 0.1
|
||
this.zoomSteps = [0.1, 0.25, 0.33, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10]
|
||
}
|
||
handleZoom(ev) {
|
||
let me = this.currentMediaItem
|
||
if (!me) return;
|
||
ev.stopPropagation()
|
||
ev.preventDefault()
|
||
let dir = -Math.sign(ev.deltaY)
|
||
, initialScale = 1/me._scaleDownFactor
|
||
, zSteps = Array.from(this.zoomSteps)
|
||
.filter(z => Math.abs((z-initialScale)/initialScale) > 0.1) // Remove zoom steps that are close to (real) 100%
|
||
.concat([initialScale]) // Add a (real) 100% step
|
||
, centerX = this.viewer.clientWidth / 2 + this.translateX
|
||
, centerY = this.viewer.clientHeight / 2 + this.translateY
|
||
, newScale = zSteps.map(z => z - this.scale).filter(z => z*dir > 0) // Pick only the steps in the needed direction
|
||
.sort((a,b) => (a-b)*dir)[0] + this.scale // Find the closest step
|
||
if (!isNaN(newScale)) { // It may be NaN in case when minimum or maximum zoom amount is achieved
|
||
this.showPercentage(me._scaleDownFactor * newScale)
|
||
this.translateX += (centerX - this.mouseX)/this.scale * (newScale-this.scale)
|
||
this.translateY += (centerY - this.mouseY)/this.scale * (newScale-this.scale)
|
||
this.scale = newScale
|
||
this.applyTransform()
|
||
this.flashLabel()
|
||
}
|
||
}
|
||
showPercentage(scale) {
|
||
this.viewer.querySelector('.scale-indicator').innerText = `| ${Math.round(scale * 100)}%`
|
||
}
|
||
// Dragging
|
||
handleDrag(ev) {
|
||
let me = this.currentMediaItem
|
||
if (!me) return;
|
||
let dx = this.mouseX - this.gripX
|
||
, dy = this.mouseY - this.gripY
|
||
if (dx != 0 || dy != 0) {
|
||
if (!this.isDragged) {
|
||
this.currentMediaItem.classList.add('no-transition')
|
||
this.isDragged = true
|
||
this.initialTransX = this.translateX
|
||
this.initialTransY = this.translateY
|
||
}
|
||
this.translateX = this.initialTransX+dx
|
||
this.translateY = this.initialTransY+dy
|
||
this.applyTransform()
|
||
}
|
||
}
|
||
// Switching
|
||
switchItem(thumb) {
|
||
let link = thumb._findParent('a')
|
||
, isVideo = link.classList.contains('movie')
|
||
let [postnum, imgurl, thumburl, imgw, imgh, thumbw, thumbh] = isVideo
|
||
? MediaViewer.parseVideo(link)
|
||
: MediaViewer.parseImg(link)
|
||
let win_w = this.viewer.clientWidth, win_h = this.viewer.clientHeight
|
||
, scaleDownFactor = Math.min(win_w / imgw, win_h / imgh, 1)
|
||
this.showPercentage(scaleDownFactor)
|
||
this.resetTransform()
|
||
this.applyTransform()
|
||
this.currentMediaItem._isVideo = isVideo
|
||
this.currentMediaItem._scaleDownFactor = scaleDownFactor
|
||
this.currentMediaItem.classList.remove('no-transition')
|
||
this.currentMediaItem.innerHTML = this.createMediaElement(thumb.src, imgw, imgh, imgurl, null, isVideo)
|
||
this.setVolume(this.currentMediaItem)
|
||
this.currentThumb.style.visibility = 'visible'
|
||
thumb.style.visibility = 'hidden'
|
||
this.post = thumb._findParent('.postbody')
|
||
this.currentThumb = thumb
|
||
this.updateCounter()
|
||
this.refreshList()
|
||
}
|
||
setVolume(me) {
|
||
if (!me._isVideo) return;
|
||
let vid = me.querySelector('video')
|
||
vid.volume = localStorage['mediaVolume'] || 1
|
||
vid.onvolumechange = function() {
|
||
localStorage['mediaVolume'] = this.muted ? 0 : this.volume
|
||
}
|
||
}
|
||
updateCounter() {
|
||
let index = this.findIndex(this.currentThumb)
|
||
this.viewer.querySelector('.item-counter').innerText = `${index+1}/${this.list.length}`
|
||
this.flashLabel()
|
||
}
|
||
flashLabel() {
|
||
clearTimeout(this.flashLabelTimeout)
|
||
this.viewer.querySelector('.mv-label').classList.add('mvl-visible')
|
||
this.flashLabelTimeout = setTimeout(() => this.viewer.querySelector('.mv-label').classList.remove('mvl-visible'), 1000)
|
||
}
|
||
// Closing
|
||
collapse() {
|
||
document.body.style.overflow = ''
|
||
let me = this.currentMediaItem
|
||
, mi_w = me.clientWidth, mi_h = me.clientHeight
|
||
, svg = me.querySelector('.placeholder-svg')
|
||
, resizeFactor = Math.min(this.viewer.clientHeight/svg.naturalHeight, this.viewer.clientWidth/svg.naturalWidth)
|
||
if (me._isVideo) {
|
||
me.querySelector('video').remove()
|
||
}
|
||
this.isDragged = false
|
||
if (resizeFactor < 1) {
|
||
mi_w = svg.naturalWidth * resizeFactor
|
||
mi_h = svg.naturalHeight * resizeFactor
|
||
}
|
||
let offScreen = true, transform = ''
|
||
if (document.body.contains(this.currentThumb)) {
|
||
let thumbBCR = this.currentThumb.getBoundingClientRect()
|
||
offScreen = !(
|
||
thumbBCR.y < window.innerHeight
|
||
&&
|
||
(thumbBCR.y + thumbBCR.height) > 0
|
||
&&
|
||
thumbBCR.x < window.innerWidth
|
||
&&
|
||
(thumbBCR.x + thumbBCR.width) > 0
|
||
)
|
||
if (!offScreen) {
|
||
let transX = (thumbBCR.x + thumbBCR.width/2) - this.viewer.clientWidth/2
|
||
, transY = (thumbBCR.y + thumbBCR.height/2) - this.viewer.clientHeight/2
|
||
me.style.transform = `translate(${transX}px, ${transY}px) scale(${thumbBCR.width/mi_w})`
|
||
}
|
||
}
|
||
if (offScreen) {
|
||
me.style.transform = 'translate(0px, 0px) scale(0)'
|
||
}
|
||
me.classList.remove('no-transition')
|
||
this.container.classList.add('mvc-collapsed')
|
||
setTimeout(() => {
|
||
this.container.remove()
|
||
this.currentThumb.style.visibility = 'visible'
|
||
if (this.onCollapse) this.onCollapse()
|
||
}, 250)
|
||
}
|
||
// Full screen on and off
|
||
toggleFullSize(on) {
|
||
document.body.style.overflow = on ? 'hidden' : ''
|
||
if (on)
|
||
this.viewer.classList.remove('mv-transparent')
|
||
else
|
||
this.viewer.classList.add('mv-transparent')
|
||
}
|
||
// Media element within container
|
||
createMediaElement(underImageSrc, imgw, imgh, imgurl, trans='', isVideo=false) {
|
||
return `<div class="media-item ${isVideo ? 'loaded' : ''}" ${trans}>
|
||
<div style="background-image: url(${underImageSrc}); max-width: ${imgw}px" class="mv-under"></div>
|
||
<${isVideo ? 'video controls autoplay loop' : 'img'} src="${imgurl}" class="mv-over" ${isVideo
|
||
? `></video><img class="video-event-overlay" src="data:image/svg+xml;charset=utf-8,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width='${imgw}' height='${Math.min(imgh, window.innerHeight)-50}'%2F%3E">`
|
||
: `onload="this.parentElement.classList.add('loaded')">`}
|
||
<img class="placeholder-svg" src="data:image/svg+xml;charset=utf-8,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width='${imgw}' height='${imgh}'%2F%3E">
|
||
</div>`
|
||
}
|
||
}
|
||
|
||
// Insert adjacent HTML and immediately return the inserted element
|
||
Element.prototype._ins = function(position='beforeend', html) {
|
||
this.insertAdjacentHTML(position, html)
|
||
position = position.toLowerCase()
|
||
if (position == 'afterbegin')
|
||
return this.firstElementChild
|
||
else if (position == 'beforeend')
|
||
return this.lastElementChild
|
||
else if (position == 'beforebegin')
|
||
return this.previousElementSibling
|
||
else
|
||
return this.nextElementSibling
|
||
}
|
||
|
||
function expandimg_old(postnum, imgurl, thumburl, imgw, imgh, thumbw, thumbh) {
|
||
let element = document.getElementById("thumb" + postnum);
|
||
if (element == null) return false;
|
||
let $element = $(element)
|
||
, $postbody = $element.parents('.postbody')
|
||
, $fc_filename = $element.parents('figure').find('.fc-filename')
|
||
, fc_width
|
||
if(typeof event !== 'undefined' && event.which === 2) return true;
|
||
if (element.getElementsByTagName('img')[0].getAttribute('alt').substring(0,4)!='full') {
|
||
$element.html('<img src="'+imgurl+'" alt="full'+postnum+'" class="thumb" height="'+imgh+'" width="'+imgw+'">');
|
||
if (! Settings.expandImgFull()) {
|
||
let img = element.getElementsByTagName('img')[0]
|
||
, max_w = document.documentElement?document.documentElement.clientWidth : document.body.clientWidth
|
||
, offset = 50
|
||
, offset_el = img
|
||
|
||
while (offset_el != null) {
|
||
offset += offset_el.offsetLeft;
|
||
offset_el = offset_el.offsetParent;
|
||
}
|
||
var new_w = max_w - offset;
|
||
if (img.width > new_w) {
|
||
var ratio = img.width / img.height;
|
||
var zoom = 1 - new_w / img.width;
|
||
var new_h = new_w / ratio;
|
||
var notice = document.createElement('div');
|
||
notice.setAttribute('class', 'filesize');
|
||
notice.style.textDecoration = 'underline';
|
||
var textNode = document.createTextNode(_l.imageDownscaledBy + " " + Math.round(zoom*100) + "% "+_l.toFit);
|
||
notice.appendChild(textNode);
|
||
element.insertBefore(notice, img);
|
||
$(img).width(new_w);
|
||
$(img).height(new_h);
|
||
}
|
||
fc_width = new_w
|
||
}
|
||
else
|
||
fc_width = imgw
|
||
if ($postbody) $postbody.addClass('postbody-expanded')
|
||
}
|
||
else {
|
||
element.innerHTML = '<img src="' + thumburl + '" alt="' + postnum + '" class="thumb" height="' + thumbh + '" width="' + thumbw + '">';
|
||
fc_width = thumbw
|
||
if ($postbody) $postbody.removeClass('postbody-expanded')
|
||
}
|
||
$fc_filename[0].style.maxWidth = `${fc_width - 50}px`
|
||
return false;
|
||
}
|
||
|
||
function expandimg(postnum, imgurl, thumburl, imgw, imgh, thumbw, thumbh) {
|
||
if (Settings.newMediaViewer()) {
|
||
let mv = new MediaViewer(postnum, imgurl, thumburl, imgw, imgh, thumbw, thumbh)
|
||
mv.onCollapse = () => mv = null //gc
|
||
}
|
||
else {
|
||
expandimg_old(postnum, imgurl, thumburl, imgw, imgh, thumbw, thumbh)
|
||
}
|
||
return false
|
||
}
|
||
|
||
// YOBA previews
|
||
var PostPreviews = {
|
||
zindex: 3,
|
||
parent: {},
|
||
|
||
_parseLink: function(el) {
|
||
let url = el.getAttribute("href")
|
||
if (url) {
|
||
let matches = url.match(/\/(.+)\/res\/([0-9]+)\.html#([0-9]+)|postbynumber\.php\?b=(.+)&p=([0-9]+)/i)
|
||
if (matches) return {
|
||
board: matches[1] || matches[4],
|
||
parent: matches[2] || '?',
|
||
post: matches[3] || matches[5],
|
||
}
|
||
}
|
||
let cl = [].find.call(el.classList, c => c.match(/ref\|.+?\|([0-9]+|\?)\|[0-9]+/))
|
||
if (cl) {
|
||
cl = cl.split('|')
|
||
return {
|
||
board: cl[1],
|
||
parent: cl[2],
|
||
post: cl[3],
|
||
}
|
||
}
|
||
return null
|
||
},
|
||
|
||
_mouseover: function(e) {
|
||
e.stopPropagation();
|
||
var isCatalog = $(this).hasClass('catalog-entry')
|
||
, parsedURL = PostPreviews._parseLink(this)
|
||
if (!parsedURL) return false;
|
||
let board = parsedURL['board']
|
||
, parentid = parsedURL['parent']
|
||
, postid = parsedURL['post']
|
||
, previewid = 'preview_'+board+'_'+postid
|
||
, $preview = $('#' + previewid)
|
||
if ($preview.length == 0) {
|
||
$('body').children().first().before('<div id="'+previewid+'"></div>');
|
||
$preview = $('#' + previewid);
|
||
$preview.addClass('reflinkpreview content-background pre-hidden actual-reflinkpreview');
|
||
$preview.mouseleave(PostPreviews._mouseout);
|
||
$preview.mouseover(PostPreviews.mouseOverPreview);
|
||
}
|
||
else {
|
||
if ($preview[0]._pinned)
|
||
return false // No need to recreate a preview in this case
|
||
}
|
||
var parent = $(this).parents('div[id^=preview]');
|
||
if (parent.length > 0) {
|
||
if (previewid == parent.attr('id')) return; // anti-recursion
|
||
for (var id in PostPreviews.parent) {
|
||
if (id == previewid || PostPreviews.parent[id] == previewid)
|
||
return
|
||
}
|
||
PostPreviews.parent[previewid] = parent.attr('id');
|
||
}
|
||
let transformOrigin;
|
||
if(e.clientY < ($(window).height() / 1.5)) {
|
||
$preview.css({top:e.pageY+5});
|
||
transformOrigin = "top "
|
||
} else {
|
||
$preview.css({bottom:$(window).height()-e.pageY+5});
|
||
transformOrigin = "bottom "
|
||
}
|
||
if(e.clientX < ($(window).width() / 1.5)) {
|
||
$preview.css({left:e.pageX+15});
|
||
transformOrigin += "left"
|
||
} else {
|
||
$preview.css({right:$(window).width()-e.pageX+15});
|
||
transformOrigin += "right"
|
||
}
|
||
$preview.css({zIndex: PostPreviews.zindex++, "transform-origin": transformOrigin});
|
||
this.style.cursor = "progress"
|
||
let preview = $preview[0]
|
||
HTMLoader.getPost(board, parentid, postid, (err, post) => {
|
||
window.setTimeout(() => {
|
||
preview.getBoundingClientRect() // force reflow or how is this thing called
|
||
$preview.html(post ? post : (_l.oops + (err !== false ? ` (${err})` : '')))
|
||
.removeClass('pre-hidden')
|
||
this.style.cursor = "pointer"
|
||
// Add pin button
|
||
let ebtns = preview.querySelector('.extrabtns')
|
||
if (!ebtns) return;
|
||
ebtns.insertAdjacentHTML('beforeEnd', `
|
||
<a href="#" class="pin-preview pinner unpinned" title="${_l.pinUnpin}">
|
||
<svg class="icon b-icon">
|
||
<use class="use-pin" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#i-pin"></use>
|
||
<use class="use-unpin" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#i-unpin"></use>
|
||
</svg>
|
||
</a>`)
|
||
let pinner = ebtns.querySelector('.pinner')
|
||
pinner.onclick = (ev) => {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
PostPreviews.pinPreview($preview[0])
|
||
}
|
||
}, 0)
|
||
})
|
||
e.preventDefault();
|
||
},
|
||
|
||
mouseOverPreview: function() {
|
||
var preview = $(this);
|
||
if ($(this).is('a')) {
|
||
let parsedURL = PostPreviews._parseLink(this)
|
||
if (!parsedURL) return;
|
||
let board = parsedURL['board']
|
||
, postid = parsedURL['post']
|
||
preview = $('#preview_'+board+"_"+postid).first();
|
||
}
|
||
while (preview.length > 0) {
|
||
clearTimeout(preview[0].fadeout)
|
||
preview = $('#' + PostPreviews.parent[preview.attr('id')])
|
||
}
|
||
},
|
||
|
||
_mouseout: function(ev, nodelay=false) {
|
||
var preview = $(this);
|
||
if ($(this).is('a')) {
|
||
if (this.predelay) {
|
||
clearTimeout(this.predelay)
|
||
}
|
||
let parsedURL = PostPreviews._parseLink(this)
|
||
if (!parsedURL) return;
|
||
let board = parsedURL['board']
|
||
, postid = parsedURL['post']
|
||
preview = $('#preview_'+board+"_"+postid).first();
|
||
}
|
||
while (preview.length > 0) {
|
||
clearTimeout(preview[0].fadeout)
|
||
let pre = preview
|
||
if (pre[0]._pinned) return;
|
||
|
||
preview[0].fadeout = setTimeout(() => {
|
||
pre.addClass('pre-hidden')
|
||
setTimeout(() => {
|
||
delete PostPreviews.parent[pre.attr('id')];
|
||
pre.remove();
|
||
}, PostPreviews._timings.transition)
|
||
}, nodelay ? 0 : PostPreviews._timings.fade)
|
||
preview = $('#' + PostPreviews.parent[preview.attr('id')]);
|
||
}
|
||
},
|
||
|
||
pinPreview: function(pre, pin, temporary=false) {
|
||
let pinner = pre.querySelector('.pinner')
|
||
if (typeof pin == 'undefined')
|
||
pin = !pinner.classList.contains('pinned')
|
||
pinner.classList.toggle('pinned', pin)
|
||
pinner.classList.toggle('unpinned', !pin)
|
||
if (temporary) {
|
||
let o = pre
|
||
o.addEventListener('mouseenter', () => PostPreviews.pinPreview(o, false))
|
||
o.classList.add('temporarily-pinned')
|
||
}
|
||
while (pre) {
|
||
pre._pinned = pin
|
||
pre = document.querySelector(`#${PostPreviews.parent[pre.id]}`)
|
||
}
|
||
},
|
||
|
||
_timings: {
|
||
predelay: 50,
|
||
transition: 200,
|
||
fade: 400
|
||
}
|
||
}
|
||
|
||
function set_inputs(id) {
|
||
let form = document.getElementById(id)
|
||
if (form) {
|
||
let name = form.querySelector('input[name=name]')
|
||
if(name && !name.value) {
|
||
let savedName = getCookie("name")
|
||
if (~savedName.indexOf('#'))
|
||
name.type = 'password'
|
||
name.value = savedName
|
||
}
|
||
if(!form.em.value) form.em.value = getCookie("email");
|
||
if(!form.postpassword.value) form.postpassword.value = get_password("postpassword");
|
||
}
|
||
}
|
||
|
||
function set_delpass(id) {
|
||
let form = document.getElementById(id)
|
||
if (! form) return;
|
||
let postpassword = form.postpassword
|
||
if (postpassword && !postpassword.value) {
|
||
postpassword.value = get_password("postpassword");
|
||
}
|
||
}
|
||
|
||
(function ($) { // [!] What the fuck is this?
|
||
$.event.special.load = {
|
||
add: function (callback) {
|
||
if ( this.nodeType === 1 && this.tagName.toLowerCase() === 'img' && this.src !== '' ) {
|
||
if ( this.complete || this.readyState === 4 ) {
|
||
callback.handler.apply(this);
|
||
}
|
||
else if ( this.readyState === 'uninitialized' && this.src.indexOf('data:') === 0 ) {
|
||
$(this).trigger('error');
|
||
}
|
||
|
||
else {
|
||
$(this).bind('load', callback.handler);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}(jQuery));
|
||
|
||
var Settings = {
|
||
_checkbox: function(clicked, settingName, defaultValue, perBoard=false) {
|
||
let settingNameGeneral = settingName
|
||
if (perBoard)
|
||
settingName = settingName+'.'+this_board_dir
|
||
if (localStorage == null) {
|
||
pups.err(_l.noLocalStorage);
|
||
return;
|
||
}
|
||
if (localStorage[settingName] == null) {
|
||
localStorage[settingName] = defaultValue;
|
||
}
|
||
if (clicked == true) {
|
||
// save it
|
||
localStorage[settingName] = $('#settings_' + settingNameGeneral).is(":checked");
|
||
} else {
|
||
// update checkbox (called on load)
|
||
if (localStorage[settingName] == 'true')
|
||
$('#settings_' + settingNameGeneral).attr("checked","checked");
|
||
else
|
||
$('#settings_' + settingNameGeneral).removeAttr("checked");
|
||
}
|
||
return (localStorage[settingName] == 'true') || (localStorage[settingName] == true) ;
|
||
},
|
||
|
||
fxEnabled: function(changed) {
|
||
var enabled = Settings._checkbox(changed, 'fxEnabled', true);
|
||
if (changed != null) {
|
||
$.fx.off = !enabled;
|
||
}
|
||
return enabled;
|
||
},
|
||
|
||
showReplies: function(changed) {
|
||
var enabled = Settings._checkbox(changed, 'showReplies', true);
|
||
if (changed != null && (typeof is_catalog === 'undefined' || !is_catalog)) {
|
||
scrollAnchor.save('replymap', '.postnode')
|
||
if (enabled) {
|
||
replyMap.showReplies()
|
||
injector.remove('hide-replies')
|
||
}
|
||
else {
|
||
injector.inject('hide-replies', '.replieslist {display: none}')
|
||
}
|
||
scrollAnchor.restore('replymap')
|
||
}
|
||
return enabled;
|
||
},
|
||
|
||
sfwMode: function(changed) {
|
||
var enabled = Settings._checkbox(changed, 'sfwMode', false);
|
||
if (enabled) {
|
||
injector.inject('sfwMode', 'figure > *, .emoji { opacity: 0.05;} figure:hover > *, .postbody:hover .emoji { opacity: 1;}');
|
||
}
|
||
else {
|
||
injector.remove('sfwMode');
|
||
}
|
||
return enabled;
|
||
},
|
||
|
||
hideCompletely: function(changed) {
|
||
var enabled = Settings._checkbox(changed, 'hideCompletely', false, true);
|
||
if (changed) {
|
||
HiddenItems.hideCompletely = enabled
|
||
HiddenItems.updateCSS()
|
||
}
|
||
return enabled;
|
||
},
|
||
|
||
expandImgFull: function(changed) {
|
||
return Settings._checkbox(changed, 'expandImgFull', false);
|
||
},
|
||
|
||
constrainWidth: function(changed) {
|
||
var enabled = Settings._checkbox(changed, 'constrainWidth', false);
|
||
if (enabled) {
|
||
injector.inject('constrainWidth', `body {
|
||
max-width: 960px;
|
||
margin: 0px auto;
|
||
}`)
|
||
}
|
||
else {
|
||
injector.remove('constrainWidth')
|
||
}
|
||
},
|
||
|
||
newMediaViewer: function(changed) {
|
||
return Settings._checkbox(changed, 'newMediaViewer', true);
|
||
},
|
||
|
||
hidePostBtn: function(changed) {
|
||
let enabled = Settings._checkbox(changed, 'hidePostBtn', true)
|
||
if (!enabled) {
|
||
injector.inject('hidePostBtn', `.hide-post-btn {display: none}`)
|
||
}
|
||
else {
|
||
injector.remove('hidePostBtn')
|
||
}
|
||
if (!changed) {
|
||
HiddenItems.lists.posts.forEach(id => {
|
||
$(`#postnode${id}`).find('.hide-post-btn').attr('title', _l.unhidePost)
|
||
.find('use').attr('xlink:href', '#i-unhide')
|
||
})
|
||
}
|
||
return enabled
|
||
},
|
||
|
||
endlessScroll: function(changed) {
|
||
let enabled = Settings._checkbox(changed, 'endlessScroll', false)
|
||
endlessScroll.toggle(enabled)
|
||
}
|
||
}
|
||
|
||
var rswap = {
|
||
i: true,
|
||
swap: function() {
|
||
if(this.i) $('#delform').before($('#rswapper')).after($('.postarea'));
|
||
else $('#delform').before($('.postarea')).after($('#rswapper'));
|
||
this.i = !this.i;
|
||
}
|
||
}
|
||
|
||
var captchalang = getCookie('captchalang') || 'ru';
|
||
function setCaptchaLang(lang) {
|
||
if(!in_array(lang, ['ru', 'en', 'num'])) return;
|
||
captchalang = lang;
|
||
set_cookie('captchalang', lang, 365);
|
||
pups.succ(_l.captchaLangChanged)
|
||
}
|
||
|
||
const offClick = []
|
||
|
||
function readyset() {
|
||
$('.make-me-readonly').each(function() {
|
||
$(this).attr('readonly', true)
|
||
.on('focus', function() {
|
||
$(this).removeAttr('readonly')
|
||
})
|
||
})
|
||
|
||
if(!ispage) $('.mgoback').show();
|
||
if(isTouch)
|
||
$('#js_settings').prepend('<a href="javascript: localStorage.setItem(\'interfaceType\', \'desktop\'); location.reload();">'+_l.returnDesktop+'</a><br>');
|
||
else
|
||
$('#js_settings').prepend('<a href="javascript: localStorage.setItem(\'interfaceType\', \'touch\'); location.reload();">'+_l.returnTouch+'</a><br>');
|
||
|
||
$('#js_settings').prepend(_l.captchalang+': <a href="javascript:setCaptchaLang(\'ru\');">Cyrillic</a> | <a href="javascript:setCaptchaLang(\'en\');">Latin</a> | <a href="javascript:setCaptchaLang(\'num\');">Numeral</a><br />');
|
||
|
||
if(Styles.$cancelLink)
|
||
$('#js_settings').prepend(Styles.$cancelLink)
|
||
|
||
let pf = document.querySelector('#postform')
|
||
if (pf)
|
||
richFileInput.initForm(pf)
|
||
|
||
pups.init();
|
||
chpok.init(true)
|
||
|
||
LatexIT.init();
|
||
checkhighlight();
|
||
checkgotothread();
|
||
checknamesave();
|
||
|
||
bnrs.init();
|
||
|
||
if(getCookie('ku_menutype')) {
|
||
var c = Cookie('ku_menutype');
|
||
if(c != 'default' && c != '')
|
||
document.getElementById('overlay_menu').style.position = c;
|
||
}
|
||
|
||
//initial post-process
|
||
processNodeInsertion()
|
||
|
||
if(!isTouch) {
|
||
cloud20.init();
|
||
$('.sect-exr').mouseenter(function() { menu_show('ms-'+$(this).data('toexpand')); });
|
||
$('#overlay_menu').mouseleave(function() { menu_show('_off_'); });
|
||
$('body').on('mouseenter', "a[class^='ref']", function(ev) {
|
||
this.predelay = setTimeout(() => PostPreviews._mouseover.bind(this)(ev), PostPreviews._timings.predelay)
|
||
})
|
||
.on('mouseleave', "a[class^='ref']", PostPreviews._mouseout)
|
||
.on('click', "a[class^='ref']", function(e) {
|
||
e.preventDefault()
|
||
let href = this.getAttribute('href')
|
||
if (!highlight(href.split('#')[1], 0)) {
|
||
redirectTo(href)
|
||
}
|
||
})
|
||
}
|
||
else {
|
||
add_mob_menu();
|
||
$('body').addClass('touch-mode');
|
||
$('.sect-exr:not([data-toexpand="_options"])').parent().hide();
|
||
$('.sect-exr').click(function() {
|
||
if($('#js_settings').is(':visible')) {
|
||
menu_show('_off_');
|
||
}
|
||
else {
|
||
menu_show('ms-_options');
|
||
}
|
||
return false;
|
||
});
|
||
offClick.push(ev => {
|
||
menu_show('_off_');
|
||
document.querySelectorAll('[id^=preview]').forEach(pre => PostPreviews._mouseout.bind(pre)(null, true))
|
||
})
|
||
$('body').on('click', "a[class^='ref']", PostPreviews._mouseover)
|
||
.on('doubletap', '.postnode', function(ev) {
|
||
ev.stopPropagation()
|
||
ev.preventDefault()
|
||
this.querySelector('.qrl').click()
|
||
})
|
||
}
|
||
|
||
$('html').click(ev => offClick.forEach(fn => fn(ev)))
|
||
|
||
/* ---------- All delegated events on body ---------- */
|
||
$('body')
|
||
.on('click', '.uib-mup', function() {
|
||
markup($(this).parents('form'), $(this).data('mups'), $(this).data('mupe'), $(this).data('imups'), $(this).data('imupe'));
|
||
return false;
|
||
})
|
||
.on('click', '.uib-bul', function() {
|
||
bullets($(this).parents('form'), $(this).data('bul'), $(this).data('imups'), $(this).data('imupe'));
|
||
return false;
|
||
})
|
||
.on('click', '.uib-tx', function() {
|
||
var target = $(this).data('target');
|
||
head.js('http://latex.codecogs.com/editor3.js', function() {
|
||
OpenLatexEditor(target,'phpBB','en-us', false, '','full');
|
||
});
|
||
return false;
|
||
})
|
||
//Video expanding
|
||
.on('click', '.movie', function(event) {expandVideo($(this), event)})
|
||
//new quick reply
|
||
.on('click', '.qrl', quickreply)
|
||
// ID highlighting
|
||
.on('click', '.hashpic', function() {
|
||
$('.highlight').removeClass('highlight')
|
||
let found = $(`.hashpic[alt=${$(this).attr('alt')}]`).each(function() {
|
||
$(this).parents('.posthead').parent().addClass('highlight')
|
||
}).length
|
||
pups.info(_l.found+': '+found, {time: 1.5})
|
||
})
|
||
.on('click', '.posttypeindicator a', function() { // TODO: what is this?
|
||
var xl = $(this);
|
||
var offset = $('[name="' + xl.attr('href').substr(1) + '"]').offset() || $('[name="' + xl.text().split('>>')[1] + '"]').offset() || false;
|
||
if(offset) {
|
||
$('html, body').animate({
|
||
scrollTop: offset.top - ( $('#overlay_menu').height() + 10 )
|
||
}, 250);
|
||
}
|
||
return false;
|
||
})
|
||
.on('click', '.dice', function() {
|
||
if(typeof $(this).data('html') === 'undefined') $(this).data('html', $(this).html());
|
||
var htm = stripHTML($(this).html());
|
||
$(this).html($(this).attr('title'));
|
||
$(this).attr('title', htm);
|
||
})
|
||
.on('click', '.reflink a', function(ev) {
|
||
ev.preventDefault()
|
||
let ref = this.parentElement
|
||
, postnode = ref._findParent('.postnode')
|
||
if (!lastActiveForm) {
|
||
quickreply.bind(postnode.querySelector('.qrl'))()
|
||
}
|
||
else {
|
||
let formBoard = is_overboard
|
||
? lastActiveForm.getAttribute('id').match(/[0-9]+\-(.+?)_qrf/)[1]
|
||
: this_board_dir
|
||
let postBody = postnode.querySelector('.postbody')
|
||
, qoutation = getPostQuotation(postBody)
|
||
insert(`>>${ref.dataset.b == formBoard ? '' : `/${ref.dataset.b}/`}${ref.dataset.id}\n${qoutation}`)
|
||
}
|
||
})
|
||
//Ultimate YOBA Youtube embeds
|
||
.on('click','.embed-play-button', function(ev) {
|
||
ev.preventDefault()
|
||
unwrapEmbed($(this).parents('figure'))
|
||
})
|
||
.on('click','.collapse-video', function(ev) {
|
||
ev.preventDefault()
|
||
wrapEmbed($(this).parents('figure'))
|
||
})
|
||
.on('mouseenter', '._country_', function() {
|
||
if(typeof $(this).attr('title') === "undefined") {
|
||
$(this).attr('title', countries[$(this).attr('src').split('flags/')[1].split('.png')[0].toUpperCase()]);
|
||
}
|
||
})
|
||
.on('click', '.audiowrap', function(ev) {
|
||
ev.preventDefault();
|
||
let $this = $(this)
|
||
var src = $this.attr('href');
|
||
let $fig = $this.parents('figure')
|
||
if (! $fig.hasClass('unwrapped')) {
|
||
$fig.addClass('unwrapped unwrapped-audio')
|
||
}
|
||
let $fsz = $fig.find('.filesize');
|
||
if (! $fsz.find('.collapse-btn').length)
|
||
$fsz.append(`
|
||
<button title="${_l.collapse}" class="emb-button collapse-video">
|
||
${makeIcon('shrink', 'b-icon')}
|
||
</button>`);
|
||
if (! $this.find('audio').length)
|
||
$this.append('<audio src="'+src+'" controls autoplay="true"></audio>')
|
||
$fsz.find('.collapse-video').click(function() {
|
||
$fig.removeClass('unwrapped unwrapped-audio')
|
||
.find('audio')[0].pause()
|
||
})
|
||
})
|
||
.on('change', '.multidel', function() {
|
||
let itemCount = $('.multidel:checked').length
|
||
|
||
if (! $('.item-count').length) {
|
||
$('.userdelete tbody').prepend(`<tr><td>
|
||
${_l.selected}: <span class="item-count"></span>
|
||
<button class="close-multisel icon-wraping-button" title="${_l.cancel}">${makeIcon('x', 'b-icon ', false)}</button>
|
||
</td></tr>`)
|
||
$('.close-multisel').click(function(ev) {
|
||
ev.preventDefault()
|
||
$('.multidel').prop('checked', false)
|
||
$('body').removeClass('select-multiple')
|
||
$('.userdelete').removeClass('ud-active')
|
||
})
|
||
}
|
||
|
||
if (itemCount > 0) {
|
||
$('.item-count').text(itemCount)
|
||
}
|
||
else {
|
||
$('.userdelete').removeClass('ud-active')
|
||
$('body').removeClass('select-multiple')
|
||
}
|
||
})
|
||
/* Post menu */
|
||
.on('click', 'label.postinfo', function(ev) {
|
||
if (! $(this).find('.multidel').is(':visible')) {
|
||
ev.stopPropagation()
|
||
ev.preventDefault()
|
||
let $this = $(this)
|
||
, $menu = $this.parent().find('.post-menu')
|
||
let on = !$menu.is(':visible')
|
||
$('.post-menu').hide()
|
||
if (! $menu.length) {
|
||
let $postnode = $(this).parents('.postnode')
|
||
, board = $postnode.data('board')
|
||
, id = $postnode.data('id')
|
||
, isOP = $postnode.hasClass('op')
|
||
/*if (!isOP)
|
||
$postnode.toggleClass('post-hidden', HiddenItems.isHidden('post', id))*/
|
||
this.insertAdjacentHTML('afterend', `<div class="content-background post-menu" style="display: none">
|
||
<ul>
|
||
<li class="menu-delete">${makeIcon('x')}<span>${_l.del}</span></li>
|
||
${!isOP ? `
|
||
${typeof opmod !== 'undefined' && +opmod || 0 ? `<li class="menu-delete menu-delete-op">${makeIcon('x')}<span>${_l.del} ${_l.asOP}</span></li>`: ''}
|
||
<li class="menu-hide">${makeIcon('hide')}<span>${_l.hidePost}</span></li>
|
||
<li class="menu-unhide">${makeIcon('unhide')}<span>${_l.unhidePost}</span></li>
|
||
`: ''}
|
||
<li class="menu-report">${makeIcon('warning')}<span>${_l.report}</span></li>
|
||
<li class="menu-link-share">${makeIcon('link')}<span>${_l.links}</span></li>
|
||
${$postnode.find('.post-ttl').length ? `
|
||
<li class="menu-cancel-timer">${makeIcon('untimer')}<span>${_l.cancelTimer}</span></li>
|
||
`: ''}
|
||
<li class="menu-select-multiple">${makeIcon('select-multiple')}<span>${_l.selectMultiple}</span></li>
|
||
${kumod_set ? `
|
||
<li class="menu-delete menu-delete-mod">${makeIcon('x')}<span>${_l.del} ${_l.asMod}</span></li>
|
||
<a href="${ku_cgipath}/manage_page.php?action=bans&banboard=${board}&banpost=${id}">
|
||
<li class="menu-ban">${makeIcon('ban')}<span>${_l.ban}...<span></li>
|
||
</a>
|
||
${isOP ? `
|
||
<a class="stickypost-btn" onclick="Ajax.modThread(this, 'stickypost'); return false"
|
||
href="${ku_cgipath}/manage_page.php?action=stickypost&board=${board}&postid=${id}">
|
||
<li>${makeIcon('pin')}<span>${_l.stickthread}<span></li>
|
||
</a>
|
||
<a class="unstickypost-btn" onclick="Ajax.modThread(this, 'unstickypost'); return false"
|
||
href="${ku_cgipath}/manage_page.php?action=unstickypost&board=${board}&postid=${id}">
|
||
<li>${makeIcon('unpin')}<span>${_l.unstickthread}<span></li>
|
||
</a>
|
||
<a class="lockpost-btn" onclick="Ajax.modThread(this, 'lockpost'); return false"
|
||
href="${ku_cgipath}/manage_page.php?action=lockpost&board=${board}&postid=${id}">
|
||
<li>${makeIcon('lock')}<span>${_l.lockthread}<span></li>
|
||
</a>
|
||
<a class="unlockpost-btn" onclick="Ajax.modThread(this, 'unlockpost'); return false"
|
||
href="${ku_cgipath}/manage_page.php?action=unlockpost&board=${board}&postid=${id}">
|
||
<li>${makeIcon('unlock')}<span>${_l.unlockthread}<span></li>
|
||
</a>`
|
||
: ''}`
|
||
: ''}
|
||
</ul>
|
||
</div>`)
|
||
$menu = $this.parent().find('.post-menu')
|
||
}
|
||
$menu.toggle(on)
|
||
}
|
||
})
|
||
// Figure menu is global because of FUCKING overflow
|
||
.on('click', '.file-menu-toggle', function(ev) {
|
||
ev.stopPropagation()
|
||
ev.preventDefault()
|
||
let $menu = $('.file-menu')
|
||
, $this = $(this)
|
||
, $fsz = $this.parent()
|
||
, visible = $menu.is(':visible')
|
||
$('.post-menu').hide()
|
||
if (visible && $menu[0].$boundTo[0] == $fsz[0]) return;
|
||
let ofs = $fsz.offset()
|
||
, offsetTop = $fsz.hasClass('embed-wrap') ? 22 : $fsz.outerHeight()
|
||
$menu.css({
|
||
'left': `${ofs.left}px`,
|
||
'top': `${ofs.top + offsetTop}px`,
|
||
'min-width': `${$fsz.outerWidth()}px`
|
||
}).show()
|
||
let fileid = $fsz.parents('figure').data('fileid')
|
||
, $pn = $fsz.parents('.postnode')
|
||
, board = $pn.data('board')
|
||
, pid = $pn.data('id')
|
||
$menu[0].__menuProps = {
|
||
fileid: fileid,
|
||
board: board
|
||
}
|
||
$menu[0].$boundTo = $fsz
|
||
// Set the proper link for file banning
|
||
$menu.find('.menu-banfile').attr('href', `${ku_cgipath}/manage_page.php?action=bans&banboard=${board}&banpost=${pid}&banfile=${fileid}`)
|
||
})
|
||
.on('click', '.post-menu li', ev => ev.stopPropagation())
|
||
.on('click', '.menu-select-multiple', function(ev) {
|
||
$('.post-menu').hide()
|
||
let $menu = $(this).parents('.post-menu')
|
||
, isFile = $menu.hasClass('file-menu')
|
||
, $md = (isFile ? $menu[0].$boundTo : $(this).parents('.posthead'))
|
||
.find('.multidel')
|
||
if (! $md.length) return;
|
||
$md.prop('checked', true).trigger('change')
|
||
$('.userdelete').addClass('ud-active')
|
||
$('body').addClass('select-multiple')
|
||
})
|
||
.on('click', '.menu-link-share', function(ev) {
|
||
let $this = $(this)
|
||
, isShown = $this.find('input').length
|
||
if (! isShown) {
|
||
let $postnode = $this.parents('.postnode')
|
||
, board = $postnode.data('board')
|
||
, id = $postnode.data('id')
|
||
, directLink = ku_boardspath + $postnode.find('.shl').attr('href')
|
||
$this.find('span').html(`<input class="pm-direct-link pm-link" type="text" value="${directLink}" title="${_l.directLink}">
|
||
<input class="pm-quote-link pm-link" type="text" value=">>/${board}/${id}" title="${_l.quoteLink}">`)
|
||
.css({'font-size': 0})
|
||
.find('.pm-direct-link').click()
|
||
}
|
||
else
|
||
$this.toggleClass('direct-or-quote')
|
||
})
|
||
.on('click', '.pm-link', function(ev) {
|
||
ev.stopPropagation()
|
||
let $this = $(this)
|
||
$('.pm-link').removeClass('selected')
|
||
$this.focus().select().addClass('selected')
|
||
})
|
||
.on('click', '.menu-delete', function() {
|
||
let $this = $(this)
|
||
, $menu = $this.parents('.post-menu')
|
||
, menu = $menu[0]
|
||
, isFile = $menu.hasClass('file-menu')
|
||
, $item = isFile ? $menu[0].$boundTo : $this.parents('.posthead')
|
||
, $pass = $menu.find('.menu-password')
|
||
$this.addClass('spin-around')
|
||
let fd = new FormData()
|
||
if ($menu.find('.menu-captcha').length) {
|
||
let $c = $menu.find('input[name=captcha]')
|
||
if (!$c.val()) {
|
||
$c.focus()
|
||
pups.warn(_l.enterCaptcha)
|
||
$this.removeClass('spin-around')
|
||
return;
|
||
}
|
||
fd.append('captcha', $c.val())
|
||
}
|
||
if (isFile)
|
||
fd.append('delete-file[]', menu.__menuProps.fileid)
|
||
else
|
||
fd.append('post[]', $item.parents('.postnode').data('id'))
|
||
let asMod = $this.hasClass('menu-delete-mod')
|
||
fd.append('moddelete', asMod)
|
||
fd.append('opdelete', +$this.hasClass('menu-delete-op'))
|
||
fd.append('board', isFile
|
||
? menu.__menuProps.board
|
||
: $item.parents('.postnode').data('board'))
|
||
fd.append('postpassword', $pass.length
|
||
? $pass.find('input').val()
|
||
: $(`#delform input[name="postpassword"]`).val())
|
||
Ajax.deleteItems(fd, data => {
|
||
$this.removeClass('spin-around')
|
||
if (!data) return;
|
||
let result = data[0]
|
||
if(!result.success && $item.is(':visible')) {
|
||
if (asMod) return;
|
||
if (!$pass.length) {
|
||
$menu.prepend(`<li class="menu-password">${makeIcon('password')}
|
||
<input value="${get_password("postpassword")}" class="make-me-readonly" type="password" placeholder="${_l.password}" readonly onfocus="this.readOnly=false"></li>`)
|
||
}
|
||
if (result.special_error && result.special_error == 'captchalocked') {
|
||
addCaptchaToMenu($menu)
|
||
}
|
||
else {
|
||
$menu.find('.menu-password input').select()
|
||
.keydown(ev => {
|
||
if (ev.key == 'Enter') {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
else if (isFile)
|
||
$('.file-menu').hide()
|
||
})
|
||
})
|
||
.on('click', '.menu-report', function() {
|
||
let $this = $(this)
|
||
, $postnode = $this.parents('.postnode')
|
||
$this.addClass('spin-around')
|
||
let fd = new FormData()
|
||
fd.append('post[]', $postnode.data('id'))
|
||
fd.append('board', $postnode.data('board'))
|
||
Ajax.reportPost(fd, data => {
|
||
$this.removeClass('spin-around')
|
||
})
|
||
})
|
||
.on('click', '.menu-hide', function() {
|
||
let $postnode = $(this).parents('.postnode')
|
||
HiddenItems.hidePost($postnode.data('id')+'-'+$postnode.data('board'))
|
||
$postnode.addClass('post-hidden')
|
||
.find('.post-menu').hide()
|
||
})
|
||
.on('click', '.menu-unhide', function() {
|
||
let $postnode = $(this).parents('.postnode')
|
||
HiddenItems.unhidePost($postnode.data('id')+'-'+$postnode.data('board'))
|
||
$postnode.removeClass('post-hidden')
|
||
})
|
||
.on('click', '.menu-cancel-timer', function() {
|
||
let $this = $(this)
|
||
, $menu = $this.parents('.post-menu')
|
||
, menu = $menu[0]
|
||
, $pass = $menu.find('.menu-password')
|
||
, $postnode = $this.parents('.postnode')
|
||
$this.addClass('spin-around')
|
||
let fd = new FormData()
|
||
if ($menu.find('.menu-captcha').length) {
|
||
let $c = $menu.find('input[name=captcha]')
|
||
if (!$c.val()) {
|
||
$c.focus()
|
||
pups.warn(_l.enterCaptcha)
|
||
$this.removeClass('spin-around')
|
||
return;
|
||
}
|
||
fd.append('captcha', $c.val())
|
||
}
|
||
fd.append('post[]', $postnode.data('id'))
|
||
fd.append('board', $postnode.data('board'))
|
||
fd.append('modsave', $this.hasClass('menu-delete-mod'))
|
||
fd.append('opsave', $this.hasClass('menu-delete-op'))
|
||
fd.append('postpassword', $pass.length
|
||
? $pass.find('input').val()
|
||
: $(`#delform input[name="postpassword"]`).val())
|
||
Ajax.cancelTimer(fd, errors => {
|
||
$this.removeClass('spin-around')
|
||
if (errors.length) {
|
||
let result = errors[0]
|
||
if (!$pass.length) {
|
||
$menu.prepend(`<li class="menu-password">${makeIcon('password')}
|
||
<input value="${get_password("postpassword")}" class="make-me-readonly" type="password" placeholder="${_l.password}" readonly onfocus="this.readOnly=false"></li>`)
|
||
}
|
||
if (result.special_error && result.special_error == 'captchalocked') {
|
||
addCaptchaToMenu($menu)
|
||
}
|
||
else {
|
||
$menu.find('.menu-password input').select()
|
||
.keydown(ev => {
|
||
if (ev.key == 'Enter') {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
else {
|
||
$menu.find('.menu-password, .menu-captcha').remove()
|
||
}
|
||
})
|
||
})
|
||
.on('click', '.csswrap', function(ev) {
|
||
ev.preventDefault()
|
||
let cssLink = $(this).attr('href')
|
||
, styleName = $(this).parent().find('.fc-filename').text()
|
||
Styles.testStyle(cssLink, styleName)
|
||
})
|
||
.on('blur', 'input[name="name"]', function() {
|
||
let isTrip = ~this.value.indexOf('#')
|
||
this.type = isTrip ? 'password' : 'text'
|
||
})
|
||
.on('focus', 'input[name="name"]', function() {
|
||
this.type ='text'
|
||
})
|
||
|
||
offClick.push(ev => {
|
||
$('.post-menu').hide()
|
||
})
|
||
|
||
$(window).resize(() => {
|
||
let $fileMenu = $('.file-menu')
|
||
if (! $fileMenu.is(':visible')) return;
|
||
let $fsz = $fileMenu[0].$boundTo
|
||
, ofs = $fsz.offset()
|
||
, offsetTop = $fsz.hasClass('embed-wrap') ? 22 : $fsz.outerHeight()
|
||
$fileMenu.css({
|
||
'left': `${ofs.left}px`,
|
||
'top': `${ofs.top + offsetTop}px`,
|
||
'min-width': `${$fsz.outerWidth()}px`
|
||
})
|
||
})
|
||
|
||
$('.userdelete').addClass('content-background reflinkpreview')
|
||
.find('tbody').append(`<tr><td><input name="moddelete" value="${_l.del} (${_l.mod_})" type="submit" class="styled-button bad-button" /></td></tr>`)
|
||
|
||
$('#delform').on('submit', function(e) {
|
||
e.preventDefault()
|
||
Ajax.deleteItems(new FormData(this))
|
||
return false
|
||
})
|
||
|
||
$('.userdelete input[type=submit]').click(function(e) {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
let fd = new FormData($(this).parents('form')[0])
|
||
if (this.name == 'deletepost' || this.name == 'moddelete') {
|
||
Ajax.deleteItems(fd, null, this.name == 'moddelete')
|
||
}
|
||
if (this.name == 'cancel_timer') {
|
||
Ajax.cancelTimer(fd)
|
||
}
|
||
if (this.name == 'reportpost') {
|
||
Ajax.reportPost(fd)
|
||
}
|
||
})
|
||
|
||
/*$('input[name=reportpost]').click(function(e) {
|
||
e.preventDefault()
|
||
Ajax.reportPost(new FormData($(this).parents('form')[0]))
|
||
})*/
|
||
if (!is_overboard)
|
||
$('#delform').after('<div id="rswapper">[<a onclick="javascript:rswap.swap();return false;" href="#">'+(ispage ? _l.NewThread : _l.reply)+'</a>]<hr /></div>');
|
||
|
||
Settings.sfwMode(false);
|
||
if (localStorage) {
|
||
for(var s in Settings) {
|
||
if (s.substring(0,1) == "_") continue;
|
||
$("#js_settings").append('<label><input type="checkbox" onchange="javascript:Settings.'+s+'(true)" name="settings_'+s+'" id="settings_'+s+'" value="true"> '+_l['settings_'+s]+'</label><br />');
|
||
Settings[s](false);
|
||
}
|
||
} else {
|
||
$("#js_settings").append("<span style=\"color:#F00\">"+_l.noLocalStorage+"</span><br />Твой браузер — говно. Скачай <a href=\"/web/20110329072959/http://google.com/chrome\" target=\"_blank\">Chome</a>, например.");
|
||
}
|
||
|
||
var textbox = document.getElementById('message');
|
||
if(textbox) {
|
||
textbox.onfocus=function(){is_entering = true;}
|
||
textbox.onblur=function(){is_entering = false;}
|
||
}
|
||
|
||
// Figure menu is global because of FUCKING overflow
|
||
$('body').append(`
|
||
<div class="content-background post-menu file-menu" style="display: none">
|
||
<ul>
|
||
<li class="menu-delete">${makeIcon('x')}<span>${_l.del}</span></li>
|
||
<li class="menu-select-multiple">${makeIcon('select-multiple')}<span>${_l.selectMultiple}</span></li>
|
||
${kumod_set ? `
|
||
<li class="menu-delete menu-delete-mod">${makeIcon('x')}<span>${_l.del} ${_l.asMod}</span></li>
|
||
<a href="${ku_cgipath}/manage_page.php?action=bans" class="menu-banfile">
|
||
<li>${makeIcon('ban')}<span>${_l.ban} ${_l.file.toLowerCase()}...</span></li>
|
||
</a>`
|
||
: ''}
|
||
</ul>
|
||
</div>
|
||
`)
|
||
|
||
//animation hooks
|
||
$(document).on('animationstart webkitAnimationStart MSAnimationStart oanimationstart', function(event) {
|
||
var $target = $(event.target)
|
||
//detect node insertions and process them
|
||
if (event.originalEvent.animationName == "nodeInserted" && !$target.hasClass('_inserted_'))
|
||
processNodeInsertion($target)
|
||
//detect window resize
|
||
if (event.originalEvent.animationName == "windowSmall") {
|
||
document.querySelectorAll('.quick-reply-form').forEach(f => {
|
||
toggleFormFloating(f, false)
|
||
toggleFormSimplify(f, true)
|
||
})
|
||
}
|
||
});
|
||
|
||
Captcha.init(is_overboard)
|
||
|
||
initForm(document.querySelector('#postform'))
|
||
|
||
if(typeof is_catalog !== 'undefined' && is_catalog) catalog.init();
|
||
|
||
$('<div id="tripinfo"></div>').addClass('content-background reflinkpreview qreplyform').hide().appendTo('body');
|
||
|
||
$('#delform').on('click', '.postertrip', function(ev) {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
var trip = $(this).text().split('!')[1]
|
||
, offset = $(this).offset(), height = $(this).height()
|
||
$.getJSON('/tripinfo.php', { trip: trip })
|
||
.done(function(data) {
|
||
var active_on = [];
|
||
_.each(data.active_on, function(board) {
|
||
active_on.push('<a target="_blank" href="'+ku_boardspath+'/'+board+'/">'+'/'+board+'/</a>');
|
||
})
|
||
$('#tripinfo')
|
||
.html(
|
||
'<div><b class="postertrip">!'+trip+'</b> ['+'<a href="https://www.google.com/search?q=!'+trip+'" target="_blank">G</a>]</div>'+
|
||
'<a style="float:left;" href="#" onclick="javascript:$(\'#tripinfo\').hide();return false;"><svg style="position: absolute; top: 3px; right: 3px;" class="icon b-icon"><use xlink:href="#i-x"></use></svg></a>'+
|
||
'<div class="trip-info-line">'+_l.threads+': '+data.threads+', '+_l.comments+': '+data.comments+'</div>'+
|
||
'<div class="trip-info-line">'+_l.active_since+': '+catalog.formatDate(data.active_since, true)+'</div>'+
|
||
'<div class="trip-info-line">'+_l.last_active+': '+catalog.formatDate(data.last_active, true)+'</div>'+
|
||
(active_on.length ? '<div class="trip-info-line">'+_l.active_on+': '+active_on.join(', ')+'</div>' : '')
|
||
)
|
||
.css({
|
||
top: offset.top + height,
|
||
left: offset.left
|
||
})
|
||
.fadeIn('fast');
|
||
})
|
||
.fail(function(error) {
|
||
console.error(error)
|
||
})
|
||
})
|
||
|
||
unreadCounter.update()
|
||
|
||
if(liveupd_ena && typeof io !== 'undefined')
|
||
updater.init()
|
||
|
||
onReady.ready()
|
||
}
|
||
|
||
/*
|
||
* jQuery Double Tap https://gist.github.com/attenzione/7098476
|
||
* Developer: Sergey Margaritov (github.com/attenzione)
|
||
* License: MIT
|
||
* Date: 22.10.2013
|
||
* Based on jquery documentation http://learn.jquery.com/events/event-extensions/
|
||
*/
|
||
(function($){
|
||
$.event.special.doubletap = {
|
||
bindType: 'touchend',
|
||
delegateType: 'touchend',
|
||
|
||
handle: function(event) {
|
||
var handleObj = event.handleObj,
|
||
targetData = jQuery.data(event.target),
|
||
now = new Date().getTime(),
|
||
delta = targetData.lastTouch ? now - targetData.lastTouch : 0,
|
||
delay = delay == null ? 300 : delay,
|
||
e = event.originalEvent
|
||
|
||
if (delta < delay && delta > 30 && !(e.touches && e.touches.length > 1)) {
|
||
targetData.lastTouch = null;
|
||
event.type = handleObj.origType;
|
||
['clientX', 'clientY', 'pageX', 'pageY'].forEach(function(property) {
|
||
event[property] = event.originalEvent.changedTouches[0][property];
|
||
})
|
||
|
||
// let jQuery handle the triggering of "doubletap" event handlers
|
||
handleObj.handler.apply(this, arguments);
|
||
} else {
|
||
targetData.lastTouch = now;
|
||
}
|
||
}
|
||
};
|
||
})(jQuery)
|
||
|
||
const NTP = {
|
||
calc: (t0, t1, t2, t3) => ((t1 - t0) + (t2 - t3)) / 2,
|
||
sync: function(force) {
|
||
this.pending = true
|
||
let _this = this
|
||
, t0 = (new Date()).valueOf()
|
||
$.ajax({
|
||
url: '/ntp.php',
|
||
success: function(servertime) {
|
||
let t1 = servertime,
|
||
t2 = servertime,
|
||
t3 = (new Date()).valueOf()
|
||
|
||
_this.offset = _this.calc(t0, t1, t2, t3)
|
||
},
|
||
error: function() {
|
||
pups.err('Failed to synchronize time with the server')
|
||
},
|
||
complete: function() {
|
||
_this.synced = true
|
||
|
||
while (_this.stack.length) {
|
||
_this.stack.shift().resolve(_this.timeWithOffset)
|
||
}
|
||
}
|
||
});
|
||
},
|
||
offset: 0,
|
||
stack: [],
|
||
get timeWithOffset() {
|
||
return new Date(new Date().getTime() + this.offset).getTime()
|
||
},
|
||
getServerTime: function() {
|
||
if (this.synced)
|
||
return Promise.resolve(this.timeWithOffset)
|
||
|
||
let promise = externallyResolvingPromise()
|
||
if (! this.pending)
|
||
this.sync()
|
||
this.stack.push(promise)
|
||
return promise.promise
|
||
}
|
||
}
|
||
|
||
function externallyResolvingPromise() {
|
||
let promiseResolve, promiseReject
|
||
, promise = new Promise((resolve, reject) => {
|
||
promiseResolve = resolve
|
||
promiseReject = reject
|
||
});
|
||
|
||
return {
|
||
promise: promise,
|
||
resolve: promiseResolve,
|
||
reject: promiseReject
|
||
}
|
||
}
|
||
|
||
// this will be applied to every new inserted node (post)
|
||
function processNodeInsertion($node) {
|
||
if(typeof $node === 'undefined')
|
||
$node = $('body');
|
||
else {
|
||
$node.addClass('_inserted_');
|
||
$node = $node.parents(":eq(1)");
|
||
}
|
||
if($node.find('.prettyprint').length)
|
||
prettyPrint.apply(window);
|
||
LatexIT.render($node);
|
||
|
||
// Post time-to-live updating
|
||
$node.find('.post-ttl:not(._inserted_)').each(function() {
|
||
let $this = $(this)
|
||
$this.addClass('_inserted_')
|
||
let iv
|
||
, updateTTL = () => {
|
||
if ($this.length) {
|
||
NTP.getServerTime().then(srvTime => {
|
||
let ms_left = $this.data('deleted-timestamp')*1000 - srvTime
|
||
if (ms_left >= 0) {
|
||
let min_left = Math.round(ms_left / 60000)
|
||
, h_left = Math.floor(min_left / 60)
|
||
min_left -= h_left * 60
|
||
$this.text(`${_.padLeft(h_left, 2, '0')}:${_.padLeft(min_left, 2, '0')}`)
|
||
}
|
||
else {
|
||
$.get(document.forms.delform.action) // call board.php to force delete posts
|
||
deleteItems([{action: 'delete_post', id: $this.parents('.postnode').data('id')}], false, updater.markOnly)
|
||
clearInterval(iv)
|
||
}
|
||
})
|
||
}
|
||
else
|
||
clearInterval(iv)
|
||
}
|
||
iv = setInterval(updateTTL, 30 * 1000)
|
||
updateTTL()
|
||
})
|
||
}
|
||
|
||
const chpok = {
|
||
init: function(atLoad = false) {
|
||
document.body.insertAdjacentHTML('beforeend', `<audio id="chpok-audio" style="display:none">
|
||
<source src="${ku_boardspath}/images/chpok.mp3">
|
||
<source src="${ku_boardspath}/images/chpok.ogg">
|
||
</audio>`)
|
||
this.initiated = true
|
||
this.audio = document.querySelector('#chpok-audio')
|
||
},
|
||
play: function() {
|
||
if (! this.initiated)
|
||
this.init()
|
||
// In chrome the sound won't play, causing a DOM exception, if a user hasn't clicked a page at least once (and it's unfixable)
|
||
this.audio.play().catch(e => _.noop)
|
||
}
|
||
}
|
||
|
||
const updater = {
|
||
newThreads: [],
|
||
init: function () {
|
||
this.socket = io.connect(liveupd_api);
|
||
let subscribeTo
|
||
if(ispage) {
|
||
subscribeTo = [liveupd_sitename+this_board_dir+':threads']
|
||
document.querySelectorAll('.postnode.op').forEach(thr => subscribeTo.push(liveupd_sitename+thr.dataset.board+':'+thr.dataset.id))
|
||
}
|
||
else {
|
||
subscribeTo = [liveupd_sitename+this_board_dir+':'+$('input[name=replythread]').val()];
|
||
_l.noNewPosts += ("<br>" + _l.threadUpdationAutomatically)
|
||
}
|
||
this.socket
|
||
.on('update', this.dispatch.bind(this))
|
||
.emit('subscribe', subscribeTo)
|
||
Object.defineProperty(this, 'markOnly', {
|
||
value: true,
|
||
writable: false,
|
||
configurable: false
|
||
})
|
||
},
|
||
dispatch: function(data) {
|
||
if (! data.action) {
|
||
pups.warn('Event with unspecified action, see console')
|
||
console.warn(data)
|
||
return
|
||
}
|
||
if (data.action == 'new_thread') {
|
||
// Do nothing if it is my post
|
||
if (data.token && Ajax.postToken && data.token==Ajax.postToken) return;
|
||
this.notifyAboutNewThreads(data)
|
||
}
|
||
if (data.action == 'new_reply') {
|
||
// Do nothing if it is my post
|
||
if (data.token && Ajax.postToken && data.token==Ajax.postToken) return;
|
||
if (ispage) {
|
||
this.notifyAboutNewRepliesOnBoardPage(data)
|
||
}
|
||
else {
|
||
this.showNewReplies(data)
|
||
}
|
||
}
|
||
if (data.action == 'delete') {
|
||
// Do nothing if it is my post
|
||
if (data.token && Ajax.delToken && data.token==Ajax.delToken) return;
|
||
deleteItems(data.items, false, this.markOnly)
|
||
}
|
||
if (data.action == 'cancel_timer') {
|
||
// Do nothing if it is my post
|
||
if (data.token && Ajax.timerToken && data.token==Ajax.timerToken) return;
|
||
let postsSaved = [], msg = ''
|
||
data.items.forEach(item => {
|
||
$(`.postnode[data-id=${item.id}] .post-ttl`).remove()
|
||
postsSaved.push(item.id)
|
||
})
|
||
if(postsSaved.length) {
|
||
msg = postsSaved.length > 1 ?
|
||
`${_l.posts} #${postsSaved.join(', #')} ${_l.savedMulti}.`
|
||
:`${_l.post} #${postsSaved[0]} ${_l.saved}.`
|
||
pups.info(msg, {time: 2 + postsSaved.length})
|
||
}
|
||
}
|
||
},
|
||
showNewReplies: function(data) {
|
||
newposts.get({
|
||
silent: true,
|
||
timestamp: data.timestamp,
|
||
onSuccess: () => {
|
||
if (! document.hasFocus())
|
||
chpok.play()
|
||
}
|
||
})
|
||
},
|
||
notifyAboutNewThreads: function(data) {
|
||
this.newThreads.push({
|
||
board: data.board,
|
||
board_desc: data.board_desc || false,
|
||
thread: data.new_thread_id
|
||
});
|
||
if (! document.hasFocus()) {
|
||
scrollAnchor.save('autoload', '.postnode')
|
||
this.showNewThreads(() => scrollAnchor.restore('autoload'))
|
||
chpok.play()
|
||
}
|
||
else if(!$('#wild_thread_appeared').length) {
|
||
$('#delform').before('<a class="xlink" onclick="updater.showNewThreads();return false" id="wild_thread_appeared"><span class="salient">'+_l.newThreadsAvailable+'</span><hr /></a>')
|
||
}
|
||
},
|
||
notifyAboutNewRepliesOnBoardPage: function(data) {
|
||
let room = data.room + '-' + data.board
|
||
if (! this.repliesOnBoardPage[room])
|
||
this.repliesOnBoardPage[room] = [data.reply_id]
|
||
else
|
||
this.repliesOnBoardPage[room].push(data.reply_id)
|
||
this.refreshNewRepliesCount(data.board, data.room, data.timestamp)
|
||
},
|
||
refreshNewRepliesCount(board, threadID, timestamp=null) {
|
||
let thread = threadID + '-' + board
|
||
, count = (this.repliesOnBoardPage[thread] || []).length
|
||
, $target = $('[id^=replies'+thread+']')
|
||
, $counter = $target.find('.fresh-replies')
|
||
let clearCounter = () => this.repliesOnBoardPage[thread] = []
|
||
if (count) {
|
||
if (! $counter.length) {
|
||
function loadNewReplies(callback) {
|
||
newposts.get({
|
||
threadid: thread,
|
||
onSuccess: () => {
|
||
clearCounter()
|
||
if (callback)
|
||
callback()
|
||
},
|
||
onError: (e) => {
|
||
console.error(e)
|
||
pups.err(_l.oops)
|
||
},
|
||
silent: true,
|
||
timestamp: timestamp
|
||
})
|
||
}
|
||
if (! document.hasFocus()) {
|
||
scrollAnchor.save('autoload', '.postnode')
|
||
loadNewReplies(() => scrollAnchor.restore('autoload'))
|
||
chpok.play()
|
||
}
|
||
else {
|
||
let $freshReplies = $target.append('<br class="fresh-replies-breaker"><a href="/'+ board +'/res/'+threadID+'.html" class="salient fresh-replies">'+_l.newReplies+': <span class="fresh-replies-number"></span></a>')
|
||
.find('.fresh-replies')
|
||
, freshReplies = $freshReplies[0]
|
||
$freshReplies.click(function(e) {
|
||
e.preventDefault()
|
||
if ($freshReplies.__alreadyLoading)
|
||
return;
|
||
freshReplies.__alreadyLoading = true
|
||
freshReplies.insertAdjacentHTML('afterBegin', '<div class="spinner"></div>')
|
||
loadNewReplies(() => $(freshReplies).remove())
|
||
})
|
||
}
|
||
}
|
||
$target.find('.fresh-replies-number').text(count)
|
||
}
|
||
else if($counter.length) {
|
||
$counter.remove()
|
||
clearCounter()
|
||
}
|
||
},
|
||
repliesOnBoardPage: {},
|
||
showNewThreads: function(callback) {
|
||
let $wta = $('#wild_thread_appeared')
|
||
if ($wta.length)
|
||
$wta.find('span')[0].insertAdjacentHTML('afterBegin', '<div class="spinner"></div>')
|
||
this.newThreads.forEach(thr => {
|
||
HTMLoader.getThread(thr.board, thr.thread, null, (err, posts) => {
|
||
if (err)
|
||
return pups.err(_l.noDataLoaded)
|
||
$wta.remove()
|
||
document.querySelector('#delform').insertAdjacentHTML('afterBegin', `
|
||
${posts}
|
||
<div id="replies${thr.thread}-${thr.board}" class="replies"></div>
|
||
</div>
|
||
<br clear="left" />
|
||
<hr>`);
|
||
if (is_overboard && thr.board_desc) {
|
||
document.querySelector('#delform .posthead').insertAdjacentHTML('afterbegin',
|
||
`<a href="/${thr.board}" target="_blank" class="over-boardlabel">/${thr.board}/ — ${thr.board_desc}</a>`)
|
||
}
|
||
this.socket.emit('subscribe', liveupd_sitename+thr.board+':'+thr.thread)
|
||
if (callback)
|
||
callback()
|
||
})
|
||
})
|
||
this.newThreads = [];
|
||
}
|
||
}
|
||
|
||
function deleteItems(items, bySelf=true, markOnly=false) {
|
||
let postsDeleted = []
|
||
, postErrors = []
|
||
, insideDeletedThread
|
||
, filesDeleted = []
|
||
, fileErrors = []
|
||
|
||
items.forEach(item => {
|
||
let deletedClasses = 'deleted'
|
||
+ (item.by_op ? ' op-deleted' : '')
|
||
+ (item.by_mod ? ' mod-deleted' : '')
|
||
|
||
if (item.action == 'delete_post' || item.itemtype == 'post') {
|
||
if (bySelf && !item.success) {
|
||
postErrors.push(item)
|
||
return
|
||
}
|
||
insideDeletedThread = false
|
||
let $thread = $(`#delform [id^=thread${item.id}]`)
|
||
, $reply = $(`#delform [id^=reply${item.id}]`)
|
||
if ($thread.length) {
|
||
if (markOnly || !ispage) {
|
||
$thread.addClass('deleted')
|
||
.find('.reply, figure').addClass(deletedClasses)
|
||
if (!ispage) {
|
||
insideDeletedThread = true
|
||
$('form[name=postform], .qrl, #rswapper').remove()
|
||
pups.warn(`${_l.thread} ${_l.deleted}!`, {time: 0})
|
||
}
|
||
}
|
||
else {
|
||
$thread.prev().remove() //unhidethread
|
||
$thread.next().remove() //br
|
||
$thread.next().remove() //hr
|
||
$thread.remove()
|
||
}
|
||
}
|
||
else if ($reply.length) {
|
||
if (markOnly)
|
||
$reply.addClass(deletedClasses)
|
||
else {
|
||
let $reply = $(`#delform [id^=reply${item.id}]`)
|
||
if ($reply.length) {
|
||
$reply.parents('.postnode').remove()
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
if (updater.repliesOnBoardPage[item.thread_id])
|
||
updater.repliesOnBoardPage[item.thread_id] = updater.repliesOnBoardPage[item.thread_id].filter(i => i!=item.id)
|
||
updater.refreshNewRepliesCount(item.thread_id)
|
||
}
|
||
if (!insideDeletedThread)
|
||
postsDeleted.push('#'+item.id)
|
||
}
|
||
|
||
if (item.action == 'delete_file' || item.itemtype == 'file') {
|
||
if (bySelf && !item.success) {
|
||
fileErrors.push(item)
|
||
return
|
||
}
|
||
let $fig = $(`figure[data-fileid=${item.id}]`)
|
||
if (markOnly)
|
||
$fig.addClass(deletedClasses)
|
||
else
|
||
$fig.replaceWith(`<div class="nothumb">${_l.fileRemoved}</div>`)
|
||
filesDeleted.push('#'+item.id)
|
||
}
|
||
|
||
if (item.action == 'delete_thread') { // A notification about deleted
|
||
updater.newThreads = updater.newThreads.filter(i => i!=item.id)
|
||
if (! updater.newThreads.length)
|
||
$('#wild_thread_appeared').remove();
|
||
pups.info(`${_l.thread} ${item.id} ${_l.deleted}.`)
|
||
}
|
||
})
|
||
|
||
if (! $('.multidel').is(":checked") && ! $('.multidel.delete-file').is(":checked")) {
|
||
$('.userdelete').removeClass('ud-active')
|
||
$('body').removeClass('select-multiple')
|
||
}
|
||
|
||
if (postsDeleted.length) {
|
||
let msg = postsDeleted.length > 1
|
||
? `${_l.posts} ${postsDeleted.join(', ')} ${_l.deletedMulti}.`
|
||
: `${_l.post} ${postsDeleted[0]} ${_l.deleted}.`
|
||
pups[bySelf ? 'succ' : 'info'](msg, {time: 2 + postsDeleted.length})
|
||
}
|
||
if (filesDeleted.length) {
|
||
let msg = filesDeleted.length > 1
|
||
? `${_l.files} ${filesDeleted.join(', ')} ${_l.deletedMulti}.`
|
||
: `${_l.file} ${filesDeleted[0]} ${_l.deleted}.`
|
||
pups[bySelf ? 'succ' : 'info'](msg, {time: 2 + filesDeleted.length})
|
||
}
|
||
if (postErrors.length)
|
||
pups.err(postErrors.map(err => `${_l.post} #${err.id}: ${err.message}`).join('<br>'),
|
||
{time: 2 + postErrors.length})
|
||
if (fileErrors.length) {
|
||
pups.err(fileErrors.map(err => `${_l.file} #${err.id}: ${err.message}`).join('<br>'),
|
||
{time: 2 + fileErrors.length})
|
||
}
|
||
}
|
||
|
||
if (+localStorage['localmod']) {
|
||
kumod_set = true
|
||
}
|
||
else {
|
||
let kumod = getCookie('kumod');
|
||
if (kumod !== '') {
|
||
if (kumod === 'allboards')
|
||
kumod_set = true
|
||
else
|
||
kumod_set = in_array(this_board_dir, kumod.split('|'));
|
||
}
|
||
}
|
||
|
||
function expandVideo_old($mov, ev) {
|
||
//good luck understanding this shitcode :^)
|
||
let $reply = $mov.parents('.reply')
|
||
if($mov.data('expanded') !== '1') {
|
||
ev.preventDefault();
|
||
var movieurl = $mov.attr('href'), imgh = $mov.data('height'), imgw = $mov.data('width'), dt = $mov.data('thumb'), postnum = $mov.data('id');
|
||
var uid = '_vframe_'+randomString(5)+(new Date().getTime());
|
||
$mov.replaceWith(function() {
|
||
return '<span id="'+uid+'" data-thumb="'+dt+'" data-width="'+imgw+'"" data-height="'+imgh+'" data-href="'+movieurl+'">'+this.innerHTML + '</span>';
|
||
});
|
||
$mov = $("#"+uid);
|
||
$mov.find('img').hide();
|
||
var video = $mov.find('video').show(), notice = '';
|
||
if(!video.length) {
|
||
$mov.find('.playable-thumb').append('<video class="thumb" src="'+movieurl+'" controls loop autoplay height="'+imgh+'" width="'+imgw+'"></video>').promise().done(function() {
|
||
video = $mov.find('video');
|
||
});
|
||
video[0].volume = localStorage['mediaVolume'] || 1
|
||
video[0].onvolumechange = function() {
|
||
localStorage['mediaVolume'] = this.muted ? 0 : this.volume
|
||
}
|
||
}
|
||
else video.get(0).play();
|
||
if(!Settings.expandImgFull()) {
|
||
var offset = 50, offset_el = video[0];
|
||
var max_w = document.documentElement?document.documentElement.clientWidth : document.body.clientWidth;
|
||
while (offset_el != null) {
|
||
offset += offset_el.offsetLeft;
|
||
offset_el = offset_el.offsetParent;
|
||
}
|
||
var new_w = max_w - offset;
|
||
if(imgw > new_w) {
|
||
var ratio = imgw / imgh;
|
||
var zoom = 1 - new_w / imgw;
|
||
var new_h = new_w / ratio;
|
||
video.width(new_w);
|
||
video.height(new_h);
|
||
notice = _l.videoDownscaledBy + " " + Math.round(zoom*100) + "% "+_l.toFit;
|
||
}
|
||
}
|
||
let $fig = $mov.parents('figure');
|
||
if (! $fig.hasClass('unwrapped')) {
|
||
$fig.addClass('unwrapped')
|
||
}
|
||
let $fsz = $mov.parent().find('.filesize');
|
||
if (! $fsz.find('.collapse-btn').length) {
|
||
$fsz.append(`
|
||
<button title="${_l.collapse}" class="emb-button collapse-video">
|
||
${makeIcon('shrink', 'b-icon')}
|
||
</button>`);
|
||
$mov.parent().find('.collapse-video').click(function() {
|
||
$fig.removeClass('unwrapped')
|
||
var uid = '_vframe_'+randomString(5)+(new Date().getTime());
|
||
$mov.replaceWith(function() {
|
||
return '<a class="movie" id="'+uid+'" data-thumb="'+dt+'" data-width="'+imgw+'"" data-height="'+imgh+'" href="'+movieurl+'">'+this.innerHTML + '</a>';
|
||
}).data('expanded', '0');
|
||
$mov = $("#"+uid);
|
||
$mov.find('video').hide()[0].pause();
|
||
$mov.find('img').show();
|
||
$(this).remove();
|
||
$mov.parents('.reply').removeClass('reply-expanded')
|
||
return false;
|
||
});
|
||
}
|
||
$mov.parents('.reply').addClass('reply-expanded')
|
||
}
|
||
}
|
||
|
||
function expandVideo($mov, ev) {
|
||
ev.preventDefault()
|
||
if (Settings.newMediaViewer()) {
|
||
let mv = new MediaViewer(...MediaViewer.parseVideo($mov[0]), true)
|
||
mv.onCollapse = () => mv = null
|
||
}
|
||
else {
|
||
expandVideo_old($mov, ev)
|
||
}
|
||
return false
|
||
}
|
||
|
||
function checknamesave() {
|
||
var checkd;
|
||
if(getCookie('name') != '') {
|
||
checkd = true;
|
||
} else {
|
||
checkd = false;
|
||
}
|
||
var doc = document.getElementById('save');
|
||
if (doc != null) doc.checked = checkd;
|
||
}
|
||
function checkgotothread() {
|
||
var checkd;
|
||
if(getCookie('tothread') == 'off') {
|
||
checkd = false;
|
||
} else {
|
||
checkd = true;
|
||
}
|
||
$("#gotothread").attr('checked', checkd);
|
||
}
|
||
|
||
function navigatepages (event) {
|
||
if (!document.getElementById) return;
|
||
if (is_entering) return;
|
||
if (window.event) event = window.event;
|
||
if (event.ctrlKey) {
|
||
var link = null;
|
||
var href = null;
|
||
var docloc = document.location.toString();
|
||
if (docloc.indexOf('/res/') != -1) {
|
||
if(event.key=='Enter') {
|
||
handleCtrlEnter(event)
|
||
}
|
||
}
|
||
else {
|
||
if (docloc.indexOf('.html') == -1 || docloc.indexOf('board.html') != -1) {
|
||
var page = 0;
|
||
var docloc_trimmed = docloc.substr(0, docloc.lastIndexOf('/') + 1);
|
||
} else {
|
||
var page = docloc.substr((docloc.lastIndexOf('/') + 1));
|
||
page = (+page.substr(0, page.indexOf('.html')));
|
||
var docloc_trimmed = docloc.substr(0, docloc.lastIndexOf('/') + 1);
|
||
}
|
||
if (page == 0) {
|
||
var docloc_valid = docloc_trimmed;
|
||
} else {
|
||
var docloc_valid = docloc_trimmed + page + '.html';
|
||
}
|
||
let match;
|
||
if(match=/#s([0-9]+)/.exec(docloc)) {
|
||
var relativepost = (+match[1]);
|
||
} else {
|
||
var relativepost = -1;
|
||
}
|
||
var maxthreads = 0;
|
||
while(document.getElementsByName('s'+(++maxthreads)).length>0){}
|
||
switch (event.key)
|
||
{
|
||
case 'Enter':
|
||
handleCtrlEnter(event)
|
||
break;
|
||
|
||
case 'ArrowLeft':
|
||
link = document.getElementById('prevPage');
|
||
break;
|
||
case 'ArrowRight':
|
||
link = document.getElementById('nextPage');
|
||
break;
|
||
|
||
case 'ArrowDown':
|
||
if (relativepost == maxthreads - 1) {
|
||
break; //var newrelativepost = 0;
|
||
} else {
|
||
var newrelativepost = relativepost + 1;
|
||
}
|
||
href = docloc_valid + '#s' + newrelativepost;
|
||
break;
|
||
|
||
case 'ArrowUp':
|
||
if (relativepost == -1 || relativepost == 0) {
|
||
break; //var newrelativepost = maxthreads - 1;
|
||
} else {
|
||
var newrelativepost = relativepost - 1;
|
||
}
|
||
href = docloc_valid + '#s' + newrelativepost;
|
||
break;
|
||
|
||
case 'Home': // ctrl+home
|
||
document.location = docloc_trimmed;
|
||
break;
|
||
}
|
||
|
||
if (link && link.action) document.location = link.action;
|
||
if (href) redirectTo(href);
|
||
}
|
||
}
|
||
else {
|
||
let mv = document.querySelector('.media-viewer:not(.mv-transparent)')
|
||
if (mv) {
|
||
if (event.key == 'ArrowLeft')
|
||
mv.querySelector('.mv-prev').click()
|
||
if (event.key == 'ArrowRight')
|
||
mv.querySelector('.mv-next').click()
|
||
if (event.key == 'Escape')
|
||
mv.querySelector('.mv-close').click()
|
||
}
|
||
}
|
||
}
|
||
if (window.document.addEventListener) { // how fucking old is this jeez
|
||
window.document.addEventListener("keydown", navigatepages, false);
|
||
} else {
|
||
window.document.attachEvent("onkeydown", navigatepages);
|
||
}
|
||
|
||
NodeList.prototype.forEach = Array.prototype.forEach;
|
||
NodeList.prototype.map = Array.prototype.map;
|
||
|
||
// More efficient parent finder
|
||
Element.prototype._findParent = function(selector) {
|
||
let node = this
|
||
while(node && !node.matches(selector)) {
|
||
node = node.parentNode
|
||
if (! node.matches) return null;
|
||
}
|
||
return node
|
||
}
|
||
Element.prototype._findNext = function(selector) {
|
||
let node = this.nextElementSibling
|
||
while(node && !node.matches(selector)) {
|
||
node = node.nextElementSibling
|
||
if (! node) return null
|
||
}
|
||
return node
|
||
}
|
||
Element.prototype._findPrevious = function(selector) {
|
||
let node = this.previousElementSibling
|
||
while(node && !node.matches(selector)) {
|
||
node = node.previousElementSibling
|
||
if (! node) return null
|
||
}
|
||
return node
|
||
}
|
||
Node.prototype._isChildOf = function(parent) {
|
||
let node = this.parentNode
|
||
while (node) {
|
||
if (node == parent) {
|
||
return true
|
||
}
|
||
node = node.parentNode
|
||
}
|
||
return false
|
||
}
|
||
|
||
var replyMap = {
|
||
showReplies: function(root=document) {
|
||
root.querySelectorAll('.postnode').forEach(post => {
|
||
let postNum = post.dataset.id
|
||
, board = post.dataset.board
|
||
, thread = post._findParent('div[id^=thread]')
|
||
, threadID = thread && thread.dataset['threadid']
|
||
, msg = post.querySelector('.postmessage')
|
||
, repliesContainer = post.querySelector('.replieslist')
|
||
if (!this.posts[board])
|
||
this.posts[board] = {}
|
||
if (this.posts[board][postNum]) {
|
||
this.posts[board][postNum].container = repliesContainer
|
||
}
|
||
else {
|
||
this.posts[board][postNum] = {
|
||
container: repliesContainer,
|
||
replies: []
|
||
}
|
||
}
|
||
let links = msg.querySelectorAll(`a[class^=ref\\|]`)
|
||
if (links.length) links.forEach(link => {
|
||
let linkData = link.className.split(' ')[0].split('|')
|
||
, linkDestBoard = linkData[1]
|
||
, linkPostNum = linkData[3]
|
||
, href = `/${board}/res/${threadID}.html#${postNum}`
|
||
, htm = `<a class="ref-reply" href="${href}">>>${board == linkDestBoard ? '' : `/${board}/`}${postNum}</a>`
|
||
if (this.posts[linkDestBoard] && this.posts[linkDestBoard][linkPostNum]) {
|
||
if (! _.includes(this.posts[linkDestBoard][linkPostNum].replies, htm)) {
|
||
this.posts[linkDestBoard][linkPostNum].replies.push(htm)
|
||
}
|
||
}
|
||
else {
|
||
if (!this.posts[linkDestBoard])
|
||
this.posts[linkDestBoard] = {}
|
||
this.posts[linkDestBoard][linkPostNum] = {
|
||
replies: [htm]
|
||
}
|
||
}
|
||
this.posts[linkDestBoard][linkPostNum].skip = false
|
||
})
|
||
})
|
||
_.each(this.posts, board => _.each(board, post => {
|
||
if (!post.skip && post.replies.length && post.container) {
|
||
post.container.innerHTML = `${_l.replies}: ${post.replies.join(', ')}`
|
||
post.skip = true
|
||
}
|
||
}))
|
||
},
|
||
posts: {}
|
||
}
|
||
|
||
var scrollAnchor = {
|
||
save: function(id, elements, parent=window, dimensions='v') {
|
||
if (!window.scrollY) {
|
||
this.saved[id] = {_top: true}
|
||
return
|
||
}
|
||
let mid = [window.innerWidth / 2, window.innerHeight / 2]
|
||
, elMap = []
|
||
, parentBCR = (parent != window)
|
||
? parent.getBoundingClientRect()
|
||
: {
|
||
left: 0,
|
||
top: 0,
|
||
right: window.innerWidth,
|
||
bottom: window.innerHeight
|
||
}
|
||
if (
|
||
parent != window
|
||
&&
|
||
(
|
||
(parentBCR.left <= 0 && parentBCR.right <= 0)
|
||
||
|
||
(parentBCR.top <= 0 && parentBCR.bottom <= 0)
|
||
||
|
||
parentBCR.left >= window.innerWidth
|
||
||
|
||
parentBCR.top >= window.innerHeight
|
||
)
|
||
) {
|
||
mid = [parentBCR.left + parentBCR.width/2, parentBCR.top + parentBCR.height/2]
|
||
}
|
||
let elems = elements instanceof Node
|
||
? [elements]
|
||
: elements instanceof NodeList
|
||
? elements
|
||
: ((parent == window ? document : parent).querySelectorAll(elements))
|
||
if (! elems.length) return;
|
||
elems.forEach(function (el) {
|
||
let bcr = el.getBoundingClientRect()
|
||
, relativeVisibleWidth = Math.pos(bcr.width - (Math.pos(parentBCR.left - bcr.left) + Math.pos(bcr.right - parentBCR.right))) / bcr.width
|
||
, relativeVisibleHeight = Math.pos(bcr.height - (Math.pos(parentBCR.top - bcr.top) + Math.pos(bcr.bottom - parentBCR.bottom))) / bcr.height
|
||
, dx = Math.abs(mid[0] - (bcr.left + bcr.width/2))
|
||
, dy = Math.abs(mid[1] - (bcr.top + bcr.height/2))
|
||
elMap.push({
|
||
el: el,
|
||
primaryVisibility: dimensions[0] == 'h' ? relativeVisibleWidth : relativeVisibleHeight,
|
||
secondaryVisibility: dimensions[0] == 'v' ? relativeVisibleWidth : relativeVisibleHeight,
|
||
primaryOffset: dimensions[0] == 'h' ? dx : dy,
|
||
secondaryOffset: dimensions[0] == 'v' ? dx : dy
|
||
})
|
||
})
|
||
elMap.sort((a, b) => {
|
||
if (b.primaryVisibility !== a.primaryVisibility) {
|
||
return b.primaryVisibility - a.primaryVisibility
|
||
}
|
||
else if (dimensions.length > 1 && b.secondaryVisibility !== a.secondaryVisibility) {
|
||
return b.secondaryVisibility - a.secondaryVisibility
|
||
}
|
||
else if (a.primaryOffset !== b.primaryOffset) {
|
||
return a.primaryOffset - b.primaryOffset
|
||
}
|
||
else if (dimensions.length > 1) {
|
||
return a.secondaryOffset - b.secondaryOffset
|
||
}
|
||
})
|
||
let anchor = elMap[0].el
|
||
, bcrBefore = anchor.getBoundingClientRect()
|
||
this.saved[id] = {
|
||
anchor: anchor,
|
||
left: bcrBefore.left,
|
||
top: bcrBefore.top,
|
||
parent: parent,
|
||
dimensions: dimensions
|
||
}
|
||
},
|
||
restore: function(id) {
|
||
return new Promise((resolve, reject) => {
|
||
let loaded = this.saved[id]
|
||
if (loaded) window.requestAnimationFrame(() => {
|
||
if (loaded._top) {
|
||
window.scrollTo(0,0)
|
||
resolve()
|
||
return
|
||
}
|
||
let bcrAfter = loaded.anchor.getBoundingClientRect()
|
||
loaded.parent.scrollBy(
|
||
loaded.dimensions.indexOf('h') !== (-1)
|
||
? bcrAfter.left - loaded.left
|
||
: 0,
|
||
loaded.dimensions.indexOf('v') !== (-1)
|
||
? bcrAfter.top - loaded.top
|
||
: 0
|
||
)
|
||
resolve()
|
||
})
|
||
else reject()
|
||
})
|
||
},
|
||
saved: {}
|
||
}
|
||
Math.pos = x => x >= 0 ? x : 0;
|
||
|
||
// requestAnimationFrame polyfill https://gist.github.com/paulirish/1579671
|
||
(function() {
|
||
var lastTime = 0;
|
||
var vendors = ['ms', 'moz', 'webkit', 'o'];
|
||
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
|
||
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
|
||
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
|
||
}
|
||
|
||
if (!window.requestAnimationFrame)
|
||
window.requestAnimationFrame = function(callback, element) {
|
||
var currTime = new Date().getTime();
|
||
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
||
var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
|
||
lastTime = currTime + timeToCall;
|
||
return id;
|
||
};
|
||
|
||
if (!window.cancelAnimationFrame)
|
||
window.cancelAnimationFrame = function(id) {
|
||
clearTimeout(id);
|
||
};
|
||
}());
|
||
|
||
/// overlay menu
|
||
var menu_current = '';
|
||
var menu_last = '';
|
||
function menu_show(id)
|
||
{
|
||
if(menu_current != '')
|
||
{
|
||
var dl = (id == '_off_') ? 125 : 0;
|
||
$('#'+menu_current).delay(dl).slideUp(100);
|
||
menu_last = menu_current;
|
||
}
|
||
if (id != '') {
|
||
if (menu_last == id && typeof $('#' + id).queue() !== 'undefined' && $('#' + id).queue().length > 0) {
|
||
$('#' + id).clearQueue();
|
||
} else {
|
||
$('#' + id).slideDown(150);
|
||
}
|
||
}
|
||
menu_current = id;
|
||
}
|
||
function menu_pin() {
|
||
if(document.getElementById('overlay_menu').style.position == 'absolute') {
|
||
document.getElementById('overlay_menu').style.position = 'fixed';
|
||
Cookie('ku_menutype', 'fixed', 365);
|
||
} else {
|
||
document.getElementById('overlay_menu').style.position = 'absolute';
|
||
Cookie('ku_menutype', 'absolute', 365);
|
||
}
|
||
}
|
||
|
||
function toggle_oldmenu(on=null) {
|
||
if (on===null)
|
||
on = getCookie('ku_oldmenu') != 'yes'
|
||
injector.inject('oldmenu', `#${on ? 'overlay_menu' : 'head_oldmenu'} { display: none }`)
|
||
if (on && !document.getElementById('head_oldmenu')) {
|
||
document.getElementById('boardlist_header').insertAdjacentHTML('afterBegin', `<div id="head_oldmenu" class="boardlist">
|
||
${document.getElementById('ns_oldmenu').innerText}
|
||
<a href=\"#\" onclick=\"javascript:toggle_oldmenu();\" class="bl-sect" style="order:1">[overlay]</a>
|
||
</div>`)
|
||
}
|
||
Cookie('ku_oldmenu', on ? 'yes' : 'no', 90);
|
||
}
|
||
|
||
var LatexIT = {
|
||
mode : 'gif',
|
||
init : function() {
|
||
if(document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"))
|
||
this.mode='svg';
|
||
},
|
||
odc: "javascript:LatexIT.replaceWithSrc(this);",
|
||
dcls: "Double click to show source",
|
||
|
||
pre : function(eqn) {
|
||
var txt=eqn.innerHTML;
|
||
if ( !txt.match(/<img.*?>/i) && !txt.match(/<object.*?>/i))
|
||
{
|
||
//Clean code
|
||
txt=txt.replace(/<br>/gi,"").replace(/<br \/>/gi,"").replace(/&/mg,'&');
|
||
var atxt = "[tex]"+txt+"[/tex]";
|
||
txt=escape(txt.replace(/\\/mg,'\\'));
|
||
// Add coloring according to style of text
|
||
var c = eval("LatexIT.normalize"+$(eqn).parent().css('color'));
|
||
var extxt = "{\\color[rgb]{"+c.r+','+c.g+','+c.b+"}"+txt+"}";
|
||
txt = " <img src=\"http://latex.codecogs.com/"+this.mode+".latex?"+ extxt +"\" title=\""+this.dcls+"\" alt=\""+atxt+"\" ondblclick=\""+this.odc+"\" border=\"0\" class=\"latex\" /> ";
|
||
}
|
||
return txt;
|
||
},
|
||
|
||
replaceWithSrc: function(eqn) {
|
||
var txt = $(eqn).attr('alt');
|
||
$(eqn).parent().html(txt);
|
||
},
|
||
|
||
render : function($scope) {
|
||
var scope = (typeof $scope === 'undefined') ? window.document : $scope[0];
|
||
var eqn = scope.getElementsByTagName("*");
|
||
for (var i=0; i<eqn.length; i++) {
|
||
if(eqn[i].getAttribute("lang") == "latex" || eqn[i].getAttribute("xml:lang") == "latex")
|
||
eqn[i].innerHTML = this.pre(eqn[i]);
|
||
}
|
||
},
|
||
|
||
normalizergb : function(r, g, b) {
|
||
return {r: (r/255).toFixed(3), g: (g/255).toFixed(2), b: (b/255).toFixed(2) }
|
||
},
|
||
normalizergba : function(r, g, b, a) {
|
||
return this.normalizergb(r, g, b);
|
||
}
|
||
};
|
||
|
||
function in_array(needle, haystack) {
|
||
return (typeof haystack !== 'object') ? (needle === haystack) : _.includes(haystack, needle)
|
||
}
|
||
|
||
(function($) {
|
||
$.fn.drags = function(opt) {
|
||
opt = $.extend({handle:"",cursor:"move"}, opt);
|
||
|
||
if(opt.handle === "") {
|
||
var $el = this;
|
||
} else {
|
||
var $el = this.find(opt.handle);
|
||
}
|
||
|
||
return $el.css('cursor', opt.cursor).on("mousedown", function(e) {
|
||
if(opt.handle === "") {
|
||
var $drag = $(this).addClass('draggable');
|
||
} else {
|
||
var $drag = $(this).addClass('active-handle').parent().addClass('draggable');
|
||
}
|
||
var z_idx = $drag.css('z-index'),
|
||
drg_h = $drag.outerHeight(),
|
||
drg_w = $drag.outerWidth(),
|
||
pos_y = $drag.offset().top + drg_h - e.pageY,
|
||
pos_x = $drag.offset().left + drg_w - e.pageX;
|
||
$drag.css('z-index', 1000).parents().on("mousemove", function(e) {
|
||
if (opt.onMove)
|
||
opt.onMove()
|
||
$('.draggable').offset({
|
||
top:e.pageY + pos_y - drg_h,
|
||
left:e.pageX + pos_x - drg_w
|
||
}).on("mouseup", function() {
|
||
$(this).removeClass('draggable').css('z-index', z_idx);
|
||
});
|
||
});
|
||
e.preventDefault(); // disable selection
|
||
}).on("mouseup", function() {
|
||
if(opt.handle === "") {
|
||
$(this).removeClass('draggable');
|
||
} else {
|
||
$(this).removeClass('active-handle').parent().removeClass('draggable');
|
||
}
|
||
});
|
||
}
|
||
$.fn.dragsOff = function(opt) {
|
||
opt = $.extend({handle:"",cursor:"default"}, opt);
|
||
|
||
if(opt.handle === "") {
|
||
var $el = this;
|
||
$(this).removeClass('draggable');
|
||
} else {
|
||
var $el = this.find(opt.handle);
|
||
$(this).removeClass('active-handle')
|
||
.parent()
|
||
.removeClass('draggable');
|
||
}
|
||
return $el.css('cursor', "default")
|
||
.off("mousedown")
|
||
.off("mouseup")
|
||
.off("mousemove");
|
||
}
|
||
/*$.fn.pin = function() {
|
||
if(this.css('position') !== 'fixed') {
|
||
var abs = {
|
||
top: this.position().top - $(document).scrollTop(),
|
||
left: this.position().left - $(document).scrollLeft()
|
||
}
|
||
this.css({
|
||
position: 'fixed',
|
||
left: abs.left,
|
||
top: abs.top
|
||
});
|
||
this.find('.pinner').removeClass('pinned').addClass('unpinned');
|
||
localStorage['pinForm'] = 0;
|
||
}
|
||
else {
|
||
var abs = {
|
||
top: this.position().top + $(document).scrollTop(),
|
||
left: this.position().left + $(document).scrollLeft()
|
||
}
|
||
this.css({
|
||
position: 'absolute',
|
||
left: abs.left,
|
||
top: abs.top
|
||
});
|
||
this.find('.pinner').removeClass('unpinned').addClass('pinned');
|
||
localStorage['pinForm'] = 1;
|
||
}
|
||
}*/
|
||
})(jQuery);
|
||
|
||
function unwrapEmbed($fig) {
|
||
$fig.addClass('unwrapped')
|
||
let $iw = $fig.find('.emb-iframe-wrapper')
|
||
if ($iw.data('h') > $iw.data('w'))
|
||
$iw.addClass('vertical-video')
|
||
if ($iw.data('site') == "Soundcloud")
|
||
$iw.addClass('soundcloud-embed')
|
||
$iw.css({
|
||
paddingBottom: `${($iw.data('h')/$iw.data('w'))*100}%`
|
||
})
|
||
let code = $iw.data('code')
|
||
, iframeOptions = `frameborder="0" scrolling="no" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""`
|
||
if ($iw.data('site') == "Youtube") {
|
||
let start = $iw.data('startraw')
|
||
$iw.append(`<iframe src="https://www.youtube-nocookie.com/embed/${code}?autoplay=1${start ? `&start=${start}` : ''}" ${iframeOptions}></iframe>`)
|
||
}
|
||
if ($iw.data('site') == "Vimeo") {
|
||
let start = $iw.data('start')
|
||
$iw.append(`<iframe src="//player.vimeo.com/video/${code}?badge=0&autoplay=1${start ? `#t=${start}` : ''}" ${iframeOptions}></iframe>`)
|
||
}
|
||
if ($iw.data('site') == "Soundcloud") {
|
||
let enccode = encodeURI(code)
|
||
let start = $iw.data('start')
|
||
$iw.append(`<iframe src="https://w.soundcloud.com/player/?url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F${enccode}" ${iframeOptions}></iframe>`)
|
||
}
|
||
if ($iw.data('site') == "Coub")
|
||
$iw.append(`<iframe src="http://coub.com/embed/${code}?muted=false&autostart=true&originalSize=false&hideTopBar=false&noSiteButtons=false&startWithHD=false" ${iframeOptions}></iframe>`)
|
||
let $ew = $fig.find('.embed-wrap')
|
||
if (! $ew.find('.collapse-video').length)
|
||
$fig.find('.embed-wrap').append(`
|
||
<button title="${_l.collapse}" class="emb-button collapse-video">
|
||
${makeIcon('shrink')}
|
||
</button>`)
|
||
}
|
||
|
||
function wrapEmbed($fig) {
|
||
$fig.removeClass('unwrapped')
|
||
$fig.find('.emb-iframe-wrapper').empty()
|
||
}
|
||
|
||
function resetForm(form, isClone=false) {
|
||
let fields = form.querySelectorAll('input[type=text]:not([name=name]), input[type=file], textarea')
|
||
fields.forEach(field => field.value = null)
|
||
form.querySelectorAll('.site-indicator').forEach(i => i.remove())
|
||
richFileInput.clear(form, !isClone)
|
||
// Remove extra inputs
|
||
form.querySelectorAll('input[name^=addembed], input[name^=addfile], input[name=ttl-enable]').forEach(i => {
|
||
i.checked = false
|
||
if (i.name == 'ttl-enable' && i.onchange)
|
||
i.onchange()
|
||
})
|
||
}
|
||
|
||
function initForm(form) {
|
||
if (!form)
|
||
return
|
||
let $form = $(form)
|
||
form.querySelectorAll('input[name^=embed]').forEach(i => {
|
||
i.addEventListener('input', function() {
|
||
let indicator = this.parentElement.querySelector('.site-indicator')
|
||
if (indicator)
|
||
indicator.remove()
|
||
let match = embedLinks.process(this.value)
|
||
if(match)
|
||
this.insertAdjacentHTML('afterend', `<img src="${ku_boardspath}/images/site-logos/${match.site}.png" class="site-indicator">`)
|
||
})
|
||
})
|
||
form.querySelectorAll('input[name^=imagefile]').forEach(i => {
|
||
i.addEventListener('change', function() {
|
||
if (this.value) {
|
||
let nextCheckbox = this.parentElement.nextElementSibling
|
||
if (nextCheckbox)
|
||
nextCheckbox.checked = true
|
||
}
|
||
})
|
||
})
|
||
// Markup
|
||
form.querySelectorAll('.opt-exp').forEach(exp => {
|
||
exp.onclick = function(ev) {
|
||
ev.preventDefault()
|
||
this.querySelector('.expandee').classList.toggle('expanded')
|
||
}
|
||
exp.onmouseleave = function(ev) {
|
||
this.hideTimeout = setTimeout(() => this.querySelector('.expandee').classList.remove('expanded'), 250)
|
||
}
|
||
exp.onmouseenter = function() {
|
||
clearTimeout(this.hideTimeout)
|
||
}
|
||
})
|
||
let cms = form.querySelector('.code_markup_select')
|
||
if (cms) {
|
||
cms.onchange = function() {
|
||
markup($form, `[code=${this.value}]`, '[/code]')
|
||
}
|
||
}
|
||
form.querySelector('.quote_select').onchange = function() {
|
||
quotes($form, this.value)
|
||
}
|
||
// for TeX
|
||
let areaID = form.id + '_textarea'
|
||
form.querySelector('textarea').id = areaID
|
||
form.querySelector('.uib-tx').dataset.target = areaID
|
||
// captcha
|
||
if (Captcha.enabled && Captcha.enabled!=='only-access')
|
||
Captcha.initForm(form)
|
||
// readonly stuff
|
||
form.querySelectorAll('.make-me-readonly').forEach(ro => {
|
||
ro.readOnly = true
|
||
ro.addEventListener('focus', () => ro.readOnly = false)
|
||
})
|
||
// Name disabling
|
||
let name = form.querySelector('input[name=disable_name]')
|
||
if (name) {
|
||
name.onchange = function() {
|
||
let off = this.checked
|
||
form.querySelector('input[name=name]').disabled = off
|
||
localStorage.setItem('post_anonymously', +off)
|
||
}
|
||
name.checked = !!+localStorage['post_anonymously']
|
||
name.onchange()
|
||
}
|
||
// Submitting
|
||
form.addEventListener('submit', function(ev) {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
Ajax.submitPost(this)
|
||
})
|
||
form.querySelectorAll('.simplified-send-row button[type="submit"]').forEach(sub => {
|
||
sub.onclick = function(ev) {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
let sage = +(this.name == 'sagebtn')
|
||
Ajax.submitPost(form, sage)
|
||
}
|
||
})
|
||
// TTL
|
||
let ttl = form.querySelector('input[name="ttl-enable"]')
|
||
if (ttl) {
|
||
ttl.onchange = function() {
|
||
let on = this.checked
|
||
, input = form.ttl
|
||
input.disabled = !on
|
||
input.min = on ? 1 : 0
|
||
if (on && input.value == 0) {
|
||
input.value = 1
|
||
}
|
||
}
|
||
ttl.onchange()
|
||
}
|
||
// Detecting last active textarea
|
||
form.querySelectorAll('textarea, input[type=text]').forEach(el => {
|
||
el.onfocus = () => {
|
||
lastActiveForm = form
|
||
}
|
||
})
|
||
// Add file button
|
||
let fileButton = form.querySelector('.s-file')
|
||
if (fileButton) {
|
||
fileButton.onclick = ev => {
|
||
ev.preventDefault()
|
||
form.querySelector('input[type=file]').click()
|
||
}
|
||
}
|
||
}
|
||
|
||
var injector = {
|
||
inject: function(alias, css) {
|
||
var id = `injector:${alias}`
|
||
var existing = document.getElementById(id)
|
||
if (existing) {
|
||
existing.innerHTML = css
|
||
return
|
||
}
|
||
var head = document.head || document.getElementsByTagName('head')[0]
|
||
, style = document.createElement('style')
|
||
style.type = 'text/css'
|
||
style.id = id
|
||
if (style.styleSheet) {
|
||
style.styleSheet.cssText = css
|
||
} else {
|
||
style.appendChild(document.createTextNode(css))
|
||
}
|
||
head.appendChild(style)
|
||
},
|
||
remove: function(alias) {
|
||
var id = `injector:${alias}`
|
||
var style = document.getElementById(id)
|
||
if (style) {
|
||
var head = document.head || document.getElementsByTagName('head')[0]
|
||
if (head)
|
||
head.removeChild(document.getElementById(id))
|
||
}
|
||
}
|
||
}
|
||
|
||
function randomString(length=10, chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
|
||
let result = ''
|
||
for (let i = length; i > 0; --i)
|
||
result += chars[Math.round(Math.random() * (chars.length - 1))]
|
||
return result
|
||
}
|
||
|
||
function add_mob_menu() {
|
||
let r = 20
|
||
, pos = 10
|
||
, delay = 300
|
||
$('#overlay_menu').hide()
|
||
document.body.insertAdjacentHTML('afterbegin', `<div id="mobile-menu"><div id="mobile-menu-contents"></div></div>`)
|
||
let mm = document.querySelector('#mobile-menu-contents')
|
||
, $mmw = $('#mobile-menu')
|
||
, htm = ''
|
||
if (overboard_dir) {
|
||
htm += `<a href="/${overboard_dir}" class="mm-item mm-brd">${overboard_desc}</a>`
|
||
}
|
||
$('<div id="mmb-_options" style="display: none"></div>').append($('#ms-_options')).appendTo($(mm))
|
||
$mmw.append(`<div id="mm-toggle">
|
||
<div class="mm-bars"></div>
|
||
<div id="mm-circle"></div>
|
||
</div>`)
|
||
let cats = boards10.list
|
||
cloud20.getBoards().then(b20 => {
|
||
cats.sort((a,b) => +a.order > +b.order)
|
||
.push({
|
||
id: '20',
|
||
name: '2.0',
|
||
boards: b20
|
||
})
|
||
cats.forEach((cat, ix) => {
|
||
if (cat.boards) {
|
||
htm += `<a href="#" class="mm-item mm-cat" data-mmbx="${ix}">${cat.name}</a>
|
||
<span class="mm-boards" id="mmb-${ix}">`
|
||
cat.boards.forEach(b => {
|
||
b.dir = b.dir || b.name
|
||
htm += `<a class="mm-item mm-brd" href="/${b.dir}">/${b.dir}/ — ${b.desc}</a>`
|
||
})
|
||
htm += '</span>'
|
||
}
|
||
})
|
||
htm += `<a href="#" class="mm-item mm-toggle-options" onclick="
|
||
$('.mm-boards').toggle(false);
|
||
$('#mmb-_options').slideToggle('fast');
|
||
$('.mmc-expanded').removeClass('mmc-expanded')
|
||
return false
|
||
">Options</a>`
|
||
mm.insertAdjacentHTML('afterBegin', `${htm}`)
|
||
$('#mm-toggle').click(function() {
|
||
if ($mmw.hasClass('mm-expanded')) return;
|
||
$mmw.addClass('mm-expanded')
|
||
let bcr = $mmw[0].getBoundingClientRect()
|
||
, x = bcr.width - r - pos // - radius/2, -position
|
||
, y = bcr.height - r - pos // - radius/2, -position
|
||
, R = Math.sqrt(x*x + y*y) // See, math is not useless after all
|
||
, scale = Math.ceil(R / r)
|
||
$('#mm-circle').css({'transform': `scale(${scale})`})
|
||
$(mm).css({
|
||
'visibility': 'visible',
|
||
'opacity': 1
|
||
})
|
||
$mmw.addClass('bars-away')
|
||
setTimeout(() => {
|
||
$mmw.addClass('mm-expanded-full')
|
||
}, delay)
|
||
})
|
||
|
||
offClick.push(() => {
|
||
if (! $mmw.hasClass('mm-expanded-full')) return;
|
||
$mmw.removeClass('mm-expanded-full bars-away')
|
||
setTimeout(() => {
|
||
$(mm).css({'visibility': 'hidden'})
|
||
$mmw.removeClass('mm-expanded')
|
||
}, delay)
|
||
$(mm).css({
|
||
'visibility': 'visible',
|
||
'opacity': 0
|
||
})
|
||
$('#mm-circle').css({'transform': `scale(1)`})
|
||
})
|
||
})
|
||
$mmw.click(function(ev) {
|
||
ev.stopPropagation()
|
||
})
|
||
.on('click', '.mm-cat[data-mmbx]', function() {
|
||
let $target = $(`#mmb-${$(this).data('mmbx')}`)
|
||
if ($target.is(':visible')) {
|
||
$target.toggle(false)
|
||
$(this).removeClass('mmc-expanded')
|
||
}
|
||
else {
|
||
$('.mm-boards').toggle(false)
|
||
$('#mmb-_options').slideUp('fast');
|
||
$target.toggle(true)
|
||
$('.mmc-expanded').removeClass('mmc-expanded')
|
||
$(this).addClass('mmc-expanded')
|
||
}
|
||
return false
|
||
})
|
||
}
|
||
|
||
var boards10 = {
|
||
get list() {
|
||
if (!this.allboards) {
|
||
this.allboards = []
|
||
document.querySelectorAll('.olm-link').forEach(o => {
|
||
o = o.querySelector('a')
|
||
if (!o) return;
|
||
let s = o.dataset.toexpand
|
||
, sd = o.innerText
|
||
, sect = document.querySelector(`#ms-${s}`)
|
||
if (!s || s=='20' || s=='_options') return;
|
||
let section = {
|
||
id: s,
|
||
name: sd,
|
||
boards: []
|
||
}
|
||
sect.querySelectorAll('a').forEach(a => {
|
||
let m = a.innerText.match(/\/(.+?)\/ - (.+)/)
|
||
if (m)
|
||
section.boards.push({
|
||
dir: m[1],
|
||
desc: m[2]
|
||
})
|
||
})
|
||
this.allboards.push(section)
|
||
})
|
||
}
|
||
return this.allboards
|
||
}
|
||
}
|
||
|
||
var cloud20 = {
|
||
init: function() {
|
||
this.getBoards().then(() => this.filter('')).catch(_.noop)
|
||
$('#boardselect').on('input', function() {
|
||
cloud20.filter($(this).val());
|
||
});
|
||
},
|
||
getBoards: function() {
|
||
return new Promise((rs, rj) => {
|
||
if (this.allboards)
|
||
rs(this.allboards)
|
||
else {
|
||
$.getJSON(ku_cgipath + '/boards20.json', data => {
|
||
this.allboards = data
|
||
rs(data)
|
||
})
|
||
.fail(e => {
|
||
pups.err(_l.unable_load_20)
|
||
rj(e)
|
||
})
|
||
}
|
||
})
|
||
},
|
||
filter: function(query) {
|
||
var res = [];
|
||
if(typeof this.allboards === "undefined") return;
|
||
if(query == '') res = this.allboards;
|
||
else {
|
||
query = query.toLowerCase();
|
||
_.each(this.allboards, function(board) {
|
||
if(board.name.toLowerCase().search(query) !== -1 || board.desc.toLowerCase().search(query) !== -1)
|
||
res.push(board);
|
||
});
|
||
}
|
||
this.display(res);
|
||
},
|
||
display: function(list) {
|
||
var newhtml = '', opts = '';
|
||
_.each(list, function(item) {
|
||
newhtml += '<a class="menu-item" title="'+ item.desc +'" href="'+ku_boardsfolder+item.name+'/">/'+item.name+'/ - '+ item.desc +'</a>';
|
||
opts += '<option value="'+item.name+'">/'+item.name+'/ - '+ item.desc +'</option>';
|
||
});
|
||
$('#boards20').html(newhtml);
|
||
$('.boardsel20').append(opts);
|
||
}
|
||
}
|
||
|
||
var countries = {
|
||
'A1': "Anonymous Proxy",
|
||
'A2': "Satellite Provider",
|
||
'O1': "Other Country",
|
||
'AD': "Andorra",
|
||
'AE': "United Arab Emirates",
|
||
'AF': "Afghanistan",
|
||
'AG': "Antigua and Barbuda",
|
||
'AI': "Anguilla",
|
||
'AL': "Albania",
|
||
'AM': "Armenia",
|
||
'AO': "Angola",
|
||
'AP': "Asia/Pacific Region",
|
||
'AQ': "Antarctica",
|
||
'AR': "Argentina",
|
||
'AS': "American Samoa",
|
||
'AT': "Austria",
|
||
'AU': "Australia",
|
||
'AW': "Aruba",
|
||
'AX': "Aland Islands",
|
||
'AZ': "Azerbaijan",
|
||
'BA': "Bosnia and Herzegovina",
|
||
'BB': "Barbados",
|
||
'BD': "Bangladesh",
|
||
'BE': "Belgium",
|
||
'BF': "Burkina Faso",
|
||
'BG': "Bulgaria",
|
||
'BH': "Bahrain",
|
||
'BI': "Burundi",
|
||
'BJ': "Benin",
|
||
'BL': "Saint Bartelemey",
|
||
'BM': "Bermuda",
|
||
'BN': "Brunei Darussalam",
|
||
'BO': "Bolivia",
|
||
'BQ': "Bonaire, Saint Eustatius and Saba",
|
||
'BR': "Brazil",
|
||
'BS': "Bahamas",
|
||
'BT': "Bhutan",
|
||
'BV': "Bouvet Island",
|
||
'BW': "Botswana",
|
||
'BY': "Belarus",
|
||
'BZ': "Belize",
|
||
'CA': "Canada",
|
||
'CC': "Cocos (Keeling) Islands",
|
||
'CD': "Congo, The Democratic Republic of the",
|
||
'CF': "Central African Republic",
|
||
'CG': "Congo",
|
||
'CH': "Switzerland",
|
||
'CI': "Cote d'Ivoire",
|
||
'CK': "Cook Islands",
|
||
'CL': "Chile",
|
||
'CM': "Cameroon",
|
||
'CN': "China",
|
||
'CO': "Colombia",
|
||
'CR': "Costa Rica",
|
||
'CU': "Cuba",
|
||
'CV': "Cape Verde",
|
||
'CW': "Curacao",
|
||
'CX': "Christmas Island",
|
||
'CY': "Cyprus",
|
||
'CZ': "Czech Republic",
|
||
'DE': "Germany",
|
||
'DJ': "Djibouti",
|
||
'DK': "Denmark",
|
||
'DM': "Dominica",
|
||
'DO': "Dominican Republic",
|
||
'DZ': "Algeria",
|
||
'EC': "Ecuador",
|
||
'EE': "Estonia",
|
||
'EG': "Egypt",
|
||
'EH': "Western Sahara",
|
||
'ER': "Eritrea",
|
||
'ES': "Spain",
|
||
'ET': "Ethiopia",
|
||
'EU': "Europe",
|
||
'FI': "Finland",
|
||
'FJ': "Fiji",
|
||
'FK': "Falkland Islands (Malvinas)",
|
||
'FM': "Micronesia, Federated States of",
|
||
'FO': "Faroe Islands",
|
||
'FR': "France",
|
||
'GA': "Gabon",
|
||
'GB': "United Kingdom",
|
||
'GD': "Grenada",
|
||
'GE': "Georgia",
|
||
'GF': "French Guiana",
|
||
'GG': "Guernsey",
|
||
'GH': "Ghana",
|
||
'GI': "Gibraltar",
|
||
'GL': "Greenland",
|
||
'GM': "Gambia",
|
||
'GN': "Guinea",
|
||
'GP': "Guadeloupe",
|
||
'GQ': "Equatorial Guinea",
|
||
'GR': "Greece",
|
||
'GS': "South Georgia and the South Sandwich Islands",
|
||
'GT': "Guatemala",
|
||
'GU': "Guam",
|
||
'GW': "Guinea-Bissau",
|
||
'GY': "Guyana",
|
||
'HK': "Hong Kong",
|
||
'HM': "Heard Island and McDonald Islands",
|
||
'HN': "Honduras",
|
||
'HR': "Croatia",
|
||
'HT': "Haiti",
|
||
'HU': "Hungary",
|
||
'ID': "Indonesia",
|
||
'IE': "Ireland",
|
||
'IL': "Israel",
|
||
'IM': "Isle of Man",
|
||
'IN': "India",
|
||
'IO': "British Indian Ocean Territory",
|
||
'IQ': "Iraq",
|
||
'IR': "Iran, Islamic Republic of",
|
||
'IS': "Iceland",
|
||
'IT': "Italy",
|
||
'JE': "Jersey",
|
||
'JM': "Jamaica",
|
||
'JO': "Jordan",
|
||
'JP': "Japan",
|
||
'KE': "Kenya",
|
||
'KG': "Kyrgyzstan",
|
||
'KH': "Cambodia",
|
||
'KI': "Kiribati",
|
||
'KM': "Comoros",
|
||
'KN': "Saint Kitts and Nevis",
|
||
'KP': "Korea, Democratic People's Republic of",
|
||
'KR': "Korea, Republic of",
|
||
'KW': "Kuwait",
|
||
'KY': "Cayman Islands",
|
||
'KZ': "Kazakhstan",
|
||
'LA': "Lao People's Democratic Republic",
|
||
'LB': "Lebanon",
|
||
'LC': "Saint Lucia",
|
||
'LI': "Liechtenstein",
|
||
'LK': "Sri Lanka",
|
||
'LR': "Liberia",
|
||
'LS': "Lesotho",
|
||
'LT': "Lithuania",
|
||
'LU': "Luxembourg",
|
||
'LV': "Latvia",
|
||
'LY': "Libyan Arab Jamahiriya",
|
||
'MA': "Morocco",
|
||
'MC': "Monaco",
|
||
'MD': "Moldova, Republic of",
|
||
'ME': "Montenegro",
|
||
'MF': "Saint Martin",
|
||
'MG': "Madagascar",
|
||
'MH': "Marshall Islands",
|
||
'MK': "Macedonia",
|
||
'ML': "Mali",
|
||
'MM': "Myanmar",
|
||
'MN': "Mongolia",
|
||
'MO': "Macao",
|
||
'MP': "Northern Mariana Islands",
|
||
'MQ': "Martinique",
|
||
'MR': "Mauritania",
|
||
'MS': "Montserrat",
|
||
'MT': "Malta",
|
||
'MU': "Mauritius",
|
||
'MV': "Maldives",
|
||
'MW': "Malawi",
|
||
'MX': "Mexico",
|
||
'MY': "Malaysia",
|
||
'MZ': "Mozambique",
|
||
'NA': "Namibia",
|
||
'NC': "New Caledonia",
|
||
'NE': "Niger",
|
||
'NF': "Norfolk Island",
|
||
'NG': "Nigeria",
|
||
'NI': "Nicaragua",
|
||
'NL': "Netherlands",
|
||
'NO': "Norway",
|
||
'NP': "Nepal",
|
||
'NR': "Nauru",
|
||
'NU': "Niue",
|
||
'NZ': "New Zealand",
|
||
'OM': "Oman",
|
||
'PA': "Panama",
|
||
'PE': "Peru",
|
||
'PF': "French Polynesia",
|
||
'PG': "Papua New Guinea",
|
||
'PH': "Philippines",
|
||
'PK': "Pakistan",
|
||
'PL': "Poland",
|
||
'PM': "Saint Pierre and Miquelon",
|
||
'PN': "Pitcairn",
|
||
'PR': "Puerto Rico",
|
||
'PS': "Palestinian Territory",
|
||
'PT': "Portugal",
|
||
'PW': "Palau",
|
||
'PY': "Paraguay",
|
||
'QA': "Qatar",
|
||
'RE': "Reunion",
|
||
'RO': "Romania",
|
||
'RS': "Serbia",
|
||
'RU': "Russian Federation",
|
||
'RW': "Rwanda",
|
||
'SA': "Saudi Arabia",
|
||
'SB': "Solomon Islands",
|
||
'SC': "Seychelles",
|
||
'SD': "Sudan",
|
||
'SE': "Sweden",
|
||
'SG': "Singapore",
|
||
'SH': "Saint Helena",
|
||
'SI': "Slovenia",
|
||
'SJ': "Svalbard and Jan Mayen",
|
||
'SK': "Slovakia",
|
||
'SL': "Sierra Leone",
|
||
'SM': "San Marino",
|
||
'SN': "Senegal",
|
||
'SO': "Somalia",
|
||
'SR': "Suriname",
|
||
'SS': "South Sudan",
|
||
'ST': "Sao Tome and Principe",
|
||
'SV': "El Salvador",
|
||
'SX': "Sint Maarten",
|
||
'SY': "Syrian Arab Republic",
|
||
'SZ': "Swaziland",
|
||
'TC': "Turks and Caicos Islands",
|
||
'TD': "Chad",
|
||
'TF': "French Southern Territories",
|
||
'TG': "Togo",
|
||
'TH': "Thailand",
|
||
'TJ': "Tajikistan",
|
||
'TK': "Tokelau",
|
||
'TL': "Timor-Leste",
|
||
'TM': "Turkmenistan",
|
||
'TN': "Tunisia",
|
||
'TO': "Tonga",
|
||
'TR': "Turkey",
|
||
'TT': "Trinidad and Tobago",
|
||
'TV': "Tuvalu",
|
||
'TW': "Taiwan",
|
||
'TZ': "Tanzania, United Republic of",
|
||
'UA': "Ukraine",
|
||
'UG': "Uganda",
|
||
'UM': "United States Minor Outlying Islands",
|
||
'US': "United States",
|
||
'UY': "Uruguay",
|
||
'UZ': "Uzbekistan",
|
||
'VA': "Holy See (Vatican City State)",
|
||
'VC': "Saint Vincent and the Grenadines",
|
||
'VE': "Venezuela",
|
||
'VG': "Virgin Islands, British",
|
||
'VI': "Virgin Islands, U.S.",
|
||
'VN': "Vietnam",
|
||
'VU': "Vanuatu",
|
||
'WF': "Wallis and Futuna",
|
||
'WS': "Samoa",
|
||
'YE': "Yemen",
|
||
'YT': "Mayotte",
|
||
'ZA': "South Africa",
|
||
'ZM': "Zambia",
|
||
'ZW': "Zimbabwe",
|
||
'XX': "OMCK",
|
||
'T1': "Tor"
|
||
}
|
||
|
||
var bnrs = {
|
||
initiated: false,
|
||
init: function() {
|
||
$.getJSON(ku_boardspath+'/bnrs.json', function(data) {
|
||
var reduced = [];
|
||
if(data.length > 1) {
|
||
_.each(data, function(bnr) {
|
||
if(bnr.link !== this_board_dir) reduced.push(bnr);
|
||
});
|
||
}
|
||
else reduced = data;
|
||
bnrs.data = reduced;
|
||
bnrs.initiated = true;
|
||
bnrs.display();
|
||
});
|
||
},
|
||
display: function() {
|
||
if(!this.initiated) return;
|
||
if(!this.data.length) return;
|
||
var reduced = [];
|
||
if(typeof this.current !== 'undefined') {
|
||
_.each(this.data, function(item) {
|
||
if(item.path !== bnrs.current) reduced.push(item)
|
||
});
|
||
}
|
||
else reduced = this.data;
|
||
var toDisplay = randomItem(reduced);
|
||
this.current = toDisplay.path;
|
||
var link = (toDisplay.link.indexOf('http') === (-1)) ? ku_boardspath+'/'+toDisplay.link : toDisplay.link;
|
||
var newhtml = '<a class="bnrsupdate" href="#" onclick="javascript:bnrs.display();return false;"></a><a href="'+link+'"><img src="'+ku_boardspath+'/images/bnrs/'+toDisplay.path+'" /></a>';
|
||
if($('.bnr').length) {
|
||
$('.bnr').html(newhtml);
|
||
}
|
||
else $('.logo').before('<div class="bnr-wrap"><div class="bnr">'+newhtml+'</div></div>');
|
||
},
|
||
}
|
||
|
||
function getRandomInt (min, max) {
|
||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||
}
|
||
|
||
function randomItem(array) {
|
||
return array[getRandomInt(0, array.length-1)];
|
||
}
|
||
|
||
var embedLinks = {
|
||
sites: [
|
||
{id: 'youtube', rx: /(?:youtu(?:\.be|be\.com)\/(?:.*v(?:\/|=)|(?:.*\/)?)([\w'-]+))/i },
|
||
{id: 'vimeo', rx: /[\w\W]*vimeo\.com\/(?:.*?)([0-9]+)(?:.*)?/ },
|
||
{id: 'coub', rx: /[\w\W]*coub\.com\/view\/([\w\W]*)[\w\W]*/ },
|
||
{id: 'soundcloud', rx: /[\w\W]*soundcloud.com\/([\w\W]*)[\w\W]*/i },
|
||
],
|
||
process: function(val) {
|
||
var result = null;
|
||
_.find(this.sites, function(site) {
|
||
var fruit = site.rx.exec(val);
|
||
if(fruit != null) {
|
||
result = {
|
||
site: site.id,
|
||
code: fruit[1]
|
||
}
|
||
}
|
||
})
|
||
return result;
|
||
}
|
||
}
|
||
|
||
var catalog = {
|
||
conf: {
|
||
sortBy: 'bumped',
|
||
layout: 'text',
|
||
respectStickied: true,
|
||
showHidden: true,
|
||
expandOnHover: true
|
||
},
|
||
saveConfig: function() {
|
||
localStorage['catalogSettings'] = JSON.stringify(this.conf);
|
||
},
|
||
init: function() {
|
||
document.body.classList.add('is-catalog')
|
||
// apply settings
|
||
if(localStorage['catalogSettings']) {
|
||
try {
|
||
var myConf = JSON.parse(localStorage['catalogSettings']);
|
||
_.each(myConf, function(val, key) {
|
||
this.conf[key] = val
|
||
}, this)
|
||
}
|
||
catch(e) {
|
||
console.error('Invalid catalog config');
|
||
localStorage.removeItem('catalogSettings');
|
||
}
|
||
}
|
||
|
||
/*var sortOptionElements = '';
|
||
_.each([
|
||
['bumped', 'bumpOrder'],
|
||
['replied', 'lastReply'],
|
||
['timestamp', 'creationDate'],
|
||
['reply_count', 'replyCount']
|
||
], (function(val_desc) {
|
||
sortOptionElements += '<option value="'+val_desc[0]+'"'+(val_desc[0]==this.conf.sortBy ? ' selected' : '')+'>'+_l[val_desc[1]]+'</option>';
|
||
}).bind(this));*/
|
||
// Catalog refresh button
|
||
var refreshBtn = `<div class="refresh-catalog" title="${_l.refreshCatalog}"><svg class="icon i-16in20"><use xlink:href="#i-refresh"></use></svg></div>`
|
||
// catalog control buttons
|
||
var sortBtns = '<div class="button-group" data-select="sortBy">';
|
||
_.each([
|
||
['bumped', 'bumpOrder', 'bump', 'i-20'],
|
||
['timestamp', 'creationDate', 'creation', 'i-20'],
|
||
['replied', 'lastReply', 'reply', 'i-16in20'],
|
||
['reply_count', 'replyCount', 'replies', 'i-20']
|
||
], (function(v_d_i) {
|
||
sortBtns += '<div class="bg-button'+(v_d_i[0]==this.conf.sortBy ? ' bgb-selected' : '')+
|
||
'" data-val="'+v_d_i[0]+'" title="'+_l.sortBy+' '+_l[v_d_i[1]]+'">\
|
||
<svg class="icon '+v_d_i[3]+'"><use xlink:href="#i-'+v_d_i[2]+'"></use></svg></div>'
|
||
}).bind(this));
|
||
sortBtns += '</div>';
|
||
var pinBtns = '<div class="button-group'+(this.conf.sortBy !== 'bumped' ? ' disabled' : '')+'" data-select="respectStickied" id="pinControl">';
|
||
_.each([
|
||
[1, 'doStick', 'pin', 'i-16in20'],
|
||
[0, 'doNotStick', 'unpin', 'i-16in20']
|
||
], (function(v_d_i) {
|
||
pinBtns += '<div class="bg-button'+(v_d_i[0]==this.conf.respectStickied ? ' bgb-selected' : '')+
|
||
'" data-val="'+v_d_i[0]+'" title="'+_l[v_d_i[1]]+'">\
|
||
<svg class="icon '+v_d_i[3]+'"><use xlink:href="#i-'+v_d_i[2]+'"></use></svg></div>'
|
||
}).bind(this));
|
||
pinBtns += '</div>';
|
||
var hideBtns = '<div class="button-group" data-select="showHidden">';
|
||
_.each([
|
||
[0, 'hideHidden', 'hide', 'i-16in20'],
|
||
[1, 'showHidden', 'unhide', 'i-16in20']
|
||
], (function(v_d_i) {
|
||
hideBtns += '<div class="bg-button'+(v_d_i[0]==this.conf.showHidden ? ' bgb-selected' : '')+
|
||
'" data-val="'+v_d_i[0]+'" title="'+_l[v_d_i[1]]+'">\
|
||
<svg class="icon '+v_d_i[3]+'"><use xlink:href="#i-'+v_d_i[2]+'"></use></svg></div>'
|
||
}).bind(this));
|
||
hideBtns += '</div>';
|
||
var layoutBtns = '<div class="button-group" data-select="layout">';
|
||
_.each([
|
||
['text', 'smallPics', 'grid-small', 'i-20'],
|
||
['gallery', 'largePics', 'gallery-grid', 'i-20'],
|
||
/*['legacy', 'legacyMode', 'legacy-grid', 'i-20']*/
|
||
], (function(v_d_i) {
|
||
layoutBtns += '<div class="bg-button'+(v_d_i[0]==this.conf.layout ? ' bgb-selected' : '')+
|
||
'" data-val="'+v_d_i[0]+'" title="'+_l[v_d_i[1]]+'">\
|
||
<svg class="icon '+v_d_i[3]+'"><use xlink:href="#i-'+v_d_i[2]+'"></use></svg></div>'
|
||
}).bind(this));
|
||
layoutBtns += '</div>';
|
||
// searck input
|
||
var searchInput = '<input name="subject" autocomplete="false" class="button-group" type="text" id="cat-search" placeholder="'+_l.search+'..." /><input type="text" name="FUCKYOUCHROMEFUCKYOU" style="display:none;"/>'
|
||
|
||
$('#catalog-controls').html(
|
||
refreshBtn+sortBtns+pinBtns+searchInput+hideBtns+layoutBtns
|
||
);
|
||
this.load();
|
||
if(this.conf.expandOnHover)
|
||
$('#catalog-contents').addClass('expand-on-hover-enabled');
|
||
// Card events
|
||
$('#catalog-contents')
|
||
.on('click', '.namedate-overlay', function() {
|
||
$(this).toggleClass('date-on name-on');
|
||
})
|
||
.on('click', '.ce-text .bigThumb', function(ev) {
|
||
ev.stopPropagation(); ev.preventDefault();
|
||
var $card = $(this).parents('.cat-entry');
|
||
$card.toggleClass('thumbExpanded');
|
||
})
|
||
.on('click', '.bigThumb audio, .bigThumb video', function(ev) {
|
||
ev.stopPropagation();
|
||
})
|
||
.on('animationstart webkitAnimationStart MSAnimationStart oanimationstart', (function(event) {
|
||
var $target = $(event.target);
|
||
if (event.originalEvent.animationName == "embed-image-insert" && !$target.hasClass('_inserted_'))
|
||
this.getEmbedThumb($target);
|
||
}).bind(this))
|
||
.on('mousedown', '.cat-prv', function(ev) {
|
||
ev.preventDefault();
|
||
PostPreviews._mouseover.bind(this)(ev);
|
||
})
|
||
.on('click', '.cat-prv', function(ev) {
|
||
ev.stopPropagation();
|
||
ev.preventDefault();
|
||
})
|
||
.on('mouseleave', '.cat-prv', function(ev) {
|
||
PostPreviews._mouseout.bind(this)(/*ev*/);
|
||
})
|
||
.on('click', '.i-hide', (function(ev) {
|
||
var $target = $(ev.currentTarget)
|
||
, $card = $target.parents('.cat-entry')
|
||
, threadID = $card.data('id')
|
||
, threadIX = _.findIndex(this.model, {'id': threadID})
|
||
, thread = this.model[threadIX];
|
||
thread.hidden = !thread.hidden;
|
||
// addClass won't work here for some reason
|
||
if(thread.hidden) {
|
||
$target[0].classList.add('pressed');
|
||
$target.html(makeIcon('unhide', '', true));
|
||
HiddenItems.hideItem('thread', threadID);
|
||
$card.addClass('thread-hidden')
|
||
}
|
||
else {
|
||
$target[0].classList.remove('pressed');
|
||
$target.html(makeIcon('hide', '', true));
|
||
HiddenItems.unhideItem('thread', threadID);
|
||
$card.removeClass('thread-hidden')
|
||
}
|
||
//invalidate rendered cache
|
||
this.model[threadIX] = thread;
|
||
delete this.rendered[this.conf.layout][threadID];
|
||
}).bind(this))
|
||
// catalog configuration
|
||
$('.bg-button').click((function(ev) {
|
||
var $target = $(ev.currentTarget);
|
||
if($target.hasClass('bgb-selected')) return;
|
||
var $group = $target.parent()
|
||
, val = $target.data('val')
|
||
, key = $group.data('select');
|
||
$group.find('.bg-button').removeClass('bgb-selected');
|
||
$target.addClass('bgb-selected');
|
||
if(key !== 'sortBy' && key !== 'layout') val = !!val;
|
||
else {
|
||
if(val == 'bumped') $('#pinControl').removeClass('disabled');
|
||
else $('#pinControl').addClass('disabled');
|
||
}
|
||
this.conf[key] = val;
|
||
this.saveConfig();
|
||
if(key !== 'showHidden') this.build();
|
||
else {
|
||
if(val) $('#catalog-contents').removeClass('hideHidden');
|
||
else $('#catalog-contents').addClass('hideHidden');
|
||
}
|
||
}).bind(this))
|
||
$('.refresh-catalog').click((function(ev) {
|
||
ev.preventDefault();
|
||
this.load();
|
||
}).bind(this));
|
||
//search
|
||
$('#cat-search').on('input', function() {
|
||
var query = $(this).val().toLowerCase().replace(/\"/, '\\"');
|
||
try {
|
||
injector.remove('cat-search');
|
||
} catch(e) {}
|
||
if(query.length)
|
||
injector.inject('cat-search', '#catalog-contents .cat-entry:not([data-search *= "'+query+'"]) { display:none; }');
|
||
else injector.remove('cat-search');
|
||
}).trigger('input');
|
||
},
|
||
load: function() {
|
||
$('#catalog-contents').html(`<div class="cat-loading"><div class="spinner"></div>${_l.loading}...</div>`);
|
||
// clear data
|
||
this.rendered = {text: {}, gallery: {}};
|
||
this.model = null;
|
||
// get contents
|
||
$.getJSON('catalog.json?v='+(new Date().getTime()))
|
||
.done(this.build.bind(this))
|
||
.fail(function(err) {
|
||
throw err;
|
||
})
|
||
},
|
||
fileTypes: {
|
||
image: ['jpg', 'gif', 'png'],
|
||
jpgThumb: ['webm', 'mp4', 'cob', 'vim', 'you'],
|
||
iconsAvailable: ['swf', 'mp3', 'ogg', 'css', 'flv'],
|
||
audio: ['mp3', 'ogg'],
|
||
embed: ['cob', 'vim', 'you']
|
||
},
|
||
authorities: ['', 'Admin', 'Mod', '?', 'God'],
|
||
formatDate: function(timestamp, short) {
|
||
if(typeof short === 'undefined') short = false;
|
||
var date = new Date(timestamp * 1000)
|
||
, Dow = this.dateLocal.dows.hasOwnProperty(locale) ? this.dateLocal.dows[locale][date.getDay()] : this.dateLocal.dows.en[date.getDay()]
|
||
, yy = _.padLeft(date.getFullYear() % 100, 2, 0)
|
||
, mo = _.padLeft(date.getMonth()+1, 2, 0)
|
||
, Mon = (locale === 'ru') ? this.dateLocal.mons.ru[date.getMonth()] : date.getMonth()+1
|
||
, dd = _.padLeft(date.getDate(), 2, 0)
|
||
, hh = _.padLeft(date.getHours(), 2, 0)
|
||
, mm = _.padLeft(date.getMinutes(), 2, 0)
|
||
, ss = _.padLeft(date.getSeconds(), 2, 0);
|
||
return (short
|
||
? ( (locale === 'ru')
|
||
? (dd+'.'+mo+'.'+yy+' в ')
|
||
: (mo+'/'+dd+'/'+yy+' @ ') )
|
||
: ( (locale === 'ru')
|
||
? (Dow+' '+dd+' '+Mon+'’'+yy+' в ')
|
||
: (mo+'/'+dd+'/'+yy+' ('+Dow+') @ ') )
|
||
) + hh+':'+mm+':'+ss;
|
||
},
|
||
dateLocal: {
|
||
dows: {
|
||
ru: ['Пнд','Втр','Срд','Чтв','Птн','Сбт','Вск'],
|
||
en: ['Sun','Mon','Tue','Wen','Thu','Fri','Sat']
|
||
},
|
||
mons: {
|
||
ru: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек']
|
||
}
|
||
},
|
||
build: function(data) {
|
||
if(typeof data === 'undefined') data = this.model;
|
||
if(!data) return;
|
||
// normalize
|
||
_.each(data, function(entry, i) {
|
||
_.each(['id', 'reply_count', 'bumped', 'replied', 'reply_count', 'timestamp', 'page', 'locked', 'stickied', 'deleted_timestamp'], function(prop) {
|
||
entry[prop] = ~~entry[prop];
|
||
})
|
||
data[i] = entry;
|
||
})
|
||
// Sort threads
|
||
if(this.conf.sortBy === 'bumped' && this.conf.respectStickied)
|
||
this.model = _.sortByOrder(data, ['stickied', 'bumped'], ['desc', 'desc']);
|
||
else {
|
||
var sby = [this.conf.sortBy];
|
||
if(this.conf.sortBy !== 'bumped')
|
||
sby.push('bumped')
|
||
this.model = _.sortByOrder(data, sby, _.repeat('desc', sby.length));
|
||
}
|
||
|
||
var html = '';
|
||
|
||
_.each(this.model, function(thread) {
|
||
html += this.buildEntry(thread);
|
||
}, this);
|
||
|
||
$('#catalog-contents').html(html);
|
||
},
|
||
getEmbedThumb: function($el) {
|
||
var site = $el.data('site'), id = $el.data('id'), img
|
||
, $thread = $el.parents('.cat-entry')
|
||
, threadID = $thread.data('id');
|
||
if(site == 'cob')
|
||
$.get(ku_boardspath+'/corpsy.php?code='+id, (function(res) {
|
||
$el.replaceWith('<img src="'+res.thumbnail_url+'">');
|
||
this.rendered[this.conf.layout][threadID] = $thread[0].outerHTML;
|
||
}).bind(this));
|
||
if(site == 'vim')
|
||
$.get('http://vimeo.com/api/v2/video/'+id+'.json', (function(res) {
|
||
$el.replaceWith('<img src="'+res[0].thumbnail_medium+'">');
|
||
this.rendered[this.conf.layout][threadID] = $thread[0].outerHTML
|
||
}).bind(this));
|
||
},
|
||
|
||
buildEntry: function(thread) {
|
||
if(this.rendered[this.conf.layout].hasOwnProperty(thread.id))
|
||
return this.rendered[this.conf.layout][thread.id];
|
||
|
||
if(!thread.processed) {
|
||
thread.url = '/'+this_board_dir+'/res/'+thread.id+'.html';
|
||
// --- Building blocks ---
|
||
// Thumbnails
|
||
var expanderBtn = '<svg class="actor icon cat-thumb-expand"><use xlink:href="#i-expand"></use></svg>',
|
||
playerBtn = '<svg class="actor icon cat-thumb-expand"><use xlink:href="#i-play"></use></svg>';
|
||
// Find first non-deleted file
|
||
let embed = thread.embeds ? (thread.embeds.find(e => e.file != 'removed') || 'removed') : null
|
||
// for images
|
||
if(!embed || embed === 'removed') {
|
||
thread.smallThumb =
|
||
'<a href="'+thread.url+'" class="smallThumb">\
|
||
<div class="nofile-removed ctx">'+(embed === 'removed' ? 'Удалён' : 'No File')+'</div>'
|
||
+'</a>';
|
||
}
|
||
else {
|
||
if (
|
||
_.includes(this.fileTypes.image, embed.file_type)
|
||
||
|
||
_.includes(this.fileTypes.jpgThumb, embed.file_type)
|
||
||
|
||
(
|
||
_.includes(this.fileTypes.audio, embed.file_type)
|
||
&&
|
||
(+embed.thumb_w) > 0
|
||
&&
|
||
(+embed.thumb_h) > 0
|
||
)
|
||
) {
|
||
let ftype = _.includes(this.fileTypes.image, embed.file_type)
|
||
? embed.file_type
|
||
: 'jpg'
|
||
, thumbURL = _.includes(this.fileTypes.embed, embed.file_type)
|
||
? `${embed.file_type}-${embed.file}-`
|
||
: embed.file
|
||
, vartype = embed.file_type == 'mp3' ? ' onerror="switchFileType(this)" extset="jpg,png,gif"' : ''
|
||
thread.smallThumb =
|
||
'<a href="'+thread.url+'" class="smallThumb">\
|
||
<img src="thumb/'+thumbURL+'c.'+ftype+'"'+vartype+'>'
|
||
+'</a>';
|
||
thread.bigThumb =
|
||
'<img src="thumb/'+thumbURL+'s.'+ftype+'"'+vartype+'>';
|
||
}
|
||
/*if(_.includes(this.fileTypes.image, thread.file_type)) {
|
||
thread.fileTypeClass = 'image';
|
||
thread.smallThumb =
|
||
'<a href="'+thread.url+'" class="smallThumb">\
|
||
<img src="thumb/'+thread.file+'c.'+(thread.file_type === 'webm' ? 'jpg' : thread.file_type)+'">'
|
||
+'</a>';
|
||
thread.bigThumb =
|
||
'<img src="thumb/'+thread.file+'s.'+(thread.file_type === 'webm' ? 'jpg' : thread.file_type)+'">';
|
||
}*/
|
||
// small thumbnail for embeds and generic files
|
||
else {
|
||
let isGeneric = !_.includes(this.fileTypes.iconsAvailable, embed.file_type)
|
||
, thumb = isGeneric
|
||
? makeGenericFileIcon(embed.file_type)
|
||
: `<img src="${(_.includes(this.fileTypes.embed, embed.file_type)
|
||
? `/inc/filetypes/generic-embed.png`
|
||
: `/inc/filetypes/${embed.file_type}.png`)}">`
|
||
// , expandable = _.includes(this.fileTypes.embed, embed.file_type) || _.includes(this.fileTypes.audio, embed.file_type)
|
||
thread.smallThumb =
|
||
`<a target="_blank" href="${thread.url}" class="smallThumb">${thumb}</a>`
|
||
// for audios
|
||
if(_.includes(this.fileTypes.audio, embed.file_type)) {
|
||
thread.bigThumb =
|
||
'<audio src="src/'+embed.file+'.'+embed.file_type+'" controls></audio>';
|
||
}
|
||
}
|
||
/*// for embeds
|
||
if(_.includes(this.fileTypes.embed, thread.file_type)) {
|
||
thread.bigThumb = (thread.file_type == 'you')
|
||
? '<img src="http://i.ytimg.com/vi/'+thread.file+'/mqdefault.jpg">'
|
||
: '<div class="cat-bt-embed" data-site="'+thread.file_type+'" data-id="'+thread.file+'"></div>';
|
||
}
|
||
// for audios
|
||
if(_.includes(this.fileTypes.audio, thread.file_type)) {
|
||
thread.bigThumb =
|
||
'<audio src="src/'+thread.file+'.'+thread.file_type+'" controls></audio>';
|
||
}*/
|
||
if (thread.bigThumb)
|
||
thread.bigThumb = '<a target="_blank" href="'+thread.url+'" class="bigThumb">'+thread.bigThumb+'</a>';
|
||
}
|
||
/*if(!embed || embed === 'removed')
|
||
thread.bigThumb = '<a target="_blank" href="'+thread.url+'" class="bigThumb">'+thread.bigThumb+'</a>';*/
|
||
|
||
//OP
|
||
thread.op = '<a target="_blank" title="'+_l.goToThread+'" target="_blank" href="'+thread.url+'" class="op-number ctx">#'+thread.id+'</a>';
|
||
|
||
//preview
|
||
thread.preview =
|
||
'<a href="'+thread.url+'#'+thread.id+'" class="actor cat-prv">\
|
||
<svg class="icon"><use xlink:href="#i-eye"></use></svg>\
|
||
</a>';
|
||
|
||
//counters
|
||
var repliesLabel =
|
||
'<svg class="icon"><use xlink:href="#i-reply"></use></svg>\
|
||
<span class="ctx reply-count">'+thread.reply_count+'</span>';
|
||
if(thread.last_reply)
|
||
repliesLabel = '<a href="'+thread.url+'#'+thread.last_reply+'" class="actor cat-prv">'+repliesLabel+'</a>';
|
||
var replies =
|
||
'<div class="infolabel">'+repliesLabel+'</div>',
|
||
images =
|
||
'<div class="infolabel">\
|
||
<svg class="icon"><use xlink:href="#i-picture"></use></svg>\
|
||
<span class="ctx image-count">'+thread.images+'</span>\
|
||
</div>',
|
||
page =
|
||
'<div class="infolabel il-page">\
|
||
<a title="'+_l.threadOnPage+' '+thread.page+'" target="_blank" href="/'+this_board_dir+'/' + ((thread.page > 0) ? thread.page+'.html' : '')+'#'+thread.id+'" class="actor">\
|
||
<svg class="icon"><use xlink:href="#i-page"></use></svg>\
|
||
<span class="ctx page-number">'+thread.page+'</span>\
|
||
</a>\
|
||
</div>';
|
||
thread.countersCombined = replies+images+page;
|
||
|
||
//Poster name+date
|
||
thread.posterauthority = +thread.posterauthority;
|
||
thread.nameDatePriority = 'date';
|
||
if(localStorage['cat_nameDatePriority'] == 'name' ||
|
||
(localStorage['cat_nameDatePriority'] != 'date' &&
|
||
(thread.name || thread.tripcode || thread.posterauthority)
|
||
)
|
||
) thread.nameDatePriority = 'name';
|
||
|
||
// Poster name
|
||
var poster =
|
||
(thread.name ? '<span class="ctx postername">'+thread.name+'</span>' : '') +
|
||
(thread.tripcode ? '<span class="ctx postertrip">!'+thread.tripcode+'</span>' : '') +
|
||
(thread.posterauthority ? '<span class="ctx admin"> ##'+this.authorities[thread.posterauthority]+'##</span>' : '');
|
||
thread.poster = '<div class="cat-poster"><span class="ctx">by </span>'+ (poster || '<span class="ctx c-postername">'+(this_board_defaultName || _l.anonymous)+'</span>')+'</div>';
|
||
|
||
// Date
|
||
var dn = ' style="display:none"';
|
||
thread.date = '<div class="ctx cat-date cat-date-long">'+this.formatDate(thread.timestamp)+'</div>';
|
||
thread.dateCompact = '<div class="ctx cat-date cat-date-short">'+this.formatDate(thread.timestamp, 1)+'</div>';
|
||
|
||
//search data
|
||
thread.searchData = _.escape(stripHTML(thread.subject + ' ' + thread.message).toLowerCase());
|
||
|
||
thread.message = thread.message.replace(/\\"/mg, '"');
|
||
|
||
thread.processed = true;
|
||
this.model[_.findIndex(this.model, {id: thread.id})] = thread;
|
||
}
|
||
//indicators
|
||
thread = this.buildIndicators(thread);
|
||
|
||
var html = this.layouts[this.conf.layout].bind(this)(thread);
|
||
this.rendered[this.conf.layout][thread.id] = html;
|
||
return html
|
||
},
|
||
buildIndicators: function(thread) {
|
||
thread.hidden = HiddenItems.isHidden('thread', thread.id)
|
||
var pin = thread.stickied ? '<svg class="foradmin-act icon i-layer-1 i-pin"><use xlink:href="#i-pin"></use></svg>' : '',
|
||
lock = thread.locked ? '<svg class="foradmin-act icon i-layer-1 i-lock"><use xlink:href="#i-lock"></use></svg>' : '',
|
||
deathmark = thread.deleted_timestamp ? '<svg class="foradmin-act icon i-layer-1 i-deathmark"><use xlink:href="#i-skull"></use></svg>' : '',
|
||
hide = '<svg class="actor icon i-layer-1 i-hide'+(thread.hidden ? ' pressed' : '')+'"><use xlink:href="#i-'+(thread.hidden ? 'unhide' : 'hide')+'"></use></svg>',
|
||
burger = '<svg class="actor icon i-burger foradmin-show"><use xlink:href="#i-burger"></use></svg>',
|
||
del = '<svg class="actor icon i-layer-2 i-delete foradmin-show"><use xlink:href="#i-x"></use></svg>',
|
||
and = '<svg class="actor icon i-layer-2 i-dnb foradmin-show"><use xlink:href="#i-and"></use></svg>',
|
||
ban = '<svg class="actor icon i-layer-2 i-ban foradmin-show"><use xlink:href="#i-ban"></use></svg>';
|
||
thread.indicatorsCombined = '<div class="indicators">'+burger+'<span class="i-layer-1">'+deathmark+pin+lock+hide+'</span><span class="i-layer-2">'+del+and+ban+'</span></div>';
|
||
return thread;
|
||
},
|
||
layouts: {
|
||
text: function(thread) {
|
||
return ''+
|
||
'<div data-id="'+thread.id+'" class="cat-entry ce-text'+(thread.hidden ? ' thread-hidden' : '')+'" data-search="'+thread.searchData+'">\
|
||
<div class="cat-card">\
|
||
<div class="ce-heda">'
|
||
+thread.smallThumb+
|
||
'<div class="cat-infoline ci-op-link">'
|
||
+thread.op
|
||
+thread.indicatorsCombined+
|
||
'</div>\
|
||
<div class="cat-infoline namedate-overlay '+thread.nameDatePriority+'-on">'
|
||
+thread.poster
|
||
+thread.date
|
||
+'</div>\
|
||
<div class="cat-infoline">'
|
||
+thread.preview
|
||
+thread.countersCombined+
|
||
'</div>\
|
||
</div>\
|
||
<div class="ce-opcontent ctx">\
|
||
<h5>'+thread.subject+'</h5>'+
|
||
thread.message+
|
||
'</div>\
|
||
</div>'
|
||
+thread.bigThumb+
|
||
'</div>'
|
||
},
|
||
gallery: function(thread) {
|
||
return ''+
|
||
'<div data-id="'+thread.id+'" class="cat-entry ce-gallery'+(thread.hidden ? ' thread-hidden' : '')+'" data-search="'+thread.searchData+'">\
|
||
<div class="cat-card">'+
|
||
thread.bigThumb+
|
||
'<div class="cat-infoline">\
|
||
<div class="ci-op-link">'+thread.op+'</div>\
|
||
<div class="counters">'+thread.countersCombined+'</div>\
|
||
</div>\
|
||
<div class="cat-infoline">'
|
||
+thread.preview+
|
||
'<div class="namedate-overlay '+thread.nameDatePriority+'-on">'
|
||
+thread.poster
|
||
+thread.dateCompact
|
||
+'</div>'
|
||
+thread.indicatorsCombined+
|
||
'</div>\
|
||
<div class="ce-opcontent ctx">\
|
||
<h5>'+thread.subject+'</h5>'+
|
||
thread.message+
|
||
'</div>\
|
||
</div>\
|
||
</div>'
|
||
}
|
||
}
|
||
}
|
||
|
||
function stripHTML(html) {
|
||
var tmp = document.implementation.createHTMLDocument("New").body;
|
||
tmp.innerHTML = html;
|
||
return tmp.textContent || tmp.innerText || "";
|
||
}
|
||
|
||
var overboard = {
|
||
get boards() {
|
||
if (!is_overboard)
|
||
return [boardid]
|
||
if (!this._boards)
|
||
this._boards = _.unique([].map.call(document.querySelectorAll('div[id^=thread]'), e => e.dataset.boardid))
|
||
return this._boards
|
||
},
|
||
idFromName: name => name!==null && $(`div[data-board="${name}"]`).parents('div[id^=thread]').data('boardid')
|
||
}
|
||
|
||
var unreadCounter = {
|
||
init: function(built) {
|
||
if (!built) {
|
||
built = + document.querySelector('meta[property="i0:buildtime"]').content
|
||
}
|
||
this.lastvisits = localStorage['lastvisits'] ? (JSON.parse(localStorage['lastvisits']) || { }) : { }
|
||
overboard.boards.forEach(boardid => {
|
||
let last_ts = this.lastvisits.hasOwnProperty(boardid) ? parseInt(this.lastvisits[boardid]) : 0;
|
||
if(last_ts < built) {
|
||
this.lastvisits[boardid] = built
|
||
localStorage.setItem('lastvisits', JSON.stringify(this.lastvisits))
|
||
}
|
||
})
|
||
},
|
||
refreshTimestamp: function(timestamp, boardName=null) { // PHP standard timestamp length
|
||
if (!this.lastvisits)
|
||
this.init()
|
||
let id = !is_overboard ? boardid : overboard.idFromName(boardName)
|
||
if (id) {
|
||
if (timestamp===null || typeof timestamp === 'undefined')
|
||
timestamp = Math.round(new Date().getTime() / 1000)
|
||
this.lastvisits[id] = timestamp
|
||
localStorage.setItem('lastvisits', JSON.stringify(this.lastvisits))
|
||
}
|
||
},
|
||
update: function() {
|
||
if (!this.lastvisits)
|
||
this.init()
|
||
$.ajax({
|
||
url: '/newpostscount.php',
|
||
data: this.lastvisits,
|
||
dataType: 'json',
|
||
success: function(data) {
|
||
$('.newposts-counter').remove()
|
||
$('.got-updates').removeClass('got-updates')
|
||
_.each(data, function(val, brd) {
|
||
if ((+val) > 0) {
|
||
let valstr = `<span class="newposts-counter"> (${val})</span>`
|
||
, $brd = $(`.menu-item[href="/${brd}/"]`)
|
||
if ($brd.length) {
|
||
$brd.append(valstr)
|
||
$(`.sect-exr[data-toexpand="${$brd.parents('.menu-sect').attr('id').split('ms-')[1]}"]`).parent().addClass('got-updates')
|
||
}
|
||
$(`.mobile-nav option[value="${brd}"]`).append(valstr)
|
||
}
|
||
})
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
var HTMLoader = {
|
||
loadThread: function(boardID, threadID, done, postID) {
|
||
$.get(threadID === '?'
|
||
? `${ku_boardspath}/postbynumber.php?b=${boardID}&p=${postID}`
|
||
: `${ku_boardspath}/${boardID}/res/${threadID}.html?${force_html_nocache ? Math.random() : ''}`)
|
||
.then(data => {
|
||
let posts = data.match(/<div\s*?class\s*?=\s*?"[^"]*?i0svcel[^"]*?"\s*?>!i0-pd:[0-9]+\s*?<\/div\s*?>[\s\S]+?<div\s*?class\s*?=\s*?"[^"]*?i0svcel[^"]*?"\s*?>!i0-pd-end\s*?<\/div\s*?>/gi)
|
||
.map(match => {
|
||
let res = match.match(/<div\s*?class\s*?=\s*?"[^"]*?i0svcel[^"]*?"\s*?>!i0-pd:([0-9]+)\s*?<\/div\s*?>([\s\S]+?)<div\s*?class\s*?=\s*?"[^"]*?i0svcel[^"]*?"\s*?>!i0-pd-end\s*?<\/div\s*?>/i);
|
||
return {
|
||
id: +res[1],
|
||
body: res[2]
|
||
}
|
||
})
|
||
if (threadID === '?') {
|
||
let thrno = data.match(/<!--i0:thrno=([0-9]+)-->/) //wtf
|
||
if (thrno)
|
||
threadID = thrno[1]
|
||
}
|
||
if (posts.length) {
|
||
let postMap = posts.map(post => {
|
||
this.cached[`${boardID}_${post.id}`] = post.body
|
||
return post.id
|
||
})
|
||
if (threadID !== '?') {
|
||
this.threadMaps[`${boardID}_${threadID}`] = postMap
|
||
}
|
||
}
|
||
done()
|
||
})
|
||
.fail(err => {
|
||
done(err.status || 'null')
|
||
})
|
||
},
|
||
loadPage: function(page, callback, boardID=this_board_dir) {
|
||
$.get(`${ku_boardspath}/${boardID}/${page==0 ? '' : `${page}.html`}`)
|
||
.then(data => {
|
||
let chunk = data.match(/<div\s*?class\s*?=\s*?"[^"]*?i0svcel[^"]*?"\s*?>!i0-page-start\s*?<\/div\s*?>\s*([\s\S]+?)\s*<div\s*?class\s*?=\s*?"[^"]*?i0svcel[^"]*?"\s*?>!i0-page-end\s*?<\/div\s*?>/i)?.[1]
|
||
if (chunk) {
|
||
document.querySelector('#delform hr:last-of-type').insertAdjacentHTML('afterend', chunk)
|
||
this.pageCleanUp()
|
||
}
|
||
callback()
|
||
})
|
||
},
|
||
pageCleanUp() {
|
||
let existingIDs = []
|
||
document.querySelectorAll('div[id^=thread]').forEach(thr => {
|
||
if (existingIDs.includes(thr.id)) {
|
||
thr.previousElementSibling.remove() // remove #unhidethread
|
||
thr.nextElementSibling.remove() // remove <br>
|
||
thr.nextElementSibling.remove() // remove </hr>
|
||
thr.remove()
|
||
}
|
||
existingIDs.push(thr.id)
|
||
})
|
||
},
|
||
getPost: function(boardID, threadID, postID, callback, secondTry) {
|
||
let cachedPost = this.cached[`${boardID}_${postID}`]
|
||
if (cachedPost) {
|
||
callback(false, cachedPost)
|
||
return
|
||
}
|
||
else {
|
||
if (!secondTry) {
|
||
let found = false
|
||
if (boardID === this_board_dir) {
|
||
let $post = $(`a[name=${postID}]`)
|
||
if ($post.length) {
|
||
let post = $post.parents('.postnode')[0].outerHTML
|
||
this.cached[`${boardID}_${postID}`] = post
|
||
found = true
|
||
callback(false, post)
|
||
}
|
||
}
|
||
if (!found) {
|
||
this.loadThread(boardID, threadID, err => {
|
||
if (err) {
|
||
callback(err)
|
||
}
|
||
else {
|
||
this.getPost(boardID, threadID, postID, callback, true)
|
||
}
|
||
}, postID)
|
||
}
|
||
}
|
||
else {
|
||
callback(null)
|
||
}
|
||
}
|
||
},
|
||
getThread: function(boardID, threadID, range, callback, secondTry, force) {
|
||
let threadMap = force ? false : this.threadMaps[`${boardID}_${threadID}`]
|
||
if (threadMap) {
|
||
if (range) {
|
||
threadMap = threadMap.filter(n => n > range[0] && n < range[1])
|
||
}
|
||
callback(false, threadMap.reduce((htm, postID) => htm + this.cached[`${boardID}_${postID}`], ''))
|
||
}
|
||
else {
|
||
if (!secondTry) {
|
||
this.loadThread(boardID, threadID, err => {
|
||
if (err) {
|
||
callback(err)
|
||
}
|
||
else {
|
||
this.getThread(boardID, threadID, range, callback, true)
|
||
}
|
||
})
|
||
}
|
||
else {
|
||
callback(null)
|
||
}
|
||
}
|
||
},
|
||
cached: {},
|
||
threadMaps: {},
|
||
cachedPostboxes: {},
|
||
getPostbox: function(boardID) {
|
||
return new Promise((resolve, reject) => {
|
||
if (boardID===false) // not overboard
|
||
return resolve(document.querySelector('#postform'))
|
||
if (this.cachedPostboxes[boardID])
|
||
return resolve(this.cachedPostboxes[boardID])
|
||
$.get(`${ku_boardspath}/${boardID}/`) // dirty deeds done dirt cheap
|
||
.then(data => {
|
||
let pb = data.match(/<div\s*?class\s*?=\s*?"[^"]*?i0svcel[^"]*?"\s*?>!i0-pb\s*?<\/div\s*?>([\s\S]+?)<div\s*?class\s*?=\s*?"[^"]*?i0svcel[^"]*?"\s*?>!i0-pb-end\s*?<\/div\s*?>/i)
|
||
if (!pb)
|
||
reject(404)
|
||
let c = document.createElement('div')
|
||
c.innerHTML = pb[1]
|
||
let hcaptchaScript
|
||
if (typeof hcaptcha === 'undefined' && (hcaptchaScript = pb[1].match(/<script src="(.+?js\.hcaptcha\.com.+?)"/))) {
|
||
head.js(hcaptchaScript[1], () => console.log('hcaptcha loaded'))
|
||
}
|
||
this.cachedPostboxes[boardID] = c.firstChild
|
||
resolve(this.cachedPostboxes[boardID])
|
||
})
|
||
.fail(err => {
|
||
reject(err.status || 'null')
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
var endlessScroll = {
|
||
toggle: function(enabled = this.enabled) {
|
||
if (enabled) {
|
||
if (!this.bound)
|
||
this.bound = this.onScroll.bind(this)
|
||
window.addEventListener('scroll', this.bound)
|
||
this.onScroll()
|
||
}
|
||
else
|
||
window.removeEventListener('scroll', this.bound)
|
||
this.enabled = enabled
|
||
},
|
||
enabled: false,
|
||
onScroll: function() {
|
||
if (this.busy || window.scrollY + window.innerHeight < document.body.scrollHeight) return;
|
||
this.busy = true
|
||
// determine next page
|
||
let next = document.querySelector('#pagination a')
|
||
if (next) {
|
||
let loader = document.querySelector('#delform hr:last-of-type')._ins('afterend', `<div class="endless-scroll-loading spin-around"></div>`)
|
||
let page = +next.innerText
|
||
HTMLoader.loadPage(page, () => {
|
||
next.insertAdjacentHTML('afterend', `<span>${page}</span>`)
|
||
next.remove()
|
||
loader.remove()
|
||
this.busy = false
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
function switchFileType(el) {
|
||
let exp = /\.([a-z0-9]+)(?=$|\?)/i
|
||
, match = el.src.match(exp)
|
||
if (!match) return;
|
||
let exts = el.getAttribute('extset').toLowerCase().split(',')
|
||
if (!exts.length) return;
|
||
let i = exts.indexOf(match[1].toLowerCase())
|
||
if (i == -1 || i >= (exts.length-1)) return;
|
||
el.src = el.src.replace(exp, `.${exts[i+1]}`)
|
||
}
|
||
|
||
function LSfetchJSON(key) {
|
||
let val = null, data = localStorage[key]
|
||
if (typeof data !== 'undefined') {
|
||
try {
|
||
val = JSON.parse(data)
|
||
}
|
||
catch(e) {
|
||
console.error(e)
|
||
localStorage.removeItem(key)
|
||
}
|
||
}
|
||
return val
|
||
}
|
||
|
||
// YOBA alerts
|
||
var pups = {
|
||
push: function(a) {
|
||
// generate a unique alert ID
|
||
a.id = (+_.uniqueId(_.now())).toString(16)
|
||
this.container.insertAdjacentHTML('afterBegin', this.buildAlert(a))
|
||
a.el = document.getElementById(`pup_${a.id}`)
|
||
// Add the popup to the animation queue
|
||
a.el.style.marginTop = -a.el.getBoundingClientRect().height + 'px'
|
||
a.el.getBoundingClientRect() // This forces browser to respect the element's updated position for future animations (no idea why but it works)
|
||
// Close on click
|
||
a.el.onclick = () => {
|
||
delete a.timeout
|
||
a.onHold = false
|
||
a.old = true
|
||
a.el.classList.add('pup-away')
|
||
a.el.style.marginTop = -a.el.getBoundingClientRect().height + 'px'
|
||
}
|
||
// Do not close if mouse is over
|
||
a.el.onmouseenter = () => {
|
||
// iterate stack from end to start
|
||
this.stack.slice(0).reverse().find(ar => {
|
||
ar.onHold = true
|
||
return (ar.id == a.id) // break
|
||
})
|
||
}
|
||
// Close after mouse leave
|
||
a.el.onmouseleave = () => {
|
||
this.stack.forEach(ae => {
|
||
ae.onHold = false
|
||
})
|
||
this.holdOffTimeout = setTimeout(() => {
|
||
this.stack.forEach(ae => {
|
||
if (!ae.onHold && ae.old && ae.el) {
|
||
ae.el.classList.add('pup-away')
|
||
ae.el.style.marginTop = -a.el.getBoundingClientRect().height + 'px'
|
||
}
|
||
})
|
||
}, 200)
|
||
}
|
||
this.queue.add(() => {
|
||
this.scheduleClose(a)
|
||
a.el.classList.remove('pup-pre')
|
||
a.el.style.marginTop = null
|
||
a.el.classList.remove('pup-noshadow')
|
||
})
|
||
this.stack.push(a)
|
||
this.save()
|
||
return a.id
|
||
},
|
||
closeByID: function(aid) {
|
||
let a = this.byID(aid)
|
||
if (a)
|
||
a.el.onclick()
|
||
},
|
||
queue: {
|
||
stack: [],
|
||
busy: false,
|
||
add: function(fn) {
|
||
return new Promise((resolve, reject) => {
|
||
if (! this.busy) {
|
||
this.timeout = setTimeout(this.next.bind(this), this.cooldown * 1000)
|
||
this.busy = true
|
||
resolve(fn())
|
||
}
|
||
else {
|
||
this.stack.push(() => resolve(fn()))
|
||
}
|
||
})
|
||
},
|
||
next: function() {
|
||
let next = this.stack.shift()
|
||
if (! next)
|
||
this.busy = false
|
||
else {
|
||
next()
|
||
this.timeout = setTimeout(this.next.bind(this), this.cooldown * 1000)
|
||
}
|
||
},
|
||
cooldown: .3
|
||
},
|
||
byID: function(aid) {
|
||
return this.stack.find(a => a.id == aid)
|
||
},
|
||
update: function(aid, upd) {
|
||
let a = this.byID(aid)
|
||
if (typeof upd !== 'object')
|
||
a = {msg: upd}
|
||
_.extend(a, upd)
|
||
// reset timeout
|
||
this.scheduleClose(a)
|
||
a.el.setAttribute('pupclass', a.cls)
|
||
a.el.querySelector('.pup-wrapped').innerHTML = this.buildAlert(a, 1)
|
||
this.save()
|
||
},
|
||
buildAlert: function(a, contentsOnly=false) {
|
||
let contents = `
|
||
<div class="alert-icon">
|
||
<svg class="icon"><use xlink:href="#i-${this.iconMap[a.cls]}"></use></svg>
|
||
</div>
|
||
<div class="alert-msg">${a.msg}</div>`
|
||
if (contentsOnly)
|
||
return contents
|
||
return `<div class="pup ${a.old ? 'pup-away pup-away-full' : 'pup-pre pup-noshadow'}" pupclass="${a.cls}" id="pup_${a.id}">
|
||
<div class="pup-wrapped">
|
||
${contents}
|
||
</div>
|
||
</div>`
|
||
},
|
||
iconMap: {
|
||
succ: 'success',
|
||
err: 'error',
|
||
info: 'info',
|
||
warn: 'warning'
|
||
},
|
||
historyToggle: function() {
|
||
if (!this.stack.length) {
|
||
this.push({cls: 'info', msg: _l.historyEmpty, destroy: true})
|
||
return
|
||
}
|
||
let on = this.container.classList.toggle('history-mode')
|
||
setTimeout(() => this.container.style.overflow = (on ? 'auto' : null), on ? 300 : 0)
|
||
},
|
||
scheduleClose: function(a) {
|
||
if (a.timeout)
|
||
clearTimeout(a.timeout)
|
||
if (a.onHold)
|
||
a.onHold = false
|
||
let time = (a.time || a.time === 0) ? a.time : this.defaultTimeout
|
||
if (time) // If time is 0 it means no timeout
|
||
a.timeout = setTimeout(() => {
|
||
a.old = true
|
||
if (! a.onHold) {
|
||
a.el.classList.add('pup-away')
|
||
a.el.style.marginTop = -a.el.getBoundingClientRect().height + 'px'
|
||
}
|
||
}, time * 1000)
|
||
},
|
||
defaultTimeout: 3.5, // in seconds
|
||
init: function(container_id="pups-container") {
|
||
document.body.insertAdjacentHTML('beforeEnd', `<div id="${container_id}"><div class="pup-history-shadow"></div></div>`)
|
||
this.container = document.getElementById(container_id)
|
||
if (! this.container) {
|
||
console.error('No popup container found')
|
||
return;
|
||
}
|
||
this.container.onclick = () => {
|
||
if (this.container.classList.contains('history-mode')) {
|
||
return this.historyToggle()
|
||
}
|
||
}
|
||
// make aliases for error classes
|
||
['err', 'warn', 'info', 'succ', 'wait'].forEach(pupclass => {
|
||
this[pupclass] = function(a, options={}) {
|
||
if (typeof a !== 'object')
|
||
a = {msg: a}
|
||
a.cls = pupclass
|
||
_.extend(a, options)
|
||
return this.push(a)
|
||
}
|
||
})
|
||
// load history from LS
|
||
let log = LSfetchJSON('I0_event_log') || []
|
||
log.forEach(a => {
|
||
a.old = true
|
||
this.container.insertAdjacentHTML('afterBegin', this.buildAlert(a))
|
||
this.stack.push(a)
|
||
})
|
||
},
|
||
historySize: 10,
|
||
save: function() {
|
||
let ss = []
|
||
_.cloneDeep(this.stack).forEach(a => {
|
||
if (a.save) {
|
||
// Clear stack from the properties that don't need to be saved
|
||
['old', 'onHold', 'timeout', 'el', 'save'].forEach(junkProperty => delete a[junkProperty])
|
||
ss.push(a)
|
||
}
|
||
})
|
||
while (ss.length > this.historySize) {
|
||
ss.shift()
|
||
}
|
||
localStorage['I0_event_log'] = JSON.stringify(ss)
|
||
},
|
||
stack: []
|
||
}
|