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