mirror of
https://github.com/discourse/discourse.git
synced 2024-12-19 05:35:59 +08:00
DEV: refactor composer-editor/d-editor, a little more (#29973)
Adds setupEditor to ComposerEditor so it can setup/destroy events when the underlying editorComponent is switched. Moves putCursorAtEnd uses (which implementation is textarea-specific) to TextareaTextManipulation. Moves insertCurrentTime and a corresponding test, which is discourse-local-dates specific, to the plugin. Moves applyList and formatCode from DEditor to the TextareaTextManipulation. Moves DEditor._applySurround to TextareaTextManipulation.applySurroundSelection Avoids resetting the textarea value on applyList and formatCode, keeping the undo history.
This commit is contained in:
parent
706987ce76
commit
85691a7f31
|
@ -43,6 +43,7 @@
|
|||
@outletArgs={{hash composer=this.composer.model editorType="composer"}}
|
||||
@topicId={{this.composer.model.topic.id}}
|
||||
@categoryId={{this.composer.model.category.id}}
|
||||
@onSetup={{this.setupEditor}}
|
||||
>
|
||||
{{yield}}
|
||||
</DEditor>
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
linkSeenMentions,
|
||||
} from "discourse/lib/link-mentions";
|
||||
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||
import {
|
||||
authorizesOneOrMoreImageExtensions,
|
||||
IMAGE_MARKDOWN_REGEX,
|
||||
|
@ -139,7 +138,7 @@ export default class ComposerEditor extends Component {
|
|||
@observes("composer.focusTarget")
|
||||
setFocus() {
|
||||
if (this.composer.focusTarget === "editor") {
|
||||
putCursorAtEnd(this.element.querySelector("textarea"));
|
||||
this.textManipulation.putCursorAtEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,21 +187,9 @@ export default class ComposerEditor extends Component {
|
|||
|
||||
@on("didInsertElement")
|
||||
_composerEditorInit() {
|
||||
const input = this.element.querySelector(".d-editor-input");
|
||||
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
||||
|
||||
input?.addEventListener(
|
||||
"scroll",
|
||||
this._throttledSyncEditorAndPreviewScroll
|
||||
);
|
||||
|
||||
this._registerImageAltTextButtonClick(preview);
|
||||
|
||||
// Focus on the body unless we have a title
|
||||
if (!this.get("composer.model.canEditTitle")) {
|
||||
putCursorAtEnd(input);
|
||||
}
|
||||
|
||||
if (this.composer.allowUpload) {
|
||||
this.uppyComposerUpload.setup(this.element);
|
||||
}
|
||||
|
@ -210,6 +197,30 @@ export default class ComposerEditor extends Component {
|
|||
this.appEvents.trigger(`${this.composerEventPrefix}:will-open`);
|
||||
}
|
||||
|
||||
@bind
|
||||
setupEditor(textManipulation) {
|
||||
this.textManipulation = textManipulation;
|
||||
|
||||
const input = this.element.querySelector(".d-editor-input");
|
||||
|
||||
input?.addEventListener(
|
||||
"scroll",
|
||||
this._throttledSyncEditorAndPreviewScroll
|
||||
);
|
||||
|
||||
// Focus on the body unless we have a title
|
||||
if (!this.get("composer.model.canEditTitle")) {
|
||||
this.textManipulation.putCursorAtEnd();
|
||||
}
|
||||
|
||||
return () => {
|
||||
input?.removeEventListener(
|
||||
"scroll",
|
||||
this._throttledSyncEditorAndPreviewScroll
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"composer.model.reply",
|
||||
"composer.model.replyLength",
|
||||
|
@ -785,7 +796,6 @@ export default class ComposerEditor extends Component {
|
|||
|
||||
@on("willDestroyElement")
|
||||
_composerClosed() {
|
||||
const input = this.element.querySelector(".d-editor-input");
|
||||
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
||||
|
||||
if (this.composer.allowUpload) {
|
||||
|
@ -802,11 +812,6 @@ export default class ComposerEditor extends Component {
|
|||
);
|
||||
});
|
||||
|
||||
input?.removeEventListener(
|
||||
"scroll",
|
||||
this._throttledSyncEditorAndPreviewScroll
|
||||
);
|
||||
|
||||
preview?.removeEventListener("click", this._handleAltTextCancelButtonClick);
|
||||
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
|
||||
preview?.removeEventListener("click", this._handleAltTextOkButtonClick);
|
||||
|
|
|
@ -21,7 +21,6 @@ import { PLATFORM_KEY_MODIFIER } from "discourse/lib/keyboard-shortcuts";
|
|||
import { linkSeenMentions } from "discourse/lib/link-mentions";
|
||||
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||
import { emojiUrlFor, generateCookFunction } from "discourse/lib/text";
|
||||
import { getHead } from "discourse/lib/textarea-text-manipulation";
|
||||
import userSearch from "discourse/lib/user-search";
|
||||
import {
|
||||
destroyUserStatuses,
|
||||
|
@ -36,8 +35,6 @@ import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
|||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const FOUR_SPACES_INDENT = "4-spaces-indent";
|
||||
|
||||
let _createCallbacks = [];
|
||||
|
||||
export function addToolbarCallback(func) {
|
||||
|
@ -145,8 +142,6 @@ export default class DEditor extends Component {
|
|||
|
||||
keymap["tab"] = () => this.textManipulation.indentSelection("right");
|
||||
keymap["shift+tab"] = () => this.textManipulation.indentSelection("left");
|
||||
keymap[`${PLATFORM_KEY_MODIFIER}+shift+.`] = () =>
|
||||
this.send("insertCurrentTime");
|
||||
|
||||
return keymap;
|
||||
}
|
||||
|
@ -485,34 +480,6 @@ export default class DEditor extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
_applyList(sel, head, exampleKey, opts) {
|
||||
if (sel.value.includes("\n")) {
|
||||
this.textManipulation.applySurround(sel, head, "", exampleKey, opts);
|
||||
} else {
|
||||
const [hval, hlen] = getHead(head);
|
||||
if (sel.start === sel.end) {
|
||||
sel.value = i18n(`composer.${exampleKey}`);
|
||||
}
|
||||
|
||||
const trimmedPre = sel.pre.trim();
|
||||
const number = sel.value.startsWith(hval)
|
||||
? sel.value.slice(hlen)
|
||||
: `${hval}${sel.value}`;
|
||||
const preLines = trimmedPre.length ? `${trimmedPre}\n\n` : "";
|
||||
|
||||
const trimmedPost = sel.post.trim();
|
||||
const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost;
|
||||
|
||||
this.set("value", `${preLines}${number}${post}`);
|
||||
this.textManipulation.selectText(preLines.length, number.length);
|
||||
}
|
||||
}
|
||||
|
||||
_applySurround(head, tail, exampleKey, opts) {
|
||||
const selected = this.textManipulation.getSelected();
|
||||
this.textManipulation.applySurround(selected, head, tail, exampleKey, opts);
|
||||
}
|
||||
|
||||
@action
|
||||
rovingButtonBar(event) {
|
||||
let target = event.target;
|
||||
|
@ -559,6 +526,27 @@ export default class DEditor extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a toolbar event object passed to toolbar buttons.
|
||||
*
|
||||
* @typedef {Object} ToolbarEvent
|
||||
* @property {function} applySurround - Applies surrounding text
|
||||
* @property {function} formatCode - Formats as code
|
||||
* @property {function} replaceText - Replaces text
|
||||
* @property {function} selectText - Selects a range of text
|
||||
* @property {function} toggleDirection - Toggles text direction
|
||||
* @property {function} getText - Gets the text
|
||||
* @property {function} addText - Adds text
|
||||
* @property {function} applyList - Applies a list format
|
||||
* @property {*} selected - The current selection
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new toolbar event object
|
||||
*
|
||||
* @param {boolean} trimLeading - Whether to trim leading whitespace
|
||||
* @returns {ToolbarEvent} An object with toolbar event actions
|
||||
*/
|
||||
newToolbarEvent(trimLeading) {
|
||||
const selected = this.textManipulation.getSelected(trimLeading);
|
||||
return {
|
||||
|
@ -574,8 +562,8 @@ export default class DEditor extends Component {
|
|||
opts
|
||||
),
|
||||
applyList: (head, exampleKey, opts) =>
|
||||
this._applyList(selected, head, exampleKey, opts),
|
||||
formatCode: (...args) => this.send("formatCode", args),
|
||||
this.textManipulation.applyList(selected, head, exampleKey, opts),
|
||||
formatCode: () => this.textManipulation.formatCode(),
|
||||
addText: (text) => this.textManipulation.addText(selected, text),
|
||||
getText: () => this.value,
|
||||
toggleDirection: () => this.textManipulation.toggleDirection(),
|
||||
|
@ -628,71 +616,6 @@ export default class DEditor extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
formatCode() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sel = this.textManipulation.getSelected("", { lineVal: true });
|
||||
const selValue = sel.value;
|
||||
const hasNewLine = selValue.includes("\n");
|
||||
const isBlankLine = sel.lineVal.trim().length === 0;
|
||||
const isFourSpacesIndent =
|
||||
this.siteSettings.code_formatting_style === FOUR_SPACES_INDENT;
|
||||
|
||||
if (!hasNewLine) {
|
||||
if (selValue.length === 0 && isBlankLine) {
|
||||
if (isFourSpacesIndent) {
|
||||
const example = i18n(`composer.code_text`);
|
||||
this.set("value", `${sel.pre} ${example}${sel.post}`);
|
||||
return this.textManipulation.selectText(
|
||||
sel.pre.length + 4,
|
||||
example.length
|
||||
);
|
||||
} else {
|
||||
return this.textManipulation.applySurround(
|
||||
sel,
|
||||
"```\n",
|
||||
"\n```",
|
||||
"paste_code_text"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return this.textManipulation.applySurround(sel, "`", "`", "code_title");
|
||||
}
|
||||
} else {
|
||||
if (isFourSpacesIndent) {
|
||||
return this.textManipulation.applySurround(
|
||||
sel,
|
||||
" ",
|
||||
"",
|
||||
"code_text"
|
||||
);
|
||||
} else {
|
||||
const preNewline = sel.pre[-1] !== "\n" && sel.pre !== "" ? "\n" : "";
|
||||
const postNewline = sel.post[0] !== "\n" ? "\n" : "";
|
||||
return this.textManipulation.addText(
|
||||
sel,
|
||||
`${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
insertCurrentTime() {
|
||||
const sel = this.textManipulation.getSelected("", { lineVal: true });
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const time = moment().format("HH:mm:ss");
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
|
||||
this.textManipulation.addText(
|
||||
sel,
|
||||
`[date=${date} time=${time} timezone="${timezone}"]`
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
handleFocusIn() {
|
||||
this.set("isEditorFocused", true);
|
||||
|
@ -715,6 +638,8 @@ export default class DEditor extends Component {
|
|||
this._applyHashtagAutocomplete();
|
||||
this._applyMentionAutocomplete();
|
||||
|
||||
const destroyEditor = this.onSetup?.(textManipulation);
|
||||
|
||||
scheduleOnce("afterRender", this, this._readyNow);
|
||||
|
||||
return () => {
|
||||
|
@ -723,6 +648,8 @@ export default class DEditor extends Component {
|
|||
this.element?.removeEventListener("paste", textManipulation.paste);
|
||||
|
||||
textManipulation.autocomplete("destroy");
|
||||
|
||||
destroyEditor?.();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -741,7 +668,11 @@ export default class DEditor extends Component {
|
|||
textManipulation,
|
||||
"replaceText"
|
||||
);
|
||||
this.appEvents.on("composer:apply-surround", this, "_applySurround");
|
||||
this.appEvents.on(
|
||||
"composer:apply-surround",
|
||||
textManipulation,
|
||||
"applySurroundSelection"
|
||||
);
|
||||
this.appEvents.on(
|
||||
"composer:indent-selected-text",
|
||||
textManipulation,
|
||||
|
@ -764,7 +695,11 @@ export default class DEditor extends Component {
|
|||
textManipulation,
|
||||
"replaceText"
|
||||
);
|
||||
this.appEvents.off("composer:apply-surround", this, "_applySurround");
|
||||
this.appEvents.off(
|
||||
"composer:apply-surround",
|
||||
textManipulation,
|
||||
"applySurroundSelection"
|
||||
);
|
||||
this.appEvents.off(
|
||||
"composer:indent-selected-text",
|
||||
textManipulation,
|
||||
|
|
|
@ -78,7 +78,7 @@ export default class Toolbar {
|
|||
icon: "code",
|
||||
preventFocus: true,
|
||||
trimLeading: true,
|
||||
action: (...args) => this.context.send("formatCode", args),
|
||||
perform: (e) => e.formatCode(),
|
||||
});
|
||||
|
||||
this.addButton({
|
||||
|
|
|
@ -3,6 +3,7 @@ import { next, schedule } from "@ember/runloop";
|
|||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import $ from "jquery";
|
||||
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||
import { generateLinkifyFunction } from "discourse/lib/text";
|
||||
import { siteDir } from "discourse/lib/text-direction";
|
||||
import toMarkdown from "discourse/lib/to-markdown";
|
||||
|
@ -28,6 +29,8 @@ const OP = {
|
|||
ADDED: 2,
|
||||
};
|
||||
|
||||
const FOUR_SPACES_INDENT = "4-spaces-indent";
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
export function getHead(head, prev) {
|
||||
|
@ -42,6 +45,7 @@ export default class TextareaTextManipulation {
|
|||
@service appEvents;
|
||||
@service siteSettings;
|
||||
@service capabilities;
|
||||
@service currentUser;
|
||||
|
||||
eventPrefix;
|
||||
textarea;
|
||||
|
@ -180,6 +184,10 @@ export default class TextareaTextManipulation {
|
|||
}
|
||||
}
|
||||
|
||||
applySurroundSelection(head, tail, exampleKey, opts) {
|
||||
this.applySurround(this.getSelected(), head, tail, exampleKey, opts);
|
||||
}
|
||||
|
||||
applySurround(sel, head, tail, exampleKey, opts) {
|
||||
const pre = sel.pre;
|
||||
const post = sel.post;
|
||||
|
@ -730,6 +738,7 @@ export default class TextareaTextManipulation {
|
|||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
async inCodeBlock() {
|
||||
return inCodeBlock(
|
||||
this.$textarea.value ?? this.$textarea.val(),
|
||||
|
@ -737,6 +746,7 @@ export default class TextareaTextManipulation {
|
|||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleDirection() {
|
||||
let currentDir = this.$textarea.attr("dir")
|
||||
? this.$textarea.attr("dir")
|
||||
|
@ -746,6 +756,75 @@ export default class TextareaTextManipulation {
|
|||
this.$textarea.attr("dir", newDir).focus();
|
||||
}
|
||||
|
||||
@bind
|
||||
applyList(sel, head, exampleKey, opts) {
|
||||
if (sel.value.includes("\n")) {
|
||||
this.applySurround(sel, head, "", exampleKey, opts);
|
||||
} else {
|
||||
const [hval, hlen] = getHead(head);
|
||||
if (sel.start === sel.end) {
|
||||
sel.value = i18n(`composer.${exampleKey}`);
|
||||
}
|
||||
|
||||
const number = sel.value.startsWith(hval)
|
||||
? sel.value.slice(hlen)
|
||||
: `${hval}${sel.value}`;
|
||||
|
||||
const preNewlines = sel.pre.trim() && "\n\n";
|
||||
const postNewlines = sel.post.trim() && "\n\n";
|
||||
|
||||
const textToInsert = `${preNewlines}${number}${postNewlines}`;
|
||||
|
||||
const preChars = sel.pre.length - sel.pre.trimEnd().length;
|
||||
const postChars = sel.post.length - sel.post.trimStart().length;
|
||||
|
||||
this._insertAt(sel.start - preChars, sel.end + postChars, textToInsert);
|
||||
this.selectText(
|
||||
sel.start + (preNewlines.length - preChars),
|
||||
number.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
formatCode() {
|
||||
const sel = this.getSelected("", { lineVal: true });
|
||||
const selValue = sel.value;
|
||||
const hasNewLine = selValue.includes("\n");
|
||||
const isBlankLine = sel.lineVal.trim().length === 0;
|
||||
const isFourSpacesIndent =
|
||||
this.siteSettings.code_formatting_style === FOUR_SPACES_INDENT;
|
||||
|
||||
if (!hasNewLine) {
|
||||
if (selValue.length === 0 && isBlankLine) {
|
||||
if (isFourSpacesIndent) {
|
||||
const example = i18n(`composer.code_text`);
|
||||
this._insertAt(sel.start, sel.end, ` ${example}`);
|
||||
return this.selectText(sel.pre.length + 4, example.length);
|
||||
} else {
|
||||
return this.applySurround(sel, "```\n", "\n```", "paste_code_text");
|
||||
}
|
||||
} else {
|
||||
return this.applySurround(sel, "`", "`", "code_title");
|
||||
}
|
||||
} else {
|
||||
if (isFourSpacesIndent) {
|
||||
return this.applySurround(sel, " ", "", "code_text");
|
||||
} else {
|
||||
const preNewline = sel.pre[-1] !== "\n" && sel.pre !== "" ? "\n" : "";
|
||||
const postNewline = sel.post[0] !== "\n" ? "\n" : "";
|
||||
return this.addText(
|
||||
sel,
|
||||
`${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
putCursorAtEnd() {
|
||||
putCursorAtEnd(this.textarea);
|
||||
}
|
||||
|
||||
autocomplete() {
|
||||
return this.$textarea.autocomplete(...arguments);
|
||||
}
|
||||
|
|
|
@ -1316,32 +1316,6 @@ acceptance("Composer - default category not set", function (needs) {
|
|||
});
|
||||
// END: Default Composer Category tests
|
||||
|
||||
acceptance("Composer - current time", function (needs) {
|
||||
needs.user();
|
||||
|
||||
test("composer insert current time shortcut", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click("#topic-footer-buttons .btn.create");
|
||||
assert.dom(".d-editor-input").exists("the composer input is visible");
|
||||
await fillIn(".d-editor-input", "and the time now is: ");
|
||||
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
|
||||
await triggerKeyEvent(".d-editor-input", "keydown", ".", {
|
||||
...metaModifier,
|
||||
shiftKey: true,
|
||||
});
|
||||
|
||||
assert.true(
|
||||
query("#reply-control .d-editor-input")
|
||||
.value.trim()
|
||||
.startsWith(`and the time now is: [date=${date}`),
|
||||
"adds the current date"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("composer buttons API", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({
|
||||
|
|
|
@ -344,6 +344,26 @@ third line`
|
|||
assert.strictEqual(textarea.selectionEnd, 23);
|
||||
});
|
||||
|
||||
test("code button does not reset undo history", async function (assert) {
|
||||
this.set("value", "existing");
|
||||
|
||||
await render(hbs`<DEditor @value={{this.value}} />`);
|
||||
const textarea = query("textarea.d-editor-input");
|
||||
textarea.selectionStart = 0;
|
||||
textarea.selectionEnd = 8;
|
||||
|
||||
await click("button.code");
|
||||
assert.strictEqual(this.value, "`existing`");
|
||||
|
||||
await click("button.code");
|
||||
assert.strictEqual(this.value, "existing");
|
||||
|
||||
document.execCommand("undo");
|
||||
assert.strictEqual(this.value, "`existing`");
|
||||
document.execCommand("undo");
|
||||
assert.strictEqual(this.value, "existing");
|
||||
});
|
||||
|
||||
test("code fences", async function (assert) {
|
||||
this.set("value", "");
|
||||
|
||||
|
@ -615,6 +635,22 @@ third line`
|
|||
assert.strictEqual(textarea.selectionEnd, 18);
|
||||
});
|
||||
|
||||
testCase(
|
||||
"list button does not reset undo history",
|
||||
async function (assert, textarea) {
|
||||
this.set("value", "existing");
|
||||
textarea.selectionStart = 0;
|
||||
textarea.selectionEnd = 8;
|
||||
|
||||
await click("button.list");
|
||||
assert.strictEqual(this.value, "1. existing");
|
||||
|
||||
document.execCommand("undo");
|
||||
|
||||
assert.strictEqual(this.value, "existing");
|
||||
}
|
||||
);
|
||||
|
||||
test("clicking the toggle-direction changes dir from ltr to rtl and back", async function (assert) {
|
||||
this.siteSettings.support_mixed_text_direction = true;
|
||||
this.siteSettings.default_locale = "en";
|
||||
|
|
|
@ -142,6 +142,7 @@ function _partitionedRanges(element) {
|
|||
}
|
||||
|
||||
function initializeDiscourseLocalDates(api) {
|
||||
const modal = api.container.lookup("service:modal");
|
||||
const siteSettings = api.container.lookup("service:site-settings");
|
||||
const defaultTitle = i18n("discourse_local_dates.default_title", {
|
||||
site_name: siteSettings.title,
|
||||
|
@ -164,25 +165,19 @@ function initializeDiscourseLocalDates(api) {
|
|||
id: "local-dates",
|
||||
group: "extras",
|
||||
icon: "calendar-days",
|
||||
sendAction: (event) =>
|
||||
toolbar.context.send("insertDiscourseLocalDate", event),
|
||||
});
|
||||
});
|
||||
perform: (event) =>
|
||||
modal.show(LocalDatesCreateModal, {
|
||||
model: { insertDate: (markup) => event.addText(markup) },
|
||||
}),
|
||||
shortcut: "Shift+.",
|
||||
shortcutAction: (event) => {
|
||||
const timezone = api.getCurrentUser().user_option.timezone;
|
||||
const time = moment().format("HH:mm:ss");
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
|
||||
api.modifyClass("component:d-editor", {
|
||||
modal: service(),
|
||||
pluginId: "discourse-local-dates",
|
||||
actions: {
|
||||
insertDiscourseLocalDate(toolbarEvent) {
|
||||
this.modal.show(LocalDatesCreateModal, {
|
||||
model: {
|
||||
insertDate: (markup) => {
|
||||
toolbarEvent.addText(markup);
|
||||
},
|
||||
},
|
||||
});
|
||||
event.addText(`[date=${date} time=${time} timezone="${timezone}"]`);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
addTextDecorateCallback(function (
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import {
|
||||
acceptance,
|
||||
metaModifier,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
|
||||
acceptance("Local Dates - composer", function (needs) {
|
||||
|
@ -128,4 +131,24 @@ acceptance("Local Dates - composer", function (needs) {
|
|||
await click("ul.formats a.moment-format");
|
||||
assert.dom("input.format-input").hasValue("LLL");
|
||||
});
|
||||
|
||||
test("composer insert current time shortcut", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("#topic-footer-buttons .btn.create");
|
||||
await fillIn(".d-editor-input", "and the time now is: ");
|
||||
|
||||
await triggerKeyEvent(".d-editor-input", "keydown", ".", {
|
||||
...metaModifier,
|
||||
shiftKey: true,
|
||||
});
|
||||
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
|
||||
assert
|
||||
.dom("#reply-control .d-editor-input")
|
||||
.hasValue(
|
||||
new RegExp(`and the time now is: \\[date=${date}`),
|
||||
"it adds the current date"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user