Rewrote code for the input box.
This commit is contained in:
parent
cd16a5d1b5
commit
46549845b9
24 changed files with 483 additions and 584 deletions
|
@ -5,12 +5,12 @@
|
|||
|
||||
.input__main {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
max-height: 140px;
|
||||
}
|
||||
|
||||
.input__text {
|
||||
flex-grow: 1;
|
||||
height: 40px;
|
||||
max-height: 140px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
|
@ -32,37 +32,15 @@
|
|||
cursor: pointer
|
||||
}
|
||||
|
||||
.input__button:before {
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900
|
||||
}
|
||||
|
||||
.input__button--markup:before {
|
||||
content: "\f121"
|
||||
}
|
||||
|
||||
.input__button--emotes:before {
|
||||
content: "\f118"
|
||||
}
|
||||
|
||||
.input__button--upload:before {
|
||||
content: "\f574"
|
||||
}
|
||||
|
||||
.input__button--send:before {
|
||||
content: "\f1d8"
|
||||
}
|
||||
|
||||
.input__menus {
|
||||
max-height: 100px;
|
||||
overflow: auto
|
||||
}
|
||||
|
||||
.input__menu {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
.input__menu:empty {
|
||||
display: none;
|
||||
padding: 1px
|
||||
}
|
||||
|
||||
.input__menu--active {
|
||||
display: block
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ a:hover {
|
|||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
visibility: none !important;
|
||||
}
|
||||
|
||||
.sjis {
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
.markup__button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
margin: 2px;
|
||||
transition: background .1s
|
||||
transition: background .1s;
|
||||
min-width: 26px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markup__link {
|
||||
color: #1e90ff;
|
||||
text-decoration: none
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markup__link:hover,
|
||||
.markup__link:focus {
|
||||
text-decoration: underline
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.chat:not(.mami-do-not-mark-links-as-visited) .markup__link--visited,
|
||||
|
|
30
src/mami.js/chatform/form.jsx
Normal file
30
src/mami.js/chatform/form.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
#include chatform/input.jsx
|
||||
#include chatform/markup.jsx
|
||||
|
||||
const MamiChatForm = function(eventTarget) {
|
||||
const input = new MamiChatFormInput(eventTarget);
|
||||
const markup = new MamiChatFormMarkup;
|
||||
|
||||
const html = <form class="input" onsubmit={ev => {
|
||||
ev.preventDefault();
|
||||
|
||||
const text = input.text;
|
||||
input.text = '';
|
||||
input.updateGrowHeight();
|
||||
|
||||
eventTarget.dispatch('send', { text: text });
|
||||
}}>
|
||||
{markup}
|
||||
{input}
|
||||
</form>;
|
||||
|
||||
return {
|
||||
get element() { return html; },
|
||||
get markup() { return markup; },
|
||||
get input() { return input; },
|
||||
|
||||
focus: () => {
|
||||
input.focus();
|
||||
},
|
||||
};
|
||||
};
|
174
src/mami.js/chatform/input.jsx
Normal file
174
src/mami.js/chatform/input.jsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
#include emotes.js
|
||||
#include users.js
|
||||
|
||||
const MamiChatFormInput = function(eventTarget) {
|
||||
const textElem = <textarea class="input__text" name="text" autofocus="autofocus"/>;
|
||||
let submitButton;
|
||||
|
||||
const html = <div class="input__main">
|
||||
{textElem}
|
||||
</div>;
|
||||
|
||||
const createButton = (info, submit) => {
|
||||
const button = <button class="input__button" title={info.title}>
|
||||
<i class={info.icon} />
|
||||
</button>;
|
||||
|
||||
if(submit) {
|
||||
button.type = 'submit';
|
||||
button.classList.add('input__button--send');
|
||||
} else {
|
||||
button.type = 'button';
|
||||
button.onclick = ev => { info.onclick(ev, button, info); };
|
||||
}
|
||||
|
||||
html.insertBefore(button, submitButton);
|
||||
if(submit)
|
||||
submitButton = button;
|
||||
};
|
||||
|
||||
createButton({
|
||||
title: 'Send',
|
||||
icon: 'fas fa-paper-plane',
|
||||
}, true);
|
||||
|
||||
let growInputField = false;
|
||||
let growLastHeight, growBaseHeight;
|
||||
const updateGrowHeight = () => {
|
||||
if(growBaseHeight === undefined)
|
||||
growBaseHeight = textElem.getBoundingClientRect().height;
|
||||
|
||||
let height = growBaseHeight;
|
||||
if(growInputField && textElem.scrollHeight > textElem.clientHeight)
|
||||
height = textElem.scrollHeight;
|
||||
|
||||
textElem.style.height = height > growBaseHeight ? `${height}px` : null;
|
||||
height = textElem.getBoundingClientRect().height;
|
||||
|
||||
if(growLastHeight !== height) {
|
||||
growLastHeight = height;
|
||||
eventTarget.dispatch('resize', {
|
||||
height: height,
|
||||
baseHeight: growBaseHeight,
|
||||
diffHeight: height - growBaseHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
textElem.addEventListener('keyup', () => { updateGrowHeight(); });
|
||||
|
||||
let newLineOnEnter = false;
|
||||
textElem.addEventListener('keydown', ev => {
|
||||
if(ev.key === 'Tab' && !ev.shiftKey && !ev.ctrlKey) {
|
||||
ev.preventDefault();
|
||||
|
||||
const text = textElem.value;
|
||||
if(text.length < 1)
|
||||
return;
|
||||
|
||||
const start = textElem.selectionStart;
|
||||
let position = start;
|
||||
let snippet = '';
|
||||
while(position >= 0 && text.charAt(position - 1) !== ' ' && text.charAt(position - 1) !== "\n") {
|
||||
--position;
|
||||
snippet = text.charAt(position) + snippet;
|
||||
}
|
||||
|
||||
let insertText;
|
||||
|
||||
if(snippet.indexOf(':') === 0) {
|
||||
let emoteRank = 0;
|
||||
if(Umi.User.hasCurrentUser())
|
||||
emoteRank = Umi.User.getCurrentUser().perms.rank;
|
||||
const emotes = MamiEmotes.findByName(emoteRank, snippet.substring(1), true);
|
||||
if(emotes.length > 0)
|
||||
insertText = `:${emotes[0]}:`;
|
||||
} else {
|
||||
const users = Umi.Users.Find(snippet);
|
||||
if(users.length === 1)
|
||||
insertText = users[0].name;
|
||||
}
|
||||
|
||||
if(insertText !== undefined) {
|
||||
const nextPos = start - snippet.length + insertText.length;
|
||||
textElem.value = text.slice(0, start - snippet.length) + insertText + text.slice(start);
|
||||
textElem.selectionEnd = textElem.selectionStart = nextPos;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(ev.key === 'Enter' && ev.shiftKey === newLineOnEnter) {
|
||||
ev.preventDefault();
|
||||
textElem.form.requestSubmit();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
textElem.addEventListener('paste', ev => {
|
||||
if(ev.clipboardData?.files.length > 0)
|
||||
eventTarget.dispatch('upload', { files: ev.clipboardData.files });
|
||||
});
|
||||
|
||||
return {
|
||||
get element() { return html; },
|
||||
|
||||
get text() { return textElem.value; },
|
||||
set text(value) {
|
||||
textElem.value = value;
|
||||
},
|
||||
|
||||
get newLineOnEnter() { return newLineOnEnter; },
|
||||
set newLineOnEnter(value) {
|
||||
newLineOnEnter = !!value;
|
||||
},
|
||||
|
||||
get growInputField() { return growInputField; },
|
||||
set growInputField(value) {
|
||||
growInputField = !!value;
|
||||
},
|
||||
|
||||
createButton: info => createButton(info),
|
||||
updateGrowHeight: updateGrowHeight,
|
||||
|
||||
setText: text => { // this one also focusses!!
|
||||
if(typeof text !== 'string')
|
||||
throw 'text must be string';
|
||||
|
||||
textElem.value = text;
|
||||
textElem.focus();
|
||||
},
|
||||
insertAtCursor: text => {
|
||||
if(typeof text !== 'string')
|
||||
throw 'text must be string';
|
||||
|
||||
const value = textElem.value;
|
||||
let start = textElem.selectionStart;
|
||||
let end = textElem.selectionEnd;
|
||||
|
||||
textElem.value = value.slice(0, start) + text + value.slice(end);
|
||||
textElem.selectionEnd = textElem.selectionStart = start + text.length;
|
||||
textElem.focus();
|
||||
},
|
||||
insertAroundSelection: (before = '', after = '') => {
|
||||
if(typeof before !== 'string')
|
||||
throw 'before must be string';
|
||||
if(typeof after !== 'string')
|
||||
throw 'after must be string';
|
||||
|
||||
const start = textElem.selectionStart;
|
||||
const end = textElem.selectionEnd;
|
||||
const length = end - start;
|
||||
const value = textElem.value;
|
||||
|
||||
textElem.value = value.slice(0, start) + before + value.slice(start, end) + after + value.slice(end);
|
||||
textElem.selectionStart = start + before.length;
|
||||
textElem.selectionEnd = end + before.length;
|
||||
textElem.focus();
|
||||
},
|
||||
|
||||
focus: () => {
|
||||
textElem.focus();
|
||||
},
|
||||
};
|
||||
};
|
19
src/mami.js/chatform/markup.jsx
Normal file
19
src/mami.js/chatform/markup.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
const MamiChatFormMarkup = function(input) {
|
||||
const buttons = <div class="input__menu"/>;
|
||||
const html = <div class="input__menus">{buttons}</div>;
|
||||
|
||||
const createButton = info => {
|
||||
buttons.appendChild(<button class="markup__button" type="button" style={info.style} onclick={ev => { info.onclick(ev, info); }}>{info.title}</button>);
|
||||
};
|
||||
|
||||
return {
|
||||
get element() { return html; },
|
||||
|
||||
get visible() { return !html.classList.contains('hidden'); },
|
||||
set visible(state) { html.classList.toggle('hidden', !state); },
|
||||
|
||||
get height() { return html.getBoundingClientRect().height; },
|
||||
|
||||
createButton: createButton,
|
||||
};
|
||||
};
|
|
@ -170,12 +170,15 @@ const MamiColourPicker = function(options) {
|
|||
dialog: pos => {
|
||||
html.classList.remove('hidden');
|
||||
|
||||
if(pos instanceof MouseEvent) {
|
||||
const ev = pos;
|
||||
if(pos instanceof MouseEvent)
|
||||
pos = pos.target;
|
||||
if(pos instanceof Element)
|
||||
pos = pos.getBoundingClientRect();
|
||||
if(pos instanceof DOMRect) {
|
||||
const bbb = pos;
|
||||
pos = {};
|
||||
|
||||
const mbb = html.getBoundingClientRect();
|
||||
const bbb = ev.target.getBoundingClientRect();
|
||||
const pbb = html.parentNode.getBoundingClientRect();
|
||||
|
||||
pos.left = bbb.left;
|
||||
|
|
|
@ -116,12 +116,15 @@ const MamiEmotePicker = function(args) {
|
|||
|
||||
html.classList.remove('hidden');
|
||||
|
||||
if(pos instanceof MouseEvent) {
|
||||
const ev = pos;
|
||||
if(pos instanceof MouseEvent)
|
||||
pos = pos.target;
|
||||
if(pos instanceof Element)
|
||||
pos = pos.getBoundingClientRect();
|
||||
if(pos instanceof DOMRect) {
|
||||
const bbb = pos;
|
||||
pos = {};
|
||||
|
||||
const mbb = html.getBoundingClientRect();
|
||||
const bbb = ev.target.getBoundingClientRect();
|
||||
const pbb = html.parentNode.getBoundingClientRect();
|
||||
|
||||
pos.right = pbb.width - bbb.left;
|
||||
|
|
|
@ -11,6 +11,7 @@ window.Umi = { UI: {} };
|
|||
#include events.js
|
||||
#include mobile.js
|
||||
#include mszauth.js
|
||||
#include parsing.js
|
||||
#include themes.js
|
||||
#include txtrigs.js
|
||||
#include uniqstr.js
|
||||
|
@ -19,6 +20,8 @@ window.Umi = { UI: {} };
|
|||
#include weeb.js
|
||||
#include worker.js
|
||||
#include audio/autoplay.js
|
||||
#include chatform/form.jsx
|
||||
#include colpick/picker.jsx
|
||||
#include controls/msgbox.jsx
|
||||
#include controls/ping.jsx
|
||||
#include controls/views.js
|
||||
|
@ -46,11 +49,7 @@ window.Umi = { UI: {} };
|
|||
#include sound/sndtest.jsx
|
||||
#include ui/chat-layout.js
|
||||
#include ui/emotes.js
|
||||
#include ui/hooks.js
|
||||
#include ui/input-menus.js
|
||||
#include ui/loading-overlay.jsx
|
||||
#include ui/markup.js
|
||||
#include ui/view.js
|
||||
|
||||
const MamiInit = async args => {
|
||||
args = MamiArgs('args', args, define => {
|
||||
|
@ -152,6 +151,7 @@ const MamiInit = async args => {
|
|||
settings.define('newLineOnEnter').default(false).create();
|
||||
settings.define('keepEmotePickerOpen').default(true).create();
|
||||
settings.define('doNotMarkLinksAsVisited').default(false).create();
|
||||
settings.define('showMarkupSelector').type(['always', 'focus', 'never']).default('always').create();
|
||||
|
||||
const noNotifSupport = !('Notification' in window);
|
||||
settings.define('enableNotifications').default(false).immutable(noNotifSupport).critical().create();
|
||||
|
@ -287,10 +287,56 @@ const MamiInit = async args => {
|
|||
},
|
||||
});
|
||||
|
||||
const layout = new Umi.UI.ChatLayout(sidebar);
|
||||
|
||||
const chatForm = new MamiChatForm(ctx.events.scopeTo('form'));
|
||||
|
||||
window.addEventListener('keydown', ev => {
|
||||
if((ev.ctrlKey && ev.key !== 'v') || ev.altKey)
|
||||
return;
|
||||
|
||||
if(!ev.target.matches('input, textarea, select, button'))
|
||||
chatForm.focus();
|
||||
});
|
||||
|
||||
settings.watch('showMarkupSelector', ev => {
|
||||
chatForm.markup.visible = ev.detail.value !== 'never';
|
||||
Umi.UI.Messages.ScrollIfNeeded(chatForm.markup.height);
|
||||
});
|
||||
|
||||
let colourPicker, colourPickerVisible = false;
|
||||
for(const bbCode of UmiBBCodes)
|
||||
if(bbCode.text !== undefined)
|
||||
chatForm.markup.createButton({
|
||||
title: bbCode.text,
|
||||
style: bbCode.style,
|
||||
onclick: ev => {
|
||||
if(bbCode.tag === 'color') {
|
||||
if(colourPicker === undefined) {
|
||||
colourPicker = new MamiColourPicker({ presets: futami.get('colours') });
|
||||
layout.getElement().appendChild(colourPicker.element);
|
||||
}
|
||||
|
||||
if(colourPickerVisible) {
|
||||
colourPicker.close();
|
||||
} else {
|
||||
colourPickerVisible = true;
|
||||
colourPicker.dialog(ev)
|
||||
.then(colour => { chatForm.input.insertAroundSelection(`[${bbCode.tag}=${MamiColour.hex(colour)}]`, `[/${bbCode.tag}]`) })
|
||||
.catch(() => {}) // noop so the console stops screaming
|
||||
.finally(() => colourPickerVisible = false);
|
||||
}
|
||||
} else
|
||||
chatForm.input.insertAroundSelection(`[${bbCode.tag}${(bbCode.arg ? '=' : '')}]`, `[/${bbCode.tag}]`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const layout = new Umi.UI.ChatLayout(chatForm, sidebar);
|
||||
await ctx.views.unshift(layout);
|
||||
|
||||
Umi.UI.Hooks.AddHooks();
|
||||
ctx.events.watch('form:resize', ev => {
|
||||
Umi.UI.Messages.ScrollIfNeeded(ev.detail.diffHeight);
|
||||
});
|
||||
|
||||
settings.watch('style', ev => {
|
||||
for(const className of layout.getElement().classList)
|
||||
|
@ -306,6 +352,8 @@ const MamiInit = async args => {
|
|||
});
|
||||
settings.watch('preventOverflow', ev => { args.parent.classList.toggle('prevent-overflow', ev.detail.value); });
|
||||
settings.watch('doNotMarkLinksAsVisited', ev => { layout.getInterface().getMessageList().getElement().classList.toggle('mami-do-not-mark-links-as-visited', ev.detail.value); });
|
||||
settings.watch('newLineOnEnter', ev => { chatForm.input.newLineOnEnter = ev.detail.value; });
|
||||
settings.watch('expandTextBox', ev => { chatForm.input.growInputField = ev.detail.value; });
|
||||
|
||||
settings.watch('minecraft', ev => {
|
||||
if(ev.detail.initial && ev.detail.value === 'no')
|
||||
|
@ -363,7 +411,7 @@ const MamiInit = async args => {
|
|||
loadingOverlay.message = 'Building menus...';
|
||||
|
||||
MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} }); // intentionally a no-op
|
||||
MamiCompat('Umi.UI.View.SetText', { value: text => console.log(`Umi.UI.View.SetText(text: ${text})`) });
|
||||
MamiCompat('Umi.UI.View.SetText', { value: text => { chatForm.input.setText(text); } });
|
||||
|
||||
const sbUsers = new MamiSidebarPanelUsers;
|
||||
sidebar.createPanel(sbUsers);
|
||||
|
@ -377,13 +425,13 @@ const MamiInit = async args => {
|
|||
name: 'action',
|
||||
text: 'Describe action',
|
||||
condition: entry => Umi.User.getCurrentUser()?.id === entry.id,
|
||||
onclick: entry => { Umi.UI.View.SetText('/me '); },
|
||||
onclick: entry => { chatForm.input.setText('/me '); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
name: 'nick',
|
||||
text: 'Set nickname',
|
||||
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canSetNick,
|
||||
onclick: entry => { Umi.UI.View.SetText('/nick '); },
|
||||
onclick: entry => { chatForm.input.setText('/nick '); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
name: 'bans',
|
||||
|
@ -415,13 +463,13 @@ const MamiInit = async args => {
|
|||
name: 'dm',
|
||||
text: 'Send direct message',
|
||||
condition: entry => Umi.User.getCurrentUser()?.id !== entry.id,
|
||||
onclick: entry => { Umi.UI.View.SetText(`/msg ${entry.name} `); },
|
||||
onclick: entry => { chatForm.input.setText(`/msg ${entry.name} `); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
name: 'kick',
|
||||
text: 'Kick from chat',
|
||||
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick,
|
||||
onclick: entry => { Umi.UI.View.SetText(`/kick ${entry.name} `); },
|
||||
onclick: entry => { chatForm.input.setText(`/kick ${entry.name} `); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
name: 'ipaddr',
|
||||
|
@ -462,6 +510,7 @@ const MamiInit = async args => {
|
|||
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('checkbox').on('always').off('never').done();
|
||||
});
|
||||
sbSettings.category(category => {
|
||||
category.header('Notifications');
|
||||
|
@ -660,36 +709,35 @@ const MamiInit = async args => {
|
|||
const sbActPing = new MamiSidebarActionPing(pingIndicator, ctx.msgbox);
|
||||
sidebar.createAction(sbActPing);
|
||||
|
||||
Umi.UI.InputMenus.Add('markup');
|
||||
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; });
|
||||
}
|
||||
chatForm.input.createButton({
|
||||
title: 'Emoticons',
|
||||
icon: 'fas fa-smile',
|
||||
onclick: (ev, button) => {
|
||||
if(emotePicker === undefined) {
|
||||
emotePicker = new MamiEmotePicker({
|
||||
onClose: () => { emotePickerVisible = false; },
|
||||
onPick: emote => {
|
||||
chatForm.input.insertAtCursor(`:${emote.strings[0]}:`);
|
||||
},
|
||||
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);
|
||||
}
|
||||
if(emotePickerVisible) {
|
||||
emotePicker.close();
|
||||
} else {
|
||||
emotePickerVisible = true;
|
||||
emotePicker.dialog(button);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
let doUpload;
|
||||
ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
|
||||
ctx.eeprom.init()
|
||||
|
@ -718,7 +766,7 @@ const MamiInit = async args => {
|
|||
} else
|
||||
text = location.protocol + upload.url;
|
||||
|
||||
Umi.UI.Markup.InsertRaw(text, '')
|
||||
chatForm.input.insertAtCursor(text);
|
||||
},
|
||||
});
|
||||
sbUploads.addOption({
|
||||
|
@ -784,13 +832,17 @@ const MamiInit = async args => {
|
|||
});
|
||||
args.parent.appendChild(uploadForm);
|
||||
|
||||
Umi.UI.InputMenus.AddButton('upload', 'Upload', () => uploadForm.click(), 'markup');
|
||||
chatForm.input.createButton({
|
||||
title: 'Upload',
|
||||
icon: 'fas fa-file-upload',
|
||||
onclick: () => { uploadForm.click(); },
|
||||
});
|
||||
|
||||
$i('umi-msg-text').onpaste = ev => {
|
||||
if(ev.clipboardData && ev.clipboardData.files.length > 0)
|
||||
for(const file of ev.clipboardData.files)
|
||||
doUpload(file);
|
||||
};
|
||||
ctx.events.watch('form:upload', ev => {
|
||||
console.info(ev);
|
||||
for(const file of ev.detail.files)
|
||||
doUpload(file);
|
||||
});
|
||||
});
|
||||
|
||||
// figure out how to display a UI for this someday
|
||||
|
@ -860,6 +912,10 @@ const MamiInit = async args => {
|
|||
const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn'));
|
||||
ctx.conMan = conMan;
|
||||
|
||||
ctx.events.watch('form:send', ev => {
|
||||
sockChat.client.sendMessage(ev.detail.text);
|
||||
});
|
||||
|
||||
sbChannels.onClickEntry = async info => {
|
||||
await sockChat.client.switchChannel(info);
|
||||
|
||||
|
|
|
@ -1,91 +1,93 @@
|
|||
#include utility.js
|
||||
#include ui/markup.js
|
||||
|
||||
if(!Umi.Parser) Umi.Parser = {};
|
||||
if(!Umi.Parser.SockChatBBcode) Umi.Parser.SockChatBBcode = {};
|
||||
|
||||
Umi.Parsing = (function() {
|
||||
const bbCodes = [
|
||||
{
|
||||
tag: 'b',
|
||||
replace: '<b>{0}</b>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'i',
|
||||
replace: '<i>{0}</i>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'u',
|
||||
replace: '<u>{0}</u>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 's',
|
||||
replace: '<del>{0}</del>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'quote',
|
||||
replace: '<q style="font-variant: small-caps;">{0}</q>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'code',
|
||||
replace: '<span style="white-space: pre-wrap; font-family: monospace;">{0}</span>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'sjis',
|
||||
replace: '<span class="sjis" style="white-space: pre-wrap;">{0}</span>',
|
||||
},
|
||||
{
|
||||
tag: 'color',
|
||||
hasArg: true,
|
||||
stripArg: ';:{}<>&|\\/~\'"',
|
||||
replace: '<span style="color:{0};">{1}</span>',
|
||||
isToggle: true,
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'img',
|
||||
stripText: '"\'',
|
||||
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span> [<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedImage(this);return false;" class="markup__link">Embed</a>]</span>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'url',
|
||||
stripText: '"\'',
|
||||
replace: '<a href="{0}" target="_blank" rel="nofollow noreferrer noopener" class="markup__link">{0}</a>',
|
||||
},
|
||||
{
|
||||
tag: 'url',
|
||||
hasArg: true,
|
||||
stripArg: '"\'',
|
||||
replace: '<a href="{0}" target="_blank" rel="nofollow noreferrer noopener" class="markup__link">{1}</a>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'video',
|
||||
stripText: '"\'',
|
||||
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span> [<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedVideo(this);return false;" class="markup__link">Embed</a>]</span>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'audio',
|
||||
stripText: '"\'',
|
||||
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span> [<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedAudio(this);return false;" class="markup__link">Embed</a>]</span>',
|
||||
button: true,
|
||||
},
|
||||
{
|
||||
tag: 'spoiler',
|
||||
stripText: '"\'',
|
||||
replace: '<span data-shit="{0}"><span>*** HIDDEN ***</span> [<a href="#" onclick="Umi.Parser.SockChatBBcode.ToggleSpoiler(this);return false;" class="markup__link">Reveal</a>]</span>',
|
||||
button: true,
|
||||
}
|
||||
];
|
||||
const UmiBBCodes = [
|
||||
{
|
||||
tag: 'b',
|
||||
text: 'B',
|
||||
style: 'font-weight: 700',
|
||||
replace: '<b>{0}</b>',
|
||||
},
|
||||
{
|
||||
tag: 'i',
|
||||
text: 'I',
|
||||
style: 'font-style: italic',
|
||||
replace: '<i>{0}</i>',
|
||||
},
|
||||
{
|
||||
tag: 'u',
|
||||
text: 'U',
|
||||
style: 'text-decoration: underline',
|
||||
replace: '<u>{0}</u>',
|
||||
},
|
||||
{
|
||||
tag: 's',
|
||||
text: 'S',
|
||||
style: 'text-decoration: line-through',
|
||||
replace: '<del>{0}</del>',
|
||||
},
|
||||
{
|
||||
tag: 'quote',
|
||||
text: 'Quote',
|
||||
replace: '<q style="font-variant: small-caps;">{0}</q>',
|
||||
},
|
||||
{
|
||||
tag: 'code',
|
||||
text: 'Code',
|
||||
replace: '<span style="white-space: pre-wrap; font-family: monospace;">{0}</span>',
|
||||
},
|
||||
{
|
||||
tag: 'sjis',
|
||||
replace: '<span class="sjis" style="white-space: pre-wrap;">{0}</span>',
|
||||
},
|
||||
{
|
||||
tag: 'color',
|
||||
text: 'Colour',
|
||||
hasArg: true,
|
||||
stripArg: ';:{}<>&|\\/~\'"',
|
||||
replace: '<span style="color:{0};">{1}</span>',
|
||||
},
|
||||
{
|
||||
tag: 'img',
|
||||
text: 'Image',
|
||||
stripText: '"\'',
|
||||
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span> [<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedImage(this);return false;" class="markup__link">Embed</a>]</span>',
|
||||
},
|
||||
{
|
||||
tag: 'url',
|
||||
stripText: '"\'',
|
||||
replace: '<a href="{0}" target="_blank" rel="nofollow noreferrer noopener" class="markup__link">{0}</a>',
|
||||
},
|
||||
{
|
||||
tag: 'url',
|
||||
text: 'URL',
|
||||
hasArg: true,
|
||||
stripArg: '"\'',
|
||||
replace: '<a href="{0}" target="_blank" rel="nofollow noreferrer noopener" class="markup__link">{1}</a>',
|
||||
},
|
||||
{
|
||||
tag: 'video',
|
||||
text: 'Video',
|
||||
stripText: '"\'',
|
||||
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span> [<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedVideo(this);return false;" class="markup__link">Embed</a>]</span>',
|
||||
},
|
||||
{
|
||||
tag: 'audio',
|
||||
text: 'Audio',
|
||||
stripText: '"\'',
|
||||
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span> [<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedAudio(this);return false;" class="markup__link">Embed</a>]</span>',
|
||||
},
|
||||
{
|
||||
tag: 'spoiler',
|
||||
text: 'Spoiler',
|
||||
stripText: '"\'',
|
||||
replace: '<span data-shit="{0}"><span>*** HIDDEN ***</span> [<a href="#" onclick="Umi.Parser.SockChatBBcode.ToggleSpoiler(this);return false;" class="markup__link">Reveal</a>]</span>',
|
||||
},
|
||||
];
|
||||
|
||||
Umi.Parsing = (function() {
|
||||
const replaceAll = function(haystack, needle, replace, ignore) {
|
||||
return haystack.replace(
|
||||
new RegExp(
|
||||
|
@ -362,21 +364,9 @@ Umi.Parsing = (function() {
|
|||
Umi.Parser.SockChatBBcode.ToggleSpoiler = toggleSpoiler;
|
||||
|
||||
return {
|
||||
Init: function() {
|
||||
for (let i = 0; i < bbCodes.length; i++) {
|
||||
const bbCode = bbCodes[i];
|
||||
if(!bbCode.button)
|
||||
continue;
|
||||
|
||||
const start = '[{0}]'.replace('{0}', bbCode.tag + (bbCode.arg ? '=' : '')), end = '[/{0}]'.replace('{0}', bbCode.tag);
|
||||
const text = (bbCode.tag.length > 1 ? bbCode.tag.substring(0, 1).toUpperCase() + bbCode.tag.substring(1) : bbCode.tag).replace('Color', 'Colour');
|
||||
|
||||
Umi.UI.Markup.Add(bbCode.tag, text, start, end);
|
||||
}
|
||||
},
|
||||
Parse: function(element, message) {
|
||||
for(let i = 0; i < bbCodes.length; i++) {
|
||||
const bbCode = bbCodes[i];
|
||||
for(let i = 0; i < UmiBBCodes.length; i++) {
|
||||
const bbCode = UmiBBCodes[i];
|
||||
|
||||
if(!bbCode.hasArg) {
|
||||
let at = 0;
|
||||
|
|
|
@ -113,6 +113,8 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
|
|||
options: undefined,
|
||||
confirm: undefined,
|
||||
disabled: info.immutable,
|
||||
trueValue: true,
|
||||
falseValue: false,
|
||||
};
|
||||
|
||||
const detectType = () => {
|
||||
|
@ -151,6 +153,20 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
|
|||
setting.options = value;
|
||||
return pub;
|
||||
},
|
||||
on: value => {
|
||||
if(typeof value === undefined)
|
||||
value = true;
|
||||
|
||||
setting.trueValue = value;
|
||||
return pub;
|
||||
},
|
||||
off: value => {
|
||||
if(typeof value === undefined)
|
||||
value = false;
|
||||
|
||||
setting.falseValue = value;
|
||||
return pub;
|
||||
},
|
||||
confirm: value => {
|
||||
if(!Array.isArray(value) && typeof value !== 'string' && value !== undefined)
|
||||
throw 'value must be an array or a string';
|
||||
|
@ -200,7 +216,15 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
|
|||
label.append(title, input);
|
||||
|
||||
if(setting.type === 'checkbox') {
|
||||
settings.watch(info.name, ev => input.checked = ev.detail.value);
|
||||
settings.watch(info.name, ev => input.checked = ev.detail.value !== setting.falseValue);
|
||||
|
||||
const toggle = () => {
|
||||
settings.set(
|
||||
info.name,
|
||||
settings.get(info.name) !== setting.falseValue ? setting.falseValue : setting.trueValue
|
||||
);
|
||||
};
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
if(setting.confirm !== undefined && input.checked !== info.fallback) {
|
||||
msgBox.show({
|
||||
|
@ -208,12 +232,12 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
|
|||
yes: { primary: false },
|
||||
no: { primary: true },
|
||||
}).then(() => {
|
||||
settings.toggle(info.name);
|
||||
toggle();
|
||||
}).catch(() => {
|
||||
input.checked = info.fallback;
|
||||
});
|
||||
} else {
|
||||
settings.toggle(info.name);
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#include compat.js
|
||||
#include mszauth.js
|
||||
#include ui/hooks.js
|
||||
|
||||
const MamiSockChat = function(protoWorker) {
|
||||
const events = protoWorker.eventTarget('sockchat');
|
||||
|
@ -23,8 +22,6 @@ const MamiSockChat = function(protoWorker) {
|
|||
client = await protoWorker.root.create('sockchat', { ping: futami.get('ping') });
|
||||
await client.setDumpPackets(dumpPackets);
|
||||
|
||||
Umi.UI.Hooks.SetCallbacks(client.sendMessage);
|
||||
|
||||
MamiCompat('Umi.Server', { get: () => client, configurable: true });
|
||||
MamiCompat('Umi.Server.SendMessage', { value: text => client.sendMessage(text), configurable: true });
|
||||
MamiCompat('Umi.Protocol.SockChat.Protocol.Instance.SendMessage', { value: text => client.sendMessage(text), configurable: true });
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
#include notices/baka.jsx
|
||||
#include sockchat/modal.js
|
||||
#include ui/emotes.js
|
||||
#include ui/markup.js
|
||||
#include ui/messages.jsx
|
||||
|
||||
const MamiSockChatHandlers = function(
|
||||
|
@ -103,9 +102,6 @@ const MamiSockChatHandlers = function(
|
|||
|
||||
sbUsers.createEntry(ev.detail.user);
|
||||
|
||||
Umi.UI.Markup.Reset();
|
||||
Umi.Parsing.Init();
|
||||
|
||||
if(ctx.views.count > 1)
|
||||
ctx.views.pop();
|
||||
};
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
#include utility.js
|
||||
|
||||
Umi.UI.ChatInputMain = function() {
|
||||
const html = $e({
|
||||
attrs: {
|
||||
id: 'umi-msg-container',
|
||||
className: 'input__main',
|
||||
},
|
||||
child: [
|
||||
{
|
||||
tag: 'textarea',
|
||||
attrs: {
|
||||
id: 'umi-msg-text',
|
||||
name: 'text',
|
||||
className: 'input__text',
|
||||
autofocus: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: 'button',
|
||||
attrs: {
|
||||
id: 'umi-msg-send',
|
||||
className: 'input__button input__button--send',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
getElement: function() {
|
||||
return html;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
#include utility.js
|
||||
|
||||
Umi.UI.ChatInputMenus = function() {
|
||||
const html = $e({
|
||||
attrs: {
|
||||
id: 'umi-msg-menu',
|
||||
className: 'input__menus',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
getElement: function() {
|
||||
return html;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
#include utility.js
|
||||
#include ui/chat-input-main.js
|
||||
#include ui/chat-input-menus.js
|
||||
|
||||
Umi.UI.ChatInput = function() {
|
||||
const menus = new Umi.UI.ChatInputMenus;
|
||||
const main = new Umi.UI.ChatInputMain;
|
||||
|
||||
const html = $e({
|
||||
tag: 'form',
|
||||
attrs: {
|
||||
id: 'umi-msg-form',
|
||||
className: 'input',
|
||||
},
|
||||
child: [
|
||||
menus,
|
||||
main,
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
getMenus: function() {
|
||||
return menus;
|
||||
},
|
||||
getMain: function() {
|
||||
return main;
|
||||
},
|
||||
getElement: function() {
|
||||
return html;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,10 +1,8 @@
|
|||
#include utility.js
|
||||
#include ui/chat-message-list.js
|
||||
#include ui/chat-input.js
|
||||
|
||||
Umi.UI.ChatInterface = function() {
|
||||
Umi.UI.ChatInterface = function(chatForm) {
|
||||
const messages = new Umi.UI.ChatMessageList;
|
||||
const input = new Umi.UI.ChatInput;
|
||||
|
||||
const html = $e({
|
||||
attrs: {
|
||||
|
@ -12,7 +10,7 @@ Umi.UI.ChatInterface = function() {
|
|||
},
|
||||
child: [
|
||||
messages,
|
||||
input,
|
||||
chatForm,
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -20,9 +18,6 @@ Umi.UI.ChatInterface = function() {
|
|||
getMessageList: function() {
|
||||
return messages;
|
||||
},
|
||||
getInput: function() {
|
||||
return input;
|
||||
},
|
||||
getElement: function() {
|
||||
return html;
|
||||
},
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
#include utility.js
|
||||
#include ui/chat-interface.js
|
||||
#include sidebar/sidebar.jsx
|
||||
|
||||
// this needs revising at some point but will suffice for now
|
||||
Umi.UI.ChatLayout = function(sideBar) {
|
||||
const main = new Umi.UI.ChatInterface;
|
||||
Umi.UI.ChatLayout = function(chatForm, sideBar) {
|
||||
const main = new Umi.UI.ChatInterface(chatForm);
|
||||
|
||||
const html = $e({
|
||||
attrs: {
|
||||
|
@ -18,9 +17,6 @@ Umi.UI.ChatLayout = function(sideBar) {
|
|||
});
|
||||
|
||||
return {
|
||||
getSideBar: function() {
|
||||
return sideBar;
|
||||
},
|
||||
getInterface: function() {
|
||||
return main;
|
||||
},
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
#include users.js
|
||||
#include utility.js
|
||||
#include ui/messages.jsx
|
||||
#include ui/view.js
|
||||
|
||||
Umi.UI.Hooks = (function() {
|
||||
let sendMessage;
|
||||
|
||||
return {
|
||||
SetCallbacks: (sendMessageFunc) => {
|
||||
sendMessage = sendMessageFunc;
|
||||
},
|
||||
AddHooks: function() {
|
||||
const msgForm = $i('umi-msg-form');
|
||||
const msgText = $i('umi-msg-text');
|
||||
|
||||
window.addEventListener('keydown', function(ev) {
|
||||
if((ev.ctrlKey && ev.key !== 'v') || ev.altKey)
|
||||
return;
|
||||
|
||||
if(!ev.target.matches('input, textarea, select, button'))
|
||||
msgText.focus();
|
||||
});
|
||||
|
||||
msgForm.addEventListener('submit', ev => {
|
||||
ev.preventDefault();
|
||||
|
||||
if(typeof sendMessage !== 'function')
|
||||
return;
|
||||
|
||||
const textField = ev.target.elements.namedItem('text');
|
||||
if(textField instanceof HTMLTextAreaElement) {
|
||||
let text = textField.value;
|
||||
textField.value = '';
|
||||
|
||||
text = text.replace(/\t/g, ' ');
|
||||
if(text.length > 0)
|
||||
sendMessage(text);
|
||||
}
|
||||
});
|
||||
|
||||
msgText.addEventListener('keyup', function(ev) {
|
||||
const isScrolledToBottom = Umi.UI.Messages.IsScrolledToBottom();
|
||||
|
||||
const elemParent = msgText.parentNode;
|
||||
let height = 40;
|
||||
|
||||
if(mami.settings.get('expandTextBox') && msgText.scrollHeight > msgText.clientHeight)
|
||||
height = msgText.scrollHeight;
|
||||
|
||||
if(height > 40)
|
||||
elemParent.style.height = height.toString() + 'px';
|
||||
else
|
||||
elemParent.style.height = null;
|
||||
|
||||
if(isScrolledToBottom)
|
||||
Umi.UI.Messages.ScrollIfNeeded();
|
||||
});
|
||||
|
||||
msgText.addEventListener('keydown', function(ev) {
|
||||
if(ev.key === 'Tab' && (!ev.shiftKey || !ev.ctrlKey)) {
|
||||
ev.preventDefault();
|
||||
|
||||
const text = Umi.UI.View.GetText();
|
||||
if(text.length < 1)
|
||||
return;
|
||||
|
||||
const start = Umi.UI.View.GetPosition();
|
||||
let position = start,
|
||||
snippet = '';
|
||||
while(position >= 0 && text.charAt(position - 1) !== ' ' && text.charAt(position - 1) !== "\n") {
|
||||
--position;
|
||||
snippet = text.charAt(position) + snippet;
|
||||
}
|
||||
|
||||
let insertText = undefined;
|
||||
|
||||
if(snippet.indexOf(':') === 0) {
|
||||
let emoteRank = 0;
|
||||
if(Umi.User.hasCurrentUser())
|
||||
emoteRank = Umi.User.getCurrentUser().perms.rank;
|
||||
const emotes = MamiEmotes.findByName(emoteRank, snippet.substring(1), true);
|
||||
if(emotes.length > 0)
|
||||
insertText = ':' + emotes[0] + ':';
|
||||
} else {
|
||||
const users = Umi.Users.Find(snippet);
|
||||
if(users.length === 1)
|
||||
insertText = users[0].name;
|
||||
}
|
||||
|
||||
if(insertText !== undefined) {
|
||||
Umi.UI.View.SetText(text.slice(0, start - snippet.length) + text.slice(start));
|
||||
Umi.UI.View.SetPosition(start - snippet.length);
|
||||
Umi.UI.View.EnterAtCursor(insertText);
|
||||
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + insertText.length);
|
||||
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition(), true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if((ev.key === 'Enter' || ev.key === 'NumpadEnter') && ev.shiftKey === mami.settings.get('newLineOnEnter')) {
|
||||
ev.preventDefault();
|
||||
msgForm.requestSubmit();
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,68 +0,0 @@
|
|||
#include utility.js
|
||||
#include ui/input-menus.js
|
||||
|
||||
Umi.UI.InputMenus = (function() {
|
||||
const ids = [];
|
||||
|
||||
const createButtonId = id => `umi-msg-menu-btn-${id}`;
|
||||
const createButton = function(id, title, onClick) {
|
||||
return $e({
|
||||
tag: 'button',
|
||||
attrs: {
|
||||
type: 'button',
|
||||
id: createButtonId(id),
|
||||
classList: ['input__button', 'input__button--' + id],
|
||||
title: title,
|
||||
onclick: onClick,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
Add: function(baseId, title, beforeButtonId) {
|
||||
if(baseId !== 'markup')
|
||||
throw 'only baseId "markup" may be added';
|
||||
|
||||
if(ids.includes(baseId))
|
||||
return;
|
||||
ids.push(baseId);
|
||||
|
||||
let beforeButton;
|
||||
if(typeof beforeButtonId === 'string')
|
||||
beforeButton = $i(createButtonId(beforeButtonId))
|
||||
if(!(beforeButton instanceof Element))
|
||||
beforeButton = $i('umi-msg-send');
|
||||
|
||||
if(typeof title === 'string')
|
||||
$i('umi-msg-container').insertBefore(createButton(baseId, title), beforeButton);
|
||||
$i('umi-msg-menu').appendChild(
|
||||
$e({
|
||||
attrs: {
|
||||
'class': ['input__menu', 'input__menu--' + baseId, 'input__menu--active'],
|
||||
id: 'umi-msg-menu-sub-' + baseId,
|
||||
tabindex: '0',
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
AddButton: function(baseId, title, onClick, beforeButtonId) {
|
||||
if(ids.includes(baseId))
|
||||
return;
|
||||
ids.push(baseId);
|
||||
|
||||
let beforeButton;
|
||||
if(typeof beforeButtonId === 'string')
|
||||
beforeButton = $i(createButtonId(beforeButtonId))
|
||||
if(!(beforeButton instanceof Element))
|
||||
beforeButton = $i('umi-msg-send');
|
||||
|
||||
$i('umi-msg-container').insertBefore(createButton(baseId, title, onClick), beforeButton);
|
||||
},
|
||||
Get: function(baseId, button) {
|
||||
const id = 'umi-msg-menu-' + (button ? 'btn' : 'sub') + '-' + baseId;
|
||||
if(ids.indexOf(baseId) >= 0)
|
||||
return $i(id);
|
||||
return null;
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,71 +0,0 @@
|
|||
#include colour.js
|
||||
#include common.js
|
||||
#include utility.js
|
||||
#include colpick/picker.jsx
|
||||
#include ui/input-menus.js
|
||||
#include ui/markup.js
|
||||
#include ui/view.js
|
||||
|
||||
Umi.UI.Markup = (function() {
|
||||
const insertRaw = function(start, end) {
|
||||
const selectionLength = Umi.UI.View.GetSelectionLength();
|
||||
Umi.UI.View.EnterAtCursor(start);
|
||||
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + selectionLength + start.length);
|
||||
Umi.UI.View.EnterAtCursor(end);
|
||||
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() - selectionLength);
|
||||
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + selectionLength, true);
|
||||
Umi.UI.View.Focus();
|
||||
};
|
||||
|
||||
let pickerTarget = document.body;
|
||||
let picker, pickerVisible = false;
|
||||
|
||||
const insert = function(ev) {
|
||||
if(this.dataset.umiTagName === 'color') {
|
||||
if(picker === undefined) {
|
||||
picker = new MamiColourPicker({ presets: futami.get('colours') });
|
||||
pickerTarget.appendChild(picker.element);
|
||||
}
|
||||
|
||||
if(pickerVisible) {
|
||||
picker.close();
|
||||
} else {
|
||||
pickerVisible = true;
|
||||
picker.dialog(ev)
|
||||
.then(colour => insertRaw(`[color=${MamiColour.hex(colour)}]`, '[/color]'))
|
||||
.catch(() => {}) // noop so the console stops screaming
|
||||
.finally(() => pickerVisible = false);
|
||||
}
|
||||
} else
|
||||
insertRaw(
|
||||
this.dataset.umiBeforeCursor,
|
||||
this.dataset.umiAfterCursor
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
SetPickerTarget: target => { pickerTarget = target; },
|
||||
Add: function(name, text, beforeCursor, afterCursor) {
|
||||
Umi.UI.InputMenus.Get('markup').appendChild($e({
|
||||
tag: 'button',
|
||||
attrs: {
|
||||
type: 'button',
|
||||
id: 'umi-msg-menu-markup-btn-' + name,
|
||||
classList: ['markup__button', 'markup__button--' + name],
|
||||
dataset: {
|
||||
umiTagName: name,
|
||||
umiBeforeCursor: beforeCursor,
|
||||
umiAfterCursor: afterCursor,
|
||||
},
|
||||
onclick: insert,
|
||||
},
|
||||
child: text,
|
||||
}));
|
||||
},
|
||||
Reset: function() {
|
||||
Umi.UI.InputMenus.Get('markup').innerHTML = '';
|
||||
},
|
||||
Insert: insert,
|
||||
InsertRaw: insertRaw,
|
||||
};
|
||||
})();
|
|
@ -375,9 +375,19 @@ Umi.UI.Messages = (function() {
|
|||
const msgsList = $i('umi-messages');
|
||||
return msgsList.scrollTop === (msgsList.scrollHeight - msgsList.offsetHeight);
|
||||
},
|
||||
ScrollIfNeeded: () => {
|
||||
ScrollIfNeeded: (offsetOrForce = 0) => {
|
||||
const msgsList = $i('umi-messages');
|
||||
if(!(msgsList instanceof Element))
|
||||
return;
|
||||
|
||||
if(typeof offsetOrForce === 'boolean' && offsetOrForce !== true)
|
||||
return;
|
||||
|
||||
if(typeof offsetOrForce === 'number' && msgsList.scrollTop < (msgsList.scrollHeight - msgsList.offsetHeight - offsetOrForce))
|
||||
return;
|
||||
|
||||
if(mami.settings.get('autoScroll'))
|
||||
$i('umi-messages').lastElementChild?.scrollIntoView({ inline: 'end' });
|
||||
msgsList.lastElementChild?.scrollIntoView({ inline: 'end' });
|
||||
},
|
||||
SwitchChannel: channel => {
|
||||
if(typeof channel === 'object' && channel !== null && 'name' in channel)
|
||||
|
@ -385,15 +395,13 @@ Umi.UI.Messages = (function() {
|
|||
if(typeof channel !== 'string')
|
||||
return;
|
||||
|
||||
const isScrolledToBottom = Umi.UI.Messages.IsScrolledToBottom();
|
||||
focusChannelName = channel;
|
||||
|
||||
const root = $i('umi-messages');
|
||||
for(const elem of root.children)
|
||||
elem.classList.toggle('hidden', elem.dataset.channel !== undefined && elem.dataset.channel !== focusChannelName);
|
||||
|
||||
if(isScrolledToBottom)
|
||||
Umi.UI.Messages.ScrollIfNeeded();
|
||||
Umi.UI.Messages.ScrollIfNeeded();
|
||||
},
|
||||
Clear: retain => {
|
||||
if(typeof retain === 'string' && !isNaN(retain))
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
#include utility.js
|
||||
|
||||
Umi.UI.View = (function() {
|
||||
const getPosition = function(end) {
|
||||
return $i('umi-msg-text')[end ? 'selectionEnd' : 'selectionStart'];
|
||||
};
|
||||
const setPosition = function(pos, end) {
|
||||
$i('umi-msg-text')[end ? 'selectionEnd' : 'selectionStart'] = pos;
|
||||
};
|
||||
|
||||
const getText = function() {
|
||||
return $i('umi-msg-text').value;
|
||||
};
|
||||
const setText = function(text) {
|
||||
$i('umi-msg-text').value = text;
|
||||
};
|
||||
|
||||
return {
|
||||
Focus: function() {
|
||||
$i('umi-msg-text').focus();
|
||||
},
|
||||
SetPosition: setPosition,
|
||||
GetPosition: getPosition,
|
||||
GetSelectionLength: function() {
|
||||
let length = getPosition(true) - getPosition();
|
||||
if(length < 0)
|
||||
length = getPosition() - getPosition(true);
|
||||
return length;
|
||||
},
|
||||
EnterAtCursor: function(text, overwrite) {
|
||||
const value = getText();
|
||||
const current = getPosition();
|
||||
let out = '';
|
||||
|
||||
out += value.slice(0, current);
|
||||
out += text;
|
||||
out += value.slice(current + (overwrite ? text.length : 0));
|
||||
|
||||
setText(out);
|
||||
setPosition(current);
|
||||
},
|
||||
GetText: getText,
|
||||
SetText: setText,
|
||||
};
|
||||
})();
|
|
@ -166,6 +166,10 @@ const SockChatProtocol = function(dispatch, options) {
|
|||
if(!ctx.isAuthed)
|
||||
throw 'must be authenticated';
|
||||
|
||||
text = text.replace(/\t/g, ' ');
|
||||
if(text.length < 1)
|
||||
return;
|
||||
|
||||
// there's actually a pretty big bug here lol
|
||||
// any unsupported command is gonna fall through to the actual channel you're in
|
||||
if(!text.startsWith('/') && ctx.pseudoChannelName !== undefined)
|
||||
|
|
Loading…
Reference in a new issue