1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-04 10:08:31 +00:00
FrankerFaceZ/src/ember/chat-input.js

1161 lines
32 KiB
JavaScript
Raw Normal View History

var FFZ = window.FrankerFaceZ,
utils = require("../utils"),
constants = require("../constants"),
is_android = navigator.userAgent.indexOf('Android') !== -1,
CHARCODES = {
AT_SIGN: 64,
COLON: 58
},
KEYCODES = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
ESC: 27,
SPACE: 32,
PAGE_UP: 33,
PAGE_DOWN: 34,
END: 35,
HOME: 36,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
TWO: 50,
COLON: 59,
FAKE_COLON: 186
},
selection_start = function(e) {
if ( typeof e.selectionStart === "number" )
return e.selectionStart;
if ( ! e.createTextRange )
return -1;
var n = document.selection.createRange(),
r = e.createTextRange();
r.moveToBookmark(n.getBookmark());
r.moveStart("character", -e.value.length);
return r.text.length;
},
move_selection = function(e, pos) {
if ( e.setSelectionRange )
e.setSelectionRange(pos, pos);
else if ( e.createTextRange ) {
var r = e.createTextRange();
r.move("character", -e.value.length);
r.move("character", pos);
r.select();
}
2016-03-26 16:09:36 -04:00
},
build_sort_key = function(item, now, is_whisper) {
if ( item.type === 'emoticon' )
return '2|' + (item.favorite ? 1 : 2) + '|' + item.sort + '|' + item.label;
2016-03-26 16:09:36 -04:00
else if ( item.type === 'emoji' )
return '3|' + (item.favorite ? 1 : 2) + '|' + item.label;
2016-03-26 16:09:36 -04:00
return '4|' + item.label;
};
// ---------------------
// Settings
// ---------------------
FFZ.settings_info.input_quick_reply = {
type: "boolean",
value: true,
category: "Chat Input",
no_bttv: true,
name: "Reply to Whispers with /r",
help: "Automatically replace /r at the start of the line with the command to whisper to the person you've whispered with most recently."
};
FFZ.settings_info.input_mru = {
type: "boolean",
value: true,
category: "Chat Input",
no_bttv: true,
name: "Chat Input History",
help: "Use the Up and Down arrows in chat to select previously sent chat messages."
};
2016-03-26 16:09:36 -04:00
FFZ.settings_info.input_complete_emotes = {
type: "select",
options: {
0: "Disabled",
1: "Channel and Sub Only",
2: "All Emoticons"
},
value: 2,
process_value: utils.process_int(2),
category: "Chat Input",
no_bttv: true,
2016-09-30 13:09:03 -04:00
name: "Tab-Complete Emoticons",
help: "Use tab completion to complete emoticon names in chat.",
on_update: function(val) {
if ( this._inputv )
Ember.propertyDidChange(this._inputv, 'ffz_emoticons');
}
2016-03-26 16:09:36 -04:00
}
FFZ.settings_info.input_complete_aliases = {
type: "select",
options: {
0: "Disabled",
1: "By Name or Alias",
2: "Aliases Only"
},
value: 1,
process_value: utils.process_int(1),
category: "Chat Input",
no_bttv: true,
name: "Tab-Complete User Aliases",
help: "Use tab completion to complete aliases you've given to users rather than their username.",
on_update: function(val) {
if ( this._inputv )
Ember.propertyDidChange(this._inputv, 'ffz_name_suggestions');
}
}
FFZ.settings_info.input_complete_name_at = {
type: "boolean",
value: true,
category: "Chat Input",
no_bttv: true,
name: "Tab-Complete Usernames with At Sign",
help: "When enabled, tab-completed usernames will have an @ sign before them if you typed one. This is default Twitch behavior, but unnecessary."
}
FFZ.settings_info.input_emoticons_case_sensitive = {
type: "boolean",
value: true,
category: "Chat Input",
no_bttv: true,
name: "Tab-Complete Emoticons Case Sensitive",
help: "When enabled, tab-completion for emoticons is case sensitive."
}
2016-03-26 16:09:36 -04:00
FFZ.settings_info.input_complete_without_prefix = {
type: "boolean",
value: true,
2016-03-26 16:09:36 -04:00
category: "Chat Input",
no_bttv: true,
2016-03-26 16:09:36 -04:00
name: "Tab-Complete Sub Emotes without Prefix",
help: "Allow you to tab complete a sub emote without including its prefix. Example: Battery into chrisBattery",
2016-03-26 16:09:36 -04:00
on_update: function(val) {
if ( this._inputv )
Ember.propertyDidChange(this._inputv, 'ffz_emoticons');
}
2016-03-26 16:09:36 -04:00
}
FFZ.settings_info.input_emoji = {
type: "boolean",
2016-10-02 18:01:40 -04:00
value: true,
category: "Chat Input",
//visible: false,
no_bttv: true,
name: "Enter Emoji By Name",
help: "Replace emoji that you type by name with the character. :+1: becomes 👍."
};
// ---------------------
// Initialization
// ---------------------
FFZ.prototype.setup_chat_input = function() {
this.log("Hooking the Ember Chat Input component.");
this.update_views("component:chat/twitch-chat-input", this.modify_chat_input);
}
FFZ.prototype.modify_chat_input = function(component) {
var f = this;
utils.ember_reopen_view(component, {
ffz_mru_index: -1,
ffz_current_suggestion: 0,
ffz_partial_word: '',
ffz_partial_word_start: -1,
ffz_suggestions_visible: false,
ffz_freeze_suggestions: -1,
ffz_suggestions_el: null,
ffz_name_suggestions: [],
ffz_chatters: [],
ffz_init: function() {
f._inputv = this;
var s = this._ffz_minimal_style = document.createElement('style');
s.id = 'ffz-minimal-chat-textarea-height';
document.head.appendChild(s);
this.set('ffz_name_suggestions', this.get('suggestions'));
2016-03-26 16:09:36 -04:00
// Redo our key bindings.
var t = this.$("textarea");
if ( ! t || ! t.length )
f.error("Cannot find textarea in Twitch Chat Input.");
t.off("keydown");
t.off("keyup");
t.on("keypress", this._ffzKeyPress.bind(this));
t.on("keydown", this._ffzKeyDown.bind(this));
t.on("paste", this._ffzPaste.bind(this));
t.attr('rows', 1);
this.ffzResizeInput();
setTimeout(this.ffzResizeInput.bind(this), 500);
},
ffz_destroy: function() {
if ( f._inputv === this )
f._inputv = undefined;
this.ffzResizeInput();
if ( this._ffz_minimal_style ) {
this._ffz_minimal_style.parentElement.removeChild(this._ffz_minimal_style);
this._ffz_minimal_style = undefined;
}
// Reset normal key bindings.
var t = this.$("textarea");
t.attr('rows', undefined);
t.off("keyup");
t.off("keydown");
t.off("keypress");
t.off("paste");
t.on("keyup", this._onKeyUp.bind(this));
t.on("keydown", this._onKeyDown.bind(this));
},
// Pasting~!
_ffzPaste: function(event) {
var data = (event.clipboardData || (event.originalEvent && event.originalEvent.clipboardData) || window.clipboardData),
text = data && data.getData('text/plain');
// If we don't have a colon, there can't be any emoji.
// Likewise, if the user doesn't want input emoji, don't convert them.
if ( ! f.settings.input_emoji || text.indexOf(':') === -1 )
return;
// Alright, check for emoji now.
var output = [],
input = text.split(':'),
last_was_emoji = false;
output.push(input.shift());
for(var i=0, l = input.length - 1; i < l; i++) {
var segment = input[i],
emoji = ! last_was_emoji ? f.emoji_data[f.emoji_names[segment]] : null;
if ( emoji ) {
output.push(emoji.raw);
last_was_emoji = true;
} else {
output.push((last_was_emoji ? '' : ':') + segment);
last_was_emoji = false;
}
}
output = output.join("") + (last_was_emoji ? '' : ':') + input[input.length-1];
// Let the browser's paste be do as it do if there weren't any emoji.
if ( output.length === text.length )
return;
// Can we get the selection in our input box?
var input = this.get('chatTextArea'),
s_val = input && input.value,
s_start = input && input.selectionStart,
s_end = input && input.selectionEnd;
if ( ! input || typeof s_start !== "number" || typeof s_end !== "number" )
return;
// Still here? We're clear to inject this ourselves then.
event.stopPropagation();
event.preventDefault();
input.value = s_val.substr(0, s_start) + output + s_val.substr(s_end);
move_selection(input, s_start + output.length);
},
// Suggestions
ffzBuildSuggestionItem: function(i, item) {
// Returns a new element for the suggestions list.
if ( ! item )
return null;
var t = this,
el = utils.createElement('div', 'suggestion'),
inner = utils.createElement('div'),
width = item.width ? (246 - item.width) + 'px' : null;
el.setAttribute('data-id', i);
el.classList.toggle('ffz-favorite', item.favorite || false);
2016-03-26 16:09:36 -04:00
if ( item.image ) {
el.classList.add('has-image');
el.classList.toggle('is-emoji', item.type === 'emoji');
el.style.backgroundImage = 'url("' + utils.quote_attr(item.image) + '")';
}
2016-03-26 16:09:36 -04:00
inner.innerHTML = item.label;
if ( width )
inner.style.maxWidth = width;
el.appendChild(inner);
if ( f.settings.input_complete_emotes && item.info ) {
var info = utils.createElement('span');
info.innerHTML = item.info;
el.classList.add('has-info');
if ( width )
info.style.maxWidth = width;
el.appendChild(info);
}
2016-03-26 16:09:36 -04:00
el.addEventListener('mouseenter', function() {
if ( t.get('ffz_freeze_suggestions') === -1 ) {
var els = el.parentElement.querySelectorAll('.suggestion'),
middle = els[Math.floor(els.length / 2)];
t.set('ffz_freeze_suggestions', middle ? parseInt(middle.getAttribute('data-id')) : i)
}
2016-03-26 16:09:36 -04:00
t.set('ffz_current_suggestion', i);
});
2016-03-26 16:09:36 -04:00
el.addEventListener('mouseup', function() {
t.ffzCompleteSuggestion(item);
});
2016-03-26 16:09:36 -04:00
el.addEventListener('wheel', function(e) {
// We want to scroll the list up or down. Harder than it sounds. In order
// to scroll it well, we should use the center item, not the one under
// the mouse.
var suggestions = t.get('ffz_sorted_suggestions'),
first_el = el.parentElement.querySelector('.suggestion:first-of-type'),
first = first_el && parseInt(first_el.getAttribute('data-id'));
2016-03-26 16:09:36 -04:00
first += event.deltaY > 0 ? 1 : -1;
2016-03-26 16:09:36 -04:00
t.set('ffz_freeze_suggestions', -1);
t.set('ffz_current_suggestion', Math.min(first + 2, suggestions.length - 1));
});
2016-03-26 16:09:36 -04:00
return el;
},
2016-03-26 16:09:36 -04:00
ffzUpdateSuggestions: function() {
var visible = this.get('ffz_suggestions_visible');
if ( visible ) {
if ( this.get('ffz_updating') )
return;
2016-03-26 16:09:36 -04:00
this.set('ffz_updating', true);
2016-03-26 16:09:36 -04:00
var el = this.ffz_suggestions_el,
current = this.get('ffz_current_suggestion') || 0;
2016-03-26 16:09:36 -04:00
if ( ! el ) {
el = this.ffz_suggestions_el = utils.createElement('div', 'suggestions ffz-suggestions');
this.get('element').appendChild(el);
2016-03-26 16:09:36 -04:00
} else
el.innerHTML = '';
2016-03-26 16:09:36 -04:00
var suggestions = this.get('ffz_sorted_suggestions'),
freeze = this.get('ffz_freeze_suggestions'),
middle = freeze === -1 ? current : freeze,
2016-03-26 16:09:36 -04:00
first = Math.max(0, middle - 2),
last = Math.min(suggestions.length, first + 5),
added = false;
2016-03-26 16:09:36 -04:00
first = Math.min(first, Math.max(0, last - 5));
2016-03-26 16:09:36 -04:00
if ( current >= suggestions.length ) {
this.set('ffz_current_suggestion', first);
current = first;
}
2016-03-26 16:09:36 -04:00
for(var i=first; i < last; i++) {
var item = suggestions[i],
item_el = this.ffzBuildSuggestionItem(i, item);
2016-03-26 16:09:36 -04:00
if ( i === current )
item_el.classList.add('highlighted');
2016-03-26 16:09:36 -04:00
if ( item_el ) {
el.appendChild(item_el);
added = true;
}
}
2016-03-26 16:09:36 -04:00
if ( ! added ) {
var item_el = utils.createElement('div', 'suggestion disabled');
item_el.textContent = 'No matches.';
el.appendChild(item_el);
}
2016-03-26 16:09:36 -04:00
this.set('ffz_updating', false);
2016-03-26 16:09:36 -04:00
} else if ( this.ffz_suggestions_el ) {
this.ffz_suggestions_el.parentElement.removeChild(this.ffz_suggestions_el);
this.ffz_suggestions_el = null;
}
2016-03-26 16:09:36 -04:00
}.observes('ffz_suggestions_visible', 'ffz_sorted_suggestions', 'ffz_current_suggestion'),
2016-03-26 16:09:36 -04:00
ffzHideSuggestions: function() {
this.set('ffz_suggestions_visible', false);
this.set('ffz_freeze_suggestions', -1);
this.set('ffz_current_suggestion', 0);
},
2016-03-26 16:09:36 -04:00
ffzShowSuggestions: function() {
this.set('ffz_current_suggestion', 0);
this.ffzFetchNameSuggestions();
this.set('ffz_freeze_suggestions', -1);
this.set('ffz_suggestions_visible', true);
this.ffzSetPartialWord();
},
2016-03-26 16:09:36 -04:00
ffzSetPartialWord: function() {
var area = this.get('chatTextArea');
if ( area && this.get('ffz_suggestions_visible') ) {
var text = this.get('textareaValue'),
ind = selection_start(area);
2016-03-26 16:09:36 -04:00
if ( ind === -1 )
return this.ffzHideSuggestions();
2016-03-26 16:09:36 -04:00
var start = text.lastIndexOf(' ', ind - 1) + 1;
this.set('ffz_partial_word_start', start);
var match = text.substr(start).match(/^[^ ]*/);
if ( match && match[0] )
this.set('ffz_partial_word', match[0]);
else if ( text.charAt(0) === '/' && text.charAt(1) !== ' ' && start === (text.indexOf(' ') + 1) )
// Assume the first word after a command is a username.
this.set('ffz_partial_word', '@');
else
this.ffzHideSuggestions();
}
}.observes('textareaValue'),
ffzFetchNameSuggestions: function() {
if ( ! this.get('ffz_suggestions_visible') )
2016-09-30 13:09:03 -04:00
this.set('ffz_name_suggestions', this.get('suggestions')());
}.observes('suggestions'),
ffzCompleteSuggestion: function(item) {
if ( ! item ) {
var suggestions = this.get('ffz_sorted_suggestions'),
current = this.get('ffz_current_suggestion');
item = suggestions && suggestions[current];
}
this.ffzHideSuggestions();
if ( ! item )
return;
var t = this,
ind = this.get('ffz_partial_word_start'),
text = this.get('textareaValue'),
2016-03-26 16:09:36 -04:00
first_char = text.charAt(0),
is_cmd = first_char === '/' || first_char === '.',
content = ((f.settings.input_complete_name_at && ! is_cmd && item.type === 'user' && this.get('ffz_partial_word').charAt(0) === '@') ? '@' : '') +
((item.command_content && is_cmd ?
item.command_content : item.content) || item.label),
trail = text.substr(ind + this.get('ffz_partial_word').length),
prefix = text.substr(0, ind) + content + (trail ? '' : ' ');
this.set('textareaValue', prefix + trail);
this.set('ffz_partial_word', '');
this.set('ffz_partial_word_start', -1);
this.trackSuggestionsCompleted();
Ember.run.next(function() {
var area = t.get('chatTextArea');
move_selection(area, prefix.length);
area.focus();
});
},
ffz_emoticons: function() {
var emotes = {},
room = this.get('parentView.context.model'),
room_id = room && room.get('id'),
tmi = room && room.tmiSession,
set_name, replacement, url, is_sub_set, fav_list,
emote_set, emote, emote_id, code,
user = f.get_user(),
ffz_sets = f.getEmotes(user && user.login, room_id),
setting = f.settings.input_complete_emotes;
if ( ! setting )
return {};
if ( tmi ) {
var es = tmi.getEmotes();
if ( es && es.emoticon_sets ) {
for(var set_id in es.emoticon_sets) {
emote_set = es.emoticon_sets[set_id];
fav_list = f.settings.favorite_emotes['twitch-' + set_id] || [];
is_sub_set = false;
set_name = f._twitch_set_to_channel[set_id];
if ( ! emote_set )
continue;
if ( set_name ) {
if ( set_name === '--global--' )
set_name = 'Twitch Global';
else if ( set_name === '--twitch-turbo--' || set_name === 'turbo' || set_name === '--turbo-faces--' )
set_name = 'Twitch Turbo';
else {
set_name = 'Channel: ' + FFZ.get_capitalization(set_name);
is_sub_set = true;
}
} else
set_name = "Unknown Source";
if ( setting === 1 && ! is_sub_set )
continue;
for(var i = 0; i < emote_set.length; i++) {
emote = emote_set[i];
code = emote && emote.code;
code = code && (constants.KNOWN_CODES[code] || code);
replacement = f.settings.replace_bad_emotes && constants.EMOTE_REPLACEMENTS[emote.id];
url = replacement ?
(constants.EMOTE_REPLACEMENT_BASE + replacement) :
(constants.TWITCH_BASE + emote.id + "/1.0");
if ( ! emotes[code] || ! emotes[code][0] )
emotes[code] = [true, code, true, is_sub_set, set_name, url, null, fav_list.indexOf(emote.id) !== -1];
if ( f.settings.input_complete_without_prefix && is_sub_set ) {
// It's a sub emote, so try splitting off the end of the code.
// It's a bit weird, but people might like it. Also, make sure
// we aren't just grabbing an initial capital.
var unprefixed = code.substr(1).match(/[A-Z].+$/);
unprefixed = unprefixed ? unprefixed[0] : null;
if ( unprefixed && ! emotes[unprefixed] )
emotes[unprefixed] = [false, code, true, is_sub_set, set_name, url, null, fav_list.indexOf(emote.id) !== -1];
}
}
}
}
}
for(var i=0; i < ffz_sets.length; i++) {
emote_set = f.emote_sets[ffz_sets[i]];
if ( ! emote_set )
continue;
if ( setting === 1 && f.default_sets.indexOf(emote_set.id) !== -1 )
continue;
set_name = (emote_set.source || "FFZ") + " " + (emote_set.title || "Global");
fav_list = f.settings.favorite_emotes[emote_set.hasOwnProperty('source_ext') ? 'ffz-ext-' + emote_set.source_ext + '-' + emote_set.source_id : 'ffz-' + emote_set.id] || [];
for(emote_id in emote_set.emoticons) {
emote = emote_set.emoticons[emote_id];
if ( ! emote.hidden && emote.name && (! emotes[emote.name] || ! emotes[emote.name][0]) )
emotes[emote.name] = [true, emote.name, false, emote_set.id, set_name, emote.urls[1], emote.width, fav_list.indexOf(emote.id) !== -1];
}
}
return emotes;
}.property(),
_setPartialName: function() { },
ffz_suggestions: function() {
var output = [],
emotes = this.get('ffz_emoticons'),
suggestions = this.get('ffz_name_suggestions'); //.mapBy('id').uniq();
if ( f.settings.input_complete_emotes ) {
// Include Emoticons
for(var emote_name in emotes) {
var emote = emotes[emote_name],
sort_factor = 9,
label = emote[1] === emote_name ? emote[1] : ('<i>' + emote[1].substr(0, emote[1].length - emote_name.length) + '</i>' + emote_name);
if ( emote[2] ) {
if ( emote[3] )
sort_factor = 1;
} else {
var set_data = f.emote_sets[emote[3]];
if ( set_data )
if ( set_data._type === 1 )
sort_factor = 3;
else
sort_factor = ffz.default_sets.indexOf(set_data.id) === -1 ? 2 : 6;
}
output.push({
type: "emoticon",
match: emote_name,
sort: sort_factor,
content: emote[1],
label: label,
info: emote[4],
image: emote[5],
width: emote[6],
favorite: emote[7] || false
});
}
if ( f.settings.parse_emoji ) {
// Include Emoji
var setting = f.settings.parse_emoji,
fav_list = f.settings.favorite_emotes['emoji'] || [];
for(var short_name in f.emoji_names) {
var eid = f.emoji_names[short_name],
emoji = f.emoji_data[eid];
if ( ! emoji || !(setting === 3 ? emoji.one : (setting === 2 ? emoji.noto : emoji.tw)) )
continue;
var sn = ':' + short_name + ':',
src = (f.settings.parse_emoji === 3 ? emoji.one_src : (f.settings.parse_emoji === 2 ? emoji.noto_src : emoji.tw_src));
output.push({
type: "emoji",
match: ':' + short_name + ':',
content: emoji.raw,
label: emoji.name,
info: sn,
image: src,
width: 18,
favorite: fav_list.indexOf(emoji.raw) !== -1
});
}
}
}
// Always include Users
var user_output = {},
alias_setting = f.settings.input_complete_aliases;
for(var i=0; i < suggestions.length; i++) {
var suggestion = suggestions[i],
name = suggestion.id,
display_name = suggestion.displayName || (name && name.capitalize()),
username_match = display_name.trim().toLowerCase() === name,
alias = f.aliases[name];
if ( user_output[name] && ! user_output[name].is_alias ) {
var token = user_output[name];
token.whispered |= suggestion.whispered;
if ( suggestion.timestamp > token.timestamp )
token.timestamp = suggestion.timestamp;
}
if ( user_output[display_name] && ! user_output[display_name].is_alias ) {
var token = user_output[display_name];
token.whispered |= suggestion.whispered;
if ( suggestion.timestamp > token.timestamp )
token.timestamp = suggestion.timestamp;
} else {
if ( alias_setting !== 2 ) {
output.push(user_output[display_name] = {
type: "user",
command_content: name,
label: display_name,
alternate_match: username_match ? null : name,
whispered: suggestion.whispered,
timestamp: suggestion.timestamp || new Date(0),
info: 'User' + (username_match ? '' : ' (' + name + ')'),
is_display_name: ! username_match,
is_alias: false
});
if ( ! username_match )
output.push(user_output[name] = {
type: "user",
label: name,
alternate_match: display_name,
whispered: suggestion.whispered,
timestamp: suggestion.timestamp || new Date(0),
info: 'User (' + display_name + ')',
is_alias: false
});
}
if ( alias && alias_setting ) {
if ( user_output[alias] && user_output[alias].is_alias ) {
var token = user_output[alias];
token.whispered |= suggestion.whispered;
token.timestamp = Math.max(token.timestamp, suggestion.timestamp);
} else if ( ! user_output[alias] )
output.push(user_output[alias] = {
type: "user",
command_content: name,
label: alias,
whispered: suggestion.whispered,
timestamp: suggestion.timestamp || new Date(0),
info: 'User Alias (' + name + ')',
is_alias: true
});
}
}
}
return output;
}.property('ffz_emoticons', 'ffz_name_suggestions'),
ffz_filtered_suggestions: Ember.computed("ffz_suggestions", "ffz_partial_word", function() {
var suggestions = this.get('ffz_suggestions'),
partial = this.get('ffz_partial_word'),
part2 = partial.substr(1),
char = partial.charAt(0);
return suggestions.filter(function(item) {
var name = item.match || item.content || item.label,
type = item.type;
if ( ! name )
return false;
if ( type === 'user' ) {
// Names are case insensitive, and we have to ignore the leading @ of our
// partial word when matching.
name = name.toLowerCase();
var part = (char === '@' ? part2 : partial).toLowerCase(),
alt_name = item.alternate_match;
return name.indexOf(part) === 0 || (alt_name && alt_name.indexOf(part) === 0);
} else if ( type === 'emoji' || ! f.settings.input_emoticons_case_sensitive ) {
name = name.toLowerCase();
return name.indexOf(partial.toLowerCase()) === 0;
}
return name.indexOf(partial) === 0;
});
}),
ffz_sorted_suggestions: Ember.computed("ffz_filtered_suggestions.[]", function() {
var text = this.get('textareaValue'),
now = Date.now(),
char = text.charAt(0),
is_command = char === '/' || char === '.',
is_whisper = is_command && text.substr(1, 2) === 'w ';
return this.get('ffz_filtered_suggestions').sort(function(a, b) {
// First off, sort users ahead of everything else.
if ( a.type === 'user' ) {
if ( b.type !== 'user' )
return -1;
else if ( is_whisper ) {
if ( a.whisper && ! b.whisper )
return -1;
else if ( ! a.whisper && b.whisper )
return 1;
}
if ( a.is_display_name && ! b.is_display_name )
return -1;
else if ( ! a.is_display_name && b.is_display_name )
return 1;
if ( a.timestamp > b.timestamp ) return -1;
else if ( a.timestamp < b.timestamp ) return 1;
var an = a.label.toLowerCase(),
bn = b.label.toLowerCase();
if ( an < bn ) return -1;
else if ( an > bn ) return 1;
return 0;
} else if ( b.type === 'user' )
return 1;
var an = build_sort_key(a, now, is_whisper),
bn = build_sort_key(b, now, is_whisper);
if ( an < bn ) return -1;
if ( an > bn ) return 1;
return 0;
});
}),
2016-03-26 16:09:36 -04:00
// Input Control
ffzOnInput: function() {
if ( ! f._chat_style || f.settings.minimal_chat < 2 || is_android )
return;
var now = Date.now(),
since = now - (this._ffz_last_resize || 0);
if ( since > 500 )
this.ffzResizeInput();
}.observes('textareaValue'),
ffzResizeInput: function() {
this._ffz_last_resize = Date.now();
var el = this.get('element'),
t = el && el.querySelector('textarea');
if ( ! t || ! f._chat_style || f.settings.minimal_chat < 2 )
return;
// Unfortunately, we need to change this with CSS.
this._ffz_minimal_style.innerHTML = 'body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textarea { height: auto !important; }';
var height = Math.max(32, Math.min(128, t.scrollHeight));
this._ffz_minimal_style.innerHTML = 'body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textarea { height: ' + height + 'px !important; }';
if ( height !== this._ffz_last_height ) {
utils.update_css(f._chat_style, "input_height", 'body.ffz-minimal-chat-input .ember-chat .chat-interface { height: ' + height + 'px !important; }' +
'body.ffz-minimal-chat-input .ember-chat .chat-messages, body.ffz-minimal-chat-input .ember-chat .chat-interface .emoticon-selector { bottom: ' + height + 'px !important; }');
f._roomv && f._roomv.get('stuckToBottom') && f._roomv._scrollToBottom();
}
this._ffz_last_height = height;
},
hideSuggestions: Ember.on("document.mouseup", function(event) {
var target = event.target,
cl = target.classList;
if ( ! this.get('ffz_suggestions_visible') || cl.contains('suggestion') || cl.contains('suggestions') || target === this.get('chatTextArea') )
return;
this.ffzHideSuggestions();
}),
_ffzKeyPress: function(event) {
var t = this,
e = event || window.event,
key = e.charCode || e.keyCode;
switch(key) {
case CHARCODES.AT_SIGN:
// If we get an @, show the menu. But only if we're at a new word
// boundary, or the start of the line.
if ( ! this.get('ffz_suggestions_visible') ) {
var ind = selection_start(this.get('chatTextArea')) - 1;
Ember.run.next(function() {
if ( ind < 0 || t.get('textareaValue').charAt(ind) === ' ' ) {
t.ffzShowSuggestions();
t.trackSuggestions("@");
}
});
}
break;
case CHARCODES.COLON:
if ( f.settings.input_emoji ) {
var textarea = this.get('chatTextArea'),
ind = selection_start(textarea);
ind > 0 && Ember.run.next(function() {
var text = t.get('textareaValue'),
emoji_start = text.lastIndexOf(':', ind - 1);
if ( emoji_start !== -1 && ind !== -1 && text.charAt(ind) === ':' ) {
var match = text.substr(emoji_start + 1, ind - emoji_start - 1),
emoji_id = f.emoji_names[match],
emoji = f.emoji_data[emoji_id];
if ( emoji ) {
var prefix = text.substr(0, emoji_start) + emoji.raw;
t.ffzHideSuggestions();
t.set('textareaValue', prefix + text.substr(ind + 1));
Ember.run.next(function() {
move_selection(t.get('chatTextArea'), prefix.length);
});
}
}
})
}
}
},
_ffzKeyDown: function(event) {
2016-03-26 16:09:36 -04:00
var t = this,
e = event || window.event,
key = e.charCode || e.keyCode;
switch(key) {
case KEYCODES.ESC:
if ( this.get('ffz_suggestions_visible') ) {
this.ffzHideSuggestions();
e.preventDefault();
}
2016-03-26 16:09:36 -04:00
break;
2016-03-26 16:09:36 -04:00
case KEYCODES.BACKSPACE:
if ( this.get('ffz_suggestions_visible') && (this.get('ffz_partial_word').length === 1 || selection_start(this.get('chatTextArea')) === 0) )
this.ffzHideSuggestions();
2016-03-26 16:09:36 -04:00
break;
2016-03-26 16:09:36 -04:00
case KEYCODES.TAB:
// If we do Ctrl-Tab or Alt-Tab. Just don't
// even think of doing suggestions.
if ( e.ctrlKey || e.altKey || e.metaKey )
break;
2016-03-26 16:09:36 -04:00
e.preventDefault();
2016-03-26 16:09:36 -04:00
var text = this.get('textareaValue');
if ( text.length === 0 )
break;
2016-03-26 16:09:36 -04:00
if ( text.charAt(0) !== '/' ) {
var parts = text.split(' ');
if ( parts[parts.length - 1].length === 0 )
break;
}
2016-03-26 16:09:36 -04:00
// If suggestions aren't visible... show them. And set that we
// triggered the suggestions with tab.
if ( ! this.get('ffz_suggestions_visible') ) {
this.ffzFetchNameSuggestions();
this.set('ffz_suggestions_visible', true);
this.ffzSetPartialWord();
this.trackSuggestions("Tab");
2016-03-26 16:09:36 -04:00
// If suggestions *are* visible, enter a suggestion.
} else
this.ffzCompleteSuggestion();
2016-03-26 16:09:36 -04:00
break;
2016-03-26 16:09:36 -04:00
case KEYCODES.PAGE_UP:
case KEYCODES.PAGE_DOWN:
// Navigate through suggestions if those are open.
if ( this.get('ffz_suggestions_visible') && !( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey ) ) {
var suggestions = this.get('ffz_sorted_suggestions'),
current = this.get('ffz_current_suggestion') + (key === KEYCODES.PAGE_UP ? -5 : 5);
2016-03-26 16:09:36 -04:00
if ( current < 0 )
current = 0;
else if ( current >= suggestions.length )
current = suggestions.length - 1;
2016-03-26 16:09:36 -04:00
this.set('ffz_freeze_suggestions', -1);
this.set('ffz_current_suggestion', current);
e.preventDefault();
}
2016-03-26 16:09:36 -04:00
break;
2016-03-26 16:09:36 -04:00
case KEYCODES.UP:
case KEYCODES.DOWN:
// First, navigate through suggestions if those are open.
if ( this.get('ffz_suggestions_visible') && !( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey ) ) {
var suggestions = this.get('ffz_sorted_suggestions'),
current = this.get('ffz_current_suggestion') + (key === KEYCODES.UP ? -1 : 1);
if ( current < 0 )
current = suggestions.length - 1;
else if ( current >= suggestions.length )
current = 0;
this.set('ffz_freeze_suggestions', -1);
this.set('ffz_current_suggestion', current);
e.preventDefault();
2016-03-26 16:09:36 -04:00
break;
// Otherwise, if we're holding any special modifiers, don't do
// anything special to avoid breaking functionality.
} else if ( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey )
break;
// If MRU is enabled, cycle through it if the cursor's position doesn't
// change as a result of this action.
else if ( f.settings.input_mru )
Ember.run.next(this.ffzCycleMRU.bind(this, key, selection_start(this.get("chatTextArea"))));
2016-03-26 16:09:36 -04:00
// If MRU isn't enabled, cycle through the whisper targets.
else
Ember.run.next(this.cycleWhisperTargets.bind(this, key));
2016-03-26 16:09:36 -04:00
break;
2016-03-26 16:09:36 -04:00
case KEYCODES.ENTER:
if ( e.shiftKey || e.shiftLeft )
break;
2016-03-26 16:09:36 -04:00
this.set('ffz_mru_index', -1);
if ( this.get('ffz_suggestions_visible') )
this.ffzCompleteSuggestion();
else {
this.set("_currentWhisperTarget", -1);
setTimeout(this.ffzResizeInput.bind(this), 25);
this.sendAction("sendMessage");
}
2016-03-26 16:09:36 -04:00
if ( e.stopPropagation )
e.stopPropagation();
2016-03-26 16:09:36 -04:00
e.preventDefault();
break;
2016-03-26 16:09:36 -04:00
case KEYCODES.SPACE:
// First things first, if we're currently showing suggestions, get rid of them.
if ( this.get('ffz_suggestions_visible') )
this.ffzHideSuggestions();
// After pressing space, if we're entering a command, do stuff!
// TODO: Better support for commands.
var sel = selection_start(this.get('chatTextArea'));
Ember.run.next(function() {
var text = t.get("textareaValue"),
ind = text.indexOf(' '),
start = ind !== -1 && text.substr(0, ind);
if ( ind !== sel )
return;
if ( f.settings.input_quick_reply && start === '/r' ) {
var target = t.get("uniqueWhisperSuggestions.0");
if ( target ) {
t.set("_currentWhisperTarget", 0);
t.set("textareaValue", "/w " + target + t.get("textareaValue").substr(2));
Ember.run.next(function() {
move_selection(t.get('chatTextArea'), 4 + target.length);
});
} else {
t.set("textareaValue", "/w " + t.get('textareaValue').substr(2));
Ember.run.next(function() {
move_selection(t.get('chatTextArea'), 3);
t.ffzFetchNameSuggestions();
t.set("ffz_suggestions_visible", true);
t.ffzSetPartialWord();
});
}
} else if ( start === '/w' || start === '/ignore' || start === '/unignore' || start === '/mod' || start === '/unmod' || start === '/ban' || start === '/unban' || start === '/timeout' || start === '/purge' ) {
t.ffzFetchNameSuggestions();
t.set("ffz_suggestions_visible", true);
t.ffzSetPartialWord();
}
});
}
},
2016-03-26 16:09:36 -04:00
ffzCycleMRU: function(key, start_ind) {
// We don't want to do this if the keys were just moving the cursor around.
var cur_pos = selection_start(this.get("chatTextArea"));
if ( start_ind !== cur_pos )
return;
var ind = this.get('ffz_mru_index'),
mru = this.get('parentView.context.model.mru_list') || [];
if ( key === KEYCODES.UP )
ind = (ind + 1) % (mru.length + 1);
else
ind = (ind + mru.length) % (mru.length + 1);
var old_val = this.get('ffz_old_mru');
if ( old_val === undefined || old_val === null ) {
old_val = this.get('textareaValue');
this.set('ffz_old_mru', old_val);
}
var new_val = mru[ind];
if ( new_val === undefined ) {
this.set('ffz_old_mru', undefined);
new_val = old_val;
}
this.set('ffz_mru_index', ind);
this.set('textareaValue', new_val);
}
});
}