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 =
;
- 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">
+
+
+ {searchElem = }
+
+
+
+
+
+
;
+
+ 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(' ');