mirror of
https://github.com/zulip/zulip.git
synced 2025-11-09 00:18:12 +00:00
compose: Use text-area-edit for syntax replacements.
Fixes #23284 The basic approach used by `text-area-edit` is same as we were using, so there is no real change. There are some nice checks in `text-area-edit` which we don't do that helps us avoid common bugs.
This commit is contained in:
@@ -11,11 +11,6 @@ const $ = require("../zjsunit/zjquery");
|
|||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
set_global("document", {
|
|
||||||
execCommand() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
set_global("navigator", {});
|
set_global("navigator", {});
|
||||||
|
|
||||||
mock_esm("../../static/js/message_lists", {
|
mock_esm("../../static/js/message_lists", {
|
||||||
@@ -29,6 +24,7 @@ const hash_util = mock_esm("../../static/js/hash_util");
|
|||||||
const channel = mock_esm("../../static/js/channel");
|
const channel = mock_esm("../../static/js/channel");
|
||||||
const compose_actions = zrequire("compose_actions");
|
const compose_actions = zrequire("compose_actions");
|
||||||
const message_lists = zrequire("message_lists");
|
const message_lists = zrequire("message_lists");
|
||||||
|
const text_field_edit = mock_esm("text-field-edit");
|
||||||
|
|
||||||
const alice = {
|
const alice = {
|
||||||
email: "alice@zulip.com",
|
email: "alice@zulip.com",
|
||||||
@@ -49,6 +45,7 @@ function make_textbox(s) {
|
|||||||
const $widget = {};
|
const $widget = {};
|
||||||
|
|
||||||
$widget.s = s;
|
$widget.s = s;
|
||||||
|
$widget[0] = "textarea";
|
||||||
$widget.focused = false;
|
$widget.focused = false;
|
||||||
|
|
||||||
$widget.caret = function (arg) {
|
$widget.caret = function (arg) {
|
||||||
@@ -57,15 +54,16 @@ function make_textbox(s) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg) {
|
// Not used right now, but could be in future.
|
||||||
$widget.insert_pos = $widget.pos;
|
// if (arg) {
|
||||||
$widget.insert_text = arg;
|
// $widget.insert_pos = $widget.pos;
|
||||||
const before = $widget.s.slice(0, $widget.pos);
|
// $widget.insert_text = arg;
|
||||||
const after = $widget.s.slice($widget.pos);
|
// const before = $widget.s.slice(0, $widget.pos);
|
||||||
$widget.s = before + arg + after;
|
// const after = $widget.s.slice($widget.pos);
|
||||||
$widget.pos += arg.length;
|
// $widget.s = before + arg + after;
|
||||||
return this;
|
// $widget.pos += arg.length;
|
||||||
}
|
// return this;
|
||||||
|
// }
|
||||||
|
|
||||||
return $widget.pos;
|
return $widget.pos;
|
||||||
};
|
};
|
||||||
@@ -79,15 +77,6 @@ function make_textbox(s) {
|
|||||||
return $widget.s;
|
return $widget.s;
|
||||||
};
|
};
|
||||||
|
|
||||||
$widget.trigger = function (type) {
|
|
||||||
if (type === "focus") {
|
|
||||||
$widget.focused = true;
|
|
||||||
} else if (type === "blur") {
|
|
||||||
$widget.focused = false;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
return $widget;
|
return $widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,93 +95,80 @@ run_test("autosize_textarea", ({override}) => {
|
|||||||
assert.ok(textarea_autosized.autosized);
|
assert.ok(textarea_autosized.autosized);
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("insert_syntax_and_focus", () => {
|
run_test("insert_syntax_and_focus", ({override}) => {
|
||||||
$("#compose-textarea").val("xyz ");
|
$("#compose-textarea").val("xyz ");
|
||||||
$("#compose-textarea").caret = function (syntax) {
|
$("#compose-textarea").caret = () => 4;
|
||||||
if (syntax !== undefined) {
|
$("#compose-textarea")[0] = "compose-textarea";
|
||||||
$("#compose-textarea").val($("#compose-textarea").val() + syntax);
|
// Since we are using a third party library, we just
|
||||||
return this;
|
// need to ensure it is being called with the right params.
|
||||||
}
|
override(text_field_edit, "insert", (elt, syntax) => {
|
||||||
return 4;
|
assert.equal(elt, "compose-textarea");
|
||||||
};
|
assert.equal(syntax, ":octopus: ");
|
||||||
|
});
|
||||||
compose_ui.insert_syntax_and_focus(":octopus:");
|
compose_ui.insert_syntax_and_focus(":octopus:");
|
||||||
assert.equal($("#compose-textarea").caret(), 4);
|
|
||||||
assert.equal($("#compose-textarea").val(), "xyz :octopus: ");
|
|
||||||
assert.ok($("#compose-textarea").is_focused());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("smart_insert", () => {
|
run_test("smart_insert", ({override}) => {
|
||||||
let $textbox = make_textbox("abc");
|
let $textbox = make_textbox("abc");
|
||||||
$textbox.caret(4);
|
$textbox.caret(4);
|
||||||
|
function override_with_expected_syntax(expected_syntax) {
|
||||||
|
override(text_field_edit, "insert", (elt, syntax) => {
|
||||||
|
assert.equal(elt, "textarea");
|
||||||
|
assert.equal(syntax, expected_syntax);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
override_with_expected_syntax(" :smile: ");
|
||||||
compose_ui.smart_insert($textbox, ":smile:");
|
compose_ui.smart_insert($textbox, ":smile:");
|
||||||
assert.equal($textbox.insert_pos, 4);
|
|
||||||
assert.equal($textbox.insert_text, " :smile: ");
|
|
||||||
assert.equal($textbox.val(), "abc :smile: ");
|
|
||||||
assert.ok($textbox.focused);
|
|
||||||
|
|
||||||
$textbox.trigger("blur");
|
override_with_expected_syntax(" :airplane: ");
|
||||||
compose_ui.smart_insert($textbox, ":airplane:");
|
compose_ui.smart_insert($textbox, ":airplane:");
|
||||||
assert.equal($textbox.insert_text, ":airplane: ");
|
|
||||||
assert.equal($textbox.val(), "abc :smile: :airplane: ");
|
|
||||||
assert.ok($textbox.focused);
|
|
||||||
|
|
||||||
$textbox.caret(0);
|
$textbox.caret(0);
|
||||||
$textbox.trigger("blur");
|
override_with_expected_syntax(":octopus: ");
|
||||||
compose_ui.smart_insert($textbox, ":octopus:");
|
compose_ui.smart_insert($textbox, ":octopus:");
|
||||||
assert.equal($textbox.insert_text, ":octopus: ");
|
|
||||||
assert.equal($textbox.val(), ":octopus: abc :smile: :airplane: ");
|
|
||||||
assert.ok($textbox.focused);
|
|
||||||
|
|
||||||
$textbox.caret($textbox.val().length);
|
$textbox.caret($textbox.val().length);
|
||||||
$textbox.trigger("blur");
|
override_with_expected_syntax(" :heart: ");
|
||||||
compose_ui.smart_insert($textbox, ":heart:");
|
compose_ui.smart_insert($textbox, ":heart:");
|
||||||
assert.equal($textbox.insert_text, ":heart: ");
|
|
||||||
assert.equal($textbox.val(), ":octopus: abc :smile: :airplane: :heart: ");
|
|
||||||
assert.ok($textbox.focused);
|
|
||||||
|
|
||||||
// Test handling of spaces for ```quote
|
// Test handling of spaces for ```quote
|
||||||
$textbox = make_textbox("");
|
$textbox = make_textbox("");
|
||||||
$textbox.caret(0);
|
$textbox.caret(0);
|
||||||
$textbox.trigger("blur");
|
override_with_expected_syntax("```quote\nquoted message\n```\n");
|
||||||
compose_ui.smart_insert($textbox, "```quote\nquoted message\n```\n");
|
compose_ui.smart_insert($textbox, "```quote\nquoted message\n```\n");
|
||||||
assert.equal($textbox.insert_text, "```quote\nquoted message\n```\n");
|
|
||||||
assert.equal($textbox.val(), "```quote\nquoted message\n```\n");
|
|
||||||
assert.ok($textbox.focused);
|
|
||||||
|
|
||||||
$textbox = make_textbox("");
|
$textbox = make_textbox("");
|
||||||
$textbox.caret(0);
|
$textbox.caret(0);
|
||||||
$textbox.trigger("blur");
|
override_with_expected_syntax("translated: [Quoting…]\n");
|
||||||
compose_ui.smart_insert($textbox, "translated: [Quoting…]\n");
|
compose_ui.smart_insert($textbox, "translated: [Quoting…]\n");
|
||||||
assert.equal($textbox.insert_text, "translated: [Quoting…]\n");
|
|
||||||
assert.equal($textbox.val(), "translated: [Quoting…]\n");
|
|
||||||
assert.ok($textbox.focused);
|
|
||||||
|
|
||||||
$textbox = make_textbox("abc");
|
$textbox = make_textbox("abc");
|
||||||
$textbox.caret(3);
|
$textbox.caret(3);
|
||||||
$textbox.trigger("blur");
|
override_with_expected_syntax(" test with space ");
|
||||||
compose_ui.smart_insert($textbox, " test with space");
|
compose_ui.smart_insert($textbox, " test with space");
|
||||||
assert.equal($textbox.insert_text, " test with space ");
|
|
||||||
assert.equal($textbox.val(), "abc test with space ");
|
|
||||||
assert.ok($textbox.focused);
|
|
||||||
|
|
||||||
// Note that we don't have any special logic for strings that are
|
// Note that we don't have any special logic for strings that are
|
||||||
// already surrounded by spaces, since we are usually inserting things
|
// already surrounded by spaces, since we are usually inserting things
|
||||||
// like emojis and file links.
|
// like emojis and file links.
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("replace_syntax", () => {
|
run_test("replace_syntax", ({override}) => {
|
||||||
$("#compose-textarea").val("abcabc");
|
$("#compose-textarea").val("abcabc");
|
||||||
|
$("#compose-textarea")[0] = "compose-textarea";
|
||||||
|
override(text_field_edit, "replace", (elt, old_syntax, new_syntax) => {
|
||||||
|
assert.equal(elt, "compose-textarea");
|
||||||
|
assert.equal(old_syntax, "a");
|
||||||
|
assert.equal(new_syntax(), "A");
|
||||||
|
});
|
||||||
compose_ui.replace_syntax("a", "A");
|
compose_ui.replace_syntax("a", "A");
|
||||||
assert.equal($("#compose-textarea").val(), "Abcabc");
|
|
||||||
|
|
||||||
compose_ui.replace_syntax(/b/g, "B");
|
|
||||||
assert.equal($("#compose-textarea").val(), "ABcaBc");
|
|
||||||
|
|
||||||
|
override(text_field_edit, "replace", (elt, old_syntax, new_syntax) => {
|
||||||
|
assert.equal(elt, "compose-textarea");
|
||||||
|
assert.equal(old_syntax, "Bca");
|
||||||
|
assert.equal(new_syntax(), "$$\\pi$$");
|
||||||
|
});
|
||||||
// Verify we correctly handle `$`s in the replacement syntax
|
// Verify we correctly handle `$`s in the replacement syntax
|
||||||
compose_ui.replace_syntax("Bca", "$$\\pi$$");
|
compose_ui.replace_syntax("Bca", "$$\\pi$$");
|
||||||
assert.equal($("#compose-textarea").val(), "A$$\\pi$$Bc");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("compute_placeholder_text", () => {
|
run_test("compute_placeholder_text", () => {
|
||||||
@@ -283,15 +259,8 @@ run_test("quote_and_reply", ({override, override_rewire}) => {
|
|||||||
let textarea_val = "";
|
let textarea_val = "";
|
||||||
let textarea_caret_pos;
|
let textarea_caret_pos;
|
||||||
|
|
||||||
$("#compose-textarea").val = function (...args) {
|
$("#compose-textarea").val = function () {
|
||||||
if (args.length === 0) {
|
|
||||||
return textarea_val;
|
return textarea_val;
|
||||||
}
|
|
||||||
|
|
||||||
textarea_val = args[0];
|
|
||||||
textarea_caret_pos = textarea_val.length;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$("#compose-textarea").caret = function (arg) {
|
$("#compose-textarea").caret = function (arg) {
|
||||||
@@ -315,6 +284,11 @@ run_test("quote_and_reply", ({override, override_rewire}) => {
|
|||||||
textarea_caret_pos += arg.length;
|
textarea_caret_pos += arg.length;
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
$("#compose-textarea")[0] = "compose-textarea";
|
||||||
|
override(text_field_edit, "insert", (elt, syntax) => {
|
||||||
|
assert.equal(elt, "compose-textarea");
|
||||||
|
assert.equal(syntax, "translated: [Quoting…]\n");
|
||||||
|
});
|
||||||
|
|
||||||
function set_compose_content_with_caret(content) {
|
function set_compose_content_with_caret(content) {
|
||||||
const caret_position = content.indexOf("%");
|
const caret_position = content.indexOf("%");
|
||||||
@@ -324,14 +298,6 @@ run_test("quote_and_reply", ({override, override_rewire}) => {
|
|||||||
$("#compose-textarea").trigger("focus");
|
$("#compose-textarea").trigger("focus");
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_compose_content_with_caret() {
|
|
||||||
const content =
|
|
||||||
textarea_val.slice(0, textarea_caret_pos) +
|
|
||||||
"%" +
|
|
||||||
textarea_val.slice(textarea_caret_pos); // insert the "%"
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset_test_state() {
|
function reset_test_state() {
|
||||||
// Reset `raw_content` property of `selected_message`.
|
// Reset `raw_content` property of `selected_message`.
|
||||||
delete selected_message.raw_content;
|
delete selected_message.raw_content;
|
||||||
@@ -342,17 +308,27 @@ run_test("quote_and_reply", ({override, override_rewire}) => {
|
|||||||
$("#compose-textarea").trigger("blur");
|
$("#compose-textarea").trigger("blur");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function override_with_quote_text(quote_text) {
|
||||||
|
override(text_field_edit, "replace", (elt, old_syntax, new_syntax) => {
|
||||||
|
assert.equal(elt, "compose-textarea");
|
||||||
|
assert.equal(old_syntax, "translated: [Quoting…]");
|
||||||
|
assert.equal(
|
||||||
|
new_syntax(),
|
||||||
|
"translated: @_**Steve Stephenson|90** [said](https://chat.zulip.org/#narrow/stream/92-learning/topic/Tornado):\n" +
|
||||||
|
"```quote\n" +
|
||||||
|
`${quote_text}\n` +
|
||||||
|
"```",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let quote_text = "Testing caret position";
|
||||||
|
override_with_quote_text(quote_text);
|
||||||
set_compose_content_with_caret("hello %there"); // "%" is used to encode/display position of focus before change
|
set_compose_content_with_caret("hello %there"); // "%" is used to encode/display position of focus before change
|
||||||
compose_actions.quote_and_reply();
|
compose_actions.quote_and_reply();
|
||||||
assert.equal(get_compose_content_with_caret(), "hello \ntranslated: [Quoting…]\n%there");
|
|
||||||
|
|
||||||
success_function({
|
success_function({
|
||||||
raw_content: "Testing caret position",
|
raw_content: quote_text,
|
||||||
});
|
});
|
||||||
assert.equal(
|
|
||||||
get_compose_content_with_caret(),
|
|
||||||
"hello \ntranslated: @_**Steve Stephenson|90** [said](https://chat.zulip.org/#narrow/stream/92-learning/topic/Tornado):\n```quote\nTesting caret position\n```\n%there",
|
|
||||||
);
|
|
||||||
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
@@ -360,15 +336,12 @@ run_test("quote_and_reply", ({override, override_rewire}) => {
|
|||||||
// add a newline before the quoted message.
|
// add a newline before the quoted message.
|
||||||
set_compose_content_with_caret("%hello there");
|
set_compose_content_with_caret("%hello there");
|
||||||
compose_actions.quote_and_reply();
|
compose_actions.quote_and_reply();
|
||||||
assert.equal(get_compose_content_with_caret(), "translated: [Quoting…]\n%hello there");
|
|
||||||
|
|
||||||
|
quote_text = "Testing with caret initially positioned at 0.";
|
||||||
|
override_with_quote_text(quote_text);
|
||||||
success_function({
|
success_function({
|
||||||
raw_content: "Testing with caret initially positioned at 0.",
|
raw_content: quote_text,
|
||||||
});
|
});
|
||||||
assert.equal(
|
|
||||||
get_compose_content_with_caret(),
|
|
||||||
"translated: @_**Steve Stephenson|90** [said](https://chat.zulip.org/#narrow/stream/92-learning/topic/Tornado):\n```quote\nTesting with caret initially positioned at 0.\n```\n%hello there",
|
|
||||||
);
|
|
||||||
|
|
||||||
override_rewire(compose_actions, "respond_to_message", () => {
|
override_rewire(compose_actions, "respond_to_message", () => {
|
||||||
// Reset compose state to replicate the re-opening of compose-box.
|
// Reset compose state to replicate the re-opening of compose-box.
|
||||||
@@ -383,15 +356,12 @@ run_test("quote_and_reply", ({override, override_rewire}) => {
|
|||||||
// quoting a message, the quoted message should be placed
|
// quoting a message, the quoted message should be placed
|
||||||
// at the beginning of compose-box.
|
// at the beginning of compose-box.
|
||||||
compose_actions.quote_and_reply();
|
compose_actions.quote_and_reply();
|
||||||
assert.equal(get_compose_content_with_caret(), "translated: [Quoting…]\n%");
|
|
||||||
|
|
||||||
|
quote_text = "Testing with compose-box closed initially.";
|
||||||
|
override_with_quote_text(quote_text);
|
||||||
success_function({
|
success_function({
|
||||||
raw_content: "Testing with compose-box closed initially.",
|
raw_content: quote_text,
|
||||||
});
|
});
|
||||||
assert.equal(
|
|
||||||
get_compose_content_with_caret(),
|
|
||||||
"translated: @_**Steve Stephenson|90** [said](https://chat.zulip.org/#narrow/stream/92-learning/topic/Tornado):\n```quote\nTesting with compose-box closed initially.\n```\n%",
|
|
||||||
);
|
|
||||||
|
|
||||||
reset_test_state();
|
reset_test_state();
|
||||||
|
|
||||||
@@ -401,15 +371,12 @@ run_test("quote_and_reply", ({override, override_rewire}) => {
|
|||||||
// message should start from the beginning of compose-box.
|
// message should start from the beginning of compose-box.
|
||||||
set_compose_content_with_caret(" \n\n \n %");
|
set_compose_content_with_caret(" \n\n \n %");
|
||||||
compose_actions.quote_and_reply();
|
compose_actions.quote_and_reply();
|
||||||
assert.equal(get_compose_content_with_caret(), "translated: [Quoting…]\n%");
|
|
||||||
|
|
||||||
|
quote_text = "Testing with compose-box containing whitespaces and newlines only.";
|
||||||
|
override_with_quote_text(quote_text);
|
||||||
success_function({
|
success_function({
|
||||||
raw_content: "Testing with compose-box containing whitespaces and newlines only.",
|
raw_content: quote_text,
|
||||||
});
|
});
|
||||||
assert.equal(
|
|
||||||
get_compose_content_with_caret(),
|
|
||||||
"translated: @_**Steve Stephenson|90** [said](https://chat.zulip.org/#narrow/stream/92-learning/topic/Tornado):\n```quote\nTesting with compose-box containing whitespaces and newlines only.\n```\n%",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("set_compose_box_top", () => {
|
run_test("set_compose_box_top", () => {
|
||||||
@@ -462,19 +429,17 @@ run_test("test_compose_height_changes", ({override, override_rewire}) => {
|
|||||||
assert.ok(!compose_box_top_set);
|
assert.ok(!compose_box_top_set);
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("format_text", () => {
|
run_test("format_text", ({override}) => {
|
||||||
let set_text = "";
|
let set_text = "";
|
||||||
let wrap_selection_called = false;
|
let wrap_selection_called = false;
|
||||||
let wrap_syntax = "";
|
let wrap_syntax = "";
|
||||||
|
|
||||||
mock_esm("text-field-edit", {
|
override(text_field_edit, "set", (field, text) => {
|
||||||
set: (field, text) => {
|
|
||||||
set_text = text;
|
set_text = text;
|
||||||
},
|
});
|
||||||
wrapSelection: (field, syntax) => {
|
override(text_field_edit, "wrapSelection", (field, syntax) => {
|
||||||
wrap_selection_called = true;
|
wrap_selection_called = true;
|
||||||
wrap_syntax = syntax;
|
wrap_syntax = syntax;
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function reset_state() {
|
function reset_state() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import autosize from "autosize";
|
import autosize from "autosize";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import {set, wrapSelection} from "text-field-edit";
|
import {insert, replace, set, wrapSelection} from "text-field-edit";
|
||||||
|
|
||||||
import * as common from "./common";
|
import * as common from "./common";
|
||||||
import {$t} from "./i18n";
|
import {$t} from "./i18n";
|
||||||
@@ -60,14 +60,9 @@ export function smart_insert($textarea, syntax) {
|
|||||||
syntax += " ";
|
syntax += " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
$textarea.trigger("focus");
|
// text-field-edit ensures `$textarea` is focused before inserting
|
||||||
|
// the new syntax.
|
||||||
// We prefer to use insertText, which supports things like undo better
|
insert($textarea[0], syntax);
|
||||||
// for rich-text editing features like inserting links. But we fall
|
|
||||||
// back to textarea.caret if the browser doesn't support insertText.
|
|
||||||
if (!document.execCommand("insertText", false, syntax)) {
|
|
||||||
$textarea.caret(syntax);
|
|
||||||
}
|
|
||||||
|
|
||||||
autosize_textarea($textarea);
|
autosize_textarea($textarea);
|
||||||
}
|
}
|
||||||
@@ -84,17 +79,12 @@ export function replace_syntax(old_syntax, new_syntax, $textarea = $("#compose-t
|
|||||||
// the way that JavaScript handles string replacements, if `old_syntax` is
|
// the way that JavaScript handles string replacements, if `old_syntax` is
|
||||||
// a string it will only replace the first instance. If `old_syntax` is
|
// a string it will only replace the first instance. If `old_syntax` is
|
||||||
// a RegExp with a global flag, it will replace all instances.
|
// a RegExp with a global flag, it will replace all instances.
|
||||||
$textarea.val(
|
|
||||||
$textarea.val().replace(
|
// We need use anonymous function for `new_syntax` to avoid JavaScript's
|
||||||
old_syntax,
|
|
||||||
() =>
|
|
||||||
// We need this anonymous function to avoid JavaScript's
|
|
||||||
// replace() function treating `$`s in new_syntax as special syntax. See
|
// replace() function treating `$`s in new_syntax as special syntax. See
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Description
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Description
|
||||||
// for details.
|
// for details.
|
||||||
new_syntax,
|
replace($textarea[0], old_syntax, () => new_syntax);
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compute_placeholder_text(opts) {
|
export function compute_placeholder_text(opts) {
|
||||||
|
|||||||
Reference in New Issue
Block a user