From d9f25d01a15521bdafec77689a171420810d1688 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 19 Aug 2024 22:18:02 -0700 Subject: [PATCH] web: Use util.the for accessing element of single-item lists. --- web/src/about_zulip.ts | 5 +++-- web/src/audible_notifications.ts | 3 ++- web/src/billing/helpers.ts | 3 ++- web/src/bootstrap_typeahead.ts | 24 ++++++++++---------- web/src/buddy_list.ts | 7 +++--- web/src/common.ts | 3 ++- web/src/compose_recipient.ts | 2 +- web/src/compose_ui.ts | 6 ++--- web/src/composebox_typeahead.ts | 13 +++++++---- web/src/condense.ts | 3 ++- web/src/copy_and_paste.ts | 7 ++++-- web/src/drafts.ts | 12 +++++----- web/src/emoji_picker.ts | 2 +- web/src/flatpickr.ts | 3 ++- web/src/inbox_ui.ts | 2 +- web/src/input_pill.ts | 18 +++++++-------- web/src/integration_url_modal.ts | 2 +- web/src/invite.ts | 9 ++++---- web/src/lightbox.ts | 16 +++++++------- web/src/message_list_data.ts | 2 +- web/src/message_viewport.ts | 6 ++--- web/src/messages_overlay_ui.ts | 25 ++++++++++++--------- web/src/playground_links_popover.ts | 7 +++--- web/src/poll_modal.ts | 4 +++- web/src/popover_menus.ts | 4 ++-- web/src/portico/help.ts | 9 ++++---- web/src/portico/integrations_dev_panel.ts | 27 +++++++++++++---------- web/src/portico/landing-page.ts | 3 ++- web/src/portico/tabbed-instructions.ts | 3 ++- web/src/read_receipts.ts | 3 ++- web/src/recent_view_ui.ts | 11 ++++----- web/src/rendered_markdown.ts | 4 ++-- web/src/scroll_util.ts | 8 ++++--- web/src/search_pill.ts | 11 +++++---- web/src/settings_components.ts | 8 +++---- web/src/settings_emoji.ts | 2 +- web/src/settings_linkifiers.ts | 5 +++-- web/src/settings_profile_fields.ts | 9 ++++---- web/src/settings_realm_domains.ts | 7 +++--- web/src/setup.ts | 3 ++- web/src/spoilers.ts | 4 +++- web/src/tippyjs.ts | 3 ++- web/src/upload_widget.ts | 5 +++-- web/src/user_profile.ts | 6 ++--- web/tests/compose_ui.test.js | 1 + web/tests/input_pill.test.js | 7 ++++++ web/tests/lib/zjquery_element.js | 1 + 47 files changed, 188 insertions(+), 140 deletions(-) diff --git a/web/src/about_zulip.ts b/web/src/about_zulip.ts index ebef9af84a..f0224314f1 100644 --- a/web/src/about_zulip.ts +++ b/web/src/about_zulip.ts @@ -7,6 +7,7 @@ import * as browser_history from "./browser_history"; import {show_copied_confirmation} from "./copied_tooltip"; import * as overlays from "./overlays"; import {realm} from "./state_data"; +import * as util from "./util"; export function launch(): void { overlays.open_overlay({ @@ -19,12 +20,12 @@ export function launch(): void { const zulip_version_clipboard = new ClipboardJS("#about-zulip .fa-copy.zulip-version"); zulip_version_clipboard.on("success", () => { - show_copied_confirmation($("#about-zulip .fa-copy.zulip-version")[0]!); + show_copied_confirmation(util.the($("#about-zulip .fa-copy.zulip-version"))); }); const zulip_merge_base_clipboard = new ClipboardJS("#about-zulip .fa-copy.zulip-merge-base"); zulip_merge_base_clipboard.on("success", () => { - show_copied_confirmation($("#about-zulip .fa-copy.zulip-merge-base")[0]!); + show_copied_confirmation(util.the($("#about-zulip .fa-copy.zulip-merge-base"))); }); } diff --git a/web/src/audible_notifications.ts b/web/src/audible_notifications.ts index efa8e2898a..ee8ec54f6d 100644 --- a/web/src/audible_notifications.ts +++ b/web/src/audible_notifications.ts @@ -1,6 +1,7 @@ import $ from "jquery"; import {user_settings} from "./user_settings"; +import * as util from "./util"; export function initialize(): void { update_notification_sound_source($("audio#user-notification-sound-audio"), user_settings); @@ -22,6 +23,6 @@ export function update_notification_sound_source( if (notification_sound !== "none") { // Load it so that it is ready to be played; without this the old sound // is played. - $container_elem[0]!.load(); + util.the($container_elem).load(); } } diff --git a/web/src/billing/helpers.ts b/web/src/billing/helpers.ts index ddbec4f2ad..c3f252be45 100644 --- a/web/src/billing/helpers.ts +++ b/web/src/billing/helpers.ts @@ -2,6 +2,7 @@ import $ from "jquery"; import {z} from "zod"; import * as loading from "../loading"; +import * as util from "../util"; export type FormDataObject = Record; @@ -162,7 +163,7 @@ export function update_discount_details( } export function is_valid_input($elem: JQuery): boolean { - return $elem[0]!.checkValidity(); + return util.the($elem).checkValidity(); } export function redirect_to_billing_with_successful_upgrade(billing_base_url: string): void { diff --git a/web/src/bootstrap_typeahead.ts b/web/src/bootstrap_typeahead.ts index 8fce7d9c2e..264ea92230 100644 --- a/web/src/bootstrap_typeahead.ts +++ b/web/src/bootstrap_typeahead.ts @@ -171,7 +171,7 @@ import getCaretCoordinates from "textarea-caret"; import * as tippy from "tippy.js"; import * as scroll_util from "./scroll_util"; -import {get_string_diff} from "./util"; +import {get_string_diff, the} from "./util"; function get_pseudo_keycode( event: JQuery.KeyDownEvent | JQuery.KeyUpEvent | JQuery.KeyPressEvent, @@ -322,7 +322,7 @@ export class Typeahead { } select(e?: JQuery.ClickEvent | JQuery.KeyUpEvent | JQuery.KeyDownEvent): this { - const val = this.values.get(this.$menu.find(".active")[0]!); + const val = this.values.get(the(this.$menu.find(".active"))); // It's possible that we got here from pressing enter with nothing highlighted. if (!this.requireHighlight && val === undefined) { return this.hide(); @@ -359,7 +359,7 @@ export class Typeahead { } set_value(): void { - const val = this.values.get(this.$menu.find(".active")[0]!); + const val = this.values.get(the(this.$menu.find(".active"))); assert(typeof val === "string"); if (this.input_element.type === "contenteditable") { this.input_element.$element.text(val); @@ -392,7 +392,7 @@ export class Typeahead { const input_element = this.input_element; if (!this.non_tippy_parent_element) { - this.instance = tippy.default(input_element.$element[0]!, { + this.instance = tippy.default(the(input_element.$element), { // Lets typeahead take the width needed to fit the content // and wraps it if it overflows the visible container. maxWidth: "none", @@ -425,7 +425,7 @@ export class Typeahead { interactive: true, appendTo: () => document.body, showOnCreate: true, - content: this.$container[0]!, + content: the(this.$container), // We expect the typeahead creator to handle when to hide / show the typeahead. trigger: "manual", arrow: false, @@ -435,8 +435,8 @@ export class Typeahead { if (input_element.type === "textarea") { const caret = getCaretCoordinates( - input_element.$element[0]!, - input_element.$element[0]!.selectionStart, + the(input_element.$element), + the(input_element.$element).selectionStart, ); // Used to consider the scroll height of textbox in the vertical offset. const scrollTop = input_element.$element.scrollTop() ?? 0; @@ -554,7 +554,7 @@ export class Typeahead { render(final_items: ItemType[], matching_items: ItemType[]): this { const $items: JQuery[] = final_items.map((item) => { const $i = $(ITEM_HTML); - this.values.set($i[0]!, item); + this.values.set(the($i), item); const item_html = this.highlighter_html(item, this.query) ?? ""; const $item_html = $i.find("a").html(item_html); @@ -743,12 +743,12 @@ export class Typeahead { this.select(e); - if (this.input_element.$element[0]!.id === "stream_message_recipient_topic") { + if (the(this.input_element.$element).id === "stream_message_recipient_topic") { assert(this.input_element.type === "input"); // Move the cursor to the end of the topic 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; + the(this.input_element.$element).selectionStart = topic_length; + the(this.input_element.$element).selectionEnd = topic_length; } break; @@ -775,7 +775,7 @@ export class Typeahead { // when shift (keycode 16) + tabbing to the topic field if ( pseudo_keycode === 16 && - this.input_element.$element[0]!.id === "stream_message_recipient_topic" + the(this.input_element.$element).id === "stream_message_recipient_topic" ) { return; } diff --git a/web/src/buddy_list.ts b/web/src/buddy_list.ts index 04a5fbcc23..3a31ed276d 100644 --- a/web/src/buddy_list.ts +++ b/web/src/buddy_list.ts @@ -26,6 +26,7 @@ import * as stream_data from "./stream_data"; import type {StreamSubscription} from "./sub_store"; import {INTERACTIVE_HOVER_DELAY} from "./tippyjs"; import {user_settings} from "./user_settings"; +import * as util from "./util"; function get_formatted_sub_count(sub_count: number): string { if (sub_count < 1000) { @@ -172,7 +173,7 @@ export class BuddyList extends BuddyListConf { // This will default to "bottom" placement for this tooltip. placement = "auto"; } - tippy.default($elem[0]!, { + tippy.default(util.the($elem), { // Because the buddy list subheadings are potential click targets // for purposes having nothing to do with the subscriber count // (collapsing/expanding), we delay showing the tooltip until the @@ -765,9 +766,7 @@ export class BuddyList extends BuddyListConf { fill_screen_with_content(): void { let height = this.height_to_fill(); - const elem = scroll_util - .get_scroll_element($(this.scroll_container_selector)) - .expectOne()[0]!; + const elem = util.the(scroll_util.get_scroll_element($(this.scroll_container_selector))); // Add a fudge factor. height += 10; diff --git a/web/src/common.ts b/web/src/common.ts index 818638a1ef..000a09031f 100644 --- a/web/src/common.ts +++ b/web/src/common.ts @@ -2,6 +2,7 @@ import $ from "jquery"; import * as tippy from "tippy.js"; import {$t} from "./i18n"; +import * as util from "./util"; export const status_classes = "alert-error alert-success alert-info alert-warning alert-loading"; @@ -108,7 +109,7 @@ function set_password_toggle_label( ): void { $(password_selector).attr("aria-label", label); if (tippy_tooltips) { - const element: tippy.ReferenceElement = $(password_selector)[0]!; + const element: tippy.ReferenceElement = util.the($(password_selector)); const tippy_instance = element._tippy ?? tippy.default(element); tippy_instance.setContent(label); } else { diff --git a/web/src/compose_recipient.ts b/web/src/compose_recipient.ts index 04ab675280..8b4348ca76 100644 --- a/web/src/compose_recipient.ts +++ b/web/src/compose_recipient.ts @@ -281,7 +281,7 @@ function on_hidden_callback(): void { // Always move focus to the topic input even if it's not empty, // since it's likely the user will want to update the topic // after updating the stream. - ui_util.place_caret_at_end($("input#stream_message_recipient_topic")[0]!); + ui_util.place_caret_at_end(util.the($("input#stream_message_recipient_topic"))); } else { if (compose_state.private_message_recipient().length === 0) { $("#private_message_recipient").trigger("focus").trigger("select"); diff --git a/web/src/compose_ui.ts b/web/src/compose_ui.ts index f8a6d221c2..9b90051424 100644 --- a/web/src/compose_ui.ts +++ b/web/src/compose_ui.ts @@ -123,9 +123,9 @@ export function insert_and_scroll_into_view( // to support `undo`, we can use a faster method. $textarea.val(content); } else if (replace_all) { - setFieldText($textarea[0]!, content); + setFieldText(util.the($textarea), content); } else { - insertTextIntoField($textarea[0]!, content); + insertTextIntoField(util.the($textarea), content); } // Blurring and refocusing ensures the cursor / selection is in view // in chromium browsers. @@ -300,7 +300,7 @@ export function replace_syntax( // for details. const old_text = $textarea.val(); - replaceFieldText($textarea[0]!, old_syntax, () => new_syntax, "after-replacement"); + replaceFieldText(util.the($textarea), old_syntax, () => new_syntax, "after-replacement"); const new_text = $textarea.val(); // When replacing content in a textarea, we need to move the cursor diff --git a/web/src/composebox_typeahead.ts b/web/src/composebox_typeahead.ts index d78aeb2ecb..4147b9156b 100644 --- a/web/src/composebox_typeahead.ts +++ b/web/src/composebox_typeahead.ts @@ -41,6 +41,7 @@ import type {UserGroup} from "./user_groups"; import * as user_pill from "./user_pill"; import type {UserPillData} from "./user_pill"; import {user_settings} from "./user_settings"; +import * as util from "./util"; // ********************************** // AN IMPORTANT NOTE ABOUT TYPEAHEADS @@ -250,7 +251,7 @@ function handle_bulleting_or_numbering( if (bulleted_numbered_list_util.strip_bullet(previous_line) === "") { // below we select and replace the last 2 characters in the textarea before // the cursor - the bullet syntax - with an empty string - $textarea[0]!.setSelectionRange($textarea.caret() - 2, $textarea.caret()); + util.the($textarea).setSelectionRange($textarea.caret() - 2, $textarea.caret()); compose_ui.insert_and_scroll_into_view("", $textarea); e.preventDefault(); return; @@ -264,7 +265,7 @@ function handle_bulleting_or_numbering( if (bulleted_numbered_list_util.strip_numbering(previous_line) === "") { // below we select then replaces the last few characters in the textarea before // the cursor - the numbering syntax - with an empty string - $textarea[0]!.setSelectionRange( + util.the($textarea).setSelectionRange( $textarea.caret() - previous_number_string.length - 2, $textarea.caret(), ); @@ -297,7 +298,7 @@ export function handle_enter($textarea: JQuery, e: JQuery.K // If the selectionStart and selectionEnd are not the same, that // means that some text was selected. - if ($textarea[0]!.selectionStart !== $textarea[0]!.selectionEnd) { + if (util.the($textarea).selectionStart !== util.the($textarea).selectionEnd) { // Replace it with the newline, remembering to resize the // textarea if needed. compose_ui.insert_and_scroll_into_view("\n", $textarea); @@ -1175,7 +1176,11 @@ export function content_typeahead_selected( $textbox.caret(beginning.length); compose_ui.autosize_textarea($textbox); }; - flatpickr.show_flatpickr(input_element.$element[0]!, on_timestamp_selection, timestamp); + flatpickr.show_flatpickr( + util.the(input_element.$element), + on_timestamp_selection, + timestamp, + ); return beginning + rest; } } diff --git a/web/src/condense.ts b/web/src/condense.ts index 96ed545bb4..bfb715da3e 100644 --- a/web/src/condense.ts +++ b/web/src/condense.ts @@ -6,6 +6,7 @@ import * as message_lists from "./message_lists"; import type {Message} from "./message_store"; import * as message_viewport from "./message_viewport"; import * as rows from "./rows"; +import * as util from "./util"; /* This library implements two related, similar concepts: @@ -151,7 +152,7 @@ function get_message_height(elem: HTMLElement): number { // This needs to be very fast. This function runs hundreds of times // when displaying a message feed view that has hundreds of message // history, which ideally should render in <100ms. - return $(elem).find(".message_content")[0]!.scrollHeight; + return util.the($(elem).find(".message_content")).scrollHeight; } export function hide_message_expander($row: JQuery): void { diff --git a/web/src/copy_and_paste.ts b/web/src/copy_and_paste.ts index 2886e0ba4f..80cc2315e1 100644 --- a/web/src/copy_and_paste.ts +++ b/web/src/copy_and_paste.ts @@ -10,6 +10,7 @@ import * as hash_util from "./hash_util"; import * as message_lists from "./message_lists"; import * as rows from "./rows"; import * as topic_link_util from "./topic_link_util"; +import * as util from "./util"; declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions @@ -135,7 +136,7 @@ function select_div($div: JQuery, selection: Selection): void { background: "#FFF", }).attr("id", "copytempdiv"); $("body").append($div); - selection.selectAllChildren($div[0]!); + selection.selectAllChildren(util.the($div)); } function remove_div(_div: JQuery, ranges: Range[]): void { @@ -629,7 +630,9 @@ function is_safe_url_paste_target($textarea: JQuery): boole export function cursor_at_markdown_link_marker($textarea: JQuery): boolean { const range = $textarea.range(); - const possible_markdown_link_markers = $textarea[0]!.value.slice(range.start - 2, range.start); + const possible_markdown_link_markers = util + .the($textarea) + .value.slice(range.start - 2, range.start); return possible_markdown_link_markers === "]("; } diff --git a/web/src/drafts.ts b/web/src/drafts.ts index 1e6856bcdf..8e2b8a7090 100644 --- a/web/src/drafts.ts +++ b/web/src/drafts.ts @@ -363,11 +363,13 @@ export function restore_message(draft: LocalStorageDraft): ComposeArguments { function draft_notify(): void { // Display a tooltip to notify the user about the saved draft. - const instance = tippy.default(".top_left_drafts .unread_count", { - content: $t({defaultMessage: "Saved as draft"}), - arrow: true, - placement: "right", - })[0]!; + const instance = util.the( + tippy.default(".top_left_drafts .unread_count", { + content: $t({defaultMessage: "Saved as draft"}), + arrow: true, + placement: "right", + }), + ); instance.show(); function remove_instance(): void { instance.destroy(); diff --git a/web/src/emoji_picker.ts b/web/src/emoji_picker.ts index cd9751c607..13d35e35f5 100644 --- a/web/src/emoji_picker.ts +++ b/web/src/emoji_picker.ts @@ -592,7 +592,7 @@ function process_keypress(e: JQuery.KeyPressEvent | JQuery.KeyDownEvent): void { export function emoji_select_tab($elt: JQuery): void { const scrolltop = $elt.scrollTop()!; - const scrollheight = $elt[0]!.scrollHeight; + const scrollheight = util.the($elt).scrollHeight; const elt_height = $elt.height()!; let currently_selected = ""; for (const o of section_head_offsets) { diff --git a/web/src/flatpickr.ts b/web/src/flatpickr.ts index 22af1b3800..a394a29152 100644 --- a/web/src/flatpickr.ts +++ b/web/src/flatpickr.ts @@ -6,6 +6,7 @@ import assert from "minimalistic-assert"; import {$t} from "./i18n"; import {user_settings} from "./user_settings"; +import * as util from "./util"; export let flatpickr_instance: flatpickr.Instance; @@ -25,7 +26,7 @@ export function show_flatpickr( ): flatpickr.Instance { const $flatpickr_input = $("").attr("id", "#timestamp_flatpickr"); - flatpickr_instance = flatpickr($flatpickr_input[0]!, { + flatpickr_instance = flatpickr(util.the($flatpickr_input), { mode: "single", enableTime: true, clickOpens: false, diff --git a/web/src/inbox_ui.ts b/web/src/inbox_ui.ts index b442681270..4227c2fe07 100644 --- a/web/src/inbox_ui.ts +++ b/web/src/inbox_ui.ts @@ -1438,7 +1438,7 @@ function move_focus_to_visible_area(): void { } const INBOX_ROW_HEIGHT = 30; - const position = $("#inbox-filters")[0]!.getBoundingClientRect(); + const position = util.the($("#inbox-filters")).getBoundingClientRect(); const inbox_center_x = (position.left + position.right) / 2; // We are aiming to get the first row if it is completely visible or the second row. const inbox_row_below_filters = position.bottom + INBOX_ROW_HEIGHT; diff --git a/web/src/input_pill.ts b/web/src/input_pill.ts index ff7e793057..640f8dd47d 100644 --- a/web/src/input_pill.ts +++ b/web/src/input_pill.ts @@ -7,6 +7,7 @@ import render_input_pill from "../templates/input_pill.hbs"; import * as keydown_util from "./keydown_util"; import * as ui_util from "./ui_util"; +import * as util from "./util"; // See https://zulip.readthedocs.io/en/latest/subsystems/input-pills.html @@ -194,9 +195,9 @@ export function create( if (idx !== -1) { store.pills[idx]!.$element.remove(); - const pill = store.pills.splice(idx, 1); + const pill = util.the(store.pills.splice(idx, 1)); if (store.onPillRemove !== undefined) { - store.onPillRemove(pill[0]!, trigger); + store.onPillRemove(pill, trigger); } // This is needed to run the "change" event handler registered in @@ -231,8 +232,7 @@ export function create( while (store.pills.length > 0) { this.removeLastPill(trigger, quiet); } - - this.clear(store.$input[0]!); + this.clear(util.the(store.$input)); }, insertManyPills(pills: string | string[]) { @@ -253,7 +253,7 @@ export function create( // when using the `text` insertion feature with jQuery the caret is // placed at the beginning of the input field, so this moves it to // the end. - ui_util.place_caret_at_end(store.$input[0]!); + ui_util.place_caret_at_end(util.the(store.$input)); // this sends a flag if the operation wasn't completely successful, // which in this case is defined as some of the pills not autofilling @@ -341,7 +341,7 @@ export function create( // if the pill is successful, it will create the pill and clear // the input. if (funcs.appendPill(store.$input.text().trim())) { - funcs.clear(store.$input[0]!); + funcs.clear(util.the(store.$input)); } e.preventDefault(); @@ -370,7 +370,7 @@ export function create( break; case "Backspace": { const $next = $pill.next(); - funcs.removePill($pill[0]!, "backspace"); + funcs.removePill(util.the($pill), "backspace"); $next.trigger("focus"); // the "Backspace" key in Firefox will go back a page if you do // not prevent it. @@ -416,8 +416,8 @@ export function create( store.$input.trigger("change"); } else { e.stopPropagation(); - const $pill = $(this).closest(".pill"); - funcs.removePill($pill[0]!, "close"); + const pill = util.the($(this).closest(".pill")); + funcs.removePill(pill, "close"); } // Since removing a pill moves the $input, typeahead needs to refresh // to appear at the correct position. diff --git a/web/src/integration_url_modal.ts b/web/src/integration_url_modal.ts index b9fab45a6a..61f285fd01 100644 --- a/web/src/integration_url_modal.ts +++ b/web/src/integration_url_modal.ts @@ -53,7 +53,7 @@ export function show_generate_integration_url_modal(api_key: string): void { }); clipboard.on("success", () => { show_copied_confirmation( - $("#generate-integration-url-modal .dialog_submit_button")[0]!, + util.the($("#generate-integration-url-modal .dialog_submit_button")), ); }); diff --git a/web/src/invite.ts b/web/src/invite.ts index 87dd0b0cab..f4fc19ed3a 100644 --- a/web/src/invite.ts +++ b/web/src/invite.ts @@ -30,6 +30,7 @@ import * as stream_pill from "./stream_pill"; import * as timerender from "./timerender"; import type {HTMLSelectOneElement} from "./types"; import * as ui_report from "./ui_report"; +import * as util from "./util"; let custom_expiration_time_input = 10; let custom_expiration_time_unit = "days"; @@ -208,7 +209,7 @@ function submit_invitation_form(): void { $("#invite-user-modal .dialog_submit_button").text($t({defaultMessage: "Invite"})); $("#invite-user-modal .dialog_submit_button").prop("disabled", false); $("#invite-user-modal .dialog_exit_button").prop("disabled", false); - $invite_status[0]!.scrollIntoView(); + util.the($invite_status).scrollIntoView(); }, }); } @@ -228,7 +229,7 @@ function generate_multiuse_invite(): void { clipboard.on("success", () => { const tippy_timeout_in_ms = 800; show_copied_confirmation( - $("#copy_generated_invite_link")[0]!, + util.the($("#copy_generated_invite_link")), () => { // Do nothing on hide }, @@ -243,7 +244,7 @@ function generate_multiuse_invite(): void { $("#invite-user-modal .dialog_submit_button").text($t({defaultMessage: "Create link"})); $("#invite-user-modal .dialog_submit_button").prop("disabled", false); $("#invite-user-modal .dialog_exit_button").prop("disabled", false); - $invite_status[0]!.scrollIntoView(); + util.the($invite_status).scrollIntoView(); }, }); } @@ -302,7 +303,7 @@ function set_streams_to_join_list_visibility(): void { const realm_has_default_streams = stream_data.get_default_stream_ids().length !== 0; const hide_streams_list = realm_has_default_streams && - $("input#invite_select_default_streams")[0]!.checked; + util.the($("input#invite_select_default_streams")).checked; if (hide_streams_list) { $(".add_streams_container").hide(); } else { diff --git a/web/src/lightbox.ts b/web/src/lightbox.ts index f5df002c39..4591389d35 100644 --- a/web/src/lightbox.ts +++ b/web/src/lightbox.ts @@ -118,12 +118,12 @@ export class PanZoomControl { // See https://github.com/anvaka/panzoom/issues/112 for upstream discussion. const {scale, x, y} = e.getTransform(); - const image_width = $(".zoom-element > img")[0]!.clientWidth * scale; - const image_height = $(".zoom-element > img")[0]!.clientHeight * scale; - const zoom_element_width = $(".zoom-element")[0]!.clientWidth * scale; - const zoom_element_height = $(".zoom-element")[0]!.clientHeight * scale; - const max_translate_x = $(".image-preview")[0]!.clientWidth; - const max_translate_y = $(".image-preview")[0]!.clientHeight; + const image_width = util.the($(".zoom-element > img")).clientWidth * scale; + const image_height = util.the($(".zoom-element > img")).clientHeight * scale; + const zoom_element_width = util.the($(".zoom-element")).clientWidth * scale; + const zoom_element_height = util.the($(".zoom-element")).clientHeight * scale; + const max_translate_x = util.the($(".image-preview")).clientWidth; + const max_translate_y = util.the($(".image-preview")).clientHeight; // When the image is dragged out of the image-preview container // (max_translate) it will be "snapped" back so that the number @@ -394,7 +394,7 @@ export function build_open_media_function( return function ($media: JQuery): void { // This is used both for clicking on media in the messagelist, as well as clicking on images // in the media list under the lightbox when it is open. - const payload = parse_media_data($media[0]!); + const payload = parse_media_data(util.the($media)); assert(payload !== undefined); if (payload.type.match("-video")) { @@ -597,7 +597,7 @@ export function initialize(): void { // Bind the pan/zoom control the newly created element. const pan_zoom_control = new PanZoomControl( - $("#lightbox_overlay .image-preview > .zoom-element")[0]!, + util.the($("#lightbox_overlay .image-preview > .zoom-element")), ); const reset_lightbox_state = function (): void { diff --git a/web/src/message_list_data.ts b/web/src/message_list_data.ts index 57852761df..a93d28d814 100644 --- a/web/src/message_list_data.ts +++ b/web/src/message_list_data.ts @@ -233,7 +233,7 @@ export class MessageListData { return true; } - const recipient_id = Number.parseInt(recipients[0]!, 10); + const recipient_id = Number.parseInt(util.the(recipients), 10); return ( !muted_users.is_user_muted(recipient_id) && !muted_users.is_user_muted(message.sender_id) diff --git a/web/src/message_viewport.ts b/web/src/message_viewport.ts index 9bbdd4869b..2aa5566e65 100644 --- a/web/src/message_viewport.ts +++ b/web/src/message_viewport.ts @@ -78,7 +78,7 @@ export function message_viewport_info(): MessageViewportInfo { export function at_rendered_bottom(): boolean { const bottom = scrollTop() + height(); // This also includes bottom whitespace. - const full_height = $scroll_container[0]!.scrollHeight; + const full_height = util.the($scroll_container).scrollHeight; // We only know within a pixel or two if we're // exactly at the bottom, due to browser quirkiness, @@ -94,7 +94,7 @@ export function bottom_rendered_message_visible(): boolean { const $last_row = rows.last_visible(); if ($last_row[0] !== undefined) { const message_bottom = $last_row[0].getBoundingClientRect().bottom; - const bottom_of_feed = $("#compose")[0]!.getBoundingClientRect().top; + const bottom_of_feed = util.the($("#compose")).getBoundingClientRect().top; return bottom_of_feed > message_bottom; } return false; @@ -214,7 +214,7 @@ const top_of_feed = new util.CachedValue({ const bottom_of_feed = new util.CachedValue({ compute_value() { - return $("#compose")[0]!.getBoundingClientRect().top; + return util.the($("#compose")).getBoundingClientRect().top; }, }); diff --git a/web/src/messages_overlay_ui.ts b/web/src/messages_overlay_ui.ts index 63fe408064..72e545417c 100644 --- a/web/src/messages_overlay_ui.ts +++ b/web/src/messages_overlay_ui.ts @@ -1,6 +1,8 @@ import $ from "jquery"; import assert from "minimalistic-assert"; +import * as util from "./util"; + export type Context = { items_container_selector: string; items_list_selector: string; @@ -73,11 +75,11 @@ export function modals_handle_events(event_key: string, context: Context): void export function set_initial_element(element_id: string, context: Context): void { if (element_id) { - const $current_element = get_element_by_id(element_id, context); - const focus_element = $current_element[0]!.children[0]; + const current_element = util.the(get_element_by_id(element_id, context)); + const focus_element = current_element.children[0]; assert(focus_element instanceof HTMLElement); activate_element(focus_element, context); - $(`.${CSS.escape(context.items_list_selector)}`)[0]!.scrollTop = 0; + util.the($(`.${CSS.escape(context.items_list_selector)}`)).scrollTop = 0; } } @@ -132,7 +134,7 @@ function initialize_focus(event_name: string, context: Context): void { } const $element = get_element_by_id(id, context); - const focus_element = $element[0]!.children[0]; + const focus_element = util.the($element).children[0]; assert(focus_element instanceof HTMLElement); activate_element(focus_element, context); } @@ -152,28 +154,29 @@ function scroll_to_element($element: JQuery, context: Context): void { const $box_item = $(`.${CSS.escape(context.box_item_selector)}`); // If focused element is first, scroll to the top. - if ($box_item.first()[0]!.parentElement === $element[0]) { - $items_list[0]!.scrollTop = 0; + if (util.the($box_item.first()).parentElement === $element[0]) { + util.the($items_list).scrollTop = 0; } // If focused element is last, scroll to the bottom. - if ($box_item.last()[0]!.parentElement === $element[0]) { - $items_list[0]!.scrollTop = $items_list[0]!.scrollHeight - ($items_list.height() ?? 0); + if (util.the($box_item.last()).parentElement === $element[0]) { + util.the($items_list).scrollTop = + util.the($items_list).scrollHeight - ($items_list.height() ?? 0); } // If focused element is cut off from the top, scroll up halfway in modal. if ($element.position().top < 55) { // 55 is the minimum distance from the top that will require extra scrolling. - $items_list[0]!.scrollTop -= $items_list[0]!.clientHeight / 2; + util.the($items_list).scrollTop -= util.the($items_list).clientHeight / 2; } // If focused element is cut off from the bottom, scroll down halfway in modal. const dist_from_top = $element.position().top; const total_dist = dist_from_top + $element[0].clientHeight; - const dist_from_bottom = $items_container[0]!.clientHeight - total_dist; + const dist_from_bottom = util.the($items_container).clientHeight - total_dist; if (dist_from_bottom < -4) { // -4 is the min dist from the bottom that will require extra scrolling. - $items_list[0]!.scrollTop += $items_list[0]!.clientHeight / 2; + util.the($items_list).scrollTop += util.the($items_list).clientHeight / 2; } } diff --git a/web/src/playground_links_popover.ts b/web/src/playground_links_popover.ts index 0b5f2439df..7eb917ad40 100644 --- a/web/src/playground_links_popover.ts +++ b/web/src/playground_links_popover.ts @@ -9,6 +9,7 @@ import * as popover_menus from "./popover_menus"; import * as realm_playground from "./realm_playground"; import type {RealmPlayground} from "./realm_playground"; import * as ui_util from "./ui_util"; +import * as util from "./util"; type RealmPlaygroundWithURL = RealmPlayground & {playground_url: string}; @@ -132,9 +133,9 @@ function register_click_handlers(): void { const playground_url = url_template.expand({code: extracted_code}); playground_store.set(playground.id, {...playground, playground_url}); } - const popover_target = $view_in_playground_button.find( - ".playground-links-popover-container", - )[0]!; + const popover_target = util.the( + $view_in_playground_button.find(".playground-links-popover-container"), + ); toggle_playground_links_popover(popover_target, playground_store); } }, diff --git a/web/src/poll_modal.ts b/web/src/poll_modal.ts index 7c1c3f4a29..2fa1201977 100644 --- a/web/src/poll_modal.ts +++ b/web/src/poll_modal.ts @@ -3,6 +3,8 @@ import SortableJS from "sortablejs"; import render_poll_modal_option from "../templates/poll_modal_option.hbs"; +import * as util from "./util"; + function create_option_row($last_option_row_input: JQuery): void { const row_html = render_poll_modal_option(); const $row_container = $last_option_row_input.closest(".simplebar-content"); @@ -46,7 +48,7 @@ export function poll_options_setup(): void { // setTimeout is needed to here to give time for simplebar to initialise setTimeout(() => { - SortableJS.create($("#add-poll-form .poll-options-list .simplebar-content")[0]!, { + SortableJS.create(util.the($("#add-poll-form .poll-options-list .simplebar-content")), { onUpdate() { // Do nothing on drag; the order is only processed on submission. }, diff --git a/web/src/popover_menus.ts b/web/src/popover_menus.ts index 2c90b38873..62e4710a63 100644 --- a/web/src/popover_menus.ts +++ b/web/src/popover_menus.ts @@ -194,7 +194,7 @@ export const default_popover_props: Partial = { // $tippy_box[0].hasAttribute("data-reference-hidden"); is the real check // but linter wants us to write it like this. const is_reference_outside_window = Object.hasOwn( - $tippy_box[0]!.dataset, + util.the($tippy_box).dataset, "referenceHidden", ); @@ -231,7 +231,7 @@ export const default_popover_props: Partial = { return; } - const reference_rect = $reference[0]!.getBoundingClientRect(); + const reference_rect = util.the($reference).getBoundingClientRect(); // This is the logic we want but since it is too expensive to run // on every scroll, we run a cheaper version of this to just check if // compose, sticky header or navbar are not obscuring the reference diff --git a/web/src/portico/help.ts b/web/src/portico/help.ts index 9faee2796c..a448a10797 100644 --- a/web/src/portico/help.ts +++ b/web/src/portico/help.ts @@ -5,6 +5,7 @@ import * as tippy from "tippy.js"; import copy_to_clipboard_svg from "../../templates/copy_to_clipboard_svg.hbs"; import * as common from "../common"; +import * as util from "../util"; import {activate_correct_tab} from "./tabbed-instructions"; @@ -37,7 +38,7 @@ function add_copy_to_clipboard_element($codehilite: JQuery): void { $($codehilite).append($copy_button); - const clipboard = new ClipboardJS($copy_button[0]!, { + const clipboard = new ClipboardJS(util.the($copy_button), { text(copy_element) { // trim to remove trailing whitespace introduced // by additional elements inside
@@ -46,14 +47,14 @@ function add_copy_to_clipboard_element($codehilite: JQuery): void {
     });
 
     // Show a tippy tooltip when the button is hovered
-    const tooltip_copy = tippy.default($copy_button[0]!, {
+    const tooltip_copy = tippy.default(util.the($copy_button), {
         content: "Copy code",
         trigger: "mouseenter",
         placement: "top",
     });
 
     // Show a tippy tooltip when the code is copied
-    const tooltip_copied = tippy.default($copy_button[0]!, {
+    const tooltip_copied = tippy.default(util.the($copy_button), {
         content: "Copied!",
         trigger: "manual",
         placement: "top",
@@ -89,7 +90,7 @@ function render_tabbed_sections(): void {
     });
 }
 
-new SimpleBar($(".sidebar")[0]!, {tabIndex: -1});
+new SimpleBar(util.the($(".sidebar")), {tabIndex: -1});
 
 // Scroll to anchor link when clicked. Note that landing-page.js has a
 // similar function; this file and landing-page.js are never included
diff --git a/web/src/portico/integrations_dev_panel.ts b/web/src/portico/integrations_dev_panel.ts
index e825ee03b0..b47423e454 100644
--- a/web/src/portico/integrations_dev_panel.ts
+++ b/web/src/portico/integrations_dev_panel.ts
@@ -3,6 +3,7 @@ import assert from "minimalistic-assert";
 import {z} from "zod";
 
 import * as channel from "../channel";
+import * as util from "../util";
 // Main JavaScript file for the integrations development panel at
 // /devtools/integrations.
 
@@ -73,13 +74,13 @@ const clear_handlers: ClearHandlers = {
         $("#fixture_name").empty();
     },
     fixture_body() {
-        $("textarea#fixture_body")[0]!.value = "";
+        util.the($("textarea#fixture_body")).value = "";
     },
     custom_http_headers() {
-        $("textarea#custom_http_headers")[0]!.value = "{}";
+        util.the($("textarea#custom_http_headers")).value = "{}";
     },
     results() {
-        $("textarea#idp-results")[0]!.value = "";
+        util.the($("textarea#idp-results")).value = "";
     },
 };
 
@@ -153,7 +154,7 @@ function set_results(response: ServerResponse): void {
         }
         data += "\nResponse:       " + response.message + "\n\n";
     }
-    $("textarea#idp-results")[0]!.value = data;
+    util.the($("textarea#idp-results")).value = data;
 }
 
 function load_fixture_body(fixture_name: string): void {
@@ -173,8 +174,8 @@ function load_fixture_body(fixture_name: string): void {
         fixture_body = JSON.stringify(fixture_body, null, 4);
     }
     assert(typeof fixture_body === "string");
-    $("textarea#fixture_body")[0]!.value = fixture_body;
-    $("textarea#custom_http_headers")[0]!.value = JSON.stringify(
+    util.the($("textarea#fixture_body")).value = fixture_body;
+    util.the($("textarea#custom_http_headers")).value = JSON.stringify(
         headers,
         null,
         4,
@@ -187,9 +188,9 @@ function load_fixture_options(integration_name: string): void {
     /* Using the integration name and loaded_fixtures object to set
     the fixture options for the fixture_names dropdown and also set
     the fixture body to the first fixture by default. */
-    const fixtures_options_dropdown = $(
-        "select:not([multiple])#fixture_name",
-    )[0]!;
+    const fixtures_options_dropdown = util.the(
+        $("select:not([multiple])#fixture_name"),
+    );
     const fixtures = loaded_fixtures.get(integration_name);
     assert(fixtures !== undefined);
     const fixtures_names = Object.keys(fixtures).sort();
@@ -396,10 +397,12 @@ $(() => {
         "results",
     ]);
 
-    $("input#stream_name")[0]!.value = "Denmark";
-    $("input#topic_name")[0]!.value = "Integrations testing";
+    util.the($("input#stream_name")).value = "Denmark";
+    util.the($("input#topic_name")).value = "Integrations testing";
 
-    const potential_default_bot = $("select:not([multiple])#bot_name")[0]![1];
+    const potential_default_bot = util.the(
+        $("select:not([multiple])#bot_name"),
+    )[1];
     assert(potential_default_bot instanceof HTMLOptionElement);
     if (potential_default_bot !== undefined) {
         potential_default_bot.selected = true;
diff --git a/web/src/portico/landing-page.ts b/web/src/portico/landing-page.ts
index cf34c0340b..e991d2deb6 100644
--- a/web/src/portico/landing-page.ts
+++ b/web/src/portico/landing-page.ts
@@ -3,6 +3,7 @@ import assert from "minimalistic-assert";
 import {z} from "zod";
 
 import {page_params} from "../base_page_params";
+import * as util from "../util";
 
 import type {UserOS} from "./tabbed-instructions";
 import {detect_user_os} from "./tabbed-instructions";
@@ -271,7 +272,7 @@ $(document).on("click", ".comparison-tab", function (this: HTMLElement) {
 
     const tab_label = z
         .enum(["tab-cloud", "tab-hosted", "tab-all"])
-        .parse($(this)[0]!.dataset.label);
+        .parse(util.the($(this)).dataset.label);
     const plans_columns_count = plans_columns_counts[tab_label];
     const visible_plans_id = `showing-${tab_label}`;
 
diff --git a/web/src/portico/tabbed-instructions.ts b/web/src/portico/tabbed-instructions.ts
index 81ea0d6990..05d03bf2a8 100644
--- a/web/src/portico/tabbed-instructions.ts
+++ b/web/src/portico/tabbed-instructions.ts
@@ -2,6 +2,7 @@ import $ from "jquery";
 
 import * as blueslip from "../blueslip";
 import * as common from "../common";
+import * as util from "../util";
 
 export type UserOS = "android" | "ios" | "mac" | "windows" | "linux";
 
@@ -58,7 +59,7 @@ export function activate_correct_tab($tabbed_section: JQuery): void {
     const $active_list_items = $li.filter(".active");
     if (!$active_list_items.length) {
         $li.first().addClass("active");
-        const tab_key = $li.first()[0]!.dataset.tabKey;
+        const tab_key = util.the($li.first()).dataset.tabKey;
         if (tab_key) {
             $blocks.filter("[data-tab-key=" + tab_key + "]").addClass("active");
         } else {
diff --git a/web/src/read_receipts.ts b/web/src/read_receipts.ts
index 9cdda320e7..601a27b778 100644
--- a/web/src/read_receipts.ts
+++ b/web/src/read_receipts.ts
@@ -13,6 +13,7 @@ import * as message_store from "./message_store";
 import * as modals from "./modals";
 import * as people from "./people";
 import * as ui_report from "./ui_report";
+import * as util from "./util";
 
 const read_receipts_api_response_schema = z.object({
     user_ids: z.array(z.number()),
@@ -89,7 +90,7 @@ export function show_user_list(message_id: number): void {
                             $("#read_receipts_modal .read_receipts_list").html(
                                 render_read_receipts(context),
                             );
-                            new SimpleBar($("#read_receipts_modal .modal__content")[0]!, {
+                            new SimpleBar(util.the($("#read_receipts_modal .modal__content")), {
                                 tabIndex: -1,
                             });
                         }
diff --git a/web/src/recent_view_ui.ts b/web/src/recent_view_ui.ts
index 9a277a1984..dac6e789e6 100644
--- a/web/src/recent_view_ui.ts
+++ b/web/src/recent_view_ui.ts
@@ -44,6 +44,7 @@ import * as unread from "./unread";
 import {user_settings} from "./user_settings";
 import * as user_status from "./user_status";
 import * as user_topics from "./user_topics";
+import * as util from "./util";
 import * as views_util from "./views_util";
 
 type Row = {
@@ -327,7 +328,7 @@ function set_table_focus(row: number, col: number, using_keyboard = false): bool
     $current_focus_elem = "table";
 
     if (using_keyboard) {
-        const scroll_element = $("html")[0]!;
+        const scroll_element = util.the($("html"));
         const half_height_of_visible_area = scroll_element.offsetHeight / 2;
         const topic_offset = topic_offset_to_visible_area($topic_row);
 
@@ -1148,10 +1149,10 @@ function topic_offset_to_visible_area($topic_row: JQuery): string | undefined {
     }
 
     // Rows are only visible below thead bottom and above compose top.
-    const thead_bottom = $("#recent-view-table-headers")[0]!.getBoundingClientRect().bottom;
+    const thead_bottom = util.the($("#recent-view-table-headers")).getBoundingClientRect().bottom;
     const compose_top = window.innerHeight - $("#compose").outerHeight(true)!;
 
-    const topic_props = $topic_row[0]!.getBoundingClientRect();
+    const topic_props = util.the($topic_row).getBoundingClientRect();
 
     // Topic is above the visible scroll region.
     if (topic_props.top < thead_bottom) {
@@ -1185,7 +1186,7 @@ function recenter_focus_if_off_screen(): void {
 
     if (topic_offset !== "visible") {
         // Get the element at the center of the table.
-        const thead_props = $("#recent-view-table-headers")[0]!.getBoundingClientRect();
+        const thead_props = util.the($("#recent-view-table-headers")).getBoundingClientRect();
         const compose_top = window.innerHeight - $("#compose").outerHeight(true)!;
         const topic_center_x = (thead_props.left + thead_props.right) / 2;
         const topic_center_y = (thead_props.bottom + compose_top) / 2;
@@ -1477,7 +1478,7 @@ function down_arrow_navigation(): void {
 }
 
 function get_page_up_down_delta(): number {
-    const thead_bottom = $("#recent-view-table-headers")[0]!.getBoundingClientRect().bottom;
+    const thead_bottom = util.the($("#recent-view-table-headers")).getBoundingClientRect().bottom;
     const compose_box_top = window.innerHeight - $("#compose").outerHeight(true)!;
     // One usually wants PageDown to move what had been the bottom row
     // to now be at the top, so one can be confident one will see
diff --git a/web/src/rendered_markdown.ts b/web/src/rendered_markdown.ts
index e45d73ab8c..4804c222c7 100644
--- a/web/src/rendered_markdown.ts
+++ b/web/src/rendered_markdown.ts
@@ -312,7 +312,7 @@ export const update_elements = ($content: JQuery): void => {
             $view_in_playground_button.attr("aria-label", title);
         }
         const $copy_button = $buttonContainer.find(".copy_codeblock");
-        const clipboard = new ClipboardJS($copy_button[0]!, {
+        const clipboard = new ClipboardJS(util.the($copy_button), {
             text(copy_element) {
                 const $code = $(copy_element).parent().siblings("code");
                 return $code.text();
@@ -320,7 +320,7 @@ export const update_elements = ($content: JQuery): void => {
         });
 
         clipboard.on("success", () => {
-            show_copied_confirmation($copy_button[0]!);
+            show_copied_confirmation(util.the($copy_button));
         });
         $codehilite.addClass("zulip-code-block");
     });
diff --git a/web/src/scroll_util.ts b/web/src/scroll_util.ts
index 8afd3c5ac8..a2446d6d4d 100644
--- a/web/src/scroll_util.ts
+++ b/web/src/scroll_util.ts
@@ -1,11 +1,13 @@
 import $ from "jquery";
 import SimpleBar from "simplebar";
 
+import * as util from "./util";
+
 // This type is helpful for testing, where we may have a dummy object instead of an actual jquery object.
 type JQueryOrZJQuery = {__zjquery?: true} & JQuery;
 
 export function get_content_element($element: JQuery): JQuery {
-    const element = $element.expectOne()[0]!;
+    const element = util.the($element);
     const sb = SimpleBar.instances.get(element);
     if (sb) {
         return $(sb.getContentElement()!);
@@ -19,7 +21,7 @@ export function get_scroll_element($element: JQueryOrZJQuery): JQuery {
         return $element;
     }
 
-    const element = $element.expectOne()[0]!;
+    const element = util.the($element);
     const sb = SimpleBar.instances.get(element);
     if (sb) {
         return $(sb.getScrollElement()!);
@@ -32,7 +34,7 @@ export function get_scroll_element($element: JQueryOrZJQuery): JQuery {
 }
 
 export function reset_scrollbar($element: JQuery): void {
-    const element = $element.expectOne()[0]!;
+    const element = util.the($element);
     const sb = SimpleBar.instances.get(element);
     if (sb) {
         sb.getScrollElement()!.scrollTop = 0;
diff --git a/web/src/search_pill.ts b/web/src/search_pill.ts
index 44803e1f90..9bd3184272 100644
--- a/web/src/search_pill.ts
+++ b/web/src/search_pill.ts
@@ -12,6 +12,7 @@ import type {User} from "./people";
 import type {NarrowTerm} from "./state_data";
 import * as user_status from "./user_status";
 import type {UserStatusEmojiInfo} from "./user_status";
+import * as util from "./util";
 
 export type SearchUserPill = {
     type: "search_user";
@@ -40,9 +41,7 @@ type SearchPill =
 export type SearchPillWidget = InputPillContainer;
 
 export function create_item_from_search_string(search_string: string): SearchPill | undefined {
-    const search_terms = Filter.parse(search_string);
-    assert(search_terms.length === 1);
-    const search_term = search_terms[0]!;
+    const search_term = util.the(Filter.parse(search_string));
     if (!Filter.is_valid_search_term(search_term)) {
         // This will cause pill validation to fail and trigger a shake animation.
         return undefined;
@@ -73,7 +72,7 @@ function on_pill_exit(
     if (!$user_pill_container.length) {
         // This is just a regular search pill, so we don't need to do fancy logic.
         const $clicked_pill = $(clicked_element).closest(".pill");
-        remove_pill($clicked_pill[0]!);
+        remove_pill(util.the($clicked_pill));
         return;
     }
     // The user-pill-container container class is used exclusively for
@@ -92,8 +91,8 @@ function on_pill_exit(
 
     // If there's only one user in this pill, delete the whole pill.
     if (user_container_pill.users.length === 1) {
-        assert(user_container_pill.users[0]!.user_id === user_id);
-        remove_pill($user_pill_container[0]!);
+        assert(util.the(user_container_pill.users).user_id === user_id);
+        remove_pill(util.the($user_pill_container));
         return;
     }
 
diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts
index 94104eb5ef..ee8e137776 100644
--- a/web/src/settings_components.ts
+++ b/web/src/settings_components.ts
@@ -390,7 +390,7 @@ function read_select_field_data_from_form(
         }
     }
     $profile_field_form.find("div.choice-row").each(function (this: HTMLElement) {
-        const text = $(this).find("input")[0]!.value;
+        const text = util.the($(this).find("input")).value;
         if (text) {
             let value = old_option_value_map.get(text);
             if (value !== undefined) {
@@ -729,7 +729,7 @@ export function get_auth_method_list_data(): Record {
     for (const method_row of $auth_method_rows) {
         const method = $(method_row).attr("data-method");
         assert(method !== undefined);
-        new_auth_methods[method] = $(method_row).find("input")[0]!.checked;
+        new_auth_methods[method] = util.the($(method_row).find("input")).checked;
     }
 
     return new_auth_methods;
@@ -1305,7 +1305,7 @@ function enable_or_disable_save_button($subsection_elem: JQuery): void {
         const $button_wrapper = $subsection_elem.find(
             ".subsection-changes-save",
         );
-        const tippy_instance = $button_wrapper[0]!._tippy;
+        const tippy_instance = util.the($button_wrapper)._tippy;
         if (disable_save_btn) {
             // avoid duplication of tippy
             if (!tippy_instance) {
@@ -1343,5 +1343,5 @@ export function initialize_disable_btn_hint_popover(
     if (hint_text !== undefined) {
         tippy_opts.content = hint_text;
     }
-    tippy.default($btn_wrapper[0]!, tippy_opts);
+    tippy.default(util.the($btn_wrapper), tippy_opts);
 }
diff --git a/web/src/settings_emoji.ts b/web/src/settings_emoji.ts
index af9ad2bfc8..2327eb35c9 100644
--- a/web/src/settings_emoji.ts
+++ b/web/src/settings_emoji.ts
@@ -265,7 +265,7 @@ function show_modal(): void {
         }
 
         const formData = new FormData();
-        const files = $("input#emoji_file_input")[0]!.files;
+        const files = util.the($("input#emoji_file_input")).files;
         assert(files !== null);
         for (const [i, file] of [...files].entries()) {
             formData.append("file-" + i, file);
diff --git a/web/src/settings_linkifiers.ts b/web/src/settings_linkifiers.ts
index 30480f4642..e4b9e9d18a 100644
--- a/web/src/settings_linkifiers.ts
+++ b/web/src/settings_linkifiers.ts
@@ -17,6 +17,7 @@ import * as settings_ui from "./settings_ui";
 import {current_user, realm} from "./state_data";
 import * as ui_report from "./ui_report";
 import * as ui_util from "./ui_util";
+import * as util from "./util";
 
 type RealmLinkifiers = typeof realm.realm_linkifiers;
 
@@ -108,7 +109,7 @@ function open_linkifier_edit_form(linkifier_id: number): void {
             submit_linkifier_form(dialog_widget_id);
         },
         on_shown() {
-            ui_util.place_caret_at_end($("#edit-linkifier-pattern")[0]!);
+            ui_util.place_caret_at_end(util.the($("#edit-linkifier-pattern")));
         },
     });
 }
@@ -197,7 +198,7 @@ export function populate_linkifiers(linkifiers_data: RealmLinkifiers): void {
     });
 
     if (current_user.is_admin) {
-        new SortableJS($linkifiers_table[0]!, {
+        new SortableJS(util.the($linkifiers_table), {
             onUpdate: update_linkifiers_order,
             handle: ".move-handle",
             filter: "input",
diff --git a/web/src/settings_profile_fields.ts b/web/src/settings_profile_fields.ts
index b1d54a0607..17323963f8 100644
--- a/web/src/settings_profile_fields.ts
+++ b/web/src/settings_profile_fields.ts
@@ -23,6 +23,7 @@ import {current_user, realm} from "./state_data";
 import type {HTMLSelectOneElement} from "./types";
 import * as ui_report from "./ui_report";
 import {place_caret_at_end} from "./ui_util";
+import * as util from "./util";
 
 type FieldChoice = {
     value: string;
@@ -424,7 +425,7 @@ function set_up_select_field_edit_form(
 
     // Add blank choice at last
     create_choice_row($choice_list);
-    SortableJS.create($choice_list[0]!, {
+    SortableJS.create(util.the($choice_list), {
         onUpdate() {
             // Do nothing on drag. We process the order on submission
         },
@@ -577,7 +578,7 @@ function open_edit_form_modal(this: HTMLElement): void {
         post_render: set_initial_values_of_profile_field,
         loading_spinner: true,
         on_shown() {
-            place_caret_at_end($("#id-custom-profile-field-name")[0]!);
+            place_caret_at_end(util.the($("#id-custom-profile-field-name")));
         },
     });
 }
@@ -709,7 +710,7 @@ export function do_populate_profile_fields(profile_fields_data: CustomProfileFie
     display_in_profile_summary_fields_limit_reached = display_in_profile_summary_fields_count >= 2;
 
     if (current_user.is_admin) {
-        const field_list = $("#admin_profile_fields_table")[0]!;
+        const field_list = util.the($("#admin_profile_fields_table"));
         SortableJS.create(field_list, {
             onUpdate: update_field_order,
             filter: "input",
@@ -727,7 +728,7 @@ function set_up_select_field(): void {
     create_choice_row($("#profile_field_choices"));
 
     if (current_user.is_admin) {
-        const choice_list = $("#profile_field_choices")[0]!;
+        const choice_list = util.the($("#profile_field_choices"));
         SortableJS.create(choice_list, {
             onUpdate() {
                 // Do nothing on drag. We process the order on submission
diff --git a/web/src/settings_realm_domains.ts b/web/src/settings_realm_domains.ts
index adb8e3b989..76abca5a09 100644
--- a/web/src/settings_realm_domains.ts
+++ b/web/src/settings_realm_domains.ts
@@ -8,6 +8,7 @@ import * as dialog_widget from "./dialog_widget";
 import {$t_html} from "./i18n";
 import {realm} from "./state_data";
 import * as ui_report from "./ui_report";
+import * as util from "./util";
 
 type RealmDomain = {
     domain: string;
@@ -112,9 +113,9 @@ export function setup_realm_domains_modal_handlers(): void {
         const $realm_domains_info = $(".realm_domains_info");
         const $widget = $("#add-realm-domain-widget");
         const domain = $widget.find(".new-realm-domain").val();
-        const allow_subdomains = $widget.find(
-            "input.new-realm-domain-allow-subdomains",
-        )[0]!.checked;
+        const allow_subdomains = util.the(
+            $widget.find("input.new-realm-domain-allow-subdomains"),
+        ).checked;
         const data = {
             domain,
             allow_subdomains: JSON.stringify(allow_subdomains),
diff --git a/web/src/setup.ts b/web/src/setup.ts
index 6e9e69881d..9828504c09 100644
--- a/web/src/setup.ts
+++ b/web/src/setup.ts
@@ -2,6 +2,7 @@ import $ from "jquery";
 
 import * as blueslip from "./blueslip";
 import * as loading from "./loading";
+import * as util from "./util";
 
 export let page_load_time: number | undefined;
 
@@ -16,7 +17,7 @@ $(() => {
     });
 
     $.fn.get_offset_to_window = function () {
-        return this[0]!.getBoundingClientRect();
+        return util.the(this).getBoundingClientRect();
     };
 
     $.fn.expectOne = function () {
diff --git a/web/src/spoilers.ts b/web/src/spoilers.ts
index 743f52ab54..ff7c09b7e5 100644
--- a/web/src/spoilers.ts
+++ b/web/src/spoilers.ts
@@ -1,5 +1,7 @@
 import $ from "jquery";
 
+import * as util from "./util";
+
 function collapse_spoiler($spoiler: JQuery): void {
     const spoiler_height = $spoiler.height() ?? 0;
 
@@ -21,7 +23,7 @@ function expand_spoiler($spoiler: JQuery): void {
     // of the content). CSS animations do not work with properties set to
     // `auto`, so we get the actual height of the content here and temporarily
     // put it explicitly on the element styling to allow the transition to work.
-    const spoiler_height = $spoiler[0]!.scrollHeight;
+    const spoiler_height = util.the($spoiler).scrollHeight;
     $spoiler.height(`${spoiler_height}px`);
     // The `spoiler-content-open` class has CSS animations defined on it which
     // will trigger on the frame after this class change.
diff --git a/web/src/tippyjs.ts b/web/src/tippyjs.ts
index 237038b5cc..cdff52f8ef 100644
--- a/web/src/tippyjs.ts
+++ b/web/src/tippyjs.ts
@@ -14,6 +14,7 @@ import * as settings_config from "./settings_config";
 import * as stream_data from "./stream_data";
 import * as ui_util from "./ui_util";
 import {user_settings} from "./user_settings";
+import * as util from "./util";
 
 // For tooltips without data-tippy-content, we use the HTML content of
 // a