mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 22:13:26 +00:00
mobile sharing: Extract shared/js/typeahead.js.
This extracts get_emoji_matcher and all the functions it depended on, most of which were in composebox_typeahead.js. We also move remove_diacritics out of the people module. This is the first major step for #13728.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
const typeahead = zrequire('typeahead', 'shared/js/typeahead');
|
||||||
set_global('i18n', global.stub_i18n);
|
set_global('i18n', global.stub_i18n);
|
||||||
zrequire('compose_state');
|
zrequire('compose_state');
|
||||||
zrequire('ui_util');
|
zrequire('ui_util');
|
||||||
@@ -662,7 +663,7 @@ run_test('initialize', () => {
|
|||||||
assert.equal(actual_value, expected_value);
|
assert.equal(actual_value, expected_value);
|
||||||
|
|
||||||
function matcher(query, person) {
|
function matcher(query, person) {
|
||||||
query = ct.clean_query_lowercase(query);
|
query = typeahead.clean_query_lowercase(query);
|
||||||
return ct.query_matches_person(query, person);
|
return ct.query_matches_person(query, person);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const typeahead = require("../shared/js/typeahead");
|
||||||
const autosize = require('autosize');
|
const autosize = require('autosize');
|
||||||
|
|
||||||
//************************************
|
//************************************
|
||||||
@@ -53,81 +54,17 @@ function get_language_matcher(query) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function clean_query(query) {
|
|
||||||
query = people.remove_diacritics(query);
|
|
||||||
// When `abc ` with a space at the end is typed in a
|
|
||||||
// contenteditable widget such as the composebox PM section, the
|
|
||||||
// space at the end was a `no break-space (U+00A0)` instead of
|
|
||||||
// `space (U+0020)`, which lead to no matches in those cases.
|
|
||||||
query = query.replace(/\u00A0/g, String.fromCharCode(32));
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.clean_query_lowercase = function (query) {
|
|
||||||
query = query.toLowerCase();
|
|
||||||
query = clean_query(query);
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
function query_matches_string(query, source_str, split_char) {
|
|
||||||
source_str = people.remove_diacritics(source_str);
|
|
||||||
|
|
||||||
// If query doesn't contain a separator, we just want an exact
|
|
||||||
// match where query is a substring of one of the target characters.
|
|
||||||
if (query.indexOf(split_char) > 0) {
|
|
||||||
// If there's a whitespace character in the query, then we
|
|
||||||
// require a perfect prefix match (e.g. for 'ab cd ef',
|
|
||||||
// query needs to be e.g. 'ab c', not 'cd ef' or 'b cd
|
|
||||||
// ef', etc.).
|
|
||||||
const queries = query.split(split_char);
|
|
||||||
const sources = source_str.split(split_char);
|
|
||||||
let i;
|
|
||||||
|
|
||||||
for (i = 0; i < queries.length - 1; i += 1) {
|
|
||||||
if (sources[i] !== queries[i]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This block is effectively a final iteration of the last
|
|
||||||
// loop. What differs is that for the last word, a
|
|
||||||
// partial match at the beginning of the word is OK.
|
|
||||||
if (sources[i] === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return sources[i].indexOf(queries[i]) === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For a single token, the match can be anywhere in the string.
|
|
||||||
return source_str.indexOf(query) !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function attempts to match a query with source's attributes.
|
|
||||||
// * query is the user-entered search query
|
|
||||||
// * Source is the object we're matching from, e.g. a user object
|
|
||||||
// * match_attrs are the values associated with the target object that
|
|
||||||
// the entered string might be trying to match, e.g. for a user
|
|
||||||
// account, there might be 2 attrs: their full name and their email.
|
|
||||||
// * split_char is the separator for this syntax (e.g. ' ').
|
|
||||||
function query_matches_source_attrs(query, source, match_attrs, split_char) {
|
|
||||||
return _.any(match_attrs, function (attr) {
|
|
||||||
const source_str = source[attr].toLowerCase();
|
|
||||||
return query_matches_string(query, source_str, split_char);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.query_matches_person = function (query, person) {
|
exports.query_matches_person = function (query, person) {
|
||||||
return query_matches_source_attrs(query, person, ["full_name", "email"], " ");
|
return typeahead.query_matches_source_attrs(query, person, ["full_name", "email"], " ");
|
||||||
};
|
};
|
||||||
|
|
||||||
function query_matches_name_description(query, user_group_or_stream) {
|
function query_matches_name_description(query, user_group_or_stream) {
|
||||||
return query_matches_source_attrs(query, user_group_or_stream, ["name", "description"], " ");
|
return typeahead.query_matches_source_attrs(query, user_group_or_stream, ["name", "description"], " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_stream_or_user_group_matcher(query) {
|
function get_stream_or_user_group_matcher(query) {
|
||||||
// Case-insensitive.
|
// Case-insensitive.
|
||||||
query = exports.clean_query_lowercase(query);
|
query = typeahead.clean_query_lowercase(query);
|
||||||
|
|
||||||
return function (user_group_or_stream) {
|
return function (user_group_or_stream) {
|
||||||
return query_matches_name_description(query, user_group_or_stream);
|
return query_matches_name_description(query, user_group_or_stream);
|
||||||
@@ -135,32 +72,22 @@ function get_stream_or_user_group_matcher(query) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function get_slash_matcher(query) {
|
function get_slash_matcher(query) {
|
||||||
query = exports.clean_query_lowercase(query);
|
query = typeahead.clean_query_lowercase(query);
|
||||||
|
|
||||||
return function (item) {
|
return function (item) {
|
||||||
return query_matches_source_attrs(query, item, ["name"], " ");
|
return typeahead.query_matches_source_attrs(query, item, ["name"], " ");
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_emoji_matcher(query) {
|
|
||||||
// replaces spaces with underscores for emoji matching
|
|
||||||
query = query.split(" ").join("_");
|
|
||||||
query = exports.clean_query_lowercase(query);
|
|
||||||
|
|
||||||
return function (emoji) {
|
|
||||||
return query_matches_source_attrs(query, emoji, ["emoji_name"], "_");
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_topic_matcher(query) {
|
function get_topic_matcher(query) {
|
||||||
query = exports.clean_query_lowercase(query);
|
query = typeahead.clean_query_lowercase(query);
|
||||||
|
|
||||||
return function (topic) {
|
return function (topic) {
|
||||||
const obj = {
|
const obj = {
|
||||||
topic: topic,
|
topic: topic,
|
||||||
};
|
};
|
||||||
|
|
||||||
return query_matches_source_attrs(query, obj, ['topic'], ' ');
|
return typeahead.query_matches_source_attrs(query, obj, ['topic'], ' ');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +402,7 @@ exports.get_pm_people = function (query) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.get_person_suggestions = function (query, opts) {
|
exports.get_person_suggestions = function (query, opts) {
|
||||||
query = exports.clean_query_lowercase(query);
|
query = typeahead.clean_query_lowercase(query);
|
||||||
|
|
||||||
const person_matcher = (item) => {
|
const person_matcher = (item) => {
|
||||||
return exports.query_matches_person(query, item);
|
return exports.query_matches_person(query, item);
|
||||||
@@ -880,7 +807,7 @@ exports.content_typeahead_selected = function (item, event) {
|
|||||||
exports.compose_content_matcher = function (completing, token) {
|
exports.compose_content_matcher = function (completing, token) {
|
||||||
switch (completing) {
|
switch (completing) {
|
||||||
case 'emoji':
|
case 'emoji':
|
||||||
return get_emoji_matcher(token);
|
return typeahead.get_emoji_matcher(token);
|
||||||
case 'slash':
|
case 'slash':
|
||||||
return get_slash_matcher(token);
|
return get_slash_matcher(token);
|
||||||
case 'stream':
|
case 'stream':
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
require("unorm"); // String.prototype.normalize polyfill for IE11
|
require("unorm"); // String.prototype.normalize polyfill for IE11
|
||||||
const IntDict = require('./int_dict').IntDict;
|
const IntDict = require('./int_dict').IntDict;
|
||||||
const FoldDict = require('./fold_dict').FoldDict;
|
const FoldDict = require('./fold_dict').FoldDict;
|
||||||
|
const typeahead = require("../shared/js/typeahead");
|
||||||
|
|
||||||
let people_dict;
|
let people_dict;
|
||||||
let people_by_name_dict;
|
let people_by_name_dict;
|
||||||
@@ -771,12 +772,6 @@ exports.incr_recipient_count = function (user_id) {
|
|||||||
pm_recipient_count_dict.set(user_id, old_count + 1);
|
pm_recipient_count_dict.set(user_id, old_count + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const unicode_marks = /\p{M}/gu;
|
|
||||||
|
|
||||||
exports.remove_diacritics = function (s) {
|
|
||||||
return s.normalize("NFKD").replace(unicode_marks, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.get_message_people = function () {
|
exports.get_message_people = function () {
|
||||||
/*
|
/*
|
||||||
message_people are roughly the people who have
|
message_people are roughly the people who have
|
||||||
@@ -824,7 +819,7 @@ exports.build_termlet_matcher = function (termlet) {
|
|||||||
let full_name = user.full_name;
|
let full_name = user.full_name;
|
||||||
if (is_ascii) {
|
if (is_ascii) {
|
||||||
// Only ignore diacritics if the query is plain ascii
|
// Only ignore diacritics if the query is plain ascii
|
||||||
full_name = exports.remove_diacritics(full_name);
|
full_name = typeahead.remove_diacritics(full_name);
|
||||||
}
|
}
|
||||||
const names = full_name.toLowerCase().split(' ');
|
const names = full_name.toLowerCase().split(' ');
|
||||||
|
|
||||||
|
|||||||
80
static/shared/js/typeahead.js
Normal file
80
static/shared/js/typeahead.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
const unicode_marks = /\p{M}/gu;
|
||||||
|
|
||||||
|
exports.remove_diacritics = function (s) {
|
||||||
|
return s.normalize("NFKD").replace(unicode_marks, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
function query_matches_string(query, source_str, split_char) {
|
||||||
|
source_str = exports.remove_diacritics(source_str);
|
||||||
|
|
||||||
|
// If query doesn't contain a separator, we just want an exact
|
||||||
|
// match where query is a substring of one of the target characters.
|
||||||
|
if (query.indexOf(split_char) > 0) {
|
||||||
|
// If there's a whitespace character in the query, then we
|
||||||
|
// require a perfect prefix match (e.g. for 'ab cd ef',
|
||||||
|
// query needs to be e.g. 'ab c', not 'cd ef' or 'b cd
|
||||||
|
// ef', etc.).
|
||||||
|
const queries = query.split(split_char);
|
||||||
|
const sources = source_str.split(split_char);
|
||||||
|
let i;
|
||||||
|
|
||||||
|
for (i = 0; i < queries.length - 1; i += 1) {
|
||||||
|
if (sources[i] !== queries[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This block is effectively a final iteration of the last
|
||||||
|
// loop. What differs is that for the last word, a
|
||||||
|
// partial match at the beginning of the word is OK.
|
||||||
|
if (sources[i] === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return sources[i].indexOf(queries[i]) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a single token, the match can be anywhere in the string.
|
||||||
|
return source_str.indexOf(query) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function attempts to match a query with source's attributes.
|
||||||
|
// * query is the user-entered search query
|
||||||
|
// * Source is the object we're matching from, e.g. a user object
|
||||||
|
// * match_attrs are the values associated with the target object that
|
||||||
|
// the entered string might be trying to match, e.g. for a user
|
||||||
|
// account, there might be 2 attrs: their full name and their email.
|
||||||
|
// * split_char is the separator for this syntax (e.g. ' ').
|
||||||
|
exports.query_matches_source_attrs = (query, source, match_attrs, split_char) => {
|
||||||
|
return _.any(match_attrs, function (attr) {
|
||||||
|
const source_str = source[attr].toLowerCase();
|
||||||
|
return query_matches_string(query, source_str, split_char);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function clean_query(query) {
|
||||||
|
query = exports.remove_diacritics(query);
|
||||||
|
// When `abc ` with a space at the end is typed in a
|
||||||
|
// contenteditable widget such as the composebox PM section, the
|
||||||
|
// space at the end was a `no break-space (U+00A0)` instead of
|
||||||
|
// `space (U+0020)`, which lead to no matches in those cases.
|
||||||
|
query = query.replace(/\u00A0/g, String.fromCharCode(32));
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.clean_query_lowercase = function (query) {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
query = clean_query(query);
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.get_emoji_matcher = (query) => {
|
||||||
|
// replaces spaces with underscores for emoji matching
|
||||||
|
query = query.split(" ").join("_");
|
||||||
|
query = exports.clean_query_lowercase(query);
|
||||||
|
|
||||||
|
return function (emoji) {
|
||||||
|
return exports.query_matches_source_attrs(query, emoji, ["emoji_name"], "_");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user