@cloudcannon/editable-regions 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +5 -0
  3. package/components/editable-array-component.ts +26 -0
  4. package/components/editable-array-item-component.ts +26 -0
  5. package/components/editable-component-component.ts +26 -0
  6. package/components/editable-image-component.ts +26 -0
  7. package/components/editable-snippet-component.ts +30 -0
  8. package/components/editable-source-component.ts +26 -0
  9. package/components/editable-text-component.ts +26 -0
  10. package/components/index.ts +35 -0
  11. package/components/ui/editable-array-item-controls.ts +123 -0
  12. package/components/ui/editable-component-controls.ts +57 -0
  13. package/components/ui/editable-region-error-card.ts +77 -0
  14. package/helpers/checks.ts +126 -0
  15. package/helpers/cloudcannon.ts +67 -0
  16. package/helpers/hydrate-editable-regions.ts +71 -0
  17. package/integrations/astro/astro-integration.mjs +110 -0
  18. package/integrations/astro/index.mjs +193 -0
  19. package/integrations/astro/modules/actions.js +74 -0
  20. package/integrations/astro/modules/assets.js +38 -0
  21. package/integrations/astro/modules/client-router.astro +5 -0
  22. package/integrations/astro/modules/content.js +116 -0
  23. package/integrations/astro/modules/i18n.js +76 -0
  24. package/integrations/astro/modules/image.astro +33 -0
  25. package/integrations/astro/modules/middleware.js +27 -0
  26. package/integrations/astro/modules/picture.astro +7 -0
  27. package/integrations/astro/modules/transitions.js +63 -0
  28. package/integrations/astro/react-renderer.mjs +40 -0
  29. package/integrations/react.mjs +32 -0
  30. package/nodes/editable-array-item.ts +427 -0
  31. package/nodes/editable-array.ts +241 -0
  32. package/nodes/editable-component.ts +273 -0
  33. package/nodes/editable-image.ts +253 -0
  34. package/nodes/editable-snippet.ts +148 -0
  35. package/nodes/editable-source.ts +245 -0
  36. package/nodes/editable-text.ts +163 -0
  37. package/nodes/editable.ts +471 -0
  38. package/nodes/index.ts +8 -0
  39. package/package.json +64 -0
  40. package/styles/editable-array-item.css +8 -0
  41. package/styles/editable-component.css +8 -0
  42. package/styles/editable-image.css +4 -0
  43. package/styles/editable-snippet.css +12 -0
  44. package/styles/editable-source.css +3 -0
  45. package/styles/editable-text.css +3 -0
  46. package/styles/index.css +101 -0
  47. package/styles/index.ts +6 -0
  48. package/styles/ui/editable-component-controls.css +104 -0
  49. package/styles/ui/editable-region-error-card.css +25 -0
  50. package/types/astro.d.ts +19 -0
  51. package/types/cloudcannon.d.ts +11 -0
  52. package/types/modules.d.ts +12 -0
  53. package/types/react.d.ts +5 -0
  54. package/types/vite.d.ts +9 -0
@@ -0,0 +1,253 @@
1
+ import { CloudCannon } from "../helpers/cloudcannon.js";
2
+ import Editable from "./editable.js";
3
+
4
+ export default class EditableImage extends Editable {
5
+ value: { src?: string; alt?: string; title?: string } | null | undefined =
6
+ undefined;
7
+ inputConfig: { src?: any; alt?: any; title?: any } = {};
8
+ imageEl?: HTMLImageElement;
9
+
10
+ configuredSrc = false;
11
+ configuredAlt = false;
12
+ configuredTitle = false;
13
+
14
+ displayError(heading: string, message: string) {
15
+ this.element.classList.add("errored");
16
+ const error = document.createElement("editable-region-error-card");
17
+ error.setAttribute("heading", heading);
18
+ error.setAttribute("message", message);
19
+ if (this.imageEl) {
20
+ this.imageEl?.replaceWith(error);
21
+ } else {
22
+ this.element.replaceChildren(error);
23
+ }
24
+ }
25
+
26
+ validateConfiguration(): boolean {
27
+ const child =
28
+ this.element instanceof HTMLImageElement
29
+ ? this.element
30
+ : this.element.querySelector("img");
31
+
32
+ if (!(child instanceof HTMLImageElement)) {
33
+ this.displayError(
34
+ "Failed to render image editable region",
35
+ "Image editable region requires an image element as its child.",
36
+ );
37
+ return false;
38
+ }
39
+
40
+ this.imageEl = child;
41
+
42
+ if (
43
+ this.element.dataset.prop === undefined &&
44
+ this.element.dataset.propSrc === undefined &&
45
+ this.element.dataset.propAlt === undefined &&
46
+ this.element.dataset.propTitle === undefined
47
+ ) {
48
+ this.displayError(
49
+ "Failed to render image editable region",
50
+ "Atleast one of data-prop, data-prop-src, data-prop-alt, or data-prop-title is required.",
51
+ );
52
+ return false;
53
+ }
54
+ return true;
55
+ }
56
+
57
+ validateValue(value: unknown): this["value"] {
58
+ if (typeof value !== "object") {
59
+ this.displayError(
60
+ "Failed to render image editable region",
61
+ `Illegal value type: ${typeof value}. Supported types are object.`,
62
+ );
63
+ return;
64
+ }
65
+
66
+ if (value === null) {
67
+ return value;
68
+ }
69
+
70
+ if ("src" in value && typeof value.src !== "string" && value.src !== null) {
71
+ this.displayError(
72
+ "Failed to render image editable region",
73
+ `Illegal value type for "src": ${typeof value.src}. Supported types are string.`,
74
+ );
75
+ return;
76
+ }
77
+
78
+ if ("alt" in value && typeof value.alt !== "string" && value.alt !== null) {
79
+ this.displayError(
80
+ "Failed to render image editable region",
81
+ `Illegal value type for "alt": ${typeof value.alt}. Supported types are string.`,
82
+ );
83
+ return;
84
+ }
85
+
86
+ if (
87
+ "title" in value &&
88
+ typeof value.title !== "string" &&
89
+ value.title !== null
90
+ ) {
91
+ this.displayError(
92
+ "Failed to render image editable region",
93
+ `Illegal value type for "title": ${typeof value.title}. Supported types are string.`,
94
+ );
95
+ return;
96
+ }
97
+
98
+ const unexpectedKey = Object.keys(value).find(
99
+ (key) => key !== "src" && key !== "alt" && key !== "title",
100
+ );
101
+
102
+ if (unexpectedKey) {
103
+ this.displayError(
104
+ "Failed to render image editable region",
105
+ `Unexpected key "${unexpectedKey}" in image editable region. Supported keys are "src", "alt", and "title".`,
106
+ );
107
+ return;
108
+ }
109
+
110
+ return value;
111
+ }
112
+
113
+ async update(): Promise<void> {
114
+ if (!this.imageEl) {
115
+ throw new Error("Element is not an HTMLImageElement");
116
+ }
117
+
118
+ if (this.configuredSrc && this.imageEl.src !== this.value?.src) {
119
+ const previewUrl = await CloudCannon.getPreviewUrl(
120
+ this.value?.src ?? "",
121
+ this.inputConfig.src,
122
+ );
123
+ this.imageEl.src = previewUrl;
124
+ const parent = this.imageEl.parentElement;
125
+ if (parent instanceof HTMLPictureElement) {
126
+ for (const sourceEl of parent.children) {
127
+ if (sourceEl instanceof HTMLSourceElement) {
128
+ sourceEl.src = previewUrl;
129
+ sourceEl.srcset = previewUrl;
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ if (this.configuredAlt && this.imageEl.alt !== this.value?.alt) {
136
+ this.imageEl.alt = this.value?.alt ?? "";
137
+ }
138
+
139
+ if (this.configuredTitle && this.imageEl.title !== this.value?.title) {
140
+ this.imageEl.title = this.value?.title ?? "";
141
+ }
142
+ }
143
+
144
+ async loadInputConfig(): Promise<void> {
145
+ this.inputConfig = {};
146
+ if (this.configuredSrc) {
147
+ this.inputConfig.src = await this.dispatchGetInputConfig(
148
+ this.element.dataset.propSrc ?? `${this.element.dataset.prop}.src`,
149
+ );
150
+ }
151
+ if (this.configuredAlt) {
152
+ this.inputConfig.alt = await this.dispatchGetInputConfig(
153
+ this.element.dataset.propAlt ?? `${this.element.dataset.prop}.alt`,
154
+ );
155
+ }
156
+ if (this.configuredTitle) {
157
+ this.inputConfig.title = await this.dispatchGetInputConfig(
158
+ this.element.dataset.propTitle ?? `${this.element.dataset.prop}.title`,
159
+ );
160
+ }
161
+ }
162
+
163
+ mount(): void {
164
+ this.configuredSrc =
165
+ !!this.element.dataset.propSrc || !!this.element.dataset.prop;
166
+ this.configuredAlt =
167
+ !!this.element.dataset.propAlt || !!this.element.dataset.prop;
168
+ this.configuredTitle =
169
+ !!this.element.dataset.propTitle || !!this.element.dataset.prop;
170
+
171
+ this.loadInputConfig().then(() => {
172
+ this.imageEl?.addEventListener("click", () => {
173
+ if (!this.value) {
174
+ throw new Error("Value is not defined");
175
+ }
176
+
177
+ const data: this["value"] = {};
178
+ if ("src" in this.value) {
179
+ data.src = this.value.src;
180
+ }
181
+ if ("alt" in this.value) {
182
+ data.alt = this.value.alt;
183
+ }
184
+ if ("title" in this.value) {
185
+ data.title = this.value.title;
186
+ }
187
+
188
+ CloudCannon.createCustomDataPanel({
189
+ title: "Edit Image",
190
+ data,
191
+ position: this.imageEl?.getBoundingClientRect(),
192
+ config: {
193
+ _inputs: {
194
+ src: {
195
+ label: "Image",
196
+ type: "image",
197
+ ...this.inputConfig.src,
198
+ },
199
+ alt: {
200
+ comment:
201
+ "A description which provides information about this image if for some reason it cannot be viewed.",
202
+ ...this.inputConfig.alt,
203
+ },
204
+ title: {
205
+ comment: "Displayed when hovering over the image.",
206
+ ...this.inputConfig.title,
207
+ },
208
+ },
209
+ },
210
+ onChange: (value): void => {
211
+ if (!value || typeof value !== "object") {
212
+ throw new Error("Invalid image data");
213
+ }
214
+
215
+ if (
216
+ "src" in value &&
217
+ this.configuredSrc &&
218
+ value.src !== this.value?.src
219
+ ) {
220
+ this.dispatchSet(
221
+ this.element.dataset.propSrc ??
222
+ `${this.element.dataset.prop}.src`,
223
+ value.src,
224
+ );
225
+ }
226
+ if (
227
+ "alt" in value &&
228
+ this.configuredAlt &&
229
+ value.alt !== this.value?.alt
230
+ ) {
231
+ this.dispatchSet(
232
+ this.element.dataset.propAlt ??
233
+ `${this.element.dataset.prop}.alt`,
234
+ value.alt,
235
+ );
236
+ }
237
+ if (
238
+ "title" in value &&
239
+ this.configuredTitle &&
240
+ value.title !== this.value?.title
241
+ ) {
242
+ this.dispatchSet(
243
+ this.element.dataset.propTitle ??
244
+ `${this.element.dataset.prop}.title`,
245
+ value.title,
246
+ );
247
+ }
248
+ },
249
+ });
250
+ });
251
+ });
252
+ }
253
+ }
@@ -0,0 +1,148 @@
1
+ import { getEditableSnippetRenderers } from "../helpers/cloudcannon.js";
2
+ import EditableComponent from "./editable-component.js";
3
+ import Editable from "./editable.js";
4
+
5
+ export default class EditableSnippet extends EditableComponent {
6
+ getComponents() {
7
+ return getEditableSnippetRenderers();
8
+ }
9
+
10
+ setupListeners(): void {
11
+ this.element.addEventListener("cloudcannon-api", async (e: any) => {
12
+ if (e.target !== this.element) {
13
+ if (!e.detail.source) {
14
+ e.detail.source = `@snippet[${this.element.getAttribute("data-cms-snippet-id")}]`;
15
+ } else {
16
+ e.detail.source = `@snippet[${this.element.getAttribute("data-cms-snippet-id")}].${e.detail.source}`;
17
+ }
18
+ }
19
+
20
+ const { absolute, snippets } = this.parseSource(e.detail.source);
21
+ if (!this.parent || absolute || snippets) {
22
+ if (this.executeApiCall(e.detail)) {
23
+ e.stopPropagation();
24
+ }
25
+ }
26
+ });
27
+ }
28
+
29
+ validateValue(value: unknown): unknown {
30
+ if (typeof value !== "object") {
31
+ return;
32
+ }
33
+
34
+ if (!value) {
35
+ return value;
36
+ }
37
+
38
+ if (
39
+ !("_snippet_type" in value) ||
40
+ typeof value._snippet_type !== "string"
41
+ ) {
42
+ return;
43
+ }
44
+
45
+ this.element.dataset.component = value._snippet_type;
46
+
47
+ return value;
48
+ }
49
+
50
+ executeApiCall(options: any): boolean {
51
+ const { snippets, source } = this.parseSource(options.source);
52
+
53
+ if (options.action === "get-input-config") {
54
+ return false;
55
+ }
56
+
57
+ const snippet = snippets.pop();
58
+
59
+ if (
60
+ snippet &&
61
+ snippet !== this.element.getAttribute("data-cms-snippet-id")
62
+ ) {
63
+ const snippetEl = document.querySelector(
64
+ `[data-cms-snippet-id="${snippet}"]`,
65
+ );
66
+ if (
67
+ !snippetEl ||
68
+ !("editable" in snippetEl) ||
69
+ !(snippetEl.editable instanceof Editable)
70
+ ) {
71
+ console.error(`Error: Snippet with ID "${snippet}" not found`);
72
+ return false;
73
+ }
74
+ return snippetEl.editable.executeApiCall({
75
+ ...options,
76
+ source,
77
+ });
78
+ }
79
+
80
+ if (source) {
81
+ this.dispatchSnippetChange(source, options);
82
+ }
83
+ return true;
84
+ }
85
+
86
+ async dispatchSnippetChange(source: string, options: any) {
87
+ switch (options.action) {
88
+ case "edit":
89
+ break;
90
+ case "set": {
91
+ const parts = source.split(".");
92
+ const lastPart = parts.pop();
93
+ const temp = await this.lookupPath(parts.join("."), this.value);
94
+
95
+ if (lastPart && temp && typeof temp === "object") {
96
+ temp[lastPart] = options.value;
97
+ }
98
+ break;
99
+ }
100
+ case "move-array-item": {
101
+ const temp = await this.lookupPath(source, this.value);
102
+ if (Array.isArray(temp)) {
103
+ const value = temp.splice(options.fromIndex, 1)[0];
104
+ temp.splice(options.toIndex, 0, value);
105
+ }
106
+ break;
107
+ }
108
+ case "remove-array-item": {
109
+ const temp = await this.lookupPath(source, this.value);
110
+ if (Array.isArray(temp)) {
111
+ temp.splice(options.fromIndex, 1);
112
+ }
113
+ break;
114
+ }
115
+ case "add-array-item": {
116
+ const temp = await this.lookupPath(source, this.value);
117
+ if (Array.isArray(temp)) {
118
+ temp.splice(options.toIndex, 0, options.value);
119
+ }
120
+
121
+ break;
122
+ }
123
+ }
124
+
125
+ this.element.dispatchEvent(
126
+ new CustomEvent("snippet-change", {
127
+ detail: {
128
+ snippetId: this.element.getAttribute("data-cms-snippet-id"),
129
+ isValid: true,
130
+ snippetData: this.value,
131
+ },
132
+ bubbles: true,
133
+ }),
134
+ );
135
+ }
136
+
137
+ mount(): void {}
138
+
139
+ validateConfiguration(): boolean {
140
+ return true;
141
+ }
142
+
143
+ resolveSource(source?: string): string | undefined {
144
+ return this.parent
145
+ ? `${this.parent.resolveSource()}.@snippet[${this.element.getAttribute("data-cms-snippet-id")}].${source}`
146
+ : `@snippet[${this.element.getAttribute("data-cms-snippet-id")}].${source}`;
147
+ }
148
+ }
@@ -0,0 +1,245 @@
1
+ import type { CloudCannonJavaScriptV1APIFile } from "@cloudcannon/javascript-api";
2
+ import { html as beautifyHtml } from "js-beautify";
3
+ import { CloudCannon } from "../helpers/cloudcannon.js";
4
+ import EditableText from "./editable-text.js";
5
+
6
+ const INDENTATION_REGEX = /^([ \t]+)[^\s]/gm;
7
+ const TAG_REGEX =
8
+ /<\s*(?<closing>\/?)\s*(?<tagname>[-a-z]+)(\s+[^>]+)*?\s*(?<selfclosing>\/?)\s*>/gi;
9
+
10
+ const HTML_VOID_ELEMENT: Record<string, boolean> = {
11
+ area: true,
12
+ base: true,
13
+ br: true,
14
+ col: true,
15
+ embed: true,
16
+ hr: true,
17
+ img: true,
18
+ input: true,
19
+ link: true,
20
+ meta: true,
21
+ param: true,
22
+ source: true,
23
+ track: true,
24
+ wbr: true,
25
+ };
26
+
27
+ export default class EditableSource extends EditableText {
28
+ file?: CloudCannonJavaScriptV1APIFile;
29
+ format = {
30
+ leading: "",
31
+ trailing: "",
32
+ indent: "",
33
+ indentSize: 0,
34
+ indentChar: "",
35
+ };
36
+
37
+ setupListeners(): void {
38
+ if (!this.element.dataset.path) {
39
+ return;
40
+ }
41
+ this.file = CloudCannon.file(this.element.dataset.path);
42
+ this.file.addEventListener("change", () => {
43
+ this.file?.get().then(this.pushValue.bind(this));
44
+ });
45
+ this.file.get().then(this.pushValue.bind(this));
46
+ }
47
+
48
+ validateConfiguration(): boolean {
49
+ const path = this.element.dataset.path;
50
+ if (typeof path !== "string") {
51
+ this.element.classList.add("errored");
52
+ const error = document.createElement("editable-region-error-card");
53
+ error.setAttribute("heading", "Failed to render source editable region");
54
+ error.setAttribute("message", "Missing required attribute data-path");
55
+ this.element.replaceChildren(error);
56
+ return false;
57
+ }
58
+
59
+ const key = this.element.dataset.key;
60
+ if (typeof key !== "string") {
61
+ this.element.classList.add("errored");
62
+ const error = document.createElement("editable-region-error-card");
63
+ error.setAttribute("heading", "Failed to render source editable region");
64
+ error.setAttribute("message", "Missing required attribute data-key");
65
+ this.element.replaceChildren(error);
66
+ return false;
67
+ }
68
+ return true;
69
+ }
70
+
71
+ validateValue(value: unknown): string | null | undefined {
72
+ if (typeof value !== "string" && value !== null) {
73
+ this.element.classList.add("errored");
74
+ const error = document.createElement("editable-region-error-card");
75
+ error.setAttribute("heading", "Failed to render source editable region");
76
+ error.setAttribute(
77
+ "message",
78
+ `Illegal value type: ${typeof value}. Supported types are string.`,
79
+ );
80
+ this.element.replaceChildren(error);
81
+ return;
82
+ }
83
+
84
+ if (typeof value === "string") {
85
+ const keyIndex = value.indexOf(`data-key="${this.element.dataset.key}"`);
86
+ if (keyIndex === -1) {
87
+ this.element.classList.add("errored");
88
+ const error = document.createElement("editable-region-error-card");
89
+ error.setAttribute(
90
+ "heading",
91
+ "Failed to render source editable region",
92
+ );
93
+ error.setAttribute(
94
+ "message",
95
+ "Failed to find element with matching data-key attribute",
96
+ );
97
+ this.element.replaceChildren(error);
98
+ return;
99
+ }
100
+
101
+ const nextKeyIndex = value.indexOf(
102
+ `data-key="${this.element.dataset.key}"`,
103
+ keyIndex + 1,
104
+ );
105
+ if (nextKeyIndex !== -1) {
106
+ this.element.classList.add("errored");
107
+ const error = document.createElement("editable-region-error-card");
108
+ error.setAttribute(
109
+ "heading",
110
+ "Failed to render source editable region",
111
+ );
112
+ error.setAttribute(
113
+ "message",
114
+ "Found duplicate data-key attribute. Make sure all source editables have unique data-key attributes",
115
+ );
116
+ this.element.replaceChildren(error);
117
+ return;
118
+ }
119
+ }
120
+
121
+ return value;
122
+ }
123
+
124
+ getSourceIndices(source: string): { start: number; end: number } {
125
+ const keyIndex = source.indexOf(`data-key="${this.element.dataset.key}"`);
126
+ let tagNameIndex = keyIndex;
127
+ while (tagNameIndex >= 0 && source[tagNameIndex] !== "<") {
128
+ tagNameIndex -= 1;
129
+ }
130
+
131
+ const stack = [
132
+ source.substring(tagNameIndex + 1, source.indexOf(" ", tagNameIndex)),
133
+ ];
134
+
135
+ const start = source.indexOf(">", keyIndex);
136
+ source = source.substring(start + 1);
137
+
138
+ for (const tagMatch of source.matchAll(TAG_REGEX)) {
139
+ if (!tagMatch?.groups) {
140
+ continue;
141
+ }
142
+
143
+ const { closing, tagname, selfclosing } = tagMatch.groups;
144
+ if (closing) {
145
+ while (stack.length > 0) {
146
+ if (stack.pop() === tagname) {
147
+ break;
148
+ }
149
+ }
150
+ } else if (!selfclosing && !HTML_VOID_ELEMENT[tagname]) {
151
+ stack.push(tagname);
152
+ }
153
+
154
+ if (stack.length === 0) {
155
+ return { start: start + 1, end: start + 1 + tagMatch.index };
156
+ }
157
+ }
158
+
159
+ return { start: start + 1, end: start + 1 + source.length };
160
+ }
161
+
162
+ update(): void {
163
+ if (!this.value) {
164
+ return;
165
+ }
166
+ const source = this.value;
167
+ for (const indentation of source.matchAll(INDENTATION_REGEX)) {
168
+ if (
169
+ !this.format.indentSize ||
170
+ indentation[1].length < this.format.indentSize
171
+ ) {
172
+ this.format.indentSize = indentation[1].length;
173
+ this.format.indentChar = indentation[0][0];
174
+ }
175
+ }
176
+
177
+ const { start, end } = this.getSourceIndices(source);
178
+ const content = source.substring(start, end);
179
+
180
+ this.format.leading = content.match(/^(\s*\n)[^\n]*?\S/)?.[1] ?? "";
181
+ this.format.trailing = content.match(/\S(\n\s*)$/)?.[1] ?? "";
182
+ this.format.indent =
183
+ content
184
+ .split("\n")
185
+ .filter((line) => line.trim().length > 0)
186
+ .reduce((acc: string | null, line) => {
187
+ if (typeof acc !== "string" || !line.startsWith(acc)) {
188
+ return line.match(/^\s*/)?.[0] ?? "";
189
+ }
190
+
191
+ return acc;
192
+ }, null) ?? "";
193
+
194
+ this.editor?.setContent(content);
195
+ }
196
+
197
+ async mountEditor(): Promise<any> {
198
+ if (this.editor) {
199
+ return this.editor;
200
+ }
201
+
202
+ this.editor = await CloudCannon.createTextEditableRegion(
203
+ this.element,
204
+ this.onChange.bind(this),
205
+ {
206
+ elementType: this.element.dataset.type,
207
+ editableType: "content",
208
+ inputConfig: { type: "html" },
209
+ },
210
+ );
211
+
212
+ if (typeof this.value === "string") {
213
+ this.update();
214
+ }
215
+
216
+ return this.editor;
217
+ }
218
+
219
+ onChange(value?: string | null) {
220
+ this.file?.get().then((source) => {
221
+ value = beautifyHtml(value ?? "", {
222
+ indent_char: this.format.indentChar,
223
+ indent_size: this.format.indentSize,
224
+ });
225
+
226
+ const { start, end } = this.getSourceIndices(source);
227
+ const content =
228
+ source.substring(0, start) +
229
+ this.format.leading +
230
+ value
231
+ .split("\n")
232
+ .map((line) => this.format.indent + line)
233
+ .join("\n") +
234
+ this.format.trailing +
235
+ source.substring(end);
236
+
237
+ if (content === this.value) {
238
+ return;
239
+ }
240
+
241
+ this.value = content;
242
+ this.file?.set(content);
243
+ });
244
+ }
245
+ }