diff options
Diffstat (limited to 'juick-www/src/main/assets/scripts.js')
-rw-r--r-- | juick-www/src/main/assets/scripts.js | 736 |
1 files changed, 736 insertions, 0 deletions
diff --git a/juick-www/src/main/assets/scripts.js b/juick-www/src/main/assets/scripts.js new file mode 100644 index 00000000..9da9ce3c --- /dev/null +++ b/juick-www/src/main/assets/scripts.js @@ -0,0 +1,736 @@ +require('whatwg-fetch'); +require('element-closest'); +require('classlist.js'); +require('url-search-params-polyfill'); +let Awesomplete = require('awesomplete'); +import * as killy from './embed'; + +if (!('remove' in Element.prototype)) { // Firefox <23 + Element.prototype.remove = function() { + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }; +} + +NodeList.prototype.forEach = Array.prototype.forEach; +HTMLCollection.prototype.forEach = Array.prototype.forEach; + +NodeList.prototype.filter = Array.prototype.filter; +HTMLCollection.prototype.filter = Array.prototype.filter; + +Element.prototype.selectText = function () { + let d = document; + if (d.body.createTextRange) { + let range = d.body.createTextRange(); + range.moveToElementText(this); + range.select(); + } else if (window.getSelection) { + let selection = window.getSelection(); + let rangeSel = d.createRange(); + rangeSel.selectNodeContents(this); + selection.removeAllRanges(); + selection.addRange(rangeSel); + } +}; + +function autosize(el) { + let offset = (!window.opera) + ? (el.offsetHeight - el.clientHeight) + : (el.offsetHeight + parseInt(window.getComputedStyle(el, null).getPropertyValue('border-top-width'))); + + let resize = function(el) { + el.style.height = 'auto'; + el.style.height = (el.scrollHeight + offset) + 'px'; + }; + + if (el.addEventListener) { + el.addEventListener('input', () => resize(el)); + } else if (el.attachEvent) { + el.attachEvent('onkeyup', () => resize(el)); + } +} + +function evilIcon(name) { + return `<div class="icon icon--${name}"><svg class="icon__cnt"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#${name}-icon"></use></svg></div>`; +} + +/* eslint-disable only-ascii/only-ascii */ +const translations = { + 'en': { + 'message.inReplyTo': 'in reply to', + 'message.reply': 'Reply', + 'message.likeThisMessage?': 'Recommend this message?', + 'postForm.pleaseInputMessageText': 'Please input message text', + 'postForm.upload': 'Upload', + 'postForm.newMessage': 'New message...', + 'postForm.imageLink': 'Link to image', + 'postForm.imageFormats': 'JPG/PNG, up to 10 MB', + 'postForm.or': 'or', + 'postForm.tags': 'Tags (space separated)', + 'postForm.submit': 'Send', + 'comment.writeComment': 'Write a comment...', + 'shareDialog.linkToMessage': 'Link to message', + 'shareDialog.messageNumber': 'Message number', + 'shareDialog.share': 'Share', + 'loginDialog.pleaseIntroduceYourself': 'Please introduce yourself', + 'loginDialog.registeredAlready': 'Registered already?', + 'loginDialog.username': 'Username', + 'loginDialog.password': 'Password', + 'loginDialog.facebook': 'Login with Facebook', + 'loginDialog.vk': 'Login with VK', + 'error.error': 'Error' + }, + 'ru': { + 'message.inReplyTo': 'в ответ на', + 'message.reply': 'Ответить', + 'message.likeThisMessage?': 'Рекомендовать это сообщение?', + 'postForm.pleaseInputMessageText': 'Пожалуйста, введите текст сообщения', + 'postForm.upload': 'загрузить', + 'postForm.newMessage': 'Новое сообщение...', + 'postForm.imageLink': 'Ссылка на изображение', + 'postForm.imageFormats': 'JPG/PNG, до 10Мб', + 'postForm.or': 'или', + 'postForm.tags': 'Теги (через пробел)', + 'postForm.submit': 'Отправить', + 'comment.writeComment': 'Написать комментарий...', + 'shareDialog.linkToMessage': 'Ссылка на сообщение', + 'shareDialog.messageNumber': 'Номер сообщения', + 'shareDialog.share': 'Поделиться', + 'loginDialog.pleaseIntroduceYourself': 'Пожалуйста, представьтесь', + 'loginDialog.registeredAlready': 'Уже зарегистрированы?', + 'loginDialog.username': 'Имя пользователя', + 'loginDialog.password': 'Пароль', + 'loginDialog.facebook': 'Войти через Facebook', + 'loginDialog.vk': 'Войти через ВКонтакте', + 'error.error': 'Ошибка' + } +}; +/* eslint-enable only-ascii/only-ascii */ + +function getLang() { + return (window.navigator.languages && window.navigator.languages[0]) + || window.navigator.userLanguage + || window.navigator.language; +} +function i18n(key, lang=undefined) { + const fallbackLang = 'ru'; + lang = lang || getLang().split('-')[0]; + return (translations[lang] && translations[lang][key]) + || translations[fallbackLang][key] + || key; +} + +var ws, + pageTitle; + +function initWS() { + let url = (window.location.protocol === 'https:' ? 'wss' : 'ws') + ':' + + '//api.juick.com/ws/'; + let hash = document.getElementById('body').getAttribute('data-hash'); + if (hash) { + url += '?hash=' + hash; + } else { + let content = document.getElementById('content'); + if (content) { + let pageMID = content.getAttribute('data-mid'); + if (pageMID) { + url += pageMID; + } + } + } + + ws = new WebSocket(url); + ws.onopen = function () { + console.log('online'); + if (!document.querySelector('#wsthread')) { + var d = document.createElement('div'); + d.id = 'wsthread'; + d.addEventListener('click', nextReply); + document.querySelector('body').appendChild(d); + pageTitle = document.title; + } + }; + ws.onclose = function () { + console.log('offline'); + ws = false; + setTimeout(function () { + initWS(); + }, 2000); + }; + ws.onmessage = function (msg) { + if (msg.data == ' ') { + ws.send(' '); + } else { + try { + var jsonMsg = JSON.parse(msg.data); + console.log('data: ' + msg.data); + wsIncomingReply(jsonMsg); + } catch (err) { + console.log(err); + } + } + }; + setInterval(wsSendKeepAlive, 90000); +} + +function wsSendKeepAlive() { + if (ws) { + ws.send(' '); + } +} + +function wsShutdown() { + if (ws) { + ws.onclose = function () { }; + ws.close(); + } +} + +function wsIncomingReply(msg) { + let content = document.getElementById('content'); + if (!content) { return; } + let pageMID = content.getAttribute('data-mid'); + if (!pageMID || pageMID != msg.mid) { return; } + let msgNum = '/' + msg.rid; + if (msg.replyto > 0) { + msgNum += ` ${i18n('message.inReplyTo')} <a href="#${msg.replyto}">/${msg.replyto}</a>`; + } + let photoDiv = (msg.attach === undefined) ? '' : ` + <div class="msg-media"><a href="//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}"> + <img src="//i.juick.com/photos-512/${msg.mid}-${msg.rid}.${msg.attach}"/></a> + </div>`; + let msgContHtml = ` + <div class="msg-cont"> + <div class="msg-header"> + <a href="/${msg.user.uname}/">${msg.user.uname}</a>: + <div class="msg-avatar"> + <a href="/${msg.user.uname}/"><img src="//i.juick.com/a/${msg.user.uid}.png" alt="${msg.user.uname}"/></a> + </div> + <div class="msg-ts"> + <a href="/${msg.mid}#${msg.rid}" title="${msg.timestamp}GMT">${msg.timestamp}</a> + </div> + </div> + <div class="msg-txt">${killy.format(msg.body, msg.mid, false)}</div>${photoDiv} + <div class="msg-links">${msgNum} · <a class="msg-reply-link" href="#">${i18n('message.reply')}</a></div> + <div class="msg-comment-target msg-comment-hidden"></div> + </div>`; + + let li = document.createElement('li'); + li.setAttribute('class', 'msg reply-new'); + li.setAttribute('id', msg.rid); + li.innerHTML = msgContHtml; + li.addEventListener('click', newReply); + li.addEventListener('mouseover', newReply); + li.querySelector('a.msg-reply-link').addEventListener('click', function (e) { + showCommentForm(msg.mid, msg.rid); + e.preventDefault(); + }); + + killy.embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a'); + + document.getElementById('replies').appendChild(li); + + updateRepliesCounter(); +} + +function newReply(e) { + var li = e.target; + li.classList.remove('reply-new'); + li.removeEventListener('click', e); + li.removeEventListener('mouseover', e); + updateRepliesCounter(); +} + +function nextReply() { + var li = document.querySelector('#replies>li.reply-new'); + if (li) { + li.classList.remove('reply-new'); + li.removeEventListener('click', this); + li.children[0].scrollIntoView(); + updateRepliesCounter(); + } +} + +function updateRepliesCounter() { + var replies = document.querySelectorAll('#replies>li.reply-new').length; + var wsthread = document.getElementById('wsthread'); + if (replies) { + wsthread.textContent = replies; + wsthread.style.display = 'block'; + document.title = '[' + replies + '] ' + pageTitle; + } else { + wsthread.style.display = 'none'; + document.title = pageTitle; + } +} + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +function postformListener(formEl, ev) { + if (ev.ctrlKey && (ev.keyCode == 10 || ev.keyCode == 13)) { + let form = formEl.closest('form'); + if (!form.onsubmit || form.onsubmit()) { + form.submit(); + } + } +} +function closeDialogListener(ev) { + ev = ev || window.event; + if (ev.keyCode == 27) { + closeDialog(); + } +} + +function newMessage(evt) { + document.querySelectorAll('#newmessage .dialogtxt').forEach(t => { + t.remove(); + }); + if (document.querySelector('#newmessage textarea').value.length == 0 + && document.querySelector('#newmessage .img').value.length == 0 + && !document.querySelector('#newmessage input[type="file"]')) { + document.querySelector('#newmessage').insertAdjacentHTML('afterbegin', `<p class="dialogtxt">${i18n('postForm.pleaseInputMessageText')}</p>`); + evt.preventDefault(); + } +} + +function showCommentForm(mid, rid) { + let reply = document.getElementById(rid); + let formTarget = reply.querySelector('div.msg-cont .msg-comment-target'); + if (formTarget) { + let formHtml = ` + <form action="/comment" method="POST" enctype="multipart/form-data"> + <input type="hidden" name="mid" value="${mid}"> + <input type="hidden" name="rid" value="${rid}"> + <div class="msg-comment"> + <div class="ta-wrapper"> + <textarea name="body" rows="1" class="reply" placeholder="${i18n('comment.writeComment')}"></textarea> + <div class="attach-photo">${evilIcon('ei-camera')}</div> + </div> + <input type="submit" value="OK"> + </div> + </form>`; + formTarget.insertAdjacentHTML('afterend', formHtml); + formTarget.remove(); + + let form = reply.querySelector('form'); + let submitButton = form.querySelector('input[type="submit"]'); + + let attachButton = form.querySelector('.msg-comment .attach-photo'); + attachButton.addEventListener('click', e => attachCommentPhoto(e.target)); + + let textarea = form.querySelector('.msg-comment textarea'); + textarea.addEventListener('keypress', e => postformListener(e.target, e)); + autosize(textarea); + + let validateMessage = () => { + let len = textarea.value.length; + if (len > 4096) { return 'Message is too long'; } + return ''; + }; + form.addEventListener('submit', e => { + let validationResult = validateMessage(); + if (validationResult) { + e.preventDefault(); + alert(validationResult); + return false; + } + submitButton.disabled = true; + }); + } + reply.querySelector('.msg-comment textarea').focus(); +} + +function attachInput() { + let inp = document.createElement('input'); + inp.setAttribute('type', 'file'); + inp.setAttribute('name', 'attach'); + inp.setAttribute('accept', 'image/jpeg,image/png'); + inp.style.visibility = 'hidden'; + return inp; +} + +function attachCommentPhoto(div) { + let input = div.querySelector('input'); + if (input) { + input.remove(); + div.classList.remove('attach-photo-active'); + } else { + let newInput = attachInput(); + newInput.addEventListener('change', function () { + div.classList.add('attach-photo-active'); + }); + newInput.click(); + div.appendChild(newInput); + } +} + +function attachMessagePhoto(div) { + var f = div.closest('form'), + finput = f.querySelector('input[type="file"]'); + if (!finput) { + var inp = attachInput(); + inp.style.float = 'left'; + inp.style.width = 0; + inp.style.height = 0; + inp.addEventListener('change', function () { + div.textContent = i18n('postForm.upload') + ' (✓)'; + }); + f.appendChild(inp); + inp.click(); + } else { + finput.remove(); + div.textContent = i18n('postForm.upload'); + } +} + +function showMessageLinksDialog(mid, rid) { + let hlink = window.location.protocol + '//juick.com/' + mid; + let mlink = '#' + mid; + if (rid > 0) { + hlink += '#' + rid; + mlink += '/' + rid; + } + let hlinkenc = encodeURIComponent(hlink); + let html = ` + <div class="dialogshare"> + ${i18n('shareDialog.linkToMessage')}: <div onclick="this.selectText()" class="dialogl">${hlink}</div> + ${i18n('shareDialog.messageNumber')}: <div onclick="this.selectText()" class="dialogl">${mlink}</div> + ${i18n('shareDialog.share')}: + <ul> + <li><a href="https://www.facebook.com/sharer/sharer.php?u=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-facebook')}</a></li> + <li><a href="https://twitter.com/intent/tweet?url=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-twitter')}</a></li> + <li><a href="https://vk.com/share.php?url=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-vk')}</a></li> + </ul> + </div>`; + + openDialog(html); +} + +function showPhotoDialog(fname) { + let width = window.innerWidth; + let height = window.innerHeight; + let minDimension = (width < height) ? width : height; + if (minDimension < 640) { + return true; // no dialog, open the link + } else if (minDimension < 1280) { + openDialog(`<a href="//i.juick.com/p/${fname}"><img src="//i.juick.com/photos-1024/${fname}"/></a>`, true); + return false; + } else { + openDialog(`<a href="//i.juick.com/p/${fname}"><img src="//i.juick.com/p/${fname}"/></a>`, true); + return false; + } +} + +function openPostDialog() { + let newmessageTemplate = ` + <form id="newmessage" action="/post" method="post" enctype="multipart/form-data"> + <textarea name="body" placeholder="${i18n('postForm.newMessage')}"></textarea> + <div> + <input class="img" name="img" placeholder="${i18n('postForm.imageLink')} (${i18n('postForm.imageFormats')})"/> + ${i18n('postForm.or')} <a href="#">${i18n('postForm.upload')}</a><br/> + <input id="tags_input" class="tags" name="tags" placeholder="${i18n('postForm.tags')}"/><br/> + <input type="submit" class="subm" value="${i18n('postForm.submit')}"/> + </div> + </form> + `; + return openDialog(newmessageTemplate); +} + +function openDialog(html, image) { + var dialogHtml = ` + <div id="dialogt"> + <div id="dialogb"></div> + <div id="dialogw"> + <div id="dialog_header"> + <div id="dialogc">${evilIcon('ei-close')}</div> + </div> + ${html} + </div> + </div>`; + let body = document.querySelector('body'); + body.classList.add('dialog-opened'); + body.insertAdjacentHTML('afterbegin', dialogHtml); + if (image) { + let header = document.querySelector('#dialog_header'); + header.classList.add('header_image'); + } + document.addEventListener('keydown', closeDialogListener); + document.querySelector('#dialogb').addEventListener('click', closeDialog); + document.querySelector('#dialogc').addEventListener('click', closeDialog); +} + +function closeDialog() { + let draft = document.querySelector('#newmessage textarea'); + if (draft) { + window.draft = draft.value; + } + document.querySelector('body').classList.remove('dialog-opened'); + document.querySelector('#dialogb').remove(); + document.querySelector('#dialogt').remove(); +} + +function openSocialWindow(a) { + var w = window.open(a.href, 'juickshare', 'width=640,height=400'); + if (window.focus) { w.focus(); } + return false; +} + +function checkUsername() { + var uname = document.querySelector('#username').textContent, + style = document.querySelector('#username').style; + fetch('//api.juick.com/users?uname=' + uname) + .then(function () { + style.background = '#FFCCCC'; + }) + .catch(function () { + style.background = '#CCFFCC'; + }); +} + +/******************************************************************************/ + +function openDialogLogin() { + let html = ` + <div class="dialoglogin"> + <p>${i18n('loginDialog.pleaseIntroduceYourself')}:</p> + <a href="/_fblogin" id="signfb">${evilIcon('ei-sc-facebook')}${i18n('loginDialog.facebook')}</a> + <a href="/_vklogin" id="signvk">${evilIcon('ei-sc-vk')}${i18n('loginDialog.vk')}</a> + <p>${i18n('loginDialog.registeredAlready')}</p> + <form action="/login" method="POST"> + <input class="signinput" type="text" name="username" placeholder="${i18n('loginDialog.username')}"/><br/> + <input class="signinput" type="password" name="password" placeholder="${i18n('loginDialog.password')}"/><br/> + <input class="signsubmit" type="submit" value="OK"/> + </form> + </div>`; + openDialog(html); + return false; +} + +/******************************************************************************/ + +function resultMessage(str) { + var result = document.createElement('p'); + result.textContent = str; + return result; +} + +function likeMessage(e, mid) { + if (confirm(i18n('message.likeThisMessage?'))) { + fetch('//juick.com/like?mid=' + mid, { + method: 'POST', + credentials: 'same-origin' + }) + .then(function (response) { + if (response.ok) { + e.closest('article').appendChild(resultMessage('OK!')); + } + }) + .catch(function () { + e.closest('article').appendChild(resultMessage(i18n('error.error'))); + }); + } + return false; +} + +/******************************************************************************/ + +function setPopular(e, mid, popular) { + fetch('//api.juick.com/messages/set_popular?mid=' + mid + + '&popular=' + popular + + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(function () { + e.closest('article').append(resultMessage('OK!')); + }); + return false; +} + +function setPrivacy(e, mid) { + fetch('//api.juick.com/messages/set_privacy?mid=' + mid + + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(function () { + e.closest('article').append(resultMessage('OK!')); + }); + return false; +} + +function getTags() { + fetch('//api.juick.com/tags?hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(response => { + return response.json(); + }) + .then(json => { + let tags = json.map(t => t.tag); + let input = document.getElementById('tags_input'); + new Awesomplete(input, {list : tags}); + }); + return false; +} + +function addTag(tag) { + document.forms['postmsg'].body.value='*'+tag+' '+document.forms['postmsg'].body.value; + return false; +} + +/******************************************************************************/ + +function ready(fn) { + if (document.readyState != 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } +} + +ready(function () { + document.querySelectorAll('textarea').forEach((ta) => { + autosize(ta); + }); + + var insertPMButtons = function (e) { + e.target.classList.add('narrowpm'); + e.target.parentNode.insertAdjacentHTML('afterend', '<input type="submit" value="OK"/>'); + e.target.removeEventListener('click', insertPMButtons); + e.preventDefault(); + }; + document.querySelectorAll('textarea.replypm').forEach(function (e) { + e.addEventListener('click', insertPMButtons); + e.addEventListener('keypress', function (e) { + postformListener(e.target, e); + }); + }); + document.querySelectorAll('#postmsg textarea').forEach(function (e) { + e.addEventListener('keypress', function (e) { + postformListener(e.target, e); + }); + }); + + var content = document.getElementById('content'); + if (content) { + var pageMID = content.getAttribute('data-mid'); + if (pageMID > 0) { + document.querySelectorAll('li.msg').forEach(li => { + let showReplyFormBtn = li.querySelector('.a-thread-comment'); + if (showReplyFormBtn) { + showReplyFormBtn.addEventListener('click', function (e) { + showCommentForm(pageMID, li.id); + e.preventDefault(); + }); + } + }); + let opMessage = document.querySelector('.msgthread'); + if (opMessage) { + let replyTextarea = opMessage.querySelector('textarea.reply'); + if (replyTextarea) { + replyTextarea.addEventListener('focus', e => showCommentForm(pageMID, 0)); + replyTextarea.addEventListener('keypress', e => postformListener(e.target, e)); + if (!window.location.hash) { + replyTextarea.focus(); + } + } + } + } + } + + var postmsg = document.getElementById('postmsg'); + if (postmsg) { + document.querySelectorAll('a').filter(t => t.href.indexOf('?') >=0).forEach(t => { + t.addEventListener('click', e => { + let params = new URLSearchParams(t.href.slice(t.href.indexOf('?') + 1)); + if (params.has('tag')) { + addTag(params.get('tag')); + e.preventDefault(); + } + }); + }); + } + + document.querySelectorAll('.msg-menu').forEach(function (el) { + el.addEventListener('click', function (e) { + var reply = e.target.closest('li'); + var rid = reply ? parseInt(reply.id) : 0; + var message = e.target.closest('section'); + var mid = message.getAttribute('data-mid') || e.target.closest('article').getAttribute('data-mid'); + showMessageLinksDialog(mid, rid); + e.preventDefault(); + }); + }); + document.querySelectorAll('.l .a-privacy').forEach(function (e) { + e.addEventListener('click', function (e) { + setPrivacy( + e.target, + e.target.closest('article').getAttribute('data-mid')); + e.preventDefault(); + }); + }); + document.querySelectorAll('.ir a[data-fname], .msg-media a[data-fname]').forEach(function (el) { + el.addEventListener('click', function (e) { + let fname = e.target.closest('[data-fname]').getAttribute('data-fname'); + if (!showPhotoDialog(fname)) { + e.preventDefault(); + } + }); + }); + document.querySelectorAll('.social a').forEach(function (e) { + e.addEventListener('click', function (e) { + openSocialWindow(e.target); + e.preventDefault(); + }); + }); + var username = document.getElementById('username'); + if (username) { + username.addEventListener('blur', function () { + checkUsername(); + }); + } + + document.querySelectorAll('.l .a-like').forEach(function (e) { + e.addEventListener('click', function (e) { + likeMessage( + e.target, + e.target.closest('article').getAttribute('data-mid')); + e.preventDefault(); + }); + }); + document.querySelectorAll('.a-login').forEach(function (el) { + el.addEventListener('click', function (e) { + openDialogLogin(); + e.preventDefault(); + }); + }); + var unfoldall = document.getElementById('unfoldall'); + if (unfoldall) { + unfoldall.addEventListener('click', function (e) { + document.querySelectorAll('#replies>li').forEach(function (e) { + e.style.display = 'block'; + }); + document.querySelectorAll('#replies .msg-comments').forEach(function (e) { + e.style.display = 'none'; + }); + e.preventDefault(); + }); + } + document.querySelectorAll('article').forEach(function (article) { + if (Array.prototype.some.call( + article.querySelectorAll('.msg-tags a'), + function (a) { + return a.textContent === 'NSFW'; + } + )) { + article.classList.add('nsfw'); + } + }); + initWS(); + + window.addEventListener('pagehide', wsShutdown); + + killy.embedAll(); +}); |