Files
zulip/static/js/emoji_picker.js
Harshit Bansal 1a43728b1e emoji_picker: Fix tracebacks in navigation if search results are empty.
This fixes 2 bugs:

* If you perform a search and search results are empty then if you try
  to navigate using arrow keys, page-down/page-up etc. it will give a
  traceback.
* Search for example 'a' and then navigate to the last of the search
  results using arrow keys. Now press 'tab' to go back the search box
  and restrict the search to for e.g. 'ab' and now try to navigate
  using arrow keys, page-up/page-down etc you will get a traceback.
2017-08-22 14:48:02 -07:00

610 lines
20 KiB
JavaScript

var emoji_picker = (function () {
var exports = {};
// The functionalities for reacting to a message with an emoji
// and composing a message with an emoji share a single widget,
// implemented as the emoji_popover.
exports.emoji_collection = {};
exports.complete_emoji_catalog = [];
var current_message_emoji_popover_elem;
var emoji_catalog_last_coordinates = {
section: 0,
index: 0,
};
var current_section = 0;
var current_index = 0;
var search_is_active = false;
var search_results = [];
var section_head_offsets = [];
function get_rendered_emoji_categories() {
if (exports.complete_emoji_catalog.length === 0) {
blueslip.error('emoji_picker: Emoji catalog empty');
return;
}
var current_emoji_categories = [];
_.each(exports.complete_emoji_catalog, function (category) {
current_emoji_categories.push({
name: category.name,
icon: category.icon,
});
});
return current_emoji_categories;
}
function get_all_emoji_categories() {
return [
{ name: "Popular", icon: "fa-thumbs-o-up" },
{ name: "People", icon: "fa-smile-o" },
{ name: "Nature", icon: "fa-leaf" },
{ name: "Foods", icon: "fa-cutlery" },
{ name: "Activity", icon: "fa-soccer-ball-o" },
{ name: "Places", icon: "fa-car" },
{ name: "Objects", icon: "fa-lightbulb-o" },
{ name: "Symbols", icon: "fa-hashtag" },
{ name: "Custom", icon: "fa-cog" },
];
}
function get_frequently_used_emojis() {
return [
'1f44d', // thumbs_up
'1f389', // party_popper
'1f642', // simple_smile
'2764', // heart
'1f6e0', // hammer_and_wrench
'1f419', // octopus
];
}
function get_total_sections() {
if (search_is_active) {
return 1;
}
return exports.complete_emoji_catalog.length;
}
function get_max_index(section) {
if (search_is_active) {
return search_results.length;
} else if (section >= 0 && section < get_total_sections()) {
return exports.complete_emoji_catalog[section].emojis.length;
}
}
function show_search_results() {
$(".emoji-popover-emoji-map").hide();
$(".emoji-popover-category-tabs").hide();
$(".emoji-search-results-container").show();
emoji_catalog_last_coordinates = {
section: current_section,
index: current_index,
};
current_section = 0;
current_index = 0;
search_is_active = true;
}
function show_emoji_catalog() {
$(".emoji-popover-emoji-map").show();
$(".emoji-popover-category-tabs").show();
$(".emoji-search-results-container").hide();
current_section = emoji_catalog_last_coordinates.section;
current_index = emoji_catalog_last_coordinates.index;
search_is_active = false;
}
exports.generate_emoji_picker_data = function (realm_emojis) {
exports.emoji_collection = {};
exports.complete_emoji_catalog = {};
exports.complete_emoji_catalog.Custom = [];
_.each(realm_emojis, function (realm_emoji, realm_emoji_name) {
exports.emoji_collection[realm_emoji_name] = {
name: realm_emoji_name,
is_realm_emoji: true,
url: realm_emoji.emoji_url,
has_reacted: false,
};
exports.complete_emoji_catalog.Custom.push(exports.emoji_collection[realm_emoji_name]);
});
_.each(emoji_codes.emoji_catalog, function (codepoints, category) {
exports.complete_emoji_catalog[category] = [];
_.each(codepoints, function (codepoint) {
if (emoji_codes.codepoint_to_name.hasOwnProperty(codepoint)) {
var emoji_name = emoji_codes.codepoint_to_name[codepoint];
if (!exports.emoji_collection.hasOwnProperty(emoji_name)) {
exports.emoji_collection[emoji_name] = {
name: emoji_name,
is_realm_emoji: false,
css_class: codepoint,
has_reacted: false,
};
exports.complete_emoji_catalog[category].push(
exports.emoji_collection[emoji_name]
);
}
}
});
});
exports.complete_emoji_catalog.Popular = [];
var frequently_used_emojis = get_frequently_used_emojis();
_.each(frequently_used_emojis, function (codepoint) {
if (emoji_codes.codepoint_to_name.hasOwnProperty(codepoint)) {
var emoji_name = emoji_codes.codepoint_to_name[codepoint];
if (exports.emoji_collection.hasOwnProperty(emoji_name)) {
exports.complete_emoji_catalog.Popular.push(exports.emoji_collection[emoji_name]);
}
}
});
var categories = get_all_emoji_categories().filter(function (category) {
return !!exports.complete_emoji_catalog[category.name];
});
exports.complete_emoji_catalog = categories.map(function (category) {
return {
name: category.name,
icon: category.icon,
emojis: exports.complete_emoji_catalog[category.name],
};
});
};
var generate_emoji_picker_content = function (id) {
var emojis_used = [];
if (id !== undefined) {
emojis_used = reactions.get_emojis_used_by_user_for_message_id(id);
}
_.each(exports.emoji_collection, function (emoji_dict) {
emoji_dict.has_reacted = _.contains(emojis_used, emoji_dict.name);
});
return templates.render('emoji_popover_content', {
message_id: id,
emoji_categories: exports.complete_emoji_catalog,
});
};
function add_scrollbar(element) {
$(element).perfectScrollbar({
suppressScrollX: true,
useKeyboard: false,
// Picked so that each mousewheel bump moves 1 emoji down.
wheelSpeed: 0.68,
});
}
function refill_section_head_offsets(popover) {
section_head_offsets = [];
popover.find('.emoji-popover-subheading').each(function () {
section_head_offsets.push({
section: $(this).attr('data-section'),
position_y: $(this).position().top,
});
});
}
exports.render_emoji_popover = function (elt, id) {
var template_args = {
class: "emoji-info-popover",
categories: get_rendered_emoji_categories(),
};
elt.popover({
// temporary patch for handling popover placement of `viewport_center`
placement: popovers.compute_placement(elt) === 'viewport_center' ?
'right' : popovers.compute_placement(elt),
template: templates.render('emoji_popover', template_args),
title: "",
content: generate_emoji_picker_content(id),
trigger: "manual",
});
elt.popover("show");
elt.prop('title', 'Add reaction...');
$('.emoji-popover-filter').focus();
add_scrollbar($(".emoji-popover-emoji-map"));
add_scrollbar($(".emoji-search-results-container"));
current_message_emoji_popover_elem = elt;
emoji_catalog_last_coordinates = {
section: 0,
index: 0,
};
show_emoji_catalog();
var popover = elt.data('popover').$tip;
refill_section_head_offsets(popover);
var $emoji_map = popover.find('.emoji-popover-emoji-map');
$emoji_map.on("scroll", function () {
emoji_picker.emoji_select_tab($emoji_map);
});
};
exports.toggle_emoji_popover = function (element, id) {
var last_popover_elem = current_message_emoji_popover_elem;
popovers.hide_all();
if (last_popover_elem !== undefined
&& last_popover_elem.get()[0] === element) {
// We want it to be the case that a user can dismiss a popover
// by clicking on the same element that caused the popover.
return;
}
$(element).closest('.message_row').toggleClass('has_popover has_emoji_popover');
var elt = $(element);
if (id !== undefined) {
current_msg_list.select_id(id);
}
if (elt.data('popover') === undefined) {
emoji_picker.render_emoji_popover(elt, id);
}
};
exports.reactions_popped = function () {
return current_message_emoji_popover_elem !== undefined;
};
exports.hide_emoji_popover = function () {
$('.has_popover').removeClass('has_popover has_emoji_popover');
if (exports.reactions_popped()) {
$(".emoji-popover-emoji-map").perfectScrollbar("destroy");
$(".emoji-search-results-container").perfectScrollbar("destroy");
current_message_emoji_popover_elem.popover("destroy");
current_message_emoji_popover_elem = undefined;
}
};
function get_selected_emoji() {
return $(".emoji-popover-emoji").filter(":focus")[0];
}
function get_rendered_emoji(section, index) {
var type = "emoji_picker_emoji";
if (search_is_active) {
type = "emoji_search_result";
}
var emoji_id = [type, section, index].join("_");
var emoji = $(".emoji-popover-emoji[data-emoji-id='" + emoji_id + "']");
if (emoji.length > 0) {
return emoji;
}
}
function filter_emojis() {
var elt = $(".emoji-popover-filter").expectOne();
var query = elt.val().trim().toLowerCase();
var message_id = $(".emoji-search-results-container").data("message-id");
var search_results_visible = $(".emoji-search-results-container").is(":visible");
if (query !== "") {
var categories = exports.complete_emoji_catalog;
var search_terms = query.split(" ");
search_results = [];
_.each(categories, function (category) {
if (category.name === "Popular") {
return;
}
var emojis = category.emojis;
_.each(emojis, function (emoji_dict) {
var match = _.every(search_terms, function (search_term) {
return emoji_dict.name.indexOf(search_term) >= 0;
});
if (match) {
search_results.push(emoji_dict);
}
});
});
var search_results_rendered = templates.render('emoji_popover_search_results', {
search_results: search_results,
message_id: message_id,
});
$('.emoji-search-results').html(search_results_rendered);
$(".emoji-search-results-container").perfectScrollbar("update");
if (!search_results_visible) {
show_search_results();
}
} else {
show_emoji_catalog();
}
}
function maybe_select_emoji(e) {
if (e.keyCode === 13) { // enter key
e.preventDefault();
var first_emoji = get_rendered_emoji(0, 0);
if (first_emoji) {
if (emoji_picker.is_composition(first_emoji)) {
first_emoji.click();
} else {
reactions.toggle_emoji_reaction(
current_msg_list.selected_id(),
first_emoji.attr('title')
);
}
}
}
}
exports.toggle_selected_emoji = function () {
// Toggle the currently selected emoji.
var message_id = current_msg_list.selected_id();
var message = message_store.get(message_id);
if (!message) {
blueslip.error('reactions: Bad message id: ' + message_id);
return;
}
var selected_emoji = get_selected_emoji();
if (selected_emoji === undefined) {
return;
}
var emoji_name = selected_emoji.title;
reactions.toggle_emoji_reaction(message_id, emoji_name);
};
function round_off_to_previous_multiple(number_to_round, multiple) {
return (number_to_round - (number_to_round % multiple));
}
function may_be_change_focused_emoji(next_section, next_index) {
var next_emoji = get_rendered_emoji(next_section, next_index);
if (next_emoji) {
current_section = next_section;
current_index = next_index;
next_emoji.focus();
return true;
}
return false;
}
function may_be_change_active_section(next_section) {
if (next_section >= 0 && next_section < get_total_sections()) {
current_section = next_section;
current_index = 0;
var offset = section_head_offsets[current_section];
if (offset) {
$(".emoji-popover-emoji-map").scrollTop(offset.position_y);
may_be_change_focused_emoji(current_section, current_index);
}
}
}
function get_next_emoji_coordinates(move_by) {
var next_section = current_section;
var next_index = current_index + move_by;
var max_len;
if (next_index < 0) {
next_section = next_section - 1;
if (next_section >= 0) {
next_index = get_max_index(next_section) - 1;
if (move_by === -6) {
max_len = get_max_index(next_section);
var prev_multiple = round_off_to_previous_multiple(max_len, 6);
next_index = prev_multiple + current_index;
next_index = next_index >= max_len
? (prev_multiple + current_index - 6)
: next_index;
}
}
} else if (next_index >= get_max_index(next_section)) {
next_section = next_section + 1;
if (next_section < get_total_sections()) {
next_index = 0;
if (move_by === 6) {
max_len = get_max_index(next_index);
next_index = current_index % 6;
next_index = next_index >= max_len ? max_len - 1 : next_index;
}
}
}
return {
section: next_section,
index: next_index,
};
}
function change_focus_to_filter() {
$('.emoji-popover-filter').focus();
// If search is active reset current selected emoji to first emoji.
if (search_is_active) {
current_section = 0;
current_index = 0;
}
}
exports.navigate = function (event_name) {
// If search is active and results are empty then return immediately.
if (search_is_active === true && search_results.length === 0) {
return;
}
var selected_emoji = get_rendered_emoji(current_section, current_index);
var is_filter_focused = $('.emoji-popover-filter').is(':focus');
var next_section = 0;
// special cases
if (is_filter_focused && event_name === 'down_arrow') {
// move down into emoji map
selected_emoji.focus();
if (current_section === 0 && current_index < 6) {
$(".emoji-popover-emoji-map").scrollTop(0);
}
return true;
} else if (current_section === 0 && current_index < 6 && event_name === 'up_arrow') {
if (selected_emoji) {
// In this case, we're move up into the reaction
// filter. Here, we override the default browser
// behavior, which in Firefox is good (preserving
// the cursor position) and in Chrome is bad (cursor
// goes to beginning) with something reasonable and
// consistent (cursor goes to the end of the filter
// string).
$('.emoji-popover-filter').focus().caret(Infinity);
$(".emoji-popover-emoji-map").scrollTop(0);
$(".emoji-search-results-container").scrollTop(0);
current_section = 0;
current_index = 0;
return true;
}
} else if (event_name === 'tab') {
if (is_filter_focused) {
selected_emoji.focus();
} else {
change_focus_to_filter();
}
return true;
} else if (event_name === 'shift_tab') {
if (!is_filter_focused) {
change_focus_to_filter();
}
return true;
} else if (event_name === 'page_up') {
next_section = current_section - 1;
may_be_change_active_section(next_section);
return true;
} else if (event_name === 'page_down') {
next_section = current_section + 1;
may_be_change_active_section(next_section);
return true;
} else if (!is_filter_focused) {
var next_coord = {};
switch (event_name) {
case 'down_arrow':
next_coord = get_next_emoji_coordinates(6);
break;
case 'up_arrow':
next_coord = get_next_emoji_coordinates(-6);
break;
case 'left_arrow':
next_coord = get_next_emoji_coordinates(-1);
break;
case 'right_arrow':
next_coord = get_next_emoji_coordinates(1);
break;
}
return may_be_change_focused_emoji(next_coord.section, next_coord.index);
}
return false;
};
exports.emoji_select_tab = function (elt) {
var scrolltop = elt.scrollTop();
var scrollheight = elt.prop('scrollHeight');
var elt_height = elt.height();
var currently_selected = "";
section_head_offsets.forEach(function (o) {
if (scrolltop + elt_height/2 >= o.position_y) {
currently_selected = o.section;
}
});
// Handles the corner case of the last category being
// smaller than half of the emoji picker height.
if (elt_height + scrolltop === scrollheight) {
currently_selected = section_head_offsets[section_head_offsets.length - 1].section;
}
// Handles the corner case of the scrolling back to top.
if (scrolltop === 0) {
currently_selected = section_head_offsets[0].section;
}
if (currently_selected) {
$('.emoji-popover-tab-item.active').removeClass('active');
$('.emoji-popover-tab-item[data-tab-name="'+currently_selected+'"]').addClass('active');
}
};
exports.register_click_handlers = function () {
$(document).on('click', '.emoji-popover-emoji.reaction', function () {
// When an emoji is clicked in the popover,
// if the user has reacted to this message with this emoji
// the reaction is removed
// otherwise, the reaction is added
var emoji_name = this.title;
var message_id = $(this).parent().parent().attr('data-message-id');
var message = message_store.get(message_id);
if (!message) {
blueslip.error('reactions: Bad message id: ' + message_id);
return;
}
if (reactions.current_user_has_reacted_to_emoji(message, emoji_name)) {
$(this).removeClass('reacted');
}
reactions.toggle_emoji_reaction(message_id, emoji_name);
});
$(document).on('click', '.emoji-popover-emoji.composition', function (e) {
var emoji_text = ':' + this.title + ':';
var textarea = $("#new_message_content");
textarea.caret(emoji_text);
textarea.focus();
e.stopPropagation();
emoji_picker.hide_emoji_popover();
});
$("#compose").on("click", "#emoji_map", function (e) {
e.preventDefault();
e.stopPropagation();
emoji_picker.toggle_emoji_popover(this);
});
$("#main_div").on("click", ".reactions_hover, .reaction_button", function (e) {
e.stopPropagation();
var message_id = rows.get_message_id(this);
emoji_picker.toggle_emoji_popover(this, message_id);
});
$("body").on("click", ".actions_popover .reaction_button", function (e) {
var msgid = $(e.currentTarget).data('message-id');
e.preventDefault();
e.stopPropagation();
// HACK: Because we need the popover to be based off an
// element that definitely exists in the page even if the
// message wasn't sent by us and thus the .reaction_hover
// element is not present, we use the message's
// .icon-vector-chevron-down element as the base for the popover.
emoji_picker.toggle_emoji_popover($(".selected_message .icon-vector-chevron-down")[0], msgid);
});
$(document).on('input', '.emoji-popover-filter', filter_emojis);
$(document).on('keydown', '.emoji-popover-filter', maybe_select_emoji);
$("body").on("click", ".emoji-popover-tab-item", function (e) {
e.stopPropagation();
e.preventDefault();
var offset = _.find(section_head_offsets, function (o) {
return o.section === $(this).attr("data-tab-name");
}.bind(this));
if (offset) {
$(".emoji-popover-emoji-map").scrollTop(offset.position_y);
}
});
};
exports.is_composition = function (emoji) {
return $(emoji).hasClass('composition');
};
exports.initialize = function () {
exports.generate_emoji_picker_data(emoji.active_realm_emojis);
};
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = emoji_picker;
}