diff --git a/src/mami.css/colpick.css b/src/mami.css/colpick.css index 6dafb86..3532f12 100644 --- a/src/mami.css/colpick.css +++ b/src/mami.css/colpick.css @@ -1,17 +1,13 @@ .colpick { accent-color: var(--colpick-colour, #000); - border-radius: 5px; border: 2px solid var(--colpick-colour, #000); - padding: 3px; + padding: 2px; display: flex; flex-direction: column; position: absolute; background-color: #444; color: #fff; box-shadow: 0 3px 10px #000; - font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; - font-size: 12px; - line-height: 20px; } .colpick-tabbed { @@ -20,14 +16,12 @@ } .colpick-tabbed-container { border: 2px solid var(--colpick-colour, #000); - border-radius: 5px 5px 0 0; height: 234px; overflow: auto; scrollbar-width: thin; } .colpick-tabbed-list { background-color: #222; - border-radius: 0 0 5px 5px; overflow: auto; } @@ -71,7 +65,6 @@ display: block; width: 60px; height: 60px; - border-radius: 5px; background: var(--colpick-colour, #000); } @@ -118,7 +111,7 @@ } .colpick-buttons-button { - border-radius: 5px; + border-radius: 0; background: #222; border-width: 0; color: #fff; @@ -156,7 +149,7 @@ border: 0; width: 42px; height: 42px; - border-radius: 5px; + border-radius: 0; text-decoration: none; color: #fff; cursor: pointer; diff --git a/src/mami.css/emopick.css b/src/mami.css/emopick.css new file mode 100644 index 0000000..23e943b --- /dev/null +++ b/src/mami.css/emopick.css @@ -0,0 +1,95 @@ +.emopick { + padding: 2px; + display: flex; + flex-direction: column; + position: absolute; + background-color: #444; + color: #fff; + box-shadow: 0 3px 10px #000; + max-width: 334px; + width: 100%; +} + +.emopick-emote { + background: transparent; + border: 0; + border-radius: 0; + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: background .1s; +} +.emopick-emote:hover, +.emopick-emote:focus { + background: #fff4; +} +.emopick-emote:active { + background: #0004; +} +.emopick-emote img { + max-width: 100%; + max-height: 100%; + display: block; + image-rendering: crisp-edges; +} + +.emopick-list { + max-height: 250px; + height: 100%; + overflow: auto; + scrollbar-width: thin; + background: #333; +} +.emopick-list-scroll { + display: flex; + flex-wrap: wrap; + gap: 2px; + padding: 2px; +} + +.emopick-search { + display: flex; + margin: 2px 0; +} +.emopick-search-input { + flex-grow: 1; + flex-shrink: 1; + padding: 4px; + border: 0; + border-radius: 0; + color: inherit; + background: #333; + font-size: inherit; + font-family: inherit; +} + +.emopick-actions { + display: flex; + justify-content: space-between; +} + +.emopick-action-toggle { + display: block; +} +.emopick-action-toggle-box { + margin: 0 2px; +} +.emopick-action-toggle-label { + margin: 0 2px; +} + +.emopick-action-button { + display: block; + border: 0; + border-radius: 0; + background: #555; + color: #fff; + font-family: inherit; + padding: 0 4px; +} +.emopick-action-button:focus { + box-shadow: 0 0 0 1px #000, inset 0 0 0 1px #fff; +} diff --git a/src/mami.css/main.css b/src/mami.css/main.css index 9ebc7a3..7a9ee02 100644 --- a/src/mami.css/main.css +++ b/src/mami.css/main.css @@ -89,6 +89,7 @@ a:hover { @include youare.css; @include colpick.css; +@include emopick.css; @include themes/archaic.css; @include themes/blue.css; diff --git a/src/mami.js/colpick/picker.jsx b/src/mami.js/colpick/picker.jsx index ac4d8b5..cd9a686 100644 --- a/src/mami.js/colpick/picker.jsx +++ b/src/mami.js/colpick/picker.jsx @@ -42,7 +42,7 @@ const MamiColourPicker = function(options) { let tabsElem, tabsContainer, tabsList; let values, buttons; - const html =
{ ev.preventDefault(); runResolve(); return false; }}> + const html = { ev.preventDefault(); runResolve(); return false; }}> {tabsElem =
{tabsContainer =
} {tabsList =
} @@ -58,7 +58,7 @@ const MamiColourPicker = function(options) {
; - const close = () => html.parentNode.removeChild(html); + const close = () => html.classList.add('hidden'); const setColour = (raw, mask) => { raw = typeof raw === 'number' ? (parseInt(raw) & 0xFFFFFF) : 0; @@ -79,15 +79,15 @@ const MamiColourPicker = function(options) { if(typeof pos !== 'object') throw 'pos must be an object'; - html.style.top = 'y' in pos && pos.y >= 0 ? `${pos.y}px` : null; - html.style.left = 'x' in pos && pos.x >= 0 ? `${pos.x}px` : ''; + html.style.bottom = 'y' in pos && pos.y >= 0 ? `${pos.y}px` : null; + html.style.left = 'x' in pos && pos.x >= 0 ? `${pos.x}px` : null; }; const tabs = new MamiTabsControl({ onAdd: ctx => { - const name = ctx.info.name, - containerName = `colpick-tab-${name}-container`, - buttonName = `colpick-tab-${name}-button`; + const name = ctx.info.name; + const containerName = `colpick-tab-${name}-container`; + const buttonName = `colpick-tab-${name}-button`; needsColour.push(ctx.info); ctx.info.onChange(setColour); @@ -107,11 +107,11 @@ const MamiColourPicker = function(options) { onSwitch: ctx => { if(ctx.from !== undefined) { ctx.from.elem.classList.toggle('colpick-tab-container-inactive', true); - $q(`.colpick-tab-${ctx.from.info.name}-button`).classList.toggle('colpick-tab-button-active', false); + tabsList.querySelector(`.colpick-tab-${ctx.from.info.name}-button`).classList.toggle('colpick-tab-button-active', false); } ctx.elem.classList.toggle('colpick-tab-container-inactive', false); - $q(`.colpick-tab-${ctx.info.name}-button`).classList.toggle('colpick-tab-button-active', true); + tabsList.querySelector(`.colpick-tab-${ctx.info.name}-button`).classList.toggle('colpick-tab-button-active', true); }, }); @@ -168,6 +168,20 @@ const MamiColourPicker = function(options) { setPosition: setPosition, close: close, dialog: pos => { + html.classList.remove('hidden'); + + if(pos instanceof MouseEvent) { + const mbb = html.getBoundingClientRect(); + const pbb = html.parentNode.getBoundingClientRect(); + let x = pos.clientX; + let y = pbb.height - pos.clientY; + + if(x > (pbb.width - mbb.width)) + x = 10; + + pos = { x: x, y: y }; + } + if(pos !== undefined) setPosition(pos); @@ -176,22 +190,5 @@ const MamiColourPicker = function(options) { promiseReject = reject; }); }, - suggestPosition: mouseEvent => { - let x = 10, y = 10; - - if(html.parentNode.clientWidth > 340) { - x = mouseEvent.clientX; - y = mouseEvent.clientY; - - const bb = html.getBoundingClientRect(); - - if(y > bb.height + 20) - y -= bb.height; - if(x > html.parentNode.clientWidth - bb.width - 20) - x -= bb.width; - } - - return { x: x, y: y }; - }, }; }; diff --git a/src/mami.js/emotes.js b/src/mami.js/emotes.js index 6664fed..7f2ca0d 100644 --- a/src/mami.js/emotes.js +++ b/src/mami.js/emotes.js @@ -37,6 +37,14 @@ const MamiEmotes = (function() { if(emote.minRank <= minRank) callback(emote); }, + all: minRank => { + const items = []; + for(const emote of emotes) + if(emote.minRank <= minRank) + items.push(emote); + + return items; + }, findByName: function(minRank, name, returnString) { const found = []; for(const emote of emotes) diff --git a/src/mami.js/emotes/picker.jsx b/src/mami.js/emotes/picker.jsx new file mode 100644 index 0000000..282a1b5 --- /dev/null +++ b/src/mami.js/emotes/picker.jsx @@ -0,0 +1,139 @@ +#include args.js +#include utility.js + +const MamiEmotePicker = function(args) { + args = MamiArgs('args', args, define => { + define('getEmotes').type('function').done(); + define('setKeepOpenOnPick').type('function').done(); + define('onPick').type('function').done(); + define('onClose').type('function').done(); + }); + + let emotes; + + let listElem, searchElem, keepOpenToggleElem; + const html =
{ + if(!keepOpenToggleElem.checked && !html.contains(ev.relatedTarget)) + close(); + }} tabindex="0"> +
+ {listElem =
} +
+ +
+ + +
+
; + + const buildList = () => { + $rc(listElem); + + for(const emote of emotes) + listElem.appendChild(); + }; + + searchElem.addEventListener('keyup', ev => { + const elems = Array.from(listElem.children); + + if(ev.key === 'Escape') { + close(); + return; + } + + if(ev.key === 'Enter' || ev.key === 'NumpadEnter') { + for(const elem of elems) + if(!elem.classList.contains('hidden')) { + elem.click(); + break; + } + + searchElem.focus(); + return; + } + + for(const elem of elems) { + const filter = searchElem.value.trim(); + let hidden = false; + if(filter !== '') { + hidden = true; + const strings = elem.dataset.strings.split(' '); + for(const string of strings) + if(string.includes(filter)) { + hidden = false; + break; + } + } + + elem.classList.toggle('hidden', hidden); + } + }); + + let promiseResolve; + const close = () => { + if(promiseResolve !== undefined) { + promiseResolve(); + promiseResolve = undefined; + } + + if(args.onClose !== undefined) + args.onClose(); + + html.classList.add('hidden'); + }; + + const setPosition = pos => { + if(typeof pos !== 'object') + throw 'pos must be an object'; + + html.style.bottom = 'y' in pos && pos.y >= 0 ? `${pos.y}px` : null; + html.style.right = 'x' in pos && pos.x >= 0 ? `${pos.x}px` : null; + }; + + return { + get element() { return html; }, + get keepOpenOnPick() { return keepOpenToggleElem.checked; }, + set keepOpenOnPick(value) { keepOpenToggleElem.checked = value; }, + setPosition: setPosition, + close: close, + dialog: pos => { + emotes = args.getEmotes(); + buildList(); + + html.classList.remove('hidden'); + + if(pos instanceof MouseEvent) { + const mbb = html.getBoundingClientRect(); + const pbb = html.parentNode.getBoundingClientRect(); + let x = pbb.width - pos.clientX; + let y = pbb.height - pos.clientY; + + if(x > mbb.width) + x -= mbb.width; + + pos = { x: x, y: y }; + } + + if(pos !== undefined) + setPosition(pos); + + searchElem.focus(); + + return new Promise(resolve => { promiseResolve = resolve; }); + }, + }; +}; diff --git a/src/mami.js/main.js b/src/mami.js/main.js index a323815..1dc63d8 100644 --- a/src/mami.js/main.js +++ b/src/mami.js/main.js @@ -23,6 +23,7 @@ window.Umi = { UI: {} }; #include controls/ping.jsx #include controls/views.js #include eeprom/eeprom.js +#include emotes/picker.jsx #include notices/baka.jsx #include notices/youare.jsx #include settings/backup.js @@ -150,6 +151,7 @@ const MamiInit = async args => { settings.define('dbgAnimDurationMulti').default(1).min(0).max(10).create(); settings.define('newLineOnEnter').default(false).create(); settings.define('showMarkupSelector').type(['always', 'focus', 'never']).default('focus').create(); + settings.define('keepEmotePickerOpen').default(true).create(); const noNotifSupport = !('Notification' in window); settings.define('enableNotifications').default(false).immutable(noNotifSupport).critical().create(); @@ -229,13 +231,8 @@ const MamiInit = async args => { MamiEmotes.loadLegacy(emotes); } catch(ex) { console.error('Failed to load emoticons.', ex); - } finally { - // this is currently called in the sock chat handlers - // does a permissions check which it can't do at this point - //Umi.UI.Emoticons.Init(); } - const onHashChange = () => { if(location.hash === '#reset') { settings.clear(true); @@ -461,6 +458,7 @@ const MamiInit = async args => { category.setting('eepromAutoInsert').title('Auto-insert uploads').done(); category.setting('autoEmbedV1').title('Auto-embed media').done(); category.setting('autoEmbedPlay').title('Auto-play embedded media').done(); + category.setting('keepEmotePickerOpen').title('Keep emoticon picker open').done(); category.setting('newLineOnEnter').title('Swap Enter and Shift+Enter behaviour').done(); category.setting('showMarkupSelector').title('Show markup buttons').type('select').options(() => { return { @@ -558,7 +556,6 @@ const MamiInit = async args => { MamiEmotes.clear(); MamiEmotes.loadLegacy(emotes); } finally { - Umi.UI.Emoticons.Init(); button.textContent = textOrig; button.disabled = false; } @@ -669,7 +666,34 @@ const MamiInit = async args => { sidebar.createAction(sbActPing); Umi.UI.InputMenus.Add('markup'); - Umi.UI.InputMenus.Add('emotes', 'Emoticons'); + Umi.UI.Markup.SetPickerTarget(layout.getElement()); + + let emotePicker, emotePickerVisible = false; + Umi.UI.InputMenus.AddButton('emotes', 'Emoticons', ev => { + if(emotePicker === undefined) { + emotePicker = new MamiEmotePicker({ + onClose: () => { emotePickerVisible = false; }, + onPick: emote => { + const emoteStr = `:${emote.strings[0]}:`; + Umi.UI.View.EnterAtCursor(emoteStr); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + emoteStr.length); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition(), true); + Umi.UI.View.Focus(); + }, + getEmotes: () => MamiEmotes.all(Umi.User.getCurrentUser().perms.rank), + setKeepOpenOnPick: value => { settings.set('keepEmotePickerOpen', value); }, + }); + layout.getElement().appendChild(emotePicker.element); + settings.watch('keepEmotePickerOpen', ev => { emotePicker.keepOpenOnPick = ev.detail.value; }); + } + + if(emotePickerVisible) { + emotePicker.close(); + } else { + emotePickerVisible = true; + emotePicker.dialog(ev); + } + }); settings.watch('showMarkupSelector', ev => { Umi.UI.InputMenus.Toggle('markup', ev.detail.value === 'always'); diff --git a/src/mami.js/sockchat/handlers.js b/src/mami.js/sockchat/handlers.js index a7d3124..d1484d2 100644 --- a/src/mami.js/sockchat/handlers.js +++ b/src/mami.js/sockchat/handlers.js @@ -104,7 +104,6 @@ const MamiSockChatHandlers = function( sbUsers.createEntry(ev.detail.user); Umi.UI.Markup.Reset(); - Umi.UI.Emoticons.Init(); Umi.Parsing.Init(); if(ctx.views.count > 1) diff --git a/src/mami.js/ui/emotes.js b/src/mami.js/ui/emotes.js index 1621fde..9ac53d7 100644 --- a/src/mami.js/ui/emotes.js +++ b/src/mami.js/ui/emotes.js @@ -1,41 +1,12 @@ #include emotes.js #include utility.js -#include ui/input-menus.js -#include ui/view.js Umi.UI.Emoticons = (function() { return { - Init: function() { - const menu = Umi.UI.InputMenus.Get('emotes'); - menu.innerHTML = ''; - - MamiEmotes.forEach(Umi.User.getCurrentUser().perms.rank, function(emote) { - menu.appendChild($e({ - tag: 'button', - attrs: { - type: 'button', - className: 'emoticon emoticon--button', - title: emote.strings[0], - dataset: { - umiEmoticon: ':' + emote.strings[0] + ':', - }, - onclick: 'Umi.UI.Emoticons.Insert(this)', - }, - child: { - tag: 'img', - attrs: { - className: 'emoticon', - src: emote.url, - alt: emote.strings[0], - }, - } - })); - }); - }, - Parse: function(element, message) { + Parse: function(element, author) { let inner = element.innerHTML; - MamiEmotes.forEach(message?.author?.perms?.rank ?? 0, function(emote) { + MamiEmotes.forEach(author?.perms?.rank ?? 0, function(emote) { const image = $e({ tag: 'img', attrs: { @@ -54,12 +25,5 @@ Umi.UI.Emoticons = (function() { element.innerHTML = inner; }, - Insert: function(sender) { - const emoticon = sender.getAttribute('data-umi-emoticon'); - Umi.UI.View.EnterAtCursor(sender.getAttribute('data-umi-emoticon')); - Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + emoticon.length); - Umi.UI.View.SetPosition(Umi.UI.View.GetPosition(), true); - Umi.UI.View.Focus(); - }, }; })(); diff --git a/src/mami.js/ui/hooks.js b/src/mami.js/ui/hooks.js index 20e72ed..6e5d072 100644 --- a/src/mami.js/ui/hooks.js +++ b/src/mami.js/ui/hooks.js @@ -23,15 +23,11 @@ Umi.UI.Hooks = (function() { }); msgForm.addEventListener('focusin', ev => { - console.info(ev); - if(mami.settings.get('showMarkupSelector') === 'focus' && Umi.UI.InputMenus.Current() === '') Umi.UI.InputMenus.Toggle('markup', true); }); msgForm.addEventListener('focusout', ev => { - console.info(ev); - - if(ev.relatedTarget instanceof Element && msgForm.contains(ev.relatedTarget)) + if(msgForm.contains(ev.relatedTarget)) return; if(mami.settings.get('showMarkupSelector') === 'focus' && Umi.UI.InputMenus.Current() === 'markup') diff --git a/src/mami.js/ui/input-menus.js b/src/mami.js/ui/input-menus.js index deeaa9c..1ea3f00 100644 --- a/src/mami.js/ui/input-menus.js +++ b/src/mami.js/ui/input-menus.js @@ -55,6 +55,9 @@ Umi.UI.InputMenus = (function() { Current: () => current, Toggle: toggle, Add: function(baseId, title, beforeButtonId) { + if(baseId !== 'markup') + throw 'only baseId "markup" may be added'; + if(ids.includes(baseId)) return; ids.push(baseId); diff --git a/src/mami.js/ui/markup.js b/src/mami.js/ui/markup.js index 553efbd..c5231c7 100644 --- a/src/mami.js/ui/markup.js +++ b/src/mami.js/ui/markup.js @@ -17,21 +17,19 @@ Umi.UI.Markup = (function() { Umi.UI.View.Focus(); }; - const pickerTarget = document.body; + let pickerTarget = document.body; let picker, pickerVisible = false; const insert = function(ev) { if(this.dataset.umiTagName === 'color' && !pickerVisible) { pickerVisible = true; - if(picker === undefined) - picker = new MamiColourPicker({ - presets: futami.get('colours'), - }); + if(picker === undefined) { + picker = new MamiColourPicker({ presets: futami.get('colours') }); + pickerTarget.appendChild(picker.element); + } - pickerTarget.appendChild(picker.element); - - picker.dialog(picker.suggestPosition(ev)) + picker.dialog(ev) .then(colour => insertRaw(`[color=${MamiColour.hex(colour)}]`, '[/color]')) .finally(() => pickerVisible = false); } else @@ -42,6 +40,7 @@ Umi.UI.Markup = (function() { }; return { + SetPickerTarget: target => { pickerTarget = target; }, Add: function(name, text, beforeCursor, afterCursor) { Umi.UI.InputMenus.Get('markup').appendChild($e({ tag: 'button', diff --git a/src/mami.js/ui/messages.jsx b/src/mami.js/ui/messages.jsx index 3e48b2f..05dea20 100644 --- a/src/mami.js/ui/messages.jsx +++ b/src/mami.js/ui/messages.jsx @@ -240,7 +240,7 @@ Umi.UI.Messages = (function() { eText.innerText = msgText; if(!skipTextParsing) { - Umi.UI.Emoticons.Parse(eText, msg); + Umi.UI.Emoticons.Parse(eText, msgAuthor); Umi.Parsing.Parse(eText, msg); const textSplit = eText.innerText.split(' ');