64344bfef6
- PHP: submit_beep accepts empty content when any attachment flag is set - JS: composer button enables when text is empty but media is attached - JS: button refreshes on every media state change via custom event - Version bump + readme changelog
1202 lines
46 KiB
JavaScript
Executable File
1202 lines
46 KiB
JavaScript
Executable File
/* Beep — Twitter-style microblog frontend */
|
|
(function () {
|
|
'use strict';
|
|
|
|
var config = window.BeepConfig || {};
|
|
var limit = (config.charLimit || 280) | 0;
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
function init() {
|
|
var feeds = document.querySelectorAll('[data-beep-feed]');
|
|
Array.prototype.forEach.call(feeds, initFeed);
|
|
// Delegate on document so dynamically injected content (thread modal,
|
|
// GIF picker, toasts) is handled too.
|
|
document.addEventListener('click', onClick);
|
|
document.addEventListener('input', onInput);
|
|
document.addEventListener('keydown', onKeydown);
|
|
document.addEventListener('keydown', onEscape);
|
|
}
|
|
|
|
var feedState = { offset: 0, loading: false, hasMore: true, currentSort: 'latest' };
|
|
|
|
function initFeed(feed) {
|
|
var compose = feed.querySelector('[data-beep-compose]');
|
|
if (compose) initCompose(compose, feed);
|
|
initSortTabs(feed);
|
|
initMedia(compose, feed);
|
|
initInfiniteScroll(feed);
|
|
}
|
|
|
|
/* ---------- Infinite scroll ---------- */
|
|
function initInfiniteScroll(feed) {
|
|
var sentinel = document.createElement('div');
|
|
sentinel.className = 'beep-scroll-sentinel';
|
|
sentinel.setAttribute('data-beep-scroll-sentinel', '');
|
|
var list = feed.querySelector('[data-beep-list]');
|
|
if (list) list.appendChild(sentinel);
|
|
|
|
if (!('IntersectionObserver' in window)) return;
|
|
var observer = new IntersectionObserver(function(entries) {
|
|
if (entries[0].isIntersecting && !feedState.loading && feedState.hasMore) {
|
|
loadMore(feed);
|
|
}
|
|
}, { rootMargin: '400px' });
|
|
observer.observe(sentinel);
|
|
feed._beepScrollObserver = observer;
|
|
}
|
|
|
|
function loadMore(feed) {
|
|
feedState.loading = true;
|
|
var sentinel = feed.querySelector('[data-beep-scroll-sentinel]');
|
|
if (sentinel) sentinel.innerHTML = '<div class="beep-loading-more"><span class="beep-spinner"></span> Loading...</div>';
|
|
|
|
fetch(config.restUrl + 'feed?sort=' + encodeURIComponent(feedState.currentSort) + '&offset=' + feedState.offset, {
|
|
credentials: 'same-origin',
|
|
headers: { 'X-WP-Nonce': config.nonce }
|
|
})
|
|
.then(handleResponse)
|
|
.then(function(data) {
|
|
feedState.hasMore = !!data.has_more;
|
|
feedState.offset = data.offset || feedState.offset + 50;
|
|
if (sentinel) sentinel.innerHTML = '';
|
|
if (data.html) {
|
|
var list = feed.querySelector('[data-beep-list]');
|
|
if (list) {
|
|
var tmp = document.createElement('div');
|
|
tmp.innerHTML = data.html;
|
|
while (tmp.firstElementChild) {
|
|
// Insert before the sentinel
|
|
list.insertBefore(tmp.firstElementChild, sentinel);
|
|
}
|
|
}
|
|
}
|
|
if (!feedState.hasMore && sentinel) {
|
|
sentinel.innerHTML = '<div class="beep-no-more">No more beeps</div>';
|
|
}
|
|
})
|
|
.catch(function() {
|
|
if (sentinel) sentinel.innerHTML = '';
|
|
})
|
|
.finally(function() { feedState.loading = false; });
|
|
}
|
|
|
|
/* ---------- Composer ---------- */
|
|
function initCompose(compose, feed) {
|
|
var input = compose.querySelector('[data-beep-input]');
|
|
var counter = compose.querySelector('[data-beep-counter]');
|
|
var button = compose.querySelector('[data-beep-submit]');
|
|
if (!input || !button) return;
|
|
|
|
function hasAttachments() {
|
|
var pendingImages = compose._beepPendingImages;
|
|
var pendingVoice = compose._beepPendingVoice;
|
|
var hasGif = !!compose._beepGifUrl;
|
|
var hasQuote = !!(compose._beepQuotedPost && compose._beepQuotedPost.id);
|
|
var hasPoll = !!collectPollData(compose);
|
|
return (
|
|
(pendingImages && pendingImages.length) ||
|
|
pendingVoice ||
|
|
hasGif ||
|
|
hasQuote ||
|
|
hasPoll
|
|
);
|
|
}
|
|
|
|
function refresh() {
|
|
autosize(input);
|
|
var remaining = limit - countChars(input.value);
|
|
counter.textContent = remaining;
|
|
counter.classList.toggle('beep-counter-warn', remaining < 20 && remaining >= 0);
|
|
counter.classList.toggle('beep-counter-bad', remaining < 0);
|
|
// Button is enabled when there's text OR any attachment (images, voice, GIF, quote, poll).
|
|
button.disabled = !(input.value.trim() !== '' || hasAttachments()) || remaining < 0;
|
|
}
|
|
input.addEventListener('input', refresh);
|
|
// Media / GIF / quote / poll changes also affect whether the button should be enabled.
|
|
compose.addEventListener('beep:attachments-changed', refresh);
|
|
refresh();
|
|
|
|
button.addEventListener('click', function () {
|
|
var content = input.value.trim();
|
|
// Allow image-only / media-only posts. Gate is "must have text OR an attachment".
|
|
if (!content && !hasAttachments()) return;
|
|
button.disabled = true;
|
|
var quotedPost = compose._beepQuotedPost;
|
|
var gifUrl = compose._beepGifUrl;
|
|
var pending = compose._beepPendingImages;
|
|
var pendingVoice = compose._beepPendingVoice;
|
|
var poll = collectPollData(compose);
|
|
|
|
var payload = { content: content };
|
|
if (quotedPost && quotedPost.id) payload.quote_id = quotedPost.id;
|
|
if (gifUrl) payload.gif_url = gifUrl;
|
|
// Tell the server what attachments are coming so it can accept empty content.
|
|
if (pending && pending.length) payload.has_images = 1;
|
|
if (pendingVoice) payload.has_voice = 1;
|
|
if (poll) payload.has_poll = 1;
|
|
|
|
fetch(config.restUrl + 'post', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(handleResponse)
|
|
.then(function (data) {
|
|
var list = feed.querySelector('[data-beep-list]');
|
|
var empty = list && list.querySelector('.beep-empty');
|
|
if (empty) empty.remove();
|
|
|
|
// Reload feed to get the fully rendered post (includes media, quotes, polls)
|
|
reloadFeed(feed, feedState.currentSort);
|
|
|
|
input.value = '';
|
|
clearQuotePreview(compose);
|
|
if (compose._beepGifUrl) {
|
|
compose._beepGifUrl = null;
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
var gifPrev = compose.querySelector('[data-beep-gif-preview]');
|
|
if (gifPrev) { gifPrev.innerHTML = ''; gifPrev.hidden = true; }
|
|
}
|
|
refresh();
|
|
|
|
// Upload pending images and voice note to this new beep
|
|
var pending = compose._beepPendingImages;
|
|
var pendingVoice = compose._beepPendingVoice;
|
|
var uploadDone = function () {
|
|
reloadFeed(feed, feedState.currentSort);
|
|
};
|
|
|
|
var tasks = [];
|
|
if (pending && pending.length && data.id) {
|
|
tasks.push(uploadPendingImages(data.id, pending, compose));
|
|
}
|
|
if (pendingVoice && data.id) {
|
|
tasks.push(uploadPendingVoice(data.id, pendingVoice, compose));
|
|
}
|
|
// Create poll if poll data exists
|
|
if (data.id) {
|
|
tasks.push(createPoll(data.id, compose).then(function() {
|
|
clearPoll(compose);
|
|
}));
|
|
}
|
|
|
|
if (tasks.length) {
|
|
Promise.all(tasks).then(uploadDone);
|
|
} else {
|
|
// No uploads, just clear poll if present
|
|
clearPoll(compose);
|
|
}
|
|
})
|
|
.catch(function () {
|
|
showToast('Could not post. Please try again.', 'error');
|
|
button.disabled = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ---------- Sort tabs ---------- */
|
|
function initSortTabs(feed) {
|
|
var tabs = feed.querySelector('[data-beep-tabs]');
|
|
if (!tabs) return;
|
|
tabs.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('[data-beep-sort]');
|
|
if (!btn) return;
|
|
e.preventDefault();
|
|
var sort = btn.dataset.beepSort;
|
|
tabs.querySelectorAll('[data-beep-sort]').forEach(function (b) {
|
|
b.classList.toggle('is-active', b === btn);
|
|
});
|
|
reloadFeed(feed, sort);
|
|
});
|
|
}
|
|
|
|
function reloadFeed(feed, sort) {
|
|
var list = feed.querySelector('[data-beep-list]');
|
|
if (!list) return;
|
|
list.classList.add('beep-loading');
|
|
feedState.offset = 0;
|
|
feedState.hasMore = true;
|
|
feedState.currentSort = sort;
|
|
fetch(config.restUrl + 'feed?sort=' + encodeURIComponent(sort), {
|
|
credentials: 'same-origin',
|
|
headers: { 'X-WP-Nonce': config.nonce }
|
|
})
|
|
.then(handleResponse)
|
|
.then(function (data) {
|
|
// Remove everything except the sentinel
|
|
var sentinel = list.querySelector('[data-beep-scroll-sentinel]');
|
|
list.innerHTML = '';
|
|
if (data.html) {
|
|
var tmp = document.createElement('div');
|
|
tmp.innerHTML = data.html;
|
|
while (tmp.firstElementChild) {
|
|
list.appendChild(tmp.firstElementChild);
|
|
}
|
|
} else {
|
|
list.innerHTML = '<div class="beep-empty"><div class="beep-empty-icon">' + emptyIcon() + '</div><p>No beeps yet.</p><span>Be the first to share something!</span></div>';
|
|
}
|
|
if (sentinel) list.appendChild(sentinel);
|
|
feedState.hasMore = !!data.has_more;
|
|
feedState.offset = data.offset || 50;
|
|
list.classList.remove('beep-loading');
|
|
})
|
|
.catch(function () {
|
|
list.classList.remove('beep-loading');
|
|
});
|
|
}
|
|
|
|
function emptyIcon() {
|
|
return '<svg viewBox="0 0 24 24" width="64" height="64" fill="#4a90c4" aria-hidden="true"><path d="M17.2 1.4c-.8-.1-1.5.2-2 .6C14.3.9 13.2.7 12.1 1c-1.4.4-2.3 1.7-2.1 3.2-2.1-.2-4.2-.9-5.7-2.5-.3-.3-.8-.3-1 0-.6.8-.9 1.7-.9 2.6 0 1.2.5 2.2 1.3 2.9-.3-.1-.6-.1-.8-.3-.4-.2-.9 0-.8.5.3 1.6 1.3 2.8 2.7 3.4-.3 0-.5 0-.8-.1-.4-.1-.8.3-.5.7.8 1.4 2.2 2.3 3.8 2.5-1.3.8-2.8 1.2-4.4 1.2-.5 0-.8.5-.5.9 1.1 1.1 2.9 1.6 5.5 1.6 5.4 0 9.8-4.4 9.8-9.8v-.2c.9-.6 1.6-1.4 2.1-2.4.2-.4-.2-.8-.6-.6-.5.2-1 .3-1.5.4.5-.5.9-1.1 1-1.8.1-.4-.4-.7-.7-.5-.7.4-1.5.6-2.3.7-.8-.7-1.8-1.1-2.9-1.1zm-.8 1.6c.7-.1 1.4.2 1.9.7.2.2.5.3.8.2.3-.1.6-.1.9-.3-.2.3-.5.5-.8.6-.3.1-.4.5-.3.7.2.3.1.5 0 .8v.2c0 4.7-3.8 8.5-8.5 8.5-1.6 0-2.9-.2-3.8-.6 1.7-.3 3.2-1 4.3-2.2.3-.3.1-.8-.3-.8-1.4 0-2.7-.6-3.5-1.7.4 0 .9-.1 1.3-.2.4-.1.4-.7 0-.8-1.5-.3-2.6-1.3-3-2.6.5.1 1 .2 1.5.1.4 0 .5-.6.1-.8C5.2 5.5 4.5 4.5 4.5 3.4c0-.4.1-.9.3-1.3 1.8 1.5 4 2.4 6.4 2.5.3 0 .5-.2.5-.5-.2-1.1.4-2.1 1.4-2.4.7-.2 1.4 0 1.9.4.3.2.7.1.9-.1.3-.4.8-.7 1.3-.7.4.1.8.2 1.2.4-.3.3-.5.6-.5 1 0 .3.3.5.5.4z"/></svg>';
|
|
}
|
|
|
|
/* ---------- Click delegation ---------- */
|
|
function onClick(e) {
|
|
var t = e.target;
|
|
|
|
var likePost = t.closest('[data-beep-like-post]');
|
|
if (likePost) { e.preventDefault(); if (requireLogin()) handleLike(likePost, 'like/' + likePost.dataset.beepLikePost); return; }
|
|
|
|
var likeReply = t.closest('[data-beep-like-reply]');
|
|
if (likeReply) { e.preventDefault(); if (requireLogin()) handleLike(likeReply, 'reply-like/' + likeReply.dataset.beepLikeReply); return; }
|
|
|
|
var replyToggle = t.closest('[data-beep-toggle-reply]');
|
|
if (replyToggle) {
|
|
e.preventDefault();
|
|
if (!requireLogin()) return;
|
|
var container = replyToggle.closest('[data-beep-post], [data-beep-reply]');
|
|
if (!container) return;
|
|
var form = container.querySelector(':scope > .beep-post-body > [data-beep-reply-form], :scope > .beep-reply-body > [data-beep-reply-form]');
|
|
if (!form) return;
|
|
form.hidden = !form.hidden;
|
|
if (!form.hidden) {
|
|
var ta = form.querySelector('textarea');
|
|
if (ta) ta.focus();
|
|
}
|
|
return;
|
|
}
|
|
|
|
var replyBtn = t.closest('[data-beep-reply-submit]');
|
|
if (replyBtn) { e.preventDefault(); handleReplySubmit(replyBtn); return; }
|
|
|
|
var shareBtn = t.closest('[data-beep-share]');
|
|
if (shareBtn) { e.preventDefault(); handleShare(shareBtn); return; }
|
|
|
|
var delPost = t.closest('[data-beep-delete]');
|
|
if (delPost) {
|
|
e.preventDefault();
|
|
handleDeleteWithUndo('delete/' + delPost.dataset.beepDelete, '[data-beep-post="' + delPost.dataset.beepDelete + '"]', 'beep');
|
|
return;
|
|
}
|
|
|
|
var delReply = t.closest('[data-beep-delete-reply]');
|
|
if (delReply) {
|
|
e.preventDefault();
|
|
handleDeleteWithUndo('delete-reply/' + delReply.dataset.beepDeleteReply, '[data-beep-reply="' + delReply.dataset.beepDeleteReply + '"]', 'reply');
|
|
return;
|
|
}
|
|
|
|
// Thread / conversation view
|
|
var threadBtn = t.closest('[data-beep-thread]');
|
|
if (threadBtn) { e.preventDefault(); openThread(threadBtn.dataset.beepThread); return; }
|
|
|
|
// Thread modal close
|
|
var threadClose = t.closest('[data-beep-thread-close]');
|
|
if (threadClose) { e.preventDefault(); closeThread(); return; }
|
|
|
|
// Quote button - open composer with quoted beep pre-loaded
|
|
var quoteBtn = t.closest('[data-beep-quote]');
|
|
if (quoteBtn) { e.preventDefault(); openComposerWithQuote(quoteBtn.dataset.beepQuote); return; }
|
|
|
|
// Quote composer tab toggle
|
|
var quoteComposerBtn = t.closest('[data-beep-quote-composer-btn]');
|
|
if (quoteComposerBtn) {
|
|
e.preventDefault();
|
|
var compose = quoteComposerBtn.closest('[data-beep-compose]');
|
|
var quoteSection = compose && compose.querySelector('[data-beep-compose-quote]');
|
|
if (quoteSection) quoteSection.hidden = !quoteSection.hidden;
|
|
return;
|
|
}
|
|
|
|
// Quote lookup - Add button
|
|
var quoteLookupBtn = t.closest('[data-beep-quote-lookup-btn]');
|
|
if (quoteLookupBtn) {
|
|
e.preventDefault();
|
|
var compose = quoteLookupBtn.closest('[data-beep-compose]');
|
|
var input = compose && compose.querySelector('[data-beep-quote-lookup]');
|
|
var query = input ? input.value.trim() : '';
|
|
if (!query) return;
|
|
fetch(config.restUrl + 'lookup?q=' + encodeURIComponent(query), {
|
|
headers: { 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(handleResponse)
|
|
.then(function(quoted) {
|
|
if (compose) {
|
|
compose._beepQuotedPost = { id: quoted.id, data: quoted };
|
|
showQuotePreview(compose, quoted);
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
}
|
|
})
|
|
.catch(function() { showToast('Beep not found.', 'error'); });
|
|
return;
|
|
}
|
|
|
|
// Quote remove button
|
|
var quoteRemoveBtn = t.closest('[data-beep-quote-remove]');
|
|
if (quoteRemoveBtn) { e.preventDefault(); var compose = quoteRemoveBtn.closest('[data-beep-compose]'); clearQuotePreview(compose); return; }
|
|
|
|
// GIF picker button
|
|
var gifBtn = t.closest('[data-beep-gif-btn]');
|
|
if (gifBtn) { e.preventDefault(); var compose = gifBtn.closest('[data-beep-compose]'); openGifPicker(compose); return; }
|
|
|
|
// GIF modal close
|
|
var gifClose = t.closest('[data-beep-gif-close]');
|
|
if (gifClose) { e.preventDefault(); closeGifPicker(); return; }
|
|
|
|
// GIF search on Enter (handled in keydown)
|
|
// GIF search button
|
|
var gifSearchBtn = t.closest('[data-beep-gif-search-btn]');
|
|
if (gifSearchBtn) {
|
|
e.preventDefault();
|
|
var compose = gifSearchBtn.closest('[data-beep-compose]') || document.querySelector('[data-beep-compose]');
|
|
var input = gifSearchBtn.closest('.beep-gif-search-bar') && gifSearchBtn.closest('.beep-gif-search-bar').querySelector('[data-beep-gif-search]');
|
|
if (input) searchGifs(input.value, compose);
|
|
return;
|
|
}
|
|
|
|
// GIF remove
|
|
var gifRemoveBtn = t.closest('[data-beep-gif-remove]');
|
|
if (gifRemoveBtn) {
|
|
e.preventDefault();
|
|
var compose = gifRemoveBtn.closest('[data-beep-compose]');
|
|
if (compose) {
|
|
compose._beepGifUrl = null;
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
var preview = compose.querySelector('[data-beep-gif-preview]');
|
|
if (preview) { preview.innerHTML = ''; preview.hidden = true; }
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Poll button - toggle poll section
|
|
var pollBtn = t.closest('[data-beep-poll-btn]');
|
|
if (pollBtn) {
|
|
e.preventDefault();
|
|
var compose = pollBtn.closest('[data-beep-compose]');
|
|
if (compose) openPollSection(compose);
|
|
return;
|
|
}
|
|
|
|
// Poll add option
|
|
var pollAddOptionBtn = t.closest('[data-beep-poll-add-option]');
|
|
if (pollAddOptionBtn) {
|
|
e.preventDefault();
|
|
var compose = pollAddOptionBtn.closest('[data-beep-compose]');
|
|
if (compose) addPollOption(compose);
|
|
return;
|
|
}
|
|
|
|
// Poll remove option
|
|
var pollRemoveOptionBtn = t.closest('[data-beep-poll-remove-option]');
|
|
if (pollRemoveOptionBtn) {
|
|
e.preventDefault();
|
|
var compose = pollRemoveOptionBtn.closest('[data-beep-compose]');
|
|
if (compose) removePollOption(compose);
|
|
return;
|
|
}
|
|
|
|
// Poll remove
|
|
var pollRemoveBtn = t.closest('[data-beep-poll-remove]');
|
|
if (pollRemoveBtn) {
|
|
e.preventDefault();
|
|
var compose = pollRemoveBtn.closest('[data-beep-compose]');
|
|
if (compose) clearPoll(compose);
|
|
return;
|
|
}
|
|
|
|
// Poll vote button
|
|
var pollVoteBtn = t.closest('[data-beep-poll-vote]');
|
|
if (pollVoteBtn) {
|
|
e.preventDefault();
|
|
if (requireLogin()) handlePollVote(pollVoteBtn);
|
|
return;
|
|
}
|
|
|
|
// Image lightbox - click on post images to view full size
|
|
var lightboxImg = t.closest('[data-beep-lightbox]');
|
|
if (lightboxImg) {
|
|
e.preventDefault();
|
|
openLightbox(lightboxImg.href || lightboxImg.querySelector('img').src);
|
|
return;
|
|
}
|
|
|
|
// Lightbox close
|
|
var lightboxClose = t.closest('.beep-lightbox');
|
|
if (lightboxClose) {
|
|
closeLightbox();
|
|
return;
|
|
}
|
|
|
|
// Modal backdrop click closes
|
|
if (t.matches && t.matches('[data-beep-thread-modal]')) { closeThread(); return; }
|
|
if (t.matches && t.matches('[data-beep-gif-modal]')) { closeGifPicker(); return; }
|
|
|
|
// Click anywhere else on a feed post opens the conversation
|
|
var openPost = t.closest('[data-beep-open-thread]');
|
|
if (openPost && !t.closest('a, button, label, input, textarea, audio, video, .beep-media-grid-item, .beep-gif-embed, [data-beep-reply-form]')) {
|
|
// Don't hijack text selection
|
|
var sel = window.getSelection && window.getSelection();
|
|
if (sel && sel.toString()) return;
|
|
openThread(openPost.dataset.beepOpenThread);
|
|
}
|
|
}
|
|
|
|
function onEscape(e) {
|
|
if (e.key !== 'Escape') return;
|
|
var lightbox = document.querySelector('.beep-lightbox');
|
|
if (lightbox) { closeLightbox(); return; }
|
|
var gifModal = document.querySelector('[data-beep-gif-modal]:not([hidden])');
|
|
if (gifModal) { closeGifPicker(); return; }
|
|
var threadModal = document.querySelector('[data-beep-thread-modal]:not([hidden])');
|
|
if (threadModal) closeThread();
|
|
}
|
|
|
|
function onInput(e) {
|
|
var input = e.target.closest('.beep-reply-input, .beep-compose-input');
|
|
if (!input) return;
|
|
autosize(input);
|
|
var counter = input.parentElement.querySelector('.beep-char-counter');
|
|
var submit = input.parentElement.querySelector('.beep-button');
|
|
if (counter) {
|
|
var remaining = limit - countChars(input.value);
|
|
counter.textContent = remaining;
|
|
counter.classList.toggle('beep-counter-warn', remaining < 20 && remaining >= 0);
|
|
counter.classList.toggle('beep-counter-bad', remaining < 0);
|
|
if (submit) submit.disabled = input.value.trim() === '' || remaining < 0;
|
|
}
|
|
}
|
|
|
|
function onKeydown(e) {
|
|
// Enter in the GIF search box runs the search
|
|
if (e.key === 'Enter' && e.target.matches && e.target.matches('[data-beep-gif-search]')) {
|
|
e.preventDefault();
|
|
var compose = document.querySelector('[data-beep-compose]');
|
|
searchGifs(e.target.value, compose);
|
|
return;
|
|
}
|
|
// Cmd/Ctrl+Enter submits beeps and replies
|
|
if (!((e.metaKey || e.ctrlKey) && e.key === 'Enter')) return;
|
|
var input = e.target.closest('textarea');
|
|
if (!input) return;
|
|
var wrap = input.closest('.beep-compose, .beep-reply-form');
|
|
if (!wrap) return;
|
|
var btn = wrap.querySelector('[data-beep-submit], [data-beep-reply-submit]');
|
|
if (btn && !btn.disabled) btn.click();
|
|
}
|
|
|
|
/* ---------- Like (works for both posts and replies) ---------- */
|
|
function handleLike(btn, urlFragment) {
|
|
var countEl = btn.querySelector('[data-beep-like-count]');
|
|
var was = btn.classList.contains('is-liked');
|
|
|
|
btn.classList.toggle('is-liked');
|
|
if (!was) {
|
|
btn.classList.add('beep-just-liked');
|
|
setTimeout(function () { btn.classList.remove('beep-just-liked'); }, 600);
|
|
}
|
|
if (countEl) {
|
|
var current = parseInt(countEl.textContent, 10) || 0;
|
|
var optimistic = current + (was ? -1 : 1);
|
|
countEl.textContent = optimistic > 0 ? optimistic : '';
|
|
}
|
|
|
|
fetch(config.restUrl + urlFragment, {
|
|
method: 'POST',
|
|
headers: { 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(handleResponse)
|
|
.then(function (data) {
|
|
if (countEl) countEl.textContent = data.count > 0 ? data.count : '';
|
|
btn.classList.toggle('is-liked', !!data.liked);
|
|
})
|
|
.catch(function () {
|
|
btn.classList.toggle('is-liked', was);
|
|
});
|
|
}
|
|
|
|
/* ---------- Reply submit (top-level or nested) ---------- */
|
|
function handleReplySubmit(btn) {
|
|
var postId = btn.dataset.beepReplySubmit;
|
|
var parent = btn.dataset.beepReplyParent || '0';
|
|
var form = btn.closest('[data-beep-reply-form]');
|
|
var input = form && form.querySelector('textarea');
|
|
if (!input) return;
|
|
var content = input.value.trim();
|
|
if (!content) return;
|
|
btn.disabled = true;
|
|
|
|
fetch(config.restUrl + 'reply/' + postId, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ content: content, parent: parseInt(parent, 10) || 0 })
|
|
})
|
|
.then(handleResponse)
|
|
.then(function (data) {
|
|
if (data.pending) {
|
|
form.innerHTML = '<div class="beep-pending">Reply submitted, awaiting moderation.</div>';
|
|
return;
|
|
}
|
|
input.value = '';
|
|
// Inside the conversation modal: refresh just the thread.
|
|
if (btn.closest('[data-beep-thread-modal]')) {
|
|
refreshThread();
|
|
return;
|
|
}
|
|
// Otherwise reload the feed with the current sort.
|
|
var feed = btn.closest('[data-beep-feed]');
|
|
if (feed) {
|
|
reloadFeed(feed, feedState.currentSort);
|
|
}
|
|
})
|
|
.catch(function () {
|
|
showToast('Could not post reply. Please try again.', 'error');
|
|
})
|
|
.finally(function () { btn.disabled = false; });
|
|
}
|
|
|
|
/* ---------- Share (copy link) ---------- */
|
|
function handleShare(btn) {
|
|
var url = btn.dataset.beepShare;
|
|
if (!url || !navigator.clipboard) return;
|
|
navigator.clipboard.writeText(url).then(function () {
|
|
showToast('Link copied to clipboard', 'success');
|
|
});
|
|
}
|
|
|
|
/* ---------- Delete with undo toast ---------- */
|
|
function handleDeleteWithUndo(pathFragment, selector, type) {
|
|
var el = document.querySelector(selector);
|
|
if (!el) return;
|
|
|
|
// Store original state
|
|
var parent = el.parentNode;
|
|
var nextSibling = el.nextSibling;
|
|
|
|
// Hide the element with animation
|
|
el.style.transition = 'opacity 0.3s ease, max-height 0.3s ease';
|
|
el.style.opacity = '0';
|
|
el.style.maxHeight = '0';
|
|
el.style.overflow = 'hidden';
|
|
|
|
// Show undo toast
|
|
var toast = document.createElement('div');
|
|
toast.className = 'beep-toast beep-toast-undo';
|
|
toast.innerHTML = '<span>' + (type === 'beep' ? 'Beep' : 'Reply') + ' deleted.</span><button class="beep-toast-undo-btn" type="button">Undo</button>';
|
|
document.body.appendChild(toast);
|
|
|
|
// Trigger animation
|
|
requestAnimationFrame(function() {
|
|
toast.classList.add('beep-toast-visible');
|
|
});
|
|
|
|
var deleteTimeout = setTimeout(function() {
|
|
// Actually delete
|
|
fetch(config.restUrl + pathFragment, {
|
|
method: 'DELETE',
|
|
headers: { 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(handleResponse)
|
|
.then(function () {
|
|
if (el && el.parentNode) el.remove();
|
|
})
|
|
.catch(function () {
|
|
// Restore on failure
|
|
restoreElement(el, parent, nextSibling);
|
|
showToast('Could not delete.', 'error');
|
|
});
|
|
dismissToast(toast);
|
|
}, 4000);
|
|
|
|
// Undo button
|
|
toast.querySelector('.beep-toast-undo-btn').addEventListener('click', function() {
|
|
clearTimeout(deleteTimeout);
|
|
restoreElement(el, parent, nextSibling);
|
|
dismissToast(toast);
|
|
});
|
|
}
|
|
|
|
function restoreElement(el, parent, nextSibling) {
|
|
el.style.transition = '';
|
|
el.style.opacity = '';
|
|
el.style.maxHeight = '';
|
|
el.style.overflow = '';
|
|
if (nextSibling) {
|
|
parent.insertBefore(el, nextSibling);
|
|
} else {
|
|
parent.appendChild(el);
|
|
}
|
|
}
|
|
|
|
function dismissToast(toast) {
|
|
toast.classList.remove('beep-toast-visible');
|
|
setTimeout(function() { toast.remove(); }, 300);
|
|
}
|
|
|
|
/* ---------- Toast notification ---------- */
|
|
function showToast(message, type) {
|
|
var existing = document.querySelector('.beep-toast.beep-toast-msg');
|
|
if (existing) existing.remove();
|
|
|
|
var toast = document.createElement('div');
|
|
toast.className = 'beep-toast beep-toast-msg beep-toast-' + (type || 'info');
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
requestAnimationFrame(function() {
|
|
toast.classList.add('beep-toast-visible');
|
|
});
|
|
setTimeout(function() { dismissToast(toast); }, 3000);
|
|
}
|
|
|
|
/* ---------- Helpers ---------- */
|
|
function requireLogin() {
|
|
if (config.isLoggedIn) return true;
|
|
if (config.loginUrl) window.location.href = config.loginUrl;
|
|
return false;
|
|
}
|
|
|
|
function autosize(el) {
|
|
el.style.height = 'auto';
|
|
el.style.height = el.scrollHeight + 'px';
|
|
}
|
|
|
|
function countChars(s) {
|
|
return Array.from(s).length;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
var div = document.createElement('div');
|
|
div.appendChild(document.createTextNode(str));
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/* ---------- Media upload (images) ---------- */
|
|
function initMedia(compose, feed) {
|
|
var input = compose && compose.querySelector('[data-beep-media-input]');
|
|
var preview = compose && compose.querySelector('[data-beep-media-preview]');
|
|
var counter = compose && compose.querySelector('[data-beep-media-count]');
|
|
var voiceInput = compose && compose.querySelector('[data-beep-voice-input]');
|
|
var voicePreview = compose && compose.querySelector('[data-beep-voice-preview]');
|
|
if (!input && !voiceInput) return;
|
|
|
|
var pendingImages = [];
|
|
var pendingVoice = null;
|
|
|
|
// Voice note input
|
|
if (voiceInput) {
|
|
voiceInput.addEventListener('change', function (e) {
|
|
var files = e.target.files;
|
|
if (!files || !files.length) return;
|
|
var file = files[0];
|
|
if (file.size > 20 * 1024 * 1024) {
|
|
showToast('Voice note must be under 20 MB.', 'error');
|
|
voiceInput.value = '';
|
|
return;
|
|
}
|
|
var accepted = ['audio/mpeg','audio/ogg','audio/wav','audio/webm','audio/mp4','audio/x-m4a'];
|
|
if (accepted.indexOf(file.type) === -1) {
|
|
showToast('Only MP3, OGG, WAV, WebM, and M4A audio are allowed.', 'error');
|
|
voiceInput.value = '';
|
|
return;
|
|
}
|
|
if (pendingVoice) {
|
|
showToast('Only one voice note per beep.', 'error');
|
|
voiceInput.value = '';
|
|
return;
|
|
}
|
|
var reader = new FileReader();
|
|
reader.onload = function (ev) {
|
|
var id = 'voice-' + Date.now();
|
|
pendingVoice = { file: file, id: id };
|
|
compose._beepPendingVoice = pendingVoice;
|
|
voicePreview.innerHTML = '<div class="beep-voice-preview-card"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08C16.39 17.43 19 14.53 19 11h-2z"/></svg><span>Voice note ready</span><button class="beep-voice-remove" data-beep-voice-remove type="button" title="Remove">×</button></div>';
|
|
voicePreview.hidden = false;
|
|
if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
};
|
|
reader.readAsDataURL(file);
|
|
voiceInput.value = '';
|
|
});
|
|
}
|
|
|
|
// Voice remove button
|
|
if (voicePreview) {
|
|
voicePreview.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('[data-beep-voice-remove]');
|
|
if (!btn) return;
|
|
pendingVoice = null;
|
|
if (compose) compose._beepPendingVoice = null;
|
|
voicePreview.innerHTML = '';
|
|
voicePreview.hidden = true;
|
|
if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
});
|
|
}
|
|
|
|
if (input) {
|
|
input.addEventListener('change', function (e) {
|
|
var files = Array.prototype.slice.call(e.target.files);
|
|
if (!files.length) return;
|
|
var room = 4 - pendingImages.length;
|
|
if (room <= 0) { input.value = ''; return; }
|
|
files = files.slice(0, room);
|
|
files.forEach(function (file) {
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
showToast('Image must be under 10 MB: ' + file.name, 'error');
|
|
return;
|
|
}
|
|
var reader = new FileReader();
|
|
reader.onload = function (ev) {
|
|
var url = ev.target.result;
|
|
var id = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
|
|
pendingImages.push({ file: file, previewUrl: url, id: id });
|
|
renderPreviews();
|
|
if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
input.value = '';
|
|
});
|
|
}
|
|
|
|
function renderPreviews() {
|
|
if (!pendingImages.length) {
|
|
preview.innerHTML = '';
|
|
preview.hidden = true;
|
|
if (counter) counter.hidden = true;
|
|
return;
|
|
}
|
|
preview.hidden = false;
|
|
if (counter) {
|
|
counter.textContent = pendingImages.length + '/4';
|
|
counter.hidden = false;
|
|
}
|
|
preview.innerHTML = pendingImages.map(function (img) {
|
|
return '<div class="beep-media-preview-item" data-preview-id="' + img.id + '">' +
|
|
'<img src="' + img.previewUrl + '" alt="Preview">' +
|
|
'<button class="beep-media-remove" type="button" title="Remove" data-remove="' + img.id + '">×</button>' +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
if (preview) {
|
|
preview.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('[data-remove]');
|
|
if (!btn) return;
|
|
var id = btn.dataset.remove;
|
|
pendingImages = pendingImages.filter(function (img) { return img.id !== id; });
|
|
compose._beepPendingImages = pendingImages;
|
|
renderPreviews();
|
|
if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
});
|
|
}
|
|
|
|
if (compose) {
|
|
compose._beepPendingImages = pendingImages;
|
|
compose._beepPendingVoice = pendingVoice;
|
|
}
|
|
|
|
// Poll input changes also affect "has attachments" (poll becomes valid mid-typing).
|
|
if (compose) {
|
|
compose.addEventListener('input', function (e) {
|
|
if (e.target.matches('[data-beep-poll-question], [data-beep-poll-option]')) {
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function uploadPendingImages(postId, pendingImages, compose) {
|
|
if (!pendingImages || !pendingImages.length) return Promise.resolve();
|
|
var promises = pendingImages.map(function (img) {
|
|
var formData = new FormData();
|
|
formData.append('file', img.file);
|
|
return fetch(config.restUrl + 'media/' + postId, {
|
|
method: 'POST',
|
|
headers: { 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin',
|
|
body: formData
|
|
})
|
|
.then(handleResponse)
|
|
.catch(function (err) {
|
|
console.warn('Image upload failed:', img.file.name, err);
|
|
});
|
|
});
|
|
return Promise.all(promises).then(function () {
|
|
pendingImages.length = 0;
|
|
if (compose) {
|
|
var preview = compose.querySelector('[data-beep-media-preview]');
|
|
if (preview) { preview.innerHTML = ''; preview.hidden = true; }
|
|
var counter = compose.querySelector('[data-beep-media-count]');
|
|
if (counter) counter.hidden = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
function uploadPendingVoice(postId, pendingVoice, compose) {
|
|
if (!pendingVoice) return Promise.resolve();
|
|
var formData = new FormData();
|
|
formData.append('file', pendingVoice.file);
|
|
return fetch(config.restUrl + 'voice/' + postId, {
|
|
method: 'POST',
|
|
headers: { 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin',
|
|
body: formData
|
|
})
|
|
.then(handleResponse)
|
|
.then(function () {
|
|
if (compose) {
|
|
compose._beepPendingVoice = null;
|
|
var vp = compose.querySelector('[data-beep-voice-preview]');
|
|
if (vp) { vp.innerHTML = ''; vp.hidden = true; }
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
console.warn('Voice upload failed:', err);
|
|
});
|
|
}
|
|
|
|
/* ---------- Thread / Conversation modal ---------- */
|
|
function openThread(postId, keepScroll) {
|
|
var modal = document.querySelector('[data-beep-thread-modal]');
|
|
var content = modal && modal.querySelector('[data-beep-thread-content]');
|
|
var loading = modal && modal.querySelector('[data-beep-thread-loading]');
|
|
if (!modal || !content) return;
|
|
modal.hidden = false;
|
|
modal._beepThreadId = postId;
|
|
if (!keepScroll) {
|
|
if (loading) loading.hidden = false;
|
|
content.innerHTML = '';
|
|
}
|
|
document.documentElement.style.overflow = 'hidden';
|
|
|
|
fetch(config.restUrl + 'thread/' + postId + '/html', {
|
|
headers: { 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(handleResponse)
|
|
.then(function(data) {
|
|
if (loading) loading.hidden = true;
|
|
content.innerHTML = data.html || '<div class="beep-thread-error">Nothing to show.</div>';
|
|
// Sync the reply count back to the feed card
|
|
if (typeof data.reply_count !== 'undefined') {
|
|
var feedPost = document.querySelector('.beep-list [data-beep-post="' + postId + '"] [data-beep-reply-count]');
|
|
if (feedPost) feedPost.textContent = data.reply_count > 0 ? data.reply_count : '';
|
|
}
|
|
})
|
|
.catch(function() {
|
|
if (loading) loading.hidden = true;
|
|
content.innerHTML = '<div class="beep-thread-error">Failed to load conversation.</div>';
|
|
});
|
|
}
|
|
|
|
function refreshThread() {
|
|
var modal = document.querySelector('[data-beep-thread-modal]:not([hidden])');
|
|
if (modal && modal._beepThreadId) openThread(modal._beepThreadId, true);
|
|
}
|
|
|
|
function closeThread() {
|
|
var modal = document.querySelector('[data-beep-thread-modal]');
|
|
if (modal) modal.hidden = true;
|
|
document.documentElement.style.overflow = '';
|
|
}
|
|
|
|
/* ---------- Quote posts ---------- */
|
|
function openComposerWithQuote(quotedId) {
|
|
var compose = document.querySelector('[data-beep-compose]');
|
|
if (!compose) return;
|
|
var form = compose.querySelector('.beep-compose-body');
|
|
if (form) form.style.display = 'block';
|
|
compose.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
|
fetch(config.restUrl + 'quote/' + quotedId, {
|
|
headers: { 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(handleResponse)
|
|
.then(function(quoted) {
|
|
compose._beepQuotedPost = { id: quotedId, data: quoted };
|
|
showQuotePreview(compose, quoted);
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
})
|
|
.catch(function(err) { console.warn('Could not load quoted beep:', err); });
|
|
}
|
|
|
|
function showQuotePreview(compose, quoted) {
|
|
var preview = compose && compose.querySelector('[data-beep-quote-preview]');
|
|
var removeBtn = compose && compose.querySelector('[data-beep-quote-remove]');
|
|
if (!preview || !quoted) return;
|
|
var author = quoted.author || {};
|
|
var avatar = escapeHtml(author.avatar || 'https://www.gravatar.com/avatar/?s=48&d=mp');
|
|
var name = escapeHtml(author.name || 'Unknown');
|
|
var username = escapeHtml(author.username || '');
|
|
var time_ago = escapeHtml(quoted.time_ago || '');
|
|
var content = escapeHtml(quoted.content || '');
|
|
preview.innerHTML = '<div class="beep-quoted-post beep-quoted-preview-card">' +
|
|
'<div class="beep-quoted-avatar"><img src="' + avatar + '" alt="' + name + '" width="36" height="36"></div>' +
|
|
'<div class="beep-quoted-body">' +
|
|
'<div class="beep-quoted-meta"><span class="beep-quoted-name">' + name + '</span> ' +
|
|
'<span class="beep-quoted-handle">@' + username + '</span> ' +
|
|
'<span class="beep-quoted-dot">·</span> ' +
|
|
'<span class="beep-quoted-time">' + time_ago + '</span></div>' +
|
|
'<div class="beep-quoted-text">' + content + '</div></div></div>';
|
|
preview.hidden = false;
|
|
if (removeBtn) removeBtn.hidden = false;
|
|
}
|
|
|
|
function clearQuotePreview(compose) {
|
|
if (!compose) return;
|
|
compose._beepQuotedPost = null;
|
|
var preview = compose.querySelector('[data-beep-quote-preview]');
|
|
var removeBtn = compose.querySelector('[data-beep-quote-remove]');
|
|
if (preview) { preview.innerHTML = ''; preview.hidden = true; }
|
|
if (removeBtn) removeBtn.hidden = true;
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
}
|
|
|
|
/* ---------- GIF Picker (Giphy) ---------- */
|
|
function openGifPicker(compose) {
|
|
var modal = document.querySelector('[data-beep-gif-modal]');
|
|
if (!modal) return;
|
|
modal.hidden = false;
|
|
var searchInput = modal.querySelector('[data-beep-gif-search]');
|
|
if (searchInput) searchInput.value = '';
|
|
var grid = modal.querySelector('[data-beep-gif-grid]');
|
|
if (grid) grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-muted);">Type to search GIFs...</p>';
|
|
if (searchInput) searchInput.focus();
|
|
}
|
|
|
|
function closeGifPicker() {
|
|
var modal = document.querySelector('[data-beep-gif-modal]');
|
|
if (modal) modal.hidden = true;
|
|
}
|
|
|
|
function searchGifs(query, compose) {
|
|
if (!query || !query.trim()) return;
|
|
var apiKey = config.giphyKey || '';
|
|
if (!apiKey) {
|
|
console.warn('Beep: No Giphy API key configured.');
|
|
return;
|
|
}
|
|
var modal = document.querySelector('[data-beep-gif-modal]');
|
|
var grid = modal && modal.querySelector('[data-beep-gif-grid]');
|
|
if (!grid) return;
|
|
grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-muted);">Searching...</p>';
|
|
|
|
var url = 'https://api.giphy.com/v1/gifs/search?api_key=' + encodeURIComponent(apiKey) + '&q=' + encodeURIComponent(query) + '&limit=30&rating=g';
|
|
|
|
fetch(url)
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(data) {
|
|
if (!data.data || !data.data.length) {
|
|
grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-muted);">No GIFs found.</p>';
|
|
return;
|
|
}
|
|
grid.innerHTML = '';
|
|
data.data.forEach(function(gif) {
|
|
var imgUrl = gif.images && gif.images.fixed_width_small && gif.images.fixed_width_small.url;
|
|
if (!imgUrl) return;
|
|
var item = document.createElement('div');
|
|
item.className = 'beep-gif-item';
|
|
var altText = escapeHtml(gif.title || '');
|
|
item.innerHTML = '<img src="' + escapeHtml(imgUrl) + '" alt="' + altText + '">';
|
|
item.addEventListener('click', function() {
|
|
selectGif(gif, compose);
|
|
});
|
|
grid.appendChild(item);
|
|
});
|
|
})
|
|
.catch(function() {
|
|
grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-muted);">Search failed.</p>';
|
|
});
|
|
}
|
|
|
|
function selectGif(gif, compose) {
|
|
if (!compose) return;
|
|
var url = gif.images && gif.images.fixed_width && gif.images.fixed_width.url;
|
|
if (!url) return;
|
|
compose._beepGifUrl = url;
|
|
var preview = compose.querySelector('[data-beep-gif-preview]');
|
|
if (preview) {
|
|
preview.innerHTML = '<div class="beep-gif-preview-card"><img src="' + escapeHtml(url) + '" alt="GIF"><button class="beep-gif-remove" data-beep-gif-remove type="button">×</button></div>';
|
|
preview.hidden = false;
|
|
}
|
|
closeGifPicker();
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
}
|
|
|
|
/* ---------- Poll helpers ---------- */
|
|
function openPollSection(compose) {
|
|
var pollSection = compose.querySelector('[data-beep-compose-poll]');
|
|
if (!pollSection) return;
|
|
pollSection.hidden = false;
|
|
var questionInput = pollSection.querySelector('[data-beep-poll-question]');
|
|
if (questionInput) questionInput.focus();
|
|
var options = pollSection.querySelectorAll('[data-beep-poll-option]');
|
|
if (options.length < 2) {
|
|
while (options.length < 2) {
|
|
addPollOption(compose);
|
|
options = pollSection.querySelectorAll('[data-beep-poll-option]');
|
|
}
|
|
}
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
}
|
|
|
|
function addPollOption(compose) {
|
|
var pollSection = compose.querySelector('[data-beep-compose-poll]');
|
|
if (!pollSection) return;
|
|
var optionsContainer = pollSection.querySelector('[data-beep-poll-options]');
|
|
if (!optionsContainer) return;
|
|
var optionCount = optionsContainer.querySelectorAll('[data-beep-poll-option]').length;
|
|
if (optionCount >= 6) return;
|
|
var newRow = document.createElement('div');
|
|
newRow.className = 'beep-poll-option-row';
|
|
newRow.innerHTML = '<input type="text" class="beep-poll-option-input" data-beep-poll-option placeholder="Option ' + (optionCount + 1) + '" maxlength="100">';
|
|
optionsContainer.appendChild(newRow);
|
|
updatePollButtons(compose);
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
}
|
|
|
|
function removePollOption(compose) {
|
|
var pollSection = compose.querySelector('[data-beep-compose-poll]');
|
|
if (!pollSection) return;
|
|
var options = pollSection.querySelectorAll('[data-beep-poll-option]');
|
|
if (options.length <= 2) return;
|
|
var lastOption = options[options.length - 1];
|
|
if (lastOption) lastOption.parentElement.remove();
|
|
updatePollButtons(compose);
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
}
|
|
|
|
function updatePollButtons(compose) {
|
|
var pollSection = compose.querySelector('[data-beep-compose-poll]');
|
|
if (!pollSection) return;
|
|
var options = pollSection.querySelectorAll('[data-beep-poll-option]');
|
|
var addBtn = pollSection.querySelector('[data-beep-poll-add-option]');
|
|
var removeBtn = pollSection.querySelector('[data-beep-poll-remove-option]');
|
|
if (addBtn) addBtn.hidden = options.length >= 6;
|
|
if (removeBtn) removeBtn.hidden = options.length <= 2;
|
|
}
|
|
|
|
function clearPoll(compose) {
|
|
if (!compose) return;
|
|
compose._beepPoll = null;
|
|
var pollSection = compose.querySelector('[data-beep-compose-poll]');
|
|
if (pollSection) {
|
|
pollSection.hidden = true;
|
|
var question = pollSection.querySelector('[data-beep-poll-question]');
|
|
if (question) question.value = '';
|
|
var options = pollSection.querySelectorAll('[data-beep-poll-option]');
|
|
if (options.length > 2) {
|
|
for (var i = options.length - 1; i >= 2; i--) {
|
|
if (options[i].parentElement) options[i].parentElement.remove();
|
|
}
|
|
}
|
|
options = pollSection.querySelectorAll('[data-beep-poll-option]');
|
|
for (var j = 0; j < options.length; j++) {
|
|
options[j].value = '';
|
|
}
|
|
}
|
|
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
|
|
}
|
|
|
|
function collectPollData(compose) {
|
|
if (!compose) return null;
|
|
var pollSection = compose.querySelector('[data-beep-compose-poll]');
|
|
if (!pollSection || pollSection.hidden) return null;
|
|
var questionInput = pollSection.querySelector('[data-beep-poll-question]');
|
|
var question = questionInput ? questionInput.value.trim() : '';
|
|
if (!question) return null;
|
|
var optionInputs = pollSection.querySelectorAll('[data-beep-poll-option]');
|
|
var options = [];
|
|
for (var i = 0; i < optionInputs.length; i++) {
|
|
var val = optionInputs[i].value.trim();
|
|
if (val) options.push(val);
|
|
}
|
|
if (options.length < 2) return null;
|
|
return { question: question, options: options };
|
|
}
|
|
|
|
function createPoll(beepId, compose) {
|
|
var pollData = collectPollData(compose);
|
|
if (!pollData) return Promise.resolve();
|
|
return fetch(config.restUrl + 'poll/' + beepId, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify(pollData)
|
|
})
|
|
.then(handleResponse)
|
|
.catch(function(err) {
|
|
console.warn('Poll creation failed:', err);
|
|
});
|
|
}
|
|
|
|
function handlePollVote(btn) {
|
|
var beepId = btn.dataset.beepPollBeep;
|
|
var optionIndex = parseInt(btn.dataset.beepPollVote, 10);
|
|
if (!beepId || isNaN(optionIndex)) return;
|
|
btn.disabled = true;
|
|
fetch(config.restUrl + 'poll/' + beepId + '/vote', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ option_index: optionIndex })
|
|
})
|
|
.then(handleResponse)
|
|
.then(function(data) {
|
|
// Inside the conversation modal: refresh just the thread.
|
|
if (btn.closest('[data-beep-thread-modal]')) { refreshThread(); return; }
|
|
var postEl = btn.closest('[data-beep-post]');
|
|
if (postEl) {
|
|
var list = postEl.closest('[data-beep-list]');
|
|
if (list) reloadFeed(list.closest('[data-beep-feed]'), feedState.currentSort);
|
|
}
|
|
})
|
|
.catch(function() {
|
|
btn.disabled = false;
|
|
});
|
|
}
|
|
|
|
/* ---------- Image Lightbox ---------- */
|
|
function openLightbox(src) {
|
|
var existing = document.querySelector('.beep-lightbox');
|
|
if (existing) existing.remove();
|
|
var el = document.createElement('div');
|
|
el.className = 'beep-lightbox';
|
|
el.innerHTML = '<img src="' + escapeHtml(src) + '" alt="Full size image">';
|
|
document.body.appendChild(el);
|
|
// Close on click outside image or Escape key
|
|
el.addEventListener('click', function(e) {
|
|
if (e.target === el || e.target.tagName !== 'IMG') closeLightbox();
|
|
});
|
|
var escHandler = function(e) {
|
|
if (e.key === 'Escape') { closeLightbox(); document.removeEventListener('keydown', escHandler); }
|
|
};
|
|
document.addEventListener('keydown', escHandler);
|
|
}
|
|
|
|
function closeLightbox() {
|
|
var el = document.querySelector('.beep-lightbox');
|
|
if (el) el.remove();
|
|
}
|
|
|
|
function handleResponse(res) {
|
|
if (!res.ok) return res.json().then(function (j) { throw j; }, function () { throw res; });
|
|
return res.json();
|
|
}
|
|
})();
|
|
/* Beep frontend — end */
|