summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jakob/utils/comments.scm4
-rw-r--r--static/css/style.css109
-rw-r--r--static/js/comment-reaction.js140
3 files changed, 187 insertions, 66 deletions
diff --git a/jakob/utils/comments.scm b/jakob/utils/comments.scm
index 15824b1..06c15b5 100644
--- a/jakob/utils/comments.scm
+++ b/jakob/utils/comments.scm
@@ -159,7 +159,9 @@
`(span (@ (class "reaction")) ,(format #f "~a ~a" emote count))))
(comment-reactions comment))
(button (@ (class "add-reaction") (hidden #t)
- (aria-label "Add reaction")) "+")))
+ (aria-label "Add reaction")
+ (data-reply-to-id ,(internal-comment-id comment)))
+ "+")))
'())
,@(if (and (not reply?)
(internal-comment? comment)
diff --git a/static/css/style.css b/static/css/style.css
index 2fee7bf..b096ca0 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -511,6 +511,115 @@ hr {
background: #cc0000;
}
+.modal-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,.4);
+ justify-content: center;
+ align-items: center;
+ z-index: 1000
+}
+
+.modal-content {
+ background-color: #fff;
+ border: 1px solid #666;
+ box-shadow: 2px 2px 0#ccc;
+ width: 320px;
+ padding: 0;
+ font-family: "Bitstream Vera Sans","DejaVu Sans",Verdana,sans-serif
+}
+
+.modal-header {
+ background: linear-gradient(to bottom,#333 0,#222 40%,#111 50%,#222 60%,#333 100%);
+ color: #fff;
+ padding: 5px 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 1px
+}
+
+#close-modal,#reaction-search {
+ font-size: 9px;
+ border: 1px solid #ccc
+}
+
+#close-modal {
+ background: #eee;
+ cursor: pointer;
+ padding: 0 4px
+}
+
+#close-modal:hover {
+ background: #2e8b57;
+ color: #fff
+}
+
+.search-container {
+ padding: 10px;
+ background: #f9f9f9;
+ border-bottom: 1px solid #ccc
+}
+
+#reaction-search {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 4px;
+ font-family: inherit
+}
+
+#reaction-search:focus {
+ outline: 0;
+ border-color: #2e8b57
+}
+
+.reaction-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill,minmax(35px,1fr));
+ gap: 4px;
+ overflow-y: auto;
+ max-height: 250px;
+ padding: 10px;
+ background: #fff
+}
+
+.reaction-item {
+ cursor: pointer;
+ font-size: 1.4rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid transparent
+}
+
+.reaction-item:hover {
+ background-color: #eee;
+ border: 1px solid #2e8b57;
+ transform: none
+}
+
+#reaction-trigger {
+ background: #eee;
+ border: 1px solid #ccc;
+ font-size: 9px;
+ padding: 5px 10px;
+ cursor: pointer;
+ text-transform: uppercase;
+ font-family: inherit
+}
+
+#reaction-trigger:hover {
+ background: #e0e0e0;
+ border-color: #999
+}
+
.legacy-warning {
background: linear-gradient(to bottom,#fff3e0 0,#ffe0b2 100%);
border: 1px dotted #fb8c00;
diff --git a/static/js/comment-reaction.js b/static/js/comment-reaction.js
index 4536717..1723c64 100644
--- a/static/js/comment-reaction.js
+++ b/static/js/comment-reaction.js
@@ -54,80 +54,90 @@ function postReaction(reaction, callback) {
xhr.send(encodeAsFormData(reaction));
}
-function selectorWidgetName(id) {
- return `emoji-picker-${id}`;
-}
+let allReactions = null;
+
+async function promptForReaction() {
+ const modal = document.getElementById('reaction-modal');
+ const searchInput = document.getElementById('reaction-search');
+ const listContainer = document.getElementById('reaction-list');
+ const closeBtn = document.getElementById('close-modal');
-// Simple Emoji picker widget.
-function selectorWidget(id) {
- function emojiButton(codepoint) {
- let span = document.createElement("span");
- span.innerHTML = String.fromCodePoint(codepoint);
- return span;
+ if (!allReactions) {
+ try {
+ const response = await fetch('/static/data-by-emoji.json');
+ allReactions = await response.json();
+ } catch (e) {
+ console.error("Failed to load reactions", e);
+ return null;
+ }
}
- let selector = document.createElement("div");
- selector.setAttribute("id", selectorWidgetName(id));
- selector.setAttribute("class", "emoji-picker");
+ return new Promise((resolve) => {
+ const render = (data) => {
+ listContainer.innerHTML = '';
+ Object.entries(data).forEach(([reaction, info]) => {
+ const span = document.createElement('span');
+ span.className = 'reaction-item';
+ span.textContent = reaction;
+ span.title = info.name;
+ span.onclick = () => cleanup(reaction);
+ listContainer.appendChild(span);
+ });
+ };
- for (let i = 0x1F600; i < 0x1F64F; i++) {
- selector.appendChild(emojiButton(i));
- }
- selector.appendChild(emojiButton(0x1F37B));
- selector.appendChild(emojiButton(0x1F440));
- selector.appendChild(emojiButton(0x1F446));
- selector.appendChild(emojiButton(0x1F447));
- selector.appendChild(emojiButton(0x1F448));
- selector.appendChild(emojiButton(0x1F449));
- selector.appendChild(emojiButton(0x1F450));
- selector.appendChild(emojiButton(0x270A));
- selector.appendChild(emojiButton(0x270B));
- selector.appendChild(emojiButton(0x270C));
- selector.appendChild(emojiButton(0x270D));
- for (let i = 0x1F90C; i < 0x1F9FF; i++) {
- selector.appendChild(emojiButton(i));
- }
- return selector;
-}
+ const handleSearch = (e) => {
+ const term = e.target.value.toLowerCase();
+ const filtered = Object.fromEntries(
+ Object.entries(allReactions).filter(([_, info]) =>
+ info.name.toLowerCase().includes(term) || info.slug.toLowerCase().includes(term)
+ )
+ );
+ render(filtered);
+ };
-// Add a "react" button to every local comment.
-window.addEventListener("load", () => {
- // Unhide additional comment actions if Javascript is enabled.
- let additionalActionsRows = document.querySelectorAll(".comment-additional-actions");
- for (let row of additionalActionsRows) {
- row.removeAttribute("hidden");
- }
+ const cleanup = (result) => {
+ modal.style.display = 'none';
+ closeBtn.onclick = null;
+ searchInput.oninput = null;
+ resolve(result);
+ };
- let reactButtons = document.querySelectorAll(".comment-react-button");
- for (let button of reactButtons) {
- let id = button.getAttribute("data-reply-to-id");
+ searchInput.value = '';
+ render(allReactions);
+ modal.style.display = 'flex';
- button.addEventListener('click', (e) => {
- // If we already made a selector widget for this comment, let's "hide" the
- // one that exists.
- let existingSelectorWidget = document.getElementById(selectorWidgetName(id));
- if (existingSelectorWidget !== null) {
- existingSelectorWidget.remove();
- e.preventDefault();
- return;
- }
+ closeBtn.onclick = () => cleanup(null);
+ searchInput.oninput = handleSearch;
- let emojiPicker = selectorWidget(id);
- button.parentNode.parentNode.insertBefore(emojiPicker, button.parentNode);
- for (let button of emojiPicker.children) {
- button.addEventListener('click', (e) => {
- postReaction({
- "id": button.getAttribute("data-react-to-id"),
- "reaction": button.innerHTML
- }, () => {
- let reactAcknowledgement = document.createElement("span");
- reactAcknowledgement.innerHTML = "successfully reacted!"
- button.replaceWith(reactAcknowledgement);
- });
- emojiPicker.remove();
- });
- }
+ modal.onclick = (e) => { if(e.target === modal) cleanup(null); };
+ });
+}
+window.addEventListener("load", () => {
+ const modalHTML = `
+<div id="reaction-modal" class="modal-overlay">
+ <div class="modal-content">
+ <div class="modal-header">
+ <span>Select a reaction</span>
+ <button id="close-modal">X</button>
+ </div>
+ <div class="search-container">
+ <input type="text" id="reaction-search" placeholder="Search reactions..." />
+ </div>
+ <div id="reaction-list" class="reaction-grid"></div>
+ </div>
+</div>`;
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
+ const reactButtons = document.querySelectorAll(".add-reaction");
+ for (const button of reactButtons) {
+ button.removeAttribute("hidden");
+ button.addEventListener('click', async (e) => {
+ let reaction = await promptForReaction();
+ console.log(reaction);
+ postReaction({
+ "id": button.getAttribute("data-reply-to-id"),
+ "reaction": reaction
+ }, window.location.reload);
e.preventDefault();
});
}