DEV: Convert ace-editor to glimmer/gjs (#28492)

This commit is contained in:
Jarek Radosz 2024-08-27 13:35:38 +02:00 committed by GitHub
parent 4a6fc45429
commit 62c8904721
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 334 additions and 318 deletions

View File

@ -120,6 +120,7 @@
<AceEditor <AceEditor
@content={{this.activeSection}} @content={{this.activeSection}}
@onChange={{fn (mut this.activeSection)}}
@editorId={{this.editorId}} @editorId={{this.editorId}}
@mode={{this.activeSectionMode}} @mode={{this.activeSectionMode}}
@autofocus="true" @autofocus="true"

View File

@ -27,6 +27,7 @@
<AceEditor <AceEditor
@content={{this.editorContents}} @content={{this.editorContents}}
@onChange={{fn (mut this.editorContents)}}
@mode={{this.currentEditorMode}} @mode={{this.currentEditorMode}}
@editorId={{this.editorId}} @editorId={{this.editorId}}
@save={{@save}} @save={{@save}}

View File

@ -40,7 +40,11 @@
</div> </div>
<div class="control-group"> <div class="control-group">
<AceEditor @content={{this.templateContent}} @mode="yaml" /> <AceEditor
@content={{this.templateContent}}
@onChange={{fn (mut this.templateContent)}}
@mode="yaml"
/>
</div> </div>
<div class="footer-buttons"> <div class="footer-buttons">

View File

@ -1,6 +1,10 @@
<div class="settings-editor"> <div class="settings-editor">
<div> <div>
<AceEditor @mode="html" @content={{this.editedContent}} /> <AceEditor
@content={{this.editedContent}}
@onChange={{fn (mut this.editedContent)}}
@mode="html"
/>
{{#each this.errors as |error|}} {{#each this.errors as |error|}}
<div class="validation-error"> <div class="validation-error">

View File

@ -0,0 +1,264 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { service } from "@ember/service";
import { modifier } from "ember-modifier";
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
import loadAce from "discourse/lib/load-ace-editor";
import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
const COLOR_VARS_REGEX =
/\$(primary|secondary|tertiary|quaternary|header_background|header_primary|highlight|danger|success|love)(\s|;|-(low|medium|high))/g;
function overridePlaceholder(ace) {
const originalPlaceholderSetter =
ace.config.$defaultOptions.editor.placeholder.set;
ace.config.$defaultOptions.editor.placeholder.set = function () {
if (!this.$updatePlaceholder) {
const originalRendererOn = this.renderer.on;
this.renderer.on = function () {};
originalPlaceholderSetter.call(this, ...arguments);
this.renderer.on = originalRendererOn;
const originalUpdatePlaceholder = this.$updatePlaceholder;
this.$updatePlaceholder = function () {
originalUpdatePlaceholder.call(this, ...arguments);
if (this.renderer.placeholderNode) {
this.renderer.placeholderNode.innerHTML = this.$placeholder || "";
}
}.bind(this);
this.on("input", this.$updatePlaceholder);
}
this.$updatePlaceholder();
};
}
// Args:
// @content
// @mode
// @disabled (boolean)
// @onChange
// @editorId
// @theme
// @autofocus
// @placeholder
// @htmlPlaceholder (boolean)
// @save
// @submit
// @setWarning
export default class AceEditor extends Component {
@service appEvents;
@tracked isLoading = true;
editor = null;
ace = null;
skipChangePropagation = false;
setContent = modifier(() => {
if (this.args.content === this.editor.getSession().getValue()) {
return;
}
this.skipChangePropagation = true;
this.editor.getSession().setValue(this.args.content || "");
this.skipChangePropagation = false;
});
constructor() {
super(...arguments);
loadAce().then((ace) => {
if (this.isDestroying || this.isDestroyed) {
return;
}
this.ace = ace;
this.isLoading = false;
});
this.appEvents.on("ace:resize", this.resize);
window.addEventListener("resize", this.resize);
this._darkModeListener = window.matchMedia("(prefers-color-scheme: dark)");
this._darkModeListener.addListener(this.setAceTheme);
}
willDestroy() {
super.willDestroy(...arguments);
this.editor?.destroy();
this._darkModeListener?.removeListener(this.setAceTheme);
window.removeEventListener("resize", this.resize);
this.appEvents.off("ace:resize", this.resize);
}
@bind
setupAce(element) {
if (this.args.htmlPlaceholder) {
overridePlaceholder(this.ace);
}
this.ace.config.set("useWorker", false);
this.editor = this.ace.edit(element);
this.editor.setShowPrintMargin(false);
this.editor.setOptions({
fontSize: "14px",
placeholder: this.args.placeholder,
});
const session = this.editor.getSession();
session.setMode(`ace/mode/${this.mode}`);
this.editor.on("change", () => {
if (!this.skipChangePropagation) {
this.args.onChange?.(session.getValue());
}
});
if (this.args.save) {
this.editor.commands.addCommand({
name: "save",
exec: () => this.args.save(),
bindKey: { mac: "cmd-s", win: "ctrl-s" },
});
}
if (this.args.submit) {
this.editor.commands.addCommand({
name: "submit",
exec: () => this.args.submit(),
bindKey: { mac: "cmd-enter", win: "ctrl-enter" },
});
}
this.editor.on("blur", () => this.warnSCSSDeprecations());
this.editor.$blockScrolling = Infinity;
this.editor.renderer.setScrollMargin(10, 10);
element.setAttribute("data-editor", this.editor);
this.changeDisabledState();
this.warnSCSSDeprecations();
if (this.autofocus) {
this.focus();
}
this.setAceTheme();
}
get mode() {
return this.args.mode || "css";
}
@bind
editorIdChanged() {
if (this.autofocus) {
this.send("focus");
}
}
@bind
modeChanged() {
this.editor?.getSession().setMode(`ace/mode/${this.mode}`);
}
@bind
placeholderChanged() {
this.editor?.setOptions({ placeholder: this.args.placeholder });
}
@bind
changeDisabledState() {
this.editor?.setOptions({
readOnly: this.args.disabled,
highlightActiveLine: !this.args.disabled,
highlightGutterLine: !this.args.disabled,
});
this.editor?.container.parentNode.parentNode.setAttribute(
"data-disabled",
this.args.disabled
);
}
warnSCSSDeprecations() {
if (
this.mode !== "scss" ||
this.args.editorId.startsWith("color_definitions") ||
!this.editor
) {
return;
}
let warnings = this.args.content
.split("\n")
.map((line, row) => {
if (line.match(COLOR_VARS_REGEX)) {
return {
row,
column: 0,
text: I18n.t("admin.customize.theme.scss_warning_inline"),
type: "warning",
};
}
})
.filter(Boolean);
this.editor.getSession().setAnnotations(warnings);
this.args.setWarning?.(
warnings.length
? I18n.t("admin.customize.theme.scss_color_variables_warning")
: false
);
}
@bind
setAceTheme() {
const schemeType = getComputedStyle(document.body)
.getPropertyValue("--scheme-type")
.trim();
const aceTheme = schemeType === "dark" ? "chaos" : "chrome";
this.editor.setTheme(`ace/theme/${aceTheme}`);
}
@bind
resize() {
this.editor?.resize();
}
@bind
focus() {
if (this.editor) {
this.editor.focus();
this.editor.navigateFileEnd();
}
}
<template>
<div class="ace-wrapper">
<ConditionalLoadingSpinner @condition={{this.isLoading}} @size="small">
<div
{{didInsert this.setupAce}}
{{this.setContent}}
{{didUpdate this.editorIdChanged @editorId}}
{{didUpdate this.modeChanged @mode}}
{{didUpdate this.placeholderChanged @placeholder}}
{{didUpdate this.changeDisabledState @disabled}}
class="ace"
...attributes
>
</div>
</ConditionalLoadingSpinner>
</div>
</template>
}

View File

@ -1,5 +0,0 @@
{{#if this.isLoading}}
{{loading-spinner size="small"}}
{{else}}
<div class="ace" ...attributes>{{this.content}}</div>
{{/if}}

View File

@ -1,262 +0,0 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import { classNames } from "@ember-decorators/component";
import { observes } from "@ember-decorators/object";
import loadAce from "discourse/lib/load-ace-editor";
import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
const COLOR_VARS_REGEX =
/\$(primary|secondary|tertiary|quaternary|header_background|header_primary|highlight|danger|success|love)(\s|;|-(low|medium|high))/g;
@classNames("ace-wrapper")
export default class AceEditor extends Component {
@service appEvents;
isLoading = true;
mode = "css";
disabled = false;
htmlPlaceholder = false;
_editor = null;
_skipContentChangeEvent = null;
constructor() {
super(...arguments);
this.appEvents.on("ace:resize", this.resize);
window.addEventListener("resize", this.resize);
this._darkModeListener = window.matchMedia("(prefers-color-scheme: dark)");
this._darkModeListener.addListener(this.setAceTheme);
}
@observes("editorId")
editorIdChanged() {
if (this.autofocus) {
this.send("focus");
}
}
didRender() {
super.didRender(...arguments);
this._skipContentChangeEvent = false;
}
@observes("content")
contentChanged() {
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
}
@observes("mode")
modeChanged() {
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.mode);
}
}
@observes("placeholder")
placeholderChanged() {
if (this._editor) {
this._editor.setOptions({
placeholder: this.placeholder,
});
}
}
@observes("disabled")
disabledStateChanged() {
this.changeDisabledState();
}
changeDisabledState() {
const editor = this._editor;
if (editor) {
const disabled = this.disabled;
editor.setOptions({
readOnly: disabled,
highlightActiveLine: !disabled,
highlightGutterLine: !disabled,
});
editor.container.parentNode.setAttribute("data-disabled", disabled);
}
}
@action
resize() {
if (this._editor) {
this._editor.resize();
}
}
didInsertElement() {
super.didInsertElement(...arguments);
this.setup();
}
async setup() {
const ace = await loadAce();
this.set("isLoading", false);
next(() => {
if (this.htmlPlaceholder) {
this._overridePlaceholder(ace);
}
ace.config.set("useWorker", false);
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
const aceElement = this.element.querySelector(".ace");
const editor = ace.edit(aceElement);
editor.setShowPrintMargin(false);
editor.setOptions({
fontSize: "14px",
placeholder: this.placeholder,
});
editor.getSession().setMode("ace/mode/" + this.mode);
editor.on("change", () => {
if (this.onChange) {
this.onChange(editor.getSession().getValue());
} else {
this._skipContentChangeEvent = true;
this.set("content", editor.getSession().getValue());
}
});
if (this.save) {
editor.commands.addCommand({
name: "save",
exec: () => {
this.save();
},
bindKey: { mac: "cmd-s", win: "ctrl-s" },
});
}
if (this.submit) {
editor.commands.addCommand({
name: "submit",
exec: () => {
this.submit();
},
bindKey: { mac: "cmd-enter", win: "ctrl-enter" },
});
}
editor.on("blur", () => {
this.warnSCSSDeprecations();
});
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10, 10);
this.element.setAttribute("data-editor", editor);
this._editor = editor;
this.changeDisabledState();
this.warnSCSSDeprecations();
if (this.autofocus) {
this.send("focus");
}
this.setAceTheme();
});
}
willDestroyElement() {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
super.willDestroyElement(...arguments);
this._darkModeListener?.removeListener(this.setAceTheme);
window.removeEventListener("resize", this.resize);
this.appEvents.off("ace:resize", this.resize);
}
get aceTheme() {
const schemeType = getComputedStyle(document.body)
.getPropertyValue("--scheme-type")
.trim();
return schemeType === "dark" ? "chaos" : "chrome";
}
@bind
setAceTheme() {
this._editor.setTheme(`ace/theme/${this.aceTheme}`);
}
warnSCSSDeprecations() {
if (
this.mode !== "scss" ||
this.editorId.startsWith("color_definitions") ||
!this._editor
) {
return;
}
let warnings = this.content
.split("\n")
.map((line, row) => {
if (line.match(COLOR_VARS_REGEX)) {
return {
row,
column: 0,
text: I18n.t("admin.customize.theme.scss_warning_inline"),
type: "warning",
};
}
})
.filter(Boolean);
this._editor.getSession().setAnnotations(warnings);
this.setWarning?.(
warnings.length
? I18n.t("admin.customize.theme.scss_color_variables_warning")
: false
);
}
@action
focus() {
if (this._editor) {
this._editor.focus();
this._editor.navigateFileEnd();
}
}
_overridePlaceholder(ace) {
const originalPlaceholderSetter =
ace.config.$defaultOptions.editor.placeholder.set;
ace.config.$defaultOptions.editor.placeholder.set = function () {
if (!this.$updatePlaceholder) {
const originalRendererOn = this.renderer.on;
this.renderer.on = function () {};
originalPlaceholderSetter.call(this, ...arguments);
this.renderer.on = originalRendererOn;
const originalUpdatePlaceholder = this.$updatePlaceholder;
this.$updatePlaceholder = function () {
originalUpdatePlaceholder.call(this, ...arguments);
if (this.renderer.placeholderNode) {
this.renderer.placeholderNode.innerHTML = this.$placeholder || "";
}
}.bind(this);
this.on("input", this.$updatePlaceholder);
}
this.$updatePlaceholder();
};
}
}

View File

@ -24,10 +24,10 @@ export default class FKControlCode extends Component {
<template> <template>
<AceEditor <AceEditor
@content={{readonly this.initialValue}} @content={{this.initialValue}}
@onChange={{this.handleInput}}
@mode={{@lang}} @mode={{@lang}}
@disabled={{@field.disabled}} @disabled={{@field.disabled}}
@onChange={{this.handleInput}}
class="form-kit__control-code" class="form-kit__control-code"
style={{this.style}} style={{this.style}}
...attributes ...attributes

View File

@ -0,0 +1,56 @@
import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import AceEditor from "discourse/components/ace-editor";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
module("Integration | Component | ace-editor", function (hooks) {
setupRenderingTest(hooks);
test("css editor", async function (assert) {
await render(<template><AceEditor @mode="css" /></template>);
assert.dom(".ace_editor").exists("it renders the ace editor");
});
test("html editor", async function (assert) {
await render(<template>
<AceEditor @mode="html" @content="<b>wat</b>" />
</template>);
assert.dom(".ace_editor").exists("it renders the ace editor");
});
test("sql editor", async function (assert) {
await render(<template>
<AceEditor @mode="sql" @content="SELECT * FROM users" />
</template>);
assert.dom(".ace_editor").exists("it renders the ace editor");
});
test("yaml editor", async function (assert) {
await render(<template>
<AceEditor @mode="yaml" @content="test: true" />
</template>);
assert.dom(".ace_editor").exists("it renders the ace editor");
});
test("javascript editor", async function (assert) {
await render(<template>
<AceEditor @mode="javascript" @content="test: true" />
</template>);
assert.dom(".ace_editor").exists("it renders the ace editor");
});
test("disabled editor", async function (assert) {
await render(<template>
<AceEditor
@mode="sql"
@content="SELECT * FROM users"
@disabled={{true}}
/>
</template>);
assert.dom(".ace_editor").exists("it renders the ace editor");
assert
.dom(".ace-wrapper")
.hasAttribute("data-disabled", "true", "it has a data-disabled attr");
});
});

View File

@ -1,47 +0,0 @@
import { render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { count, exists } from "discourse/tests/helpers/qunit-helpers";
module("Integration | Component | ace-editor", function (hooks) {
setupRenderingTest(hooks);
test("css editor", async function (assert) {
await render(hbs`<AceEditor @mode="css" />`);
assert.ok(exists(".ace_editor"), "it renders the ace editor");
});
test("html editor", async function (assert) {
await render(hbs`<AceEditor @mode="html" @content="<b>wat</b>" />`);
assert.ok(exists(".ace_editor"), "it renders the ace editor");
});
test("sql editor", async function (assert) {
await render(hbs`<AceEditor @mode="sql" @content="SELECT * FROM users" />`);
assert.ok(exists(".ace_editor"), "it renders the ace editor");
});
test("yaml editor", async function (assert) {
await render(hbs`<AceEditor @mode="yaml" @content="test: true" />`);
assert.ok(exists(".ace_editor"), "it renders the ace editor");
});
test("javascript editor", async function (assert) {
await render(hbs`<AceEditor @mode="javascript" @content="test: true" />`);
assert.ok(exists(".ace_editor"), "it renders the ace editor");
});
test("disabled editor", async function (assert) {
await render(hbs`
<AceEditor @mode="sql" @content="SELECT * FROM users" @disabled={{true}} />
`);
assert.ok(exists(".ace_editor"), "it renders the ace editor");
assert.strictEqual(
count(".ace-wrapper[data-disabled]"),
1,
"it has a data-disabled attr"
);
});
});