typeahead: Convert module to typescript.

This contains two more complex changes:

- The default versions of sorter and matcher assume that ItemType is a
string. But the Typeahead class works on a generic ItemType and I'm
not aware of a way to assert that this function is only called for
typeaheads with string items. For `matcher`, we can assert that the
items are strings. `sorter` is now a required option instead of an
optional one that could fall back to the default.

- `element` can be either an `input` element or a `contenteditable`
`div`. We distinguish between them using `.is("[contenteditable]"))`
but TypeScript doesn't understand that. So we replaced `this.$element`
with `this.input_element` where `input_element` is an object with the
`$element` and also a `type` specifying which type of element it is
(input or contenteditable).
This commit is contained in:
evykassirer 2024-03-17 22:14:23 -07:00 committed by Tim Abbott
parent 3f5be23854
commit c7b12c996c
11 changed files with 315 additions and 174 deletions

View File

@ -277,7 +277,7 @@ Files: web/third/bootstrap/css/bootstrap-btn.css
Copyright: 2011-2014 Twitter, Inc.
License: Expat
Files: web/src/bootstrap_typeahead.js
Files: web/src/bootstrap_typeahead.ts
Copyright: 2012 Twitter, Inc.
License: Apache-2.0
Comment: Bootstrap typeahead. The software has been modified.

View File

@ -61,7 +61,7 @@ EXEMPT_FILES = make_set(
"web/src/billing/helpers.ts",
"web/src/blueslip.ts",
"web/src/blueslip_stacktrace.ts",
"web/src/bootstrap_typeahead.js",
"web/src/bootstrap_typeahead.ts",
"web/src/browser_history.ts",
"web/src/buddy_list.ts",
"web/src/click_handlers.js",

View File

@ -133,12 +133,15 @@
* ============================================================ */
import $ from "jquery";
import assert from "minimalistic-assert";
import {insertTextIntoField} from "text-field-edit";
import {get_string_diff} from "./util";
function get_pseudo_keycode(event) {
const isComposing = (event.originalEvent && event.originalEvent.isComposing) || false;
function get_pseudo_keycode(
event: JQuery.KeyDownEvent | JQuery.KeyUpEvent | JQuery.KeyPressEvent,
): number {
const isComposing = event.originalEvent?.isComposing ?? false;
/* We treat IME compose enter keypresses as a separate -13 key. */
if (event.keyCode === 13 && isComposing) {
return -13;
@ -146,6 +149,25 @@ function get_pseudo_keycode(event) {
return event.keyCode;
}
export function defaultSorter(items: string[], query: string): string[] {
const beginswith = [];
const caseSensitive = [];
const caseInsensitive = [];
let item;
while ((item = items.shift())) {
if (item.toLowerCase().startsWith(query.toLowerCase())) {
beginswith.push(item);
} else if (item.includes(query)) {
caseSensitive.push(item);
} else {
caseInsensitive.push(item);
}
}
return [...beginswith, ...caseSensitive, ...caseInsensitive];
}
/* TYPEAHEAD PUBLIC CLASS DEFINITION
* ================================= */
@ -156,83 +178,131 @@ const MENU_HTML = '<ul class="typeahead-menu"></ul>';
const ITEM_HTML = "<li><a></a></li>";
const MIN_LENGTH = 1;
const Typeahead = function (element, options) {
this.$element = $(element);
this.items = options.items ?? 8;
this.matcher = options.matcher ?? this.matcher;
this.sorter = options.sorter ?? this.sorter;
this.highlighter_html = options.highlighter_html;
this.updater = options.updater ?? this.updater;
this.$container = $(CONTAINER_HTML).appendTo(options.parentElement ?? "body");
this.$menu = $(MENU_HTML).appendTo(this.$container);
this.$header = $(HEADER_ELEMENT_HTML).appendTo(this.$container);
this.source = options.source;
this.shown = false;
this.mouse_moved_since_typeahead = false;
this.dropup = options.dropup ?? false;
this.fixed = options.fixed ?? false;
this.automated = options.automated ?? (() => false);
this.trigger_selection = options.trigger_selection ?? (() => false);
this.on_escape = options.on_escape;
// return a string to show in typeahead header or false.
this.header_html = options.header_html ?? (() => false);
// return a string to show in typeahead items or false.
this.option_label = options.option_label ?? (() => false);
this.stopAdvance = options.stopAdvance ?? false;
this.advanceKeyCodes = options.advanceKeyCodes ?? [];
this.openInputFieldOnKeyUp = options.openInputFieldOnKeyUp;
this.closeInputFieldOnHide = options.closeInputFieldOnHide;
this.tabIsEnter = options.tabIsEnter ?? true;
this.helpOnEmptyStrings = options.helpOnEmptyStrings ?? false;
this.naturalSearch = options.naturalSearch ?? false;
this.parentElement = options.parentElement;
type InputElement =
| {
$element: JQuery;
type: "contenteditable";
}
| {
$element: JQuery<HTMLInputElement>;
type: "input";
};
if (this.fixed) {
this.$container.css("position", "fixed");
class Typeahead<ItemType extends string | object> {
input_element: InputElement;
items: number;
matcher: (item: ItemType) => boolean;
sorter: (items: ItemType[]) => ItemType[];
highlighter_html: (item: ItemType) => string | undefined;
updater: (
item: ItemType,
event?: JQuery.ClickEvent | JQuery.KeyUpEvent | JQuery.KeyDownEvent,
) => string | undefined;
$container: JQuery;
$menu: JQuery;
$header: JQuery;
source: (query: string) => ItemType[];
dropup: boolean;
fixed: boolean;
automated: () => boolean;
trigger_selection: (event: JQuery.KeyDownEvent) => boolean;
on_escape?: () => void;
// returns a string to show in typeahead header or false.
header_html: () => string | false;
// returns a string to show in typeahead items or false.
option_label: (matching_items: ItemType[], item: ItemType) => string | false;
suppressKeyPressRepeat = false;
query = "";
mouse_moved_since_typeahead = false;
shown = false;
openInputFieldOnKeyUp?: () => void;
closeInputFieldOnHide?: () => void;
helpOnEmptyStrings: boolean;
tabIsEnter: boolean;
naturalSearch: boolean;
stopAdvance: boolean;
advanceKeyCodes: number[];
parentElement?: string;
constructor(input_element: InputElement, options: TypeaheadOptions<ItemType>) {
this.input_element = input_element;
if (this.input_element.type === "contenteditable") {
assert(this.input_element.$element.is("[contenteditable]"));
} else {
assert(!this.input_element.$element.is("[contenteditable]"));
}
this.items = options.items ?? 8;
this.matcher = options.matcher ?? ((item) => this.defaultMatcher(item));
this.sorter = options.sorter;
this.highlighter_html = options.highlighter_html;
this.updater = options.updater ?? ((items) => this.defaultUpdater(items));
this.$container = $(CONTAINER_HTML).appendTo(options.parentElement ?? "body");
this.$menu = $(MENU_HTML).appendTo(this.$container);
this.$header = $(HEADER_ELEMENT_HTML).appendTo(this.$container);
this.source = options.source;
this.dropup = options.dropup ?? false;
this.fixed = options.fixed ?? false;
this.automated = options.automated ?? (() => false);
this.trigger_selection = options.trigger_selection ?? (() => false);
this.on_escape = options.on_escape;
// return a string to show in typeahead header or false.
this.header_html = options.header_html ?? (() => false);
// return a string to show in typeahead items or false.
this.option_label = options.option_label ?? (() => false);
this.stopAdvance = options.stopAdvance ?? false;
this.advanceKeyCodes = options.advanceKeyCodes ?? [];
this.openInputFieldOnKeyUp = options.openInputFieldOnKeyUp;
this.closeInputFieldOnHide = options.closeInputFieldOnHide;
this.tabIsEnter = options.tabIsEnter ?? true;
this.helpOnEmptyStrings = options.helpOnEmptyStrings ?? false;
this.naturalSearch = options.naturalSearch ?? false;
this.parentElement = options.parentElement;
if (this.fixed) {
this.$container.css("position", "fixed");
}
// The naturalSearch option causes arrow keys to immediately
// update the search box with the underlying values from the
// search suggestions.
this.listen();
}
// The naturalSearch option causes arrow keys to immediately
// update the search box with the underlying values from the
// search suggestions.
this.listen();
};
Typeahead.prototype = {
constructor: Typeahead,
select(e) {
select(e?: JQuery.ClickEvent | JQuery.KeyUpEvent | JQuery.KeyDownEvent): this {
const val = this.$menu.find(".active").data("typeahead-value");
if (this.$element.is("[contenteditable]")) {
this.$element.text(this.updater(val, e)).trigger("change");
if (this.input_element.type === "contenteditable") {
this.input_element.$element.text(this.updater(val, e) ?? "").trigger("change");
// Empty text after the change event handler
// converts the input text to html elements.
this.$element.text("");
this.input_element.$element.text("");
} else {
const after_text = this.updater(val, e);
const [from, to_before, to_after] = get_string_diff(this.$element.val(), after_text);
const after_text = this.updater(val, e) ?? "";
const element_val = this.input_element.$element.val();
assert(element_val !== undefined);
const [from, to_before, to_after] = get_string_diff(element_val, after_text);
const replacement = after_text.slice(from, to_after);
// select / highlight the minimal text to be replaced
this.$element[0].setSelectionRange(from, to_before);
insertTextIntoField(this.$element[0], replacement);
this.$element.trigger("change");
this.input_element.$element[0].setSelectionRange(from, to_before);
insertTextIntoField(this.input_element.$element[0], replacement);
this.input_element.$element.trigger("change");
}
return this.hide();
},
}
set_value() {
set_value(): void {
const val = this.$menu.find(".active").data("typeahead-value");
if (this.$element.is("[contenteditable]")) {
this.$element.text(val);
if (this.input_element.type === "contenteditable") {
this.input_element.$element.text(val);
} else {
this.$element.val(val);
this.input_element.$element.val(val);
}
},
}
updater(item) {
defaultUpdater(item: ItemType): ItemType {
return item;
},
}
show() {
show(): this {
const header_text_html = this.header_html();
if (header_text_html) {
this.$header.find("span#typeahead-header-text").html(header_text_html);
@ -243,26 +313,25 @@ Typeahead.prototype = {
// If a parent element was specified, we shouldn't manually
// position the element, since it's already in the right place.
if (!this.parentElement) {
if (this.parentElement === undefined) {
let pos;
if (this.fixed) {
// Relative to screen instead of to page
pos = this.$element[0].getBoundingClientRect();
pos = this.input_element.$element[0].getBoundingClientRect();
} else {
pos = this.$element.offset();
pos = this.input_element.$element.offset();
}
pos = $.extend({}, pos, {
height: this.$element[0].offsetHeight,
height: this.input_element.$element[0].offsetHeight,
// Zulip patch: Workaround for iOS safari problems
top: this.input_element.$element.get_offset_to_window().top,
});
// Zulip patch: Workaround for iOS safari problems
pos.top = this.$element.get_offset_to_window().top;
let top_pos = pos.top + pos.height;
if (this.dropup) {
top_pos = pos.top - this.$container.outerHeight();
top_pos = pos.top - this.$container.outerHeight()!;
}
// Zulip patch: Avoid typeahead going off top of screen.
@ -280,21 +349,22 @@ Typeahead.prototype = {
this.shown = true;
this.mouse_moved_since_typeahead = false;
return this;
},
}
hide() {
hide(): this {
this.$container.hide();
this.shown = false;
if (this.closeInputFieldOnHide !== undefined) {
this.closeInputFieldOnHide();
}
return this;
},
}
lookup(hideOnEmpty) {
this.query = this.$element.is("[contenteditable]")
? this.$element.text()
: this.$element.val();
lookup(hideOnEmpty: boolean): this {
this.query =
this.input_element.type === "contenteditable"
? this.input_element.$element.text()
: this.input_element.$element.val() ?? "";
if (
(!this.helpOnEmptyStrings || hideOnEmpty) &&
@ -309,9 +379,9 @@ Typeahead.prototype = {
this.hide();
}
return items ? this.process(items) : this;
},
}
process(items) {
process(items: ItemType[]): this {
const matching_items = $.grep(items, (item) => this.matcher(item));
const final_items = this.sorter(matching_items);
@ -324,35 +394,17 @@ Typeahead.prototype = {
return this;
}
return this.render(final_items.slice(0, this.items), matching_items).show();
},
}
matcher(item) {
defaultMatcher(item: ItemType): boolean {
assert(typeof item === "string");
return item.toLowerCase().includes(this.query.toLowerCase());
},
}
sorter(items) {
const beginswith = [];
const caseSensitive = [];
const caseInsensitive = [];
let item;
while ((item = items.shift())) {
if (item.toLowerCase().startsWith(this.query.toLowerCase())) {
beginswith.push(item);
} else if (item.includes(this.query)) {
caseSensitive.push(item);
} else {
caseInsensitive.push(item);
}
}
return [...beginswith, ...caseSensitive, ...caseInsensitive];
},
render(final_items, matching_items) {
const $items = final_items.map((item) => {
render(final_items: ItemType[], matching_items: ItemType[]): this {
const $items: JQuery[] = final_items.map((item) => {
const $i = $(ITEM_HTML).data("typeahead-value", item);
const item_html = this.highlighter_html(item);
const item_html = this.highlighter_html(item) ?? "";
const $item_html = $i.find("a").html(item_html);
const option_label_html = this.option_label(matching_items, item);
@ -366,9 +418,9 @@ Typeahead.prototype = {
$items[0].addClass("active");
this.$menu.empty().append($items);
return this;
},
}
next() {
next(): void {
const $active = this.$menu.find(".active").removeClass("active");
let $next = $active.next();
@ -381,9 +433,9 @@ Typeahead.prototype = {
if (this.naturalSearch) {
this.set_value();
}
},
}
prev() {
prev(): void {
const $active = this.$menu.find(".active").removeClass("active");
let $prev = $active.prev();
@ -396,10 +448,10 @@ Typeahead.prototype = {
if (this.naturalSearch) {
this.set_value();
}
},
}
listen() {
this.$element
listen(): void {
$(this.input_element.$element)
.on("blur", this.blur.bind(this))
.on("keypress", this.keypress.bind(this))
.on("keyup", this.keyup.bind(this))
@ -412,24 +464,24 @@ Typeahead.prototype = {
.on("mousemove", "li", this.mousemove.bind(this));
$(window).on("resize", this.resizeHandler.bind(this));
},
}
unlisten() {
unlisten(): void {
this.$container.remove();
const events = ["blur", "keydown", "keyup", "keypress", "mousemove"];
for (const event_ of events) {
this.$element.off(event_);
$(this.input_element.$element).off(event_);
}
this.$element.removeData("typeahead");
},
this.input_element.$element.removeData("typeahead");
}
resizeHandler() {
resizeHandler(): void {
if (this.shown) {
this.show();
}
},
}
maybeStopAdvance(e) {
maybeStopAdvance(e: JQuery.KeyPressEvent | JQuery.KeyUpEvent | JQuery.KeyDownEvent): void {
const pseudo_keycode = get_pseudo_keycode(e);
if (
(this.stopAdvance || (pseudo_keycode !== 9 && pseudo_keycode !== 13)) &&
@ -437,9 +489,9 @@ Typeahead.prototype = {
) {
e.stopPropagation();
}
},
}
move(e) {
move(e: JQuery.KeyDownEvent | JQuery.KeyPressEvent): void {
if (!this.shown) {
return;
}
@ -470,18 +522,18 @@ Typeahead.prototype = {
}
this.maybeStopAdvance(e);
},
}
mousemove(e) {
mousemove(e: JQuery.MouseMoveEvent): void {
if (!this.mouse_moved_since_typeahead) {
/* Undo cursor disabling in mouseenter handler. */
$(e.currentTarget).find("a").css("cursor", "");
this.mouse_moved_since_typeahead = true;
this.mouseenter(e);
}
},
}
keydown(e) {
keydown(e: JQuery.KeyDownEvent): void {
const pseudo_keycode = get_pseudo_keycode(e);
if (this.trigger_selection(e)) {
if (!this.shown) {
@ -492,17 +544,17 @@ Typeahead.prototype = {
}
this.suppressKeyPressRepeat = ![40, 38, 9, 13, 27].includes(pseudo_keycode);
this.move(e);
},
}
keypress(e) {
keypress(e: JQuery.KeyPressEvent): void {
if (!this.suppressKeyPressRepeat) {
this.move(e);
return;
}
this.maybeStopAdvance(e);
},
}
keyup(e) {
keyup(e: JQuery.KeyUpEvent): void {
const pseudo_keycode = get_pseudo_keycode(e);
switch (pseudo_keycode) {
@ -519,10 +571,11 @@ Typeahead.prototype = {
this.select(e);
if (e.currentTarget.id === "stream_message_recipient_topic") {
assert(this.input_element.type === "input");
// Move the cursor to the end of the topic
const topic_length = this.$element.val().length;
this.$element[0].selectionStart = topic_length;
this.$element[0].selectionEnd = topic_length;
const topic_length = this.input_element.$element.val()!.length;
this.input_element.$element[0].selectionStart = topic_length;
this.input_element.$element[0].selectionEnd = topic_length;
}
break;
@ -566,16 +619,20 @@ Typeahead.prototype = {
this.maybeStopAdvance(e);
e.preventDefault();
},
}
blur(e) {
blur(e: JQuery.BlurEvent): void {
// Blurs that move focus to elsewhere within the parent element shouldn't
// hide the typeahead.
if (this.parentElement && $(e.relatedTarget).parents(this.parentElement).length > 0) {
if (
this.parentElement !== undefined &&
e.relatedTarget &&
$(e.relatedTarget).parents(this.parentElement).length > 0
) {
return;
}
setTimeout(() => {
if (!this.$container.is(":hover") && !this.$element.is(":focus")) {
if (!this.$container.is(":hover") && !this.input_element.$element.is(":focus")) {
// We do not hide the typeahead in case it is being hovered over,
// or if the focus is immediately back in the input field (likely
// when using compose formatting buttons).
@ -584,18 +641,18 @@ Typeahead.prototype = {
// refocus the input if the user clicked on the typeahead
// so that clicking elsewhere registers as a blur and hides
// the typeahead.
this.$element.trigger("focus");
this.input_element.$element.trigger("focus");
}
}, 150);
},
}
element_click() {
element_click(): void {
// update / hide the typeahead menu if the user clicks anywhere
// inside the typing area, to avoid misplaced typeahead insertion.
this.lookup(false);
},
}
click(e) {
click(e: JQuery.ClickEvent): void {
e.stopPropagation();
e.preventDefault();
// The original bootstrap code expected `mouseenter` to be called
@ -607,9 +664,9 @@ Typeahead.prototype = {
// handler here.
this.mouseenter(e);
this.select(e);
},
}
mouseenter(e) {
mouseenter(e: JQuery.MouseEnterEvent | JQuery.ClickEvent | JQuery.MouseMoveEvent): void {
if (!this.mouse_moved_since_typeahead) {
// Prevent the annoying interaction where your mouse happens
// to be in the space where typeahead will open. (This would
@ -624,17 +681,48 @@ Typeahead.prototype = {
}
this.$menu.find(".active").removeClass("active");
$(e.currentTarget).addClass("active");
},
};
}
}
/* TYPEAHEAD PLUGIN DEFINITION
* =========================== */
export function create($element, options) {
$element.data("typeahead", new Typeahead($element, options));
type TypeaheadOptions<ItemType> = {
highlighter_html: (item: ItemType) => string | undefined;
items: number;
source: (query: string) => ItemType[];
// optional options
advanceKeyCodes?: number[];
automated?: () => boolean;
closeInputFieldOnHide?: () => void;
dropup?: boolean;
fixed?: boolean;
header_html?: () => string | false;
helpOnEmptyStrings?: boolean;
matcher?: (item: ItemType) => boolean;
naturalSearch?: boolean;
on_escape?: () => void;
openInputFieldOnKeyUp?: () => void;
option_label?: (matching_items: ItemType[], item: ItemType) => string | false;
parentElement?: string;
sorter: (items: ItemType[]) => ItemType[];
stopAdvance?: boolean;
tabIsEnter?: boolean;
trigger_selection?: (event: JQuery.KeyDownEvent) => boolean;
updater: (
item: ItemType,
event?: JQuery.ClickEvent | JQuery.KeyUpEvent | JQuery.KeyDownEvent,
) => string | undefined;
};
export function create<ItemType extends string | object>(
input_element: InputElement,
options: TypeaheadOptions<ItemType>,
): void {
input_element.$element.data("typeahead", new Typeahead(input_element, options));
}
export function lookup($element) {
export function lookup($element: JQuery): void {
const typeahead = $element.data("typeahead");
typeahead.lookup();
}

View File

@ -585,7 +585,7 @@ export function get_person_suggestions(query, opts) {
export function get_stream_topic_data(hacky_this) {
const opts = {};
const $message_row = hacky_this.$element.closest(".message_row");
const $message_row = hacky_this.input_element.$element.closest(".message_row");
if ($message_row.length === 1) {
// we are editing a message so we try to use its keys.
const msg = message_store.get(rows.id($message_row));
@ -667,7 +667,7 @@ const ALLOWED_MARKDOWN_FEATURES = {
};
export function get_candidates(query) {
const split = split_at_cursor(query, this.$element);
const split = split_at_cursor(query, this.input_element.$element);
let current_token = tokenize_compose_str(split[0]);
if (current_token === "") {
return false;
@ -861,10 +861,10 @@ export function content_highlighter_html(item) {
}
export function content_typeahead_selected(item, event) {
const pieces = split_at_cursor(this.query, this.$element);
const pieces = split_at_cursor(this.query, this.input_element.$element);
let beginning = pieces[0];
let rest = pieces[1];
const $textbox = this.$element;
const $textbox = this.input_element.$element;
// Accepting some typeahead selections, like polls, will generate
// placeholder text that is selected, in order to clarify for the
// user what a given parameter is for. This object stores the
@ -1008,7 +1008,11 @@ export function content_typeahead_selected(item, event) {
$textbox.caret(beginning.length, beginning.length);
compose_ui.autosize_textarea($textbox);
};
flatpickr.show_flatpickr(this.$element[0], on_timestamp_selection, timestamp);
flatpickr.show_flatpickr(
this.input_element.$element[0],
on_timestamp_selection,
timestamp,
);
return beginning + rest;
}
}
@ -1094,6 +1098,10 @@ export function compose_trigger_selection(event) {
}
export function initialize_topic_edit_typeahead(form_field, stream_name, dropup) {
const bootstrap_typeahead_input = {
$element: form_field,
type: "input",
};
const options = {
fixed: true,
dropup,
@ -1113,7 +1121,7 @@ export function initialize_topic_edit_typeahead(form_field, stream_name, dropup)
},
items: 5,
};
bootstrap_typeahead.create(form_field, options);
bootstrap_typeahead.create(bootstrap_typeahead_input, options);
}
function get_header_html() {
@ -1141,7 +1149,11 @@ function get_header_html() {
}
export function initialize_compose_typeahead(selector) {
bootstrap_typeahead.create($(selector), {
const bootstrap_typeahead_input = {
$element: $(selector),
type: "input",
};
bootstrap_typeahead.create(bootstrap_typeahead_input, {
items: max_num_items,
dropup: true,
fixed: true,
@ -1172,7 +1184,11 @@ export function initialize({on_enter_send}) {
$("form#send_message_form").on("keydown", (e) => handle_keydown(e, {on_enter_send}));
$("form#send_message_form").on("keyup", handle_keyup);
bootstrap_typeahead.create($("input#stream_message_recipient_topic"), {
const stream_message_typeahead_input = {
$element: $("input#stream_message_recipient_topic"),
type: "input",
};
bootstrap_typeahead.create(stream_message_typeahead_input, {
source() {
return topics_seen_for(compose_state.stream_id());
},
@ -1197,7 +1213,11 @@ export function initialize({on_enter_send}) {
header_html: render_topic_typeahead_hint,
});
bootstrap_typeahead.create($("#private_message_recipient"), {
const private_message_typeahead_input = {
$element: $("#private_message_recipient"),
type: "contenteditable",
};
bootstrap_typeahead.create(private_message_typeahead_input, {
source: get_pm_people,
items: max_num_items,
dropup: true,

View File

@ -165,13 +165,20 @@ export function initialize_custom_pronouns_type_fields(element_id) {
$t({defaultMessage: "she/her"}),
$t({defaultMessage: "they/them"}),
];
bootstrap_typeahead.create($(element_id).find(".pronouns_type_field"), {
const bootstrap_typeahead_input = {
$element: $(element_id).find(".pronouns_type_field"),
type: "input",
};
bootstrap_typeahead.create(bootstrap_typeahead_input, {
items: 3,
fixed: true,
helpOnEmptyStrings: true,
source() {
return commonly_used_pronouns;
},
sorter(items) {
return bootstrap_typeahead.defaultSorter(items, this.query);
},
highlighter_html(item) {
return typeahead_helper.render_typeahead_item({primary: item});
},

View File

@ -32,7 +32,11 @@ export function set_up($input, pills, opts) {
const include_users = opts.user;
const exclude_bots = opts.exclude_bots;
bootstrap_typeahead.create($input, {
const bootstrap_typeahead_input = {
$element: $input,
type: "contenteditable",
};
bootstrap_typeahead.create(bootstrap_typeahead_input, {
items: 5,
fixed: true,
dropup: true,

View File

@ -57,7 +57,11 @@ export function initialize({on_narrow_search}) {
// just represents the key of the hash, so it's redundant.)
let search_map = new Map();
bootstrap_typeahead.create($search_query_box, {
const bootstrap_typeahead_input = {
$element: $search_query_box,
type: "input",
};
bootstrap_typeahead.create(bootstrap_typeahead_input, {
source(query) {
const suggestions = search_suggestion.get_suggestions(query);
// Update our global search_map hash

View File

@ -152,7 +152,12 @@ function build_page() {
const $search_pygments_box = $("#playground_pygments_language");
let language_labels = new Map();
bootstrap_typeahead.create($search_pygments_box, {
const bootstrap_typeahead_input = {
$element: $search_pygments_box,
type: "input",
};
bootstrap_typeahead.create(bootstrap_typeahead_input, {
source(query) {
language_labels = realm_playground.get_pygments_typeahead_list_for_settings(query);
return [...language_labels.keys()];
@ -165,6 +170,9 @@ function build_page() {
const q = this.query.trim().toLowerCase();
return item.toLowerCase().startsWith(q);
},
sorter(items) {
return bootstrap_typeahead.defaultSorter(items, this.query);
},
});
$search_pygments_box.on("click", (e) => {

View File

@ -431,11 +431,14 @@ test("topics_seen_for", ({override, override_rewire}) => {
test("content_typeahead_selected", ({override}) => {
const fake_this = {
query: "",
$element: {},
input_element: {
$element: {},
type: "input",
},
};
let caret_called1 = false;
let caret_called2 = false;
fake_this.$element.caret = function (...args) {
fake_this.input_element.$element.caret = function (...args) {
if (args.length === 0) {
// .caret() used in split_at_cursor
caret_called1 = true;
@ -448,7 +451,7 @@ test("content_typeahead_selected", ({override}) => {
return this;
};
let range_called = false;
fake_this.$element.range = function (...args) {
fake_this.input_element.$element.range = function (...args) {
const [arg1, arg2] = args;
// .range() used in setTimeout
assert.ok(arg2 > arg1);
@ -690,7 +693,7 @@ test("content_typeahead_selected", ({override}) => {
// Test special case to not close code blocks if there is text afterward
fake_this.query = "```p\nsome existing code";
fake_this.token = "p";
fake_this.$element.caret = () => 4; // Put cursor right after ```p
fake_this.input_element.$element.caret = () => 4; // Put cursor right after ```p
actual_value = ct.content_typeahead_selected.call(fake_this, "python");
expected_value = "```python\nsome existing code";
assert.equal(actual_value, expected_value);
@ -757,8 +760,8 @@ test("initialize", ({override, override_rewire, mock_template}) => {
let topic_typeahead_called = false;
let pm_recipient_typeahead_called = false;
let compose_textarea_typeahead_called = false;
override(bootstrap_typeahead, "create", ($element, options) => {
switch ($element) {
override(bootstrap_typeahead, "create", (input_element, options) => {
switch (input_element.$element) {
case $("input#stream_message_recipient_topic"): {
override_rewire(stream_topic_history, "get_recent_topic_names", (stream_id) => {
assert.equal(stream_id, sweden_stream.stream_id);
@ -987,14 +990,17 @@ test("initialize", ({override, override_rewire, mock_template}) => {
// properly set as the .source(). All its features are tested later on
// in test_begins_typeahead().
let fake_this = {
$element: {},
input_element: {
$element: {},
type: "input",
},
};
let caret_called = false;
fake_this.$element.caret = () => {
fake_this.input_element.$element.caret = () => {
caret_called = true;
return 7;
};
fake_this.$element.closest = () => [];
fake_this.input_element.$element.closest = () => [];
let actual_value = options.source.call(fake_this, "test #s");
assert.deepEqual(sorted_names_from(actual_value), ["Sweden", "The Netherlands"]);
assert.ok(caret_called);
@ -1318,6 +1324,10 @@ test("begins_typeahead", ({override, override_rewire}) => {
override(stream_topic_history_util, "get_server_history", noop);
const begin_typehead_this = {
input_element: {
$element: {},
type: "input",
},
options: {
completions: {
emoji: true,

View File

@ -132,8 +132,8 @@ run_test("set_up", ({mock_template, override}) => {
}
let opts = {};
override(bootstrap_typeahead, "create", ($element, config) => {
assert.equal($element, $fake_input);
override(bootstrap_typeahead, "create", (input_element, config) => {
assert.equal(input_element.$element, $fake_input);
assert.equal(config.items, 5);
assert.ok(config.fixed);
assert.ok(config.dropup);

View File

@ -36,8 +36,8 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
search_suggestion.max_num_of_search_results = 999;
let terms;
override(bootstrap_typeahead, "create", ($element, opts) => {
assert.equal($element, $search_query_box);
override(bootstrap_typeahead, "create", (input_element, opts) => {
assert.equal(input_element.$element, $search_query_box);
assert.equal(opts.items, 999);
assert.equal(opts.naturalSearch, true);
assert.equal(opts.helpOnEmptyStrings, true);