Файл: public/assets/js/main.js
Строк: 1542
<?php
import * as bootstrap from 'bootstrap'
import { __ } from './translate.js'
import './globals.js'
import './tiptap-editor.js'
import './prettify.js'
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
function getNavbarHeight() {
let max = 0
document.querySelectorAll('.app-header, .app-topnav').forEach(el => {
max = Math.max(max, el.getBoundingClientRect().bottom)
})
if (!max) {
document.querySelectorAll('body > *, body > * > *').forEach(el => {
if (window.getComputedStyle(el).position === 'fixed') {
const rect = el.getBoundingClientRect()
if (rect.top >= 0 && rect.top < 5 && rect.bottom > 0
&& rect.bottom < window.innerHeight * 0.5
&& rect.width > window.innerWidth * 0.5) {
max = Math.max(max, rect.bottom)
}
}
})
}
return max
}
function initShortView(container = document) {
container.querySelectorAll('.section-content.short-view:not(.clamped):not(.expanded)').forEach(function (el) {
const hiddenPixels = el.scrollHeight - el.clientHeight
if (hiddenPixels > 100) {
el.classList.add('clamped')
const btn = document.createElement('button')
btn.type = 'button'
btn.className = 'btn btn-sm btn-adaptive mt-2'
btn.textContent = 'Показать полностью'
btn.addEventListener('click', function () {
el.classList.add('expanded')
el.classList.remove('clamped')
btn.remove()
})
el.after(btn)
} else if (hiddenPixels > 0) {
el.classList.remove('short-view')
}
})
}
function ajax({ url, type = 'GET', data = null, dataType = 'json', beforeSend, complete, success, error }) {
if (beforeSend) beforeSend()
const options = {
method: type.toUpperCase(),
headers: {
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest',
}
}
if (data) {
if (data instanceof FormData) {
options.body = data
} else {
options.headers['Content-Type'] = 'application/x-www-form-urlencoded'
options.body = new URLSearchParams(data)
}
}
fetch(url, options)
.then(res => dataType === 'json' ? res.json() : res.text())
.then(responseData => { if (success) success(responseData) })
.catch(err => { if (error) error(null, err.message, err) })
.finally(() => { if (complete) complete() })
}
function applyMask(el, mask) {
el.addEventListener('input', function () {
const digits = el.value.replace(/D/g, '')
let result = ''
let di = 0
for (let i = 0; i < mask.length && di < digits.length; i++) {
result += mask[i] === '0' ? digits[di++] : mask[i]
}
el.value = result
})
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el))
const popovers = document.querySelectorAll('[data-bs-toggle="popover"]')
popovers.forEach(el => new bootstrap.Popover(el))
document.body.addEventListener('click', function (e) {
if (!e.target.closest('[data-bs-toggle="popover"]') && !e.target.closest('.popover')) {
popovers.forEach(el => bootstrap.Popover.getInstance(el)?.hide())
}
})
const colorpicker = document.querySelector('.colorpicker')
const colorpickerAddon = document.querySelector('.colorpicker-addon')
if (colorpicker && colorpickerAddon) {
colorpicker.addEventListener('input', () => colorpickerAddon.value = colorpicker.value)
colorpickerAddon.addEventListener('input', () => colorpicker.value = colorpickerAddon.value)
}
document.querySelectorAll('.phone').forEach(el => applyMask(el, '+0 000 000-00-00-00'))
document.querySelectorAll('.birthday').forEach(el => applyMask(el, '00.00.0000'))
const scrollupBtn = document.querySelector('.scrollup')
if (scrollupBtn) {
window.addEventListener('scroll', function () {
const visible = window.scrollY > 200
scrollupBtn.style.opacity = visible ? '1' : '0'
scrollupBtn.style.pointerEvents = visible ? 'auto' : 'none'
})
scrollupBtn.addEventListener('click', function () {
window.scrollTo({ top: 0, behavior: 'smooth' })
return false
})
}
document.querySelector('.js-messages-block')?.addEventListener('show.bs.dropdown', function () {
getNewMessages()
})
function setTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme)
const icon = theme === 'dark' ? 'fa-moon' : 'fa-sun'
const themeIcon = document.getElementById('theme-icon-active')
if (themeIcon) themeIcon.className = `fa-regular ${icon} fa-lg`
ajax({ type: 'POST', url: '/ajax/set-theme', data: { theme } })
}
document.querySelectorAll('[data-bs-theme-value]').forEach(el => {
el.addEventListener('click', () => setTheme(el.dataset.bsThemeValue))
})
if (window.location.hash) {
const initialHash = window.location.hash
if (initialHash === '#comments') {
history.replaceState(null, '', location.pathname + location.search)
}
setTimeout(function () {
const target = document.querySelector(initialHash)
if (target) {
const navbarHeight = getNavbarHeight()
window.scrollTo({ top: target.getBoundingClientRect().top + window.scrollY - navbarHeight, behavior: 'instant' })
}
}, 100)
} else if (new URLSearchParams(location.search).has('page')) {
const commentsEl = document.querySelector('#comments')
if (commentsEl) {
setTimeout(function () {
const navbarHeight = getNavbarHeight()
window.scrollTo({ top: commentsEl.getBoundingClientRect().top + window.scrollY - navbarHeight, behavior: 'instant' })
}, 100)
}
}
setTimeout(initShortView, 300)
prettyPrint()
tags.init('.input-tag', {
allowNew: true,
server: '/blogs/tags-search',
liveServer: true,
clearEnd: true,
allowClear: true,
suggestionsThreshold: 2,
max: 10,
separator: [','],
addOnBlur: true,
})
fancybox.bind('[data-fancybox]:not(.fancybox-exclude)', {})
let hoveredCarousel = null
document.querySelectorAll('.f-carousel').forEach(el => {
const carousel = fancyCarousel(el, { infinite: true, adaptiveHeight: true }, fancyCarouselPlugins)
carousel.init()
el.addEventListener('mouseenter', () => hoveredCarousel = carousel)
el.addEventListener('mouseleave', () => { if (hoveredCarousel === carousel) hoveredCarousel = null })
})
document.addEventListener('keydown', e => {
if (!hoveredCarousel) return
if (e.key === 'ArrowLeft') {
e.preventDefault()
hoveredCarousel.prev()
} else if (e.key === 'ArrowRight') {
e.preventDefault()
hoveredCarousel.next()
}
})
document.querySelectorAll('.slide-thumb-link[data-type="html5video"]').forEach(link => {
const video = link.querySelector('video')
if (!video) return
const setThumb = () => {
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth || 160
canvas.height = video.videoHeight || 90
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height)
link.dataset.thumb = canvas.toDataURL('image/jpeg', 0.8)
}
video.readyState >= 2 ? setThumb() : video.addEventListener('loadeddata', setThumb, { once: true })
})
})
/* Показ формы загрузки файла */
window.showAttachForm = function () {
const btn = document.querySelector('.js-attach-button')
const form = document.querySelector('.js-attach-form')
if (btn) btn.style.display = 'none'
if (form) form.style.display = 'block'
return false
}
/* Переход к форме ввода */
window.postJump = function () {
const form = document.querySelector('.section-form')
if (form) {
const navbarHeight = getNavbarHeight()
window.scrollTo({ top: form.getBoundingClientRect().top + window.scrollY - navbarHeight, behavior: 'smooth' })
}
}
/* Сворачивание/разворачивание */
window.toggleComment = function (id) {
const body = document.getElementById('comment-body-' + id)
const expandLabel = document.getElementById('comment-expand-' + id)
const ctrl = document.getElementById('comment-ctrl-' + id)
if (!body) return
const isHidden = body.classList.toggle('d-none')
if (expandLabel) expandLabel.classList.toggle('d-none', !isHidden)
if (ctrl) {
const icon = ctrl.querySelector('i')
if (icon) icon.className = isHidden ? 'fa fa-plus' : 'fa fa-minus'
const line = ctrl.querySelector('.comment-thread-line')
if (line) line.classList.toggle('d-none', isHidden)
}
}
/* Открыть форму ответа под комментарием */
window.openReplyForm = function (id, callback) {
document.querySelectorAll('.reply-form').forEach(function (f) {
f.classList.add('d-none')
})
const form = document.getElementById('reply-form-' + id)
if (!form) return false
form.classList.remove('d-none')
const textarea = form.querySelector('textarea')
const editorId = textarea?.id
if (textarea && editorId && !window._tiptapEditors?.[editorId]) {
textarea.classList.add('tiptap')
import('./tiptap.js').then(({ initEditors }) => {
initEditors([textarea])
const ed = window._tiptapEditors?.[editorId]
ed?.commands.focus()
callback?.(ed)
})
} else {
const ed = window._tiptapEditors?.[editorId]
ed?.commands.focus()
callback?.(ed)
}
return false
}
/* Закрыть форму ответа */
window.closeReplyForm = function (id) {
document.getElementById('reply-form-' + id)?.classList.add('d-none')
}
/* Тогл панели форматирования в форме ответа */
window.toggleReplyToolbar = function (btn) {
btn.closest('.reply-form').classList.toggle('toolbar-visible')
}
/* AJAX отправка формы ответа на комментарий */
document.addEventListener('submit', function (e) {
const form = e.target.closest('.reply-form form')
if (!form) return
e.preventDefault()
const errorEl = form.querySelector('.reply-error')
if (errorEl) errorEl.textContent = ''
const submitBtn = form.querySelector('button[type="submit"], button:not([type="button"])')
if (submitBtn) submitBtn.disabled = true
fetch(form.action, {
method: 'POST',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
body: new FormData(form),
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d } }) })
.then(function ({ ok, data }) {
if (ok) {
const hash = data.redirect.includes('#') ? data.redirect.split('#')[1] : ''
window.location.hash = hash
window.location.reload()
} else {
const msg = Object.values(data.errors || {}).flat().join(', ')
if (errorEl) errorEl.textContent = msg
if (submitBtn) submitBtn.disabled = false
}
})
.catch(function () {
if (submitBtn) submitBtn.disabled = false
})
})
/* Схлопывание/разворачивание ветки комментариев */
document.addEventListener('click', function (e) {
const btn = e.target.closest('.comment-collapse-btn')
if (!btn) return
const id = btn.dataset.id
const children = document.getElementById('comment-children-' + id)
if (!children) return
const icon = btn.querySelector('i')
const collapsed = children.classList.toggle('d-none')
icon.className = collapsed ? 'fa fa-plus text-muted' : 'fa fa-minus text-muted'
})
/* Переключение языка (ajax, без перезагрузки на /language) */
document.addEventListener('click', function (e) {
const el = e.target.closest('[data-lang]')
if (!el) return
e.preventDefault()
ajax({
url: '/language/' + el.dataset.lang,
type: 'POST',
success: () => location.reload(),
})
})
/* Ответ на сообщение (для форумов/стен без вложенных комментариев) */
window.postReply = function (el) {
postJump()
const authorEl = el.closest('.section')?.querySelector('.section-author')
const author = authorEl?.dataset.login || authorEl?.textContent.trim()
if (!author) return false
const editor = window._tiptapActiveEditor
if (!editor) return false
if (authorEl.matches('a')) {
editor.chain().focus('end', { scrollIntoView: false }).insertContent([
{ type: 'mention', attrs: { id: author, label: author } },
{ type: 'text', text: ' ' },
]).run()
} else {
editor.chain().focus('end', { scrollIntoView: false }).insertContent({ type: 'text', text: author + ', ' }).run()
}
return false
}
/* Цитирование сообщения */
function doInsertQuote (editor, authorEl, author, date, message) {
if (!message) {
if (author) {
editor.chain().focus('end', { scrollIntoView: false }).insertContent([
{ type: 'mention', attrs: { id: author, label: author } },
{ type: 'text', text: ' ' },
]).run()
}
return
}
const quoteContent = [
{
type: 'blockquote',
attrs: { author: author ? (authorEl.matches('a') ? '@' : '') + author + (date ? ' ' + date : '') : (date || null) },
content: [{ type: 'paragraph', content: [{ type: 'text', text: message }] }],
},
{ type: 'paragraph' },
]
if (editor.isEmpty) {
editor.chain().focus('end', { scrollIntoView: false }).setContent({ type: 'doc', content: quoteContent }).run()
} else {
const doc = editor.state.doc
const lastChild = doc.lastChild
const insertPos = (lastChild && lastChild.type.name === 'paragraph' && lastChild.childCount === 0)
? doc.content.size - lastChild.nodeSize
: doc.content.size
editor.chain().focus('end', { scrollIntoView: false }).insertContentAt(insertPos, quoteContent).run()
}
}
window.postQuote = function (el) {
const commentItem = el.closest('.comment-item')
if (commentItem) {
const id = commentItem.dataset.id
const rcRight = el.closest('.comment-right')
const authorEl = rcRight?.querySelector('.section-author')
const author = authorEl?.dataset.login || authorEl?.textContent.trim() || null
const dateEl = rcRight?.querySelector('.section-date')
const date = (dateEl?.dataset.date || dateEl?.textContent || '').trim()
const clone = rcRight?.querySelector('.section-message')?.cloneNode(true)
clone?.querySelectorAll('blockquote').forEach(bq => bq.remove())
const message = clone?.textContent.trim() || ''
openReplyForm(id, function (editor) {
if (editor) doInsertQuote(editor, authorEl, author, date, message)
})
return false
}
postJump()
const post = el.closest('.section')
const authorEl = post?.querySelector('.section-author')
const author = authorEl?.dataset.login || authorEl?.textContent.trim() || null
const dateEl = post?.querySelector('.section-date')
const date = (dateEl?.dataset.date || dateEl?.textContent || '').trim()
const clone = post?.querySelector('.section-message')?.cloneNode(true)
const editor = window._tiptapActiveEditor
if (!editor) return false
clone?.querySelectorAll('blockquote').forEach(bq => bq.remove())
const message = clone?.textContent.trim() || ''
doInsertQuote(editor, authorEl, author, date, message)
return false
}
/* Подтверждение действия */
const confirmedElements = new WeakSet()
window.confirmAction = function (el) {
const message = el.dataset.confirm || 'Вы уверены?'
if (confirmedElements.has(el)) {
confirmedElements.delete(el)
return true
}
confirm(message, function (result) {
if (!result) return
const form = el.matches('form') ? el : el.closest('form')
if (form) {
confirmedElements.add(form)
form.submit()
} else {
const href = el.getAttribute('href')
if (href) window.location.href = href
}
})
return false
}
/* Отправка жалобы на спам */
window.sendComplaint = function (el) {
confirm(__('confirm_complain_submit'), function (result) {
if (!result) return
ajax({
data: { id: el.dataset.id, type: el.dataset.type, page: el.dataset.page },
dataType: 'json', type: 'post', url: '/ajax/complaint',
success: function (data) {
if (!data.success) { notyf.error(data.message); return }
el.classList.add('text-muted', 'pe-none')
el.querySelector('i').className = 'fa fa-check'
notyf.success(__('complain_submitted'))
}
})
})
return false
}
/* Добавление или удаление закладок */
window.bookmark = function (el) {
ajax({
data: { tid: el.dataset.tid },
dataType: 'json', type: 'post', url: '/forums/bookmarks/perform',
success: function (data) {
if (!data.success) { notyf.error(data.message); return }
notyf.success(data.message)
el.textContent = data.type === 'added' ? el.dataset.from : el.dataset.to
}
})
return false
}
/* Удаление записей */
window.deletePost = function (el) {
confirm(__('confirm_message_delete'), function (result) {
if (!result) return
ajax({
url: el.getAttribute('href'), type: 'delete', dataType: 'json',
success: function (data) {
if (data.success) {
notyf.success(data.message)
el.closest('.section').style.display = 'none'
} else {
notyf.error(data.message)
}
}
})
})
return false
}
/* Редактирование комментария в модальном окне */
window.openEditModal = function (el) {
const id = el.dataset.id
const baseUrl = el.dataset.url
const modalEl = document.getElementById('editCommentModal')
document.getElementById('edit-comment-id').value = id
modalEl.dataset.editUrl = baseUrl + '/' + id
modalEl.querySelector('input[type="file"]')?.setAttribute('data-id', id)
const msgEl = document.getElementById('edit-comment-msg')
if (msgEl) msgEl.dataset.relateId = id
const filesContainer = modalEl.querySelector('.js-files')
if (filesContainer) filesContainer.innerHTML = ''
const dataPromise = fetch(baseUrl + '/' + id, {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': csrfToken }
}).then(r => r.json()).then(data => {
const scope = modalEl.querySelector('form')
data.files?.forEach(file => {
const templateEl = scope?.querySelector(file.isImage ? '.js-image-template' : '.js-file-template')
const template = templateEl?.cloneNode(true)
if (!template) return
if (file.isImage) {
template.querySelector('img')?.setAttribute('src', file.path)
} else {
const link = template.querySelector('.js-file-link')
if (link) { link.href = file.path; link.textContent = file.name }
const sizeEl = template.querySelector('.js-file-size')
if (sizeEl) sizeEl.textContent = file.size
}
template.querySelector('.js-file-delete')?.setAttribute('data-id', file.id)
filesContainer?.insertAdjacentHTML('beforeend', template.innerHTML)
})
return data.text || ''
})
const onShown = async () => {
modalEl.removeEventListener('shown.bs.modal', onShown)
const text = await dataPromise
if (!window._tiptapEditors?.['edit-comment-msg'] && msgEl) {
msgEl.classList.add('tiptap')
const { initEditors } = await import('./tiptap.js')
initEditors([msgEl])
await new Promise(resolve => requestAnimationFrame(resolve))
}
const editor = window._tiptapEditors?.['edit-comment-msg']
if (editor) {
editor.commands.setContent(text, true)
editor.resetChanged()
}
}
modalEl.addEventListener('shown.bs.modal', onShown)
bootstrap.Modal.getOrCreateInstance(modalEl).show()
return false
}
document.getElementById('editCommentModal')?.addEventListener('hide.bs.modal', function (e) {
const editor = window._tiptapEditors?.['edit-comment-msg']
if (editor?.getIsChanged()) {
e.preventDefault()
if (window.confirm(__('confirm_discard_changes'))) {
editor.resetChanged()
bootstrap.Modal.getInstance(this)?.hide()
}
}
})
document.getElementById('editCommentForm')?.addEventListener('submit', function (e) {
e.preventDefault()
const modalEl = document.getElementById('editCommentModal')
const id = document.getElementById('edit-comment-id').value
const msg = document.getElementById('edit-comment-msg').value
const url = modalEl?.dataset.editUrl
ajax({
data: { msg },
dataType: 'json', type: 'patch', url,
success: function (data) {
if (data.success) {
bootstrap.Modal.getInstance(modalEl)?.hide()
window.location.hash = '#comment_' + id
window.location.reload()
} else {
notyf.error(data.message)
}
}
})
})
/* Удаление комментариев */
window.deleteComment = function (el) {
const item = el.closest('.comment-item, .section')
confirm(__('confirm_message_delete'), function (result) {
if (!result) return
ajax({
dataType: 'json', type: 'delete', url: '/comments/' + el.dataset.id,
success: function (data) {
if (data.success) {
notyf.success(__('message_deleted'))
if (item) {
const content = item.querySelector('.comment-content')
if (content) content.innerHTML = '<div class="comment-removed text-muted fst-italic small mb-2">' + __('comment_removed') + '</div>'
}
} else {
notyf.error(data.message)
}
}
})
})
return false
}
/* Изменение рейтинга */
window.changeRating = function (el) {
ajax({
data: { id: el.dataset.id, type: el.dataset.type, vote: el.dataset.vote },
dataType: 'json', type: 'post', url: '/ajax/rating',
success: function (data) {
if (data.success) {
const ratingBlock = el.closest('.js-rating')
ratingBlock?.querySelectorAll('a').forEach(a => a.classList.remove('active'))
if (!data.cancel) el.classList.add('active')
const rating = ratingBlock?.querySelector('.rating-value')
if (rating) rating.innerHTML = data.rating
} else if (data.message) {
notyf.error(data.message)
}
}
})
return false
}
/* Удаляет запись из истории рейтинга */
window.deleteRating = function (el) {
confirm(__('confirm_message_delete'), function (result) {
if (!result) return
ajax({
data: { id: el.dataset.id },
dataType: 'json', type: 'post', url: '/ratings/delete',
success: (data) => {
if (data.success) {
notyf.success(__('record_deleted'))
el.closest('.section').style.display = 'none'
} else {
notyf.error(data.message)
}
}
})
})
return false
}
/* Удаляет запись из списка жалоб */
window.deleteSpam = function (el) {
ajax({
data: { id: el.dataset.id },
dataType: 'json', type: 'post', url: '/admin/spam/delete',
success: function (data) {
if (data.success) {
notyf.success(__('record_deleted'))
el.closest('.section').style.display = 'none'
} else {
notyf.error(data.message)
}
}
})
return false
}
/* Удаляет запись со стены сообщений */
window.deleteWall = function (el) {
confirm(__('confirm_message_delete'), function (result) {
if (!result) return
ajax({
data: { id: el.dataset.id, login: el.dataset.login },
dataType: 'json', type: 'post', url: '/walls/' + el.dataset.login + '/delete',
success: function (data) {
if (data.success) {
notyf.success(__('record_deleted'))
el.closest('.section').style.display = 'none'
} else {
notyf.error(data.message)
}
}
})
})
return false
}
/* Копирует текст в буфер обмена */
window.copyToClipboard = function (el) {
const container = el.closest('.input-group') ?? el.parentElement
const field = container?.querySelector('input, textarea')
const text = el.dataset.copy ?? field?.value ?? ''
const fallback = () => {
if (field) { field.select(); document.execCommand('copy'); return }
if (!text) return
const ta = document.createElement('textarea')
ta.value = text
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
try { document.execCommand('copy') } catch (e) {}
document.body.removeChild(ta)
}
if (navigator.clipboard?.writeText && text) {
navigator.clipboard.writeText(text).catch(fallback)
} else {
fallback()
}
// Галочка на иконке триггера
const icon = el.querySelector('i')
if (icon && !icon.dataset.copyReset) {
const prev = icon.className
icon.dataset.copyReset = '1'
icon.className = 'fas fa-check'
setTimeout(() => {
icon.className = prev
delete icon.dataset.copyReset
}, 1500)
}
// Подсказка «скопировано»: на .input-group-text либо на самом триггере
const tooltipEl = container?.querySelector('.input-group-text')
?? (el.matches('[data-bs-toggle="tooltip"]') ? el : null)
if (tooltipEl) {
const original = tooltipEl.getAttribute('data-bs-original-title') ?? tooltipEl.getAttribute('title')
const tip = bootstrap.Tooltip.getOrCreateInstance(tooltipEl)
tooltipEl.setAttribute('data-bs-original-title', __('copied'))
tip.update()
tip.show()
if (original !== null) {
setTimeout(() => {
tooltipEl.setAttribute('data-bs-original-title', original)
tip.update()
}, 1500)
}
}
return false
}
/* Загрузка файла */
window.submitFile = function (el) {
const form = new FormData()
form.append('file', el.files[0])
form.append('id', el.dataset.id)
form.append('type', el.dataset.type)
const scope = el.closest('form') ?? document
const filesContainer = scope.querySelector('.js-files')
ajax({
data: form, type: 'post', dataType: 'json', url: '/ajax/file/upload',
beforeSend: () => filesContainer?.insertAdjacentHTML('beforeend', '<i class="fas fa-spinner fa-spin fa-3x mx-3"></i>'),
complete: () => filesContainer?.querySelectorAll('.fa-spinner').forEach(s => s.remove()),
success: function (data) {
if (!data.success) { notyf.error(data.message); return }
const isMedia = data.type === 'image' || data.type === 'video'
const templateEl = scope.querySelector(isMedia ? '.js-image-template' : '.js-file-template')
const template = templateEl?.cloneNode(true)
if (data.type === 'image') {
template?.querySelector('img')?.setAttribute('src', data.path)
} else if (data.type === 'video') {
const img = template?.querySelector('img')
if (img) {
const wrap = img.parentElement
const video = document.createElement('video')
video.src = data.path
video.className = img.className
video.preload = 'metadata'
img.replaceWith(video)
wrap?.insertAdjacentHTML('beforeend', '<span class="slide-play-icon">▶</span>')
}
} else {
const link = template?.querySelector('.js-file-link')
if (link) { link.href = data.path; link.textContent = data.name }
const sizeEl = template?.querySelector('.js-file-size')
if (sizeEl) sizeEl.textContent = data.size
}
template?.querySelector('a')?.setAttribute('data-id', data.id)
if (template) filesContainer?.insertAdjacentHTML('beforeend', template.innerHTML)
},
error: (_, textStatus) => notyf.error('Ошибка загрузки файла: ' + textStatus)
})
el.value = ''
return false
}
/* Удаление медиафайла (изображения или видео) из редактора */
window.cutMedia = function (path) {
if (!path) return
const editor = window._tiptapActiveEditor
if (!editor) return
const normalize = (src) => { try { return new URL(src).pathname } catch { return src } }
const normalizedPath = normalize(path)
const { state, dispatch } = editor.view
const tr = state.tr
const positions = []
state.doc.descendants(function (node, pos) {
const src = node.attrs.src ?? node.attrs.href
if (['image', 'video'].includes(node.type.name) && normalize(src) === normalizedPath) {
positions.push({ pos, size: node.nodeSize })
}
})
positions.reverse().forEach(({ pos, size }) => tr.delete(pos, pos + size))
// noinspection JSUnresolvedReference
if (tr.docChanged) dispatch(tr)
}
/* Удаление файла */
window.deleteFile = function (el) {
confirm(__('confirm_file_delete'), function (result) {
if (!result) return
ajax({
url: '/ajax/file/delete', type: 'POST', dataType: 'json',
data: { id: el.dataset.id, type: el.dataset.type },
success: function (data) {
if (!data.success) { notyf.error(data.message); return }
if (data.path) cutMedia(data.path)
el.closest('.js-file').style.display = 'none'
},
error: (_, textStatus) => notyf.error('Ошибка удаления файла: ' + textStatus)
})
})
return false
}
/* Показывает форму для повторной отправки кода подтверждения */
window.resendingCode = function () {
const link = document.querySelector('.js-resending-link')
const form = document.querySelector('.js-resending-form')
if (link) link.style.display = 'none'
if (form) form.style.display = 'block'
return false
}
/* Показывает панель с запросами */
window.showQueries = function () {
const el = document.querySelector('.js-queries')
if (!el) return
el.style.display = getComputedStyle(el).display === 'none' ? '' : 'none'
}
/* Update message count */
window.updateMessageCount = function (newCount) {
const data = JSON.parse(localStorage.getItem('messageData') || '{}')
data.countMessages = parseInt(newCount) || 0
localStorage.setItem('messageData', JSON.stringify(data))
localStorage.setItem('messageCount', newCount)
window.dispatchEvent(new Event('storage'))
}
/* Get new messages */
window.getNewMessages = function () {
const notifyItem = document.querySelector('.js-messages-block .app-nav__item')
const badge = notifyItem?.querySelector('.badge')
const titleSpan = document.querySelector('.app-notification__title span')
const messagesList = document.querySelector('.js-messages-block .js-messages')
ajax({
dataType: 'json', type: 'GET', url: '/messages/new',
beforeSend: () => messagesList?.insertAdjacentHTML('beforeend', '<li class="js-message-spin text-center"><i class="fas fa-spinner fa-spin fa-2x my-2"></i></li>'),
complete: () => messagesList?.querySelectorAll('.js-message-spin').forEach(s => s.remove()),
success(data) {
if (!data?.success) {
badge?.remove()
if (titleSpan) titleSpan.textContent = 0
return
}
const count = data.countMessages
if (badge) {
badge.textContent = count
} else if (notifyItem) {
const newBadge = document.createElement('span')
newBadge.className = 'badge bg-notify'
newBadge.textContent = count
notifyItem.append(newBadge)
}
updateMessageCount(count)
if (titleSpan) titleSpan.textContent = count
if (messagesList) {
messagesList.innerHTML = ''
messagesList.insertAdjacentHTML('beforeend', data.dialogues)
}
}
})
return false
}
/* Инициализирует главное изображение слайдера */
window.initSlideMainImage = function (el) {
const mainHref = el.getAttribute('href')
const slider = el.closest('.media-file')
slider?.querySelectorAll('.slide-thumb-link').forEach(l => l.classList.remove('fancybox-exclude'))
slider?.querySelectorAll(`.slide-thumb-link[href="${mainHref}"]`).forEach(l => l.classList.add('fancybox-exclude'))
}
/* Инициализирует миниатюру слайдера */
window.initSlideThumbImage = function (el) {
const href = el.getAttribute('href')
const isVideo = el.dataset.type === 'html5video'
const fancyboxGroup = el.dataset.fancybox ?? ''
const slider = el.closest('.media-file')
const mainInner = slider?.querySelector('.slide-main-inner')
if (!mainInner) return false
if (el.querySelector('.slide-thumb-image, .slide-thumb-video')?.classList.contains('active')) return false
mainInner.querySelector('video')?.pause()
if (isVideo) {
mainInner.innerHTML = `<video src="${href}" class="img-fluid rounded" controls preload="metadata"></video>`
} else {
const alt = (el.querySelector('img')?.getAttribute('alt') ?? '').replace(/"/g, '"')
mainInner.innerHTML =
`<a href="${href}" class="slide-main-link" data-fancybox="${fancyboxGroup}" onclick="return initSlideMainImage(this)">` +
`<img src="${href}" alt="${alt}" class="img-fluid rounded slide-main-img">` +
`</a>`
}
slider?.querySelectorAll('.slide-thumb-image').forEach(img => img.classList.remove('active'))
slider?.querySelectorAll('.slide-thumb-video').forEach(v => v.classList.remove('active'))
const thumb = el.querySelector('.slide-thumb-image, .slide-thumb-video')
thumb?.classList.add('active')
return false
}
let checkTimeout
/* Проверка логина */
window.checkLogin = function (el) {
const block = el.closest('.mb-3')
const message = block?.querySelector('.invalid-feedback')
const login = el.value.trim()
if (login.length < 3) {
block?.classList.remove('is-valid', 'is-invalid')
if (message) message.textContent = ''
return
}
clearTimeout(checkTimeout)
checkTimeout = setTimeout(function () {
ajax({
url: '/check-login', type: 'POST', dataType: 'json',
data: { login },
success: (data) => {
block?.classList.toggle('is-valid', data.success)
block?.classList.toggle('is-invalid', !data.success)
if (message) message.textContent = data.success ? '' : data.message
},
error: () => {
block?.classList.remove('is-valid')
block?.classList.add('is-invalid')
}
})
}, 1000)
return false
}
const confirmDialogEl = document.createElement('dialog')
confirmDialogEl.className = 'confirm-dialog'
confirmDialogEl.innerHTML = `
<p class="confirm-message"></p>
<div class="confirm-footer">
<button type="button" class="btn btn-secondary btn-sm js-confirm-cancel"></button>
<button type="button" class="btn btn-primary btn-sm js-confirm-ok"></button>
</div>`
document.body.appendChild(confirmDialogEl)
function confirm(message, callback) {
confirmDialogEl.querySelector('.confirm-message').textContent = message
confirmDialogEl.querySelector('.js-confirm-ok').textContent = __('buttons.ok')
confirmDialogEl.querySelector('.js-confirm-cancel').textContent = __('buttons.cancel')
confirmDialogEl.querySelector('.js-confirm-ok').onclick = () => { confirmDialogEl.close(); callback(true) }
confirmDialogEl.querySelector('.js-confirm-cancel').onclick = () => { confirmDialogEl.close(); callback(false) }
confirmDialogEl.showModal()
}
// Кнопка "Загрузить ещё" для ленты
const feedContainer = document.getElementById('feed-container')
const feedSentinel = document.getElementById('feed-sentinel')
if (feedContainer && feedSentinel) {
let loading = false
const getNextUrl = () => {
const items = feedContainer.querySelectorAll('.feed-pagination')
return items[items.length - 1]?.dataset.next || ''
}
const loader = document.createElement('div')
loader.className = 'feed-loader d-none'
loader.innerHTML = '<span></span><span></span><span></span><span></span><span></span>'
feedSentinel.before(loader)
const createLoadMoreButton = () => {
const btn = document.createElement('button')
btn.type = 'button'
btn.className = 'btn btn-primary d-block mx-auto my-3'
btn.textContent = __('buttons.load_more')
btn.addEventListener('click', async () => {
const nextUrl = getNextUrl()
if (!nextUrl || loading) return
loading = true
btn.remove()
loader.classList.remove('d-none')
try {
const response = await fetch(nextUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
const html = await response.text()
const temp = document.createElement('div')
temp.innerHTML = html
if (temp.querySelector('.feed-pagination')?.dataset.empty === '1') return
feedContainer.append(...temp.children)
setTimeout(initShortView, 100)
if (getNextUrl()) loader.before(createLoadMoreButton())
} finally {
loading = false
loader.classList.add('d-none')
}
})
return btn
}
if (getNextUrl()) loader.before(createLoadMoreButton())
}
?>