@@ -0,0 +1,208 @@ | |||
define([ | |||
'html!ui/templates/messages/tplTab' | |||
], function ( | |||
tplTab | |||
) { | |||
const extensionObj = { | |||
processChat: function (msgConfig) { | |||
const { message, event: keyboardEvent } = msgConfig; | |||
const { key } = keyboardEvent; | |||
const { el, currentChannel } = this; | |||
const optionContainer = this.find('.channelOptions'); | |||
if (message.length) { | |||
if (el.hasClass('picking')) | |||
msgConfig.cancel = true; | |||
return; | |||
} | |||
if (key === 'Enter') { | |||
const selectedSubPick = optionContainer.find('.option.selected'); | |||
if (selectedSubPick.length) { | |||
this.onPickSubChannel(selectedSubPick.html(), currentChannel); | |||
return; | |||
} | |||
} | |||
//If we're busy picking a sub channel, we can use keyboard nav | |||
const isPicking = el.hasClass('picking'); | |||
const currentSelection = optionContainer.find('.option.selected'); | |||
if (isPicking && currentSelection.length) { | |||
const delta = { | |||
ArrowUp: -1, | |||
ArrowDown: 1 | |||
}[key]; | |||
if (delta) { | |||
const options = optionContainer.find('.option'); | |||
const currentIndex = currentSelection.eq(0).index(); | |||
let nextIndex = (currentIndex + delta) % options.length; | |||
currentSelection.removeClass('selected'); | |||
options.eq(nextIndex).addClass('selected'); | |||
} | |||
} | |||
const pick = { | |||
'%': 'party', | |||
'!': 'global', | |||
$: 'custom', | |||
'@': 'direct' | |||
}[key]; | |||
if (!pick) { | |||
if (isPicking) | |||
msgConfig.cancel = true; | |||
return; | |||
} | |||
if (currentChannel === pick) { | |||
if (pick === 'direct') | |||
this.lastPrivateChannel = null; | |||
else if (pick === 'custom') | |||
this.lastCustomChannel = null; | |||
} | |||
this.onPickChannel(pick, true); | |||
msgConfig.cancel = true; | |||
}, | |||
onPickChannel: function (channel, autoPickSub) { | |||
this.currentChannel = channel; | |||
this.currentSubChannel = null; | |||
const showSubChannels = ( | |||
['direct', 'custom'].includes(channel) && | |||
( | |||
!autoPickSub || | |||
( | |||
channel === 'direct' && | |||
!this.lastPrivateChannel | |||
) || | |||
( | |||
channel === 'custom' && | |||
!this.lastCustomChannel | |||
) | |||
) | |||
); | |||
if (!showSubChannels) { | |||
this.find('.channelOptions').removeClass('active'); | |||
let showValue = { | |||
direct: this.lastPrivateChannel, | |||
custom: this.lastCustomChannel | |||
}[channel]; | |||
if (channel === 'direct' || channel === 'custom') | |||
this.currentSubChannel = showValue; | |||
showValue = showValue || channel; | |||
this.find('.channelPicker').html(showValue); | |||
this.find('input').focus(); | |||
this.el.removeClass('picking'); | |||
} else | |||
this.onShowChannelOptions(channel); | |||
}, | |||
onPickSubChannel: function (subChannel, channel) { | |||
this.currentSubChannel = subChannel; | |||
this.find('.channelOptions').removeClass('active'); | |||
this.find('.channelPicker').html(subChannel); | |||
const elInput = this.find('input'); | |||
elInput.focus(); | |||
if (channel === 'custom') { | |||
if (subChannel === 'join new') { | |||
elInput.val('/join channelName'); | |||
elInput[0].setSelectionRange(6, 17); | |||
} else if (subChannel === 'leave') { | |||
elInput.val('/leave channelName'); | |||
elInput[0].setSelectionRange(7, 18); | |||
} | |||
} | |||
this.el.removeClass('picking'); | |||
}, | |||
onShowChannelOptions: function (currentPick) { | |||
const optionContainer = this.find('.channelOptions') | |||
.addClass('active') | |||
.empty(); | |||
const options = []; | |||
let handlerOnClick = this.onPickChannel; | |||
this.el.addClass('picking'); | |||
if (!currentPick) { | |||
options.push('global', 'custom'); | |||
if (this.privateChannels.length) | |||
options.push('direct'); | |||
//Hack...surely we can find a more sane way to do this | |||
if ($('.uiParty .member').length) | |||
options.push('party'); | |||
} else { | |||
handlerOnClick = this.onPickSubChannel; | |||
if (currentPick === 'direct') | |||
options.push(...this.privateChannels); | |||
else if (currentPick === 'custom') | |||
options.push(...this.customChannels, 'join new', 'leave'); | |||
} | |||
if (!options.length) { | |||
this.onPickChannel('global'); | |||
return; | |||
} | |||
let addSelectStyleTo = null; | |||
if (currentPick) | |||
addSelectStyleTo = this.currentSubChannel || options[0]; | |||
options.forEach(o => { | |||
const shortcut = { | |||
global: ' (!)', | |||
direct: ' (@)', | |||
party: ' (%)', | |||
custom: ' ($)' | |||
}[o] || ''; | |||
const html = `<div class='option' shortcut='${shortcut}'>${o}</div>`; | |||
const el = $(html) | |||
.appendTo(optionContainer) | |||
.on('click', handlerOnClick.bind(this, o, currentPick)) | |||
.on('hover', this.stopKeyboardNavForOptions.bind(this)); | |||
if (o === addSelectStyleTo) | |||
el.addClass('selected'); | |||
}); | |||
}, | |||
stopKeyboardNavForOptions: function () { | |||
this.find('.channelOptions .option.selected').removeClass('selected'); | |||
} | |||
}; | |||
return { | |||
init: function () { | |||
$.extend(this, extensionObj); | |||
//This whole hoverFilter business is a filthy hack | |||
this.find('.channelPicker, .channelOptions, .filter:not(.channel)') | |||
.on('mouseover', this.onFilterHover.bind(this, true)) | |||
.on('mouseleave', this.onFilterHover.bind(this, false)); | |||
this.find('.channelPicker').on('click', this.onShowChannelOptions.bind(this, null)); | |||
} | |||
}; | |||
}); |
@@ -3,6 +3,8 @@ define([ | |||
'html!ui/templates/messages/template', | |||
'html!ui/templates/messages/tplTab', | |||
'css!ui/templates/messages/styles', | |||
'ui/templates/messages/mobile', | |||
'ui/templates/messages/channelPicker', | |||
'js/input', | |||
'js/system/client', | |||
'js/config' | |||
@@ -11,6 +13,8 @@ define([ | |||
template, | |||
tplTab, | |||
styles, | |||
messagesMobile, | |||
channelPicker, | |||
input, | |||
client, | |||
config | |||
@@ -44,27 +48,13 @@ define([ | |||
'onKeyUp' | |||
].forEach(e => this.onEvent(e, this[e].bind(this))); | |||
//This whole hoverFilter business is a filthy hack | |||
this.find('.channelPicker, .channelOptions, .filter:not(.channel)') | |||
.on('mouseover', this.onFilterHover.bind(this, true)) | |||
.on('mouseleave', this.onFilterHover.bind(this, false)); | |||
this.find('.channelPicker').on('click', this.onShowChannelOptions.bind(this, null)); | |||
this.find('.filter:not(.channel)').on('click', this.onClickFilter.bind(this)); | |||
if (isMobile) { | |||
this.kbUpper = 0; | |||
channelPicker.init.call(this); | |||
this.el.on('click', this.toggle.bind(this, true)); | |||
this.renderKeyboard(); | |||
$(tplTab) | |||
.appendTo(this.find('.filters')) | |||
.addClass('btnClose') | |||
.html('x') | |||
.on('click', this.toggle.bind(this, false, true)); | |||
} else { | |||
if (isMobile) | |||
messagesMobile.init.call(this); | |||
else { | |||
this.find('input') | |||
.on('keydown', this.sendChat.bind(this)) | |||
.on('input', this.enforceMaxMsgLength.bind(this)) | |||
@@ -90,105 +80,6 @@ define([ | |||
elTime.html(timeString); | |||
}, | |||
renderKeyboard: function () { | |||
this.find('.keyboard').remove(); | |||
let container = $('<div class="keyboard"></div>') | |||
.appendTo(this.el); | |||
let keyboard = { | |||
0: 'qwertyuiop|asdfghjkl|zxcvbnm', | |||
1: 'QWERTYUIOP|ASDFGHJKL|ZXCVBNM', | |||
2: '1234567890|@#&*-+=()|_$"\';/' | |||
}[this.kbUpper].split(''); | |||
//Hacky: Insert control characters in correct positions | |||
//Backspace goes after 'm' | |||
if (this.kbUpper === 0) { | |||
keyboard.splice(keyboard.indexOf('z'), 0, 'caps'); | |||
keyboard.splice(keyboard.indexOf('m') + 1, 0, '<<'); | |||
} else if (this.kbUpper === 1) { | |||
keyboard.splice(keyboard.indexOf('Z'), 0, 'caps'); | |||
keyboard.splice(keyboard.indexOf('M') + 1, 0, '<<'); | |||
} else if (this.kbUpper === 2) | |||
keyboard.splice(keyboard.indexOf('/') + 1, 0, '<<'); | |||
keyboard.push(...['|', '123', ',', 'space', '.', 'send']); | |||
let row = 0; | |||
keyboard.forEach(k => { | |||
if (k === '|') { | |||
row++; | |||
const postGapCount = row === 4 ? 0 : row - 1; | |||
for (let i = 0; i < postGapCount; i++) | |||
$('<div class="gap" />').appendTo(container); | |||
$('<div class="newline" />').appendTo(container); | |||
const preGapCount = row === 3 ? 0 : row; | |||
for (let i = 0; i < preGapCount; i++) | |||
$('<div class="gap" />').appendTo(container); | |||
return; | |||
} | |||
let className = (k.length === 1) ? 'key' : 'key special'; | |||
if (k === ' ') { | |||
k = '.'; | |||
className = 'key hidden'; | |||
} | |||
className += ' ' + k; | |||
let elKey = $(`<div class="${className}">${k}</div>`) | |||
.appendTo(container); | |||
if (!className.includes('hidden')) | |||
elKey.on('click', this.clickKey.bind(this, k)); | |||
}); | |||
}, | |||
clickKey: function (key) { | |||
window.navigator.vibrate(20); | |||
let elInput = this.find('input'); | |||
const handler = { | |||
caps: () => { | |||
this.kbUpper = (this.kbUpper + 1) % 2; | |||
this.renderKeyboard(); | |||
}, | |||
123: () => { | |||
this.kbUpper = (this.kbUpper === 2) ? 0 : 2; | |||
this.renderKeyboard(); | |||
}, | |||
space: () => this.clickKey(' '), | |||
'<<': () => { | |||
elInput.val(elInput.val().slice(0, -1)); | |||
this.find('.input').html(elInput.val()); | |||
}, | |||
send: () => { | |||
this.sendChat({ which: 13 }); | |||
this.find('.input').html(''); | |||
this.find('input').val(''); | |||
} | |||
}[key]; | |||
if (handler) { | |||
handler(); | |||
return; | |||
} | |||
elInput.val(elInput.val() + key); | |||
this.enforceMaxMsgLength(); | |||
this.find('.input').html(elInput.val()); | |||
}, | |||
enforceMaxMsgLength: function () { | |||
let textbox = this.find('input'); | |||
let val = textbox.val(); | |||
@@ -425,71 +316,6 @@ define([ | |||
e.stopPropagation(); | |||
}, | |||
processChat: function (msgConfig) { | |||
const { message, event: keyboardEvent } = msgConfig; | |||
const { key } = keyboardEvent; | |||
const { el, currentChannel } = this; | |||
const optionContainer = this.find('.channelOptions'); | |||
if (message.length) { | |||
if (el.hasClass('picking')) | |||
msgConfig.cancel = true; | |||
return; | |||
} | |||
if (key === 'Enter') { | |||
const selectedSubPick = optionContainer.find('.option.selected'); | |||
if (selectedSubPick.length) { | |||
this.onPickSubChannel(selectedSubPick.html(), currentChannel); | |||
return; | |||
} | |||
} | |||
//If we're busy picking a sub channel, we can use keyboard nav | |||
const isPicking = el.hasClass('picking'); | |||
const currentSelection = optionContainer.find('.option.selected'); | |||
if (isPicking && currentSelection.length) { | |||
const delta = { | |||
ArrowUp: -1, | |||
ArrowDown: 1 | |||
}[key]; | |||
if (delta) { | |||
const options = optionContainer.find('.option'); | |||
const currentIndex = currentSelection.eq(0).index(); | |||
let nextIndex = (currentIndex + delta) % options.length; | |||
currentSelection.removeClass('selected'); | |||
options.eq(nextIndex).addClass('selected'); | |||
} | |||
} | |||
const pick = { | |||
'%': 'party', | |||
'!': 'global', | |||
$: 'custom', | |||
'@': 'direct' | |||
}[key]; | |||
if (!pick) { | |||
if (isPicking) | |||
msgConfig.cancel = true; | |||
return; | |||
} | |||
if (currentChannel === pick) { | |||
if (pick === 'direct') | |||
this.lastPrivateChannel = null; | |||
else if (pick === 'custom') | |||
this.lastCustomChannel = null; | |||
} | |||
this.onPickChannel(pick, true); | |||
msgConfig.cancel = true; | |||
}, | |||
sendChat: function (e) { | |||
let textbox = this.find('input'); | |||
let msgConfig = { | |||
@@ -538,122 +364,6 @@ define([ | |||
}); | |||
this.toggle(); | |||
}, | |||
onPickChannel: function (channel, autoPickSub) { | |||
this.currentChannel = channel; | |||
this.currentSubChannel = null; | |||
const showSubChannels = ( | |||
['direct', 'custom'].includes(channel) && | |||
( | |||
!autoPickSub || | |||
( | |||
channel === 'direct' && | |||
!this.lastPrivateChannel | |||
) || | |||
( | |||
channel === 'custom' && | |||
!this.lastCustomChannel | |||
) | |||
) | |||
); | |||
if (!showSubChannels) { | |||
this.find('.channelOptions').removeClass('active'); | |||
let showValue = { | |||
direct: this.lastPrivateChannel, | |||
custom: this.lastCustomChannel | |||
}[channel]; | |||
if (channel === 'direct' || channel === 'custom') | |||
this.currentSubChannel = showValue; | |||
showValue = showValue || channel; | |||
this.find('.channelPicker').html(showValue); | |||
this.find('input').focus(); | |||
this.el.removeClass('picking'); | |||
} else | |||
this.onShowChannelOptions(channel); | |||
}, | |||
onPickSubChannel: function (subChannel, channel) { | |||
this.currentSubChannel = subChannel; | |||
this.find('.channelOptions').removeClass('active'); | |||
this.find('.channelPicker').html(subChannel); | |||
const elInput = this.find('input'); | |||
elInput.focus(); | |||
if (channel === 'custom') { | |||
if (subChannel === 'join new') { | |||
elInput.val('/join channelName'); | |||
elInput[0].setSelectionRange(6, 17); | |||
} else if (subChannel === 'leave') { | |||
elInput.val('/leave channelName'); | |||
elInput[0].setSelectionRange(7, 18); | |||
} | |||
} | |||
this.el.removeClass('picking'); | |||
}, | |||
onShowChannelOptions: function (currentPick) { | |||
const optionContainer = this.find('.channelOptions') | |||
.addClass('active') | |||
.empty(); | |||
const options = []; | |||
let handlerOnClick = this.onPickChannel; | |||
this.el.addClass('picking'); | |||
if (!currentPick) { | |||
options.push('global', 'custom'); | |||
if (this.privateChannels.length) | |||
options.push('direct'); | |||
//Hack...surely we can find a more sane way to do this | |||
if ($('.uiParty .member').length) | |||
options.push('party'); | |||
} else { | |||
handlerOnClick = this.onPickSubChannel; | |||
if (currentPick === 'direct') | |||
options.push(...this.privateChannels); | |||
else if (currentPick === 'custom') | |||
options.push(...this.customChannels, 'join new', 'leave'); | |||
} | |||
if (!options.length) { | |||
this.onPickChannel('global'); | |||
return; | |||
} | |||
let addSelectStyleTo = null; | |||
if (currentPick) | |||
addSelectStyleTo = this.currentSubChannel || options[0]; | |||
options.forEach(o => { | |||
const html = `<div class='option'>${o}</div>`; | |||
const el = $(html) | |||
.appendTo(optionContainer) | |||
.on('click', handlerOnClick.bind(this, o, currentPick)) | |||
.on('hover', this.stopKeyboardNavForOptions.bind(this)); | |||
if (o === addSelectStyleTo) | |||
el.addClass('selected'); | |||
}); | |||
}, | |||
stopKeyboardNavForOptions: function () { | |||
this.find('.channelOptions .option.selected').removeClass('selected'); | |||
} | |||
}; | |||
}); |
@@ -0,0 +1,123 @@ | |||
define([ | |||
'html!ui/templates/messages/tplTab' | |||
], function ( | |||
tplTab | |||
) { | |||
const extensionObj = { | |||
renderKeyboard: function () { | |||
this.find('.keyboard').remove(); | |||
let container = $('<div class="keyboard"></div>') | |||
.appendTo(this.el); | |||
let keyboard = { | |||
0: 'qwertyuiop|asdfghjkl|zxcvbnm', | |||
1: 'QWERTYUIOP|ASDFGHJKL|ZXCVBNM', | |||
2: '1234567890|@#&*-+=()|_$"\';/' | |||
}[this.kbUpper].split(''); | |||
//Hacky: Insert control characters in correct positions | |||
//Backspace goes after 'm' | |||
if (this.kbUpper === 0) { | |||
keyboard.splice(keyboard.indexOf('z'), 0, 'caps'); | |||
keyboard.splice(keyboard.indexOf('m') + 1, 0, '<<'); | |||
} else if (this.kbUpper === 1) { | |||
keyboard.splice(keyboard.indexOf('Z'), 0, 'caps'); | |||
keyboard.splice(keyboard.indexOf('M') + 1, 0, '<<'); | |||
} else if (this.kbUpper === 2) | |||
keyboard.splice(keyboard.indexOf('/') + 1, 0, '<<'); | |||
keyboard.push(...['|', '123', ',', 'space', '.', 'send']); | |||
let row = 0; | |||
keyboard.forEach(k => { | |||
if (k === '|') { | |||
row++; | |||
const postGapCount = row === 4 ? 0 : row - 1; | |||
for (let i = 0; i < postGapCount; i++) | |||
$('<div class="gap" />').appendTo(container); | |||
$('<div class="newline" />').appendTo(container); | |||
const preGapCount = row === 3 ? 0 : row; | |||
for (let i = 0; i < preGapCount; i++) | |||
$('<div class="gap" />').appendTo(container); | |||
return; | |||
} | |||
let className = (k.length === 1) ? 'key' : 'key special'; | |||
if (k === ' ') { | |||
k = '.'; | |||
className = 'key hidden'; | |||
} | |||
className += ' ' + k; | |||
let elKey = $(`<div class="${className}">${k}</div>`) | |||
.appendTo(container); | |||
if (!className.includes('hidden')) | |||
elKey.on('click', this.clickKey.bind(this, k)); | |||
}); | |||
}, | |||
clickKey: function (key) { | |||
window.navigator.vibrate(20); | |||
let elInput = this.find('input'); | |||
const handler = { | |||
caps: () => { | |||
this.kbUpper = (this.kbUpper + 1) % 2; | |||
this.renderKeyboard(); | |||
}, | |||
123: () => { | |||
this.kbUpper = (this.kbUpper === 2) ? 0 : 2; | |||
this.renderKeyboard(); | |||
}, | |||
space: () => this.clickKey(' '), | |||
'<<': () => { | |||
elInput.val(elInput.val().slice(0, -1)); | |||
this.find('.input').html(elInput.val()); | |||
}, | |||
send: () => { | |||
this.sendChat({ which: 13 }); | |||
this.find('.input').html(''); | |||
this.find('input').val(''); | |||
} | |||
}[key]; | |||
if (handler) { | |||
handler(); | |||
return; | |||
} | |||
elInput.val(elInput.val() + key); | |||
this.enforceMaxMsgLength(); | |||
this.find('.input').html(elInput.val()); | |||
} | |||
}; | |||
return { | |||
init: function () { | |||
$.extend(this, extensionObj); | |||
this.kbUpper = 0; | |||
this.el.on('click', this.toggle.bind(this, true)); | |||
this.renderKeyboard(); | |||
$(tplTab) | |||
.appendTo(this.find('.filters')) | |||
.addClass('btnClose') | |||
.html('x') | |||
.on('click', this.toggle.bind(this, false, true)); | |||
} | |||
}; | |||
}); |
@@ -198,14 +198,21 @@ | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
color: @blueA; | |||
color: @grayB; | |||
background-color: @blackA; | |||
cursor: pointer; | |||
padding: 0px 10px; | |||
&:after { | |||
content: attr(shortcut); | |||
margin-left: 10px; | |||
color: @grayC; | |||
} | |||
&:hover, | |||
&.selected { | |||
background-color: @grayD; | |||
color: @white; | |||
} | |||
} | |||
@@ -267,9 +274,15 @@ | |||
} | |||
&.picking { | |||
> *:not(.channelOptions) { | |||
opacity: 0.4; | |||
pointer-events: none; | |||
&:before { | |||
position: absolute; | |||
content: ''; | |||
left: 0px; | |||
top: 0px; | |||
width: 100%; | |||
height: 100%; | |||
background-color: @blackD; | |||
opacity: 0.7; | |||
} | |||
} | |||