diff --git a/www/css/cytube.css b/www/css/cytube.css
index 719340cd..98f8c01c 100644
--- a/www/css/cytube.css
+++ b/www/css/cytube.css
@@ -604,34 +604,51 @@ table td {
border-top-width: 0;
}
-.emotelist-table {
- margin: auto;
-}
+.emotelist-table { margin: auto; }
+.emote-preview-container { width: 60px; height: 60px; float: left; text-align: center; white-space: nowrap; margin: 3px; }
+.emote-preview-hax { display: inline-block; vertical-align: middle; height: 100%; }
+.emote-preview { max-width: 56px; max-height: 56px; cursor: pointer; }
+.emotelist-paginator-container { text-align: center; }
-.emote-preview-container {
- width: 100px;
- height: 100px;
- float: left;
- text-align: center;
- white-space: nowrap;
- margin: 5px;
+/* Emote browser panel */
+#emote-browser {
+ display: none;
+ position: fixed;
+ z-index: 1060;
+ width: 340px;
+ background: #272b30;
+ border: 1px solid #555;
+ border-radius: 4px;
+ box-shadow: 0 4px 14px rgba(0,0,0,.6);
+ padding: 6px;
}
-
-.emote-preview-hax {
- display: inline-block;
- vertical-align: middle;
- height: 100%;
+#emote-browser-search {
+ width: 100%;
+ margin-bottom: 6px;
+ background: #1a1d20;
+ color: #c8c8c8;
+ border-color: #555;
}
-
-.emote-preview {
- max-width: 100px;
- max-height: 100px;
+#emote-browser-search:focus { border-color: #888; outline: none; box-shadow: none; }
+#emote-browser-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ max-height: 320px;
+ overflow-y: auto;
+}
+.emote-browser-item {
+ width: 52px;
+ height: 52px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
cursor: pointer;
+ border-radius: 3px;
+ flex-shrink: 0;
}
-
-.emotelist-paginator-container {
- text-align: center;
-}
+.emote-browser-item:hover { background: #3a3f44; }
+.emote-browser-item img { max-width: 48px; max-height: 48px; object-fit: contain; }
#leftcontrols .btn {
margin-right: 5px;
@@ -880,3 +897,7 @@ body.tv #controlsrow,
body.tv #playlistrow {
display: none;
}
+
+.emote-suggest-item { padding: 3px 8px; cursor: pointer; display: flex; align-items: center; gap: 7px; font-size: 13px; color: #c8c8c8; }
+.emote-suggest-item img { width: 24px; height: 24px; object-fit: contain; flex-shrink: 0; }
+.emote-suggest-item:hover, .emote-suggest-item.active { background: #3a3f44; color: #fff; }
diff --git a/www/js/ui.js b/www/js/ui.js
index a9c84d22..4717438f 100644
--- a/www/js/ui.js
+++ b/www/js/ui.js
@@ -165,9 +165,78 @@ function chatTabComplete(chatline) {
chatline.setSelectionRange(result.newPosition, result.newPosition);
}
+/* emote autocomplete */
+var EMOTE_SUGGEST_IDX = 0;
+function emoteLastWord() {
+ var words = $("#chatline").val().split(" ");
+ return words[words.length - 1].toLowerCase();
+}
+function emoteAccept() {
+ var item = $("#emote-suggestions .active");
+ if (!item.length) item = $("#emote-suggestions").children().first();
+ if (!item.length) return;
+ var words = $("#chatline").val().split(" ");
+ words[words.length - 1] = item.data("name") + " ";
+ $("#chatline").val(words.join(" "));
+ $("#emote-suggestions").hide();
+}
+function emoteRefresh() {
+ var partial = emoteLastWord();
+ var popup = $("#emote-suggestions");
+ if (partial.length < 2 || !CHANNEL.emotes || !CHANNEL.emotes.length) { popup.hide(); return; }
+ var matches = CHANNEL.emotes.filter(function(e) {
+ return e.name.toLowerCase().indexOf(partial) === 0;
+ }).slice(0, 8);
+ if (!matches.length) { popup.hide(); return; }
+ popup.empty();
+ matches.forEach(function(e) {
+ $("
").addClass("emote-suggest-item")
+ .data("name", e.name)
+ .html('

' + e.name)
+ .appendTo(popup);
+ });
+ popup.children().first().addClass("active");
+ EMOTE_SUGGEST_IDX = 0;
+ var cl = $("#chatline"), off = cl.offset();
+ popup.css({ left: off.left, width: cl.outerWidth() }).show();
+ popup.css("top", off.top - popup.outerHeight() - 2);
+}
+$("#chatline").on("input", emoteRefresh);
+$(document).on("mousedown", function(e) {
+ if (!$(e.target).closest("#emote-suggestions, #chatline").length)
+ $("#emote-suggestions").hide();
+});
+$(document).on("mousedown", "#emote-suggestions .emote-suggest-item", function() {
+ EMOTE_SUGGEST_IDX = $(this).index();
+ $("#emote-suggestions .active").removeClass("active");
+ $(this).addClass("active");
+ emoteAccept();
+ $("#chatline").focus();
+});
+$("body").append('
');
+
$("#chatline").on('keydown', function(ev) {
+ var open = $("#emote-suggestions").is(":visible");
+ if (open) {
+ if (ev.keyCode == 27) { // Escape
+ $("#emote-suggestions").hide();
+ ev.preventDefault(); return false;
+ } else if (ev.keyCode == 9 || (ev.keyCode == 39 && this.selectionStart === this.value.length)) { // Tab or right arrow at end
+ emoteAccept();
+ ev.preventDefault(); return false;
+ } else if (ev.keyCode == 38 || ev.keyCode == 40) { // Up/down navigate
+ var items = $("#emote-suggestions").children();
+ items.eq(EMOTE_SUGGEST_IDX).removeClass("active");
+ EMOTE_SUGGEST_IDX = ev.keyCode == 38
+ ? (EMOTE_SUGGEST_IDX - 1 + items.length) % items.length
+ : (EMOTE_SUGGEST_IDX + 1) % items.length;
+ items.eq(EMOTE_SUGGEST_IDX).addClass("active")[0].scrollIntoView({ block: "nearest" });
+ ev.preventDefault(); return false;
+ }
+ }
// Enter/return
if(ev.keyCode == 13) {
+ $("#emote-suggestions").hide();
if (CHATTHROTTLE) {
return;
}
@@ -878,17 +947,101 @@ applyOpts();
})();
var EMOTELISTMODAL = $("#emotelist");
-$("#emotelistbtn").on('click', function () {
- EMOTELISTMODAL.modal();
-});
-
-EMOTELISTMODAL.on('shown.bs.modal', function () { $('.emotelist-search').trigger('focus') });
EMOTELISTMODAL.find(".emotelist-alphabetical").change(function () {
USEROPTS.emotelist_sort = this.checked;
setOpt("emotelist_sort", USEROPTS.emotelist_sort);
});
EMOTELISTMODAL.find(".emotelist-alphabetical").prop("checked", USEROPTS.emotelist_sort);
+/* emote browser panel */
+var EMOTE_BROWSER_OFFSET = 0;
+var EMOTE_BROWSER_BATCH = 40;
+var EMOTE_BROWSER_FILTER = '';
+
+$('body').append(
+ '
'
+);
+
+function emoteBrowserMatches() {
+ if (!CHANNEL.emotes) return [];
+ var f = EMOTE_BROWSER_FILTER.toLowerCase();
+ return f ? CHANNEL.emotes.filter(function(e) { return e.name.toLowerCase().indexOf(f) !== -1; })
+ : CHANNEL.emotes;
+}
+
+function emoteBrowserRenderMore() {
+ var matches = emoteBrowserMatches();
+ var end = Math.min(EMOTE_BROWSER_OFFSET + EMOTE_BROWSER_BATCH, matches.length);
+ var grid = document.getElementById('emote-browser-grid');
+ for (var i = EMOTE_BROWSER_OFFSET; i < end; i++) {
+ (function(emote) {
+ var item = document.createElement('div');
+ item.className = 'emote-browser-item';
+ item.title = emote.name;
+ var img = document.createElement('img');
+ img.src = emote.image;
+ item.appendChild(img);
+ item.addEventListener('click', function() {
+ var cl = document.getElementById('chatline');
+ var val = cl.value;
+ if (val && !val.charAt(val.length - 1).match(/\s/)) val += ' ';
+ cl.value = val + emote.name;
+ $("#emote-browser").hide();
+ cl.focus();
+ });
+ grid.appendChild(item);
+ })(matches[i]);
+ }
+ EMOTE_BROWSER_OFFSET = end;
+}
+
+function emoteBrowserReset() {
+ EMOTE_BROWSER_OFFSET = 0;
+ document.getElementById('emote-browser-grid').innerHTML = '';
+ emoteBrowserRenderMore();
+}
+
+function emoteBrowserPosition() {
+ var btn = $("#emotelistbtn"), off = btn.offset();
+ var panel = $("#emote-browser");
+ var pw = panel.outerWidth(), ph = panel.outerHeight();
+ var ww = $(window).width(), wh = $(window).height();
+ var left = off.left;
+ if (left + pw > ww - 8) left = Math.max(8, off.left + btn.outerWidth() - pw);
+ var top = off.top - ph - 4;
+ if (top < 8) top = off.top + btn.outerHeight() + 4;
+ panel.css({ top: top, left: left });
+}
+
+$("#emotelistbtn").on('click', function () {
+ var panel = $("#emote-browser");
+ if (panel.is(':visible')) { panel.hide(); return; }
+ EMOTE_BROWSER_FILTER = '';
+ $("#emote-browser-search").val('');
+ emoteBrowserReset();
+ panel.show();
+ emoteBrowserPosition();
+ document.getElementById('emote-browser-search').focus();
+});
+
+$(document).on('click.emotebrowser', function (e) {
+ if (!$(e.target).closest('#emote-browser, #emotelistbtn').length)
+ $("#emote-browser").hide();
+});
+
+$(document).on('input', '#emote-browser-search', function () {
+ EMOTE_BROWSER_FILTER = this.value;
+ emoteBrowserReset();
+});
+
+document.getElementById('emote-browser-grid').addEventListener('scroll', function () {
+ if (this.scrollTop + this.clientHeight >= this.scrollHeight - 60)
+ emoteBrowserRenderMore();
+});
+
$("#fullscreenbtn").on('click', function () {
var elem = document.querySelector("#videowrap .embed-responsive");
// this shit is why frontend web development sucks