diff --git a/packages/component-library/package.json b/packages/component-library/package.json
index 692371e7a..cb1644561 100644
--- a/packages/component-library/package.json
+++ b/packages/component-library/package.json
@@ -55,6 +55,7 @@
"@tiptap/extension-link": "^2.10.0",
"@tiptap/extension-list-item": "^2.10.0",
"@tiptap/extension-ordered-list": "^2.10.0",
+ "@tiptap/extension-placeholder": "^2.10.4",
"@tiptap/extension-subscript": "^2.9.1",
"@tiptap/extension-superscript": "^2.9.1",
"@tiptap/extension-table": "^2.10.3",
diff --git a/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.interactive.stories.ts b/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.interactive.stories.ts
index da9968229..a6428a658 100644
--- a/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.interactive.stories.ts
+++ b/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.interactive.stories.ts
@@ -47,6 +47,46 @@ export const VisualTestRenderEditorInlineMode: MtTextEditorStory = defineStory({
},
});
+export const VisualTestRenderDisabledEditor: MtTextEditorStory = defineStory({
+ name: "Should render the disabled text editor",
+ args: {
+ disabled: true,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ expect(canvas.getByText("82 characters")).toBeDefined();
+ },
+});
+
+export const VisualTestRenderPlaceholder: MtTextEditorStory = defineStory({
+ name: "Should render the placeholder inside text editor",
+ args: {
+ placeholder: "Type something...",
+ modelValue: "",
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ expect(canvas.getByText("0 characters")).toBeDefined();
+ },
+});
+
+export const VisualTestRenderError: MtTextEditorStory = defineStory({
+ name: "Should render a error in text editor",
+ args: {
+ error: {
+ code: 500,
+ detail: "Error while saving!",
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ expect(canvas.getByText("82 characters")).toBeDefined();
+ },
+});
+
export const VisualTestRenderEditorInlineModeSelected: MtTextEditorStory = defineStory({
name: "Should render the bubble menu in inline mode when text is selected",
args: {
diff --git a/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.stories.ts b/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.stories.ts
index 8cd5307f8..0d3cb6e6e 100644
--- a/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.stories.ts
+++ b/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.stories.ts
@@ -17,6 +17,7 @@ export default {
args: {
modelValue: `
Hello World
Some text
Lorem
Ipsum
First | Second | Third |
---|
Lorem | Ipsum | non |
dolor | sit | amet |
After table
`,
updateModelValue: fn(),
+ label: "My Text editor",
},
render: (args) => ({
components: { MtTextEditor, MtTextEditorToolbarButtonColor },
diff --git a/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.vue b/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.vue
index 58d4f73c9..5f8bea158 100644
--- a/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.vue
+++ b/packages/component-library/src/components/form/mt-text-editor/mt-text-editor.vue
@@ -1,5 +1,9 @@
+
+
@@ -58,11 +62,17 @@
class="mt-text-editor__code-editor"
wrap
basic
+ :disabled="disabled"
/>
+
+
@@ -132,6 +144,7 @@ import Table from "@tiptap/extension-table";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import TableRow from "@tiptap/extension-table-row";
+import Placeholder from '@tiptap/extension-placeholder'
import mtTextEditorToolbar, { type CustomButton } from "./_internal/mt-text-editor-toolbar.vue";
import mtTextEditorToolbarButtonColor, {
colorButton,
@@ -145,6 +158,7 @@ import mtTextEditorToolbarButtonTable, {
import mtTextEditorToolbarButton from "./_internal/mt-text-editor-toolbar-button.vue";
import mtPopoverItem from "@/components/overlay/mt-popover-item/mt-popover-item.vue";
import mtPopover from "@/components/overlay/mt-popover/mt-popover.vue";
+import mtFieldError from "../_internal/mt-field-error/mt-field-error.vue";
import CodeMirror from "vue-codemirror6";
import { computed, h, reactive, ref, watch, type PropType } from "vue";
import { html } from "@codemirror/lang-html";
@@ -212,11 +226,48 @@ const props = defineProps({
type: Array as PropType,
default: () => [],
},
+ /**
+ * Add disabled state to the editor
+ */
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+
+ /**
+ * Add placeholder text to the editor
+ */
+ placeholder: {
+ type: String,
+ default: "",
+ },
+
+ /**
+ * An error in your business logic related to this field.
+ *
+ * @example {"code": 500, "detail": "Error while saving"}
+ */
+ error: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+
+ /**
+ * A label for your text field. Usually used to guide the user what value this field controls.
+ */
+ label: {
+ type: String,
+ required: false,
+ default: null,
+ },
});
const componentClasses = computed(() => {
return {
"mt-text-editor--inline-edit": props.isInlineEdit,
+ "mt-text-editor--disabled": props.disabled,
+ "mt-text-editor--error": !!props.error,
};
});
@@ -245,6 +296,10 @@ const editor = useEditor({
TableRow,
TableHeader,
TableCell,
+ Placeholder.configure({
+ placeholder: props.placeholder,
+ showOnlyWhenEditable: true,
+ }),
...(props.tipTapConfig.extensions ?? []),
],
content: props.modelValue,
@@ -256,6 +311,7 @@ const editor = useEditor({
onUpdate: ({ editor }) => {
emit("update:modelValue", editor.getHTML());
},
+ editable: !props.disabled,
});
watch(
@@ -275,6 +331,17 @@ watch(
},
);
+watch(
+ () => props.disabled,
+ (newValue) => {
+ editor.value?.setEditable(!newValue);
+ },
+);
+
+const globalToolbarButtonDisabled = computed(() => {
+ return props.disabled || showCodeEditor.value;
+});
+
/**
* Custom buttons
*/
@@ -336,6 +403,14 @@ watch(
background-color: var(--color-elevation-surface-default);
}
+label {
+ display: block;
+ font-size: var(--font-size-xs);
+ line-height: 1rem;
+ color: var(--color-text-primary-default);
+ margin-bottom: var(--scale-size-8);
+}
+
.mt-text-editor__box {
border: 1px solid var(--color-border-primary-default);
border-radius: var(--border-radius-xs);
@@ -378,7 +453,7 @@ watch(
h5,
h6 {
font-weight: var(--font-weight-semibold);
- color: var(--color-text-secondary-default);
+ color: var(--color-text-primary-default);
letter-spacing: 0;
margin-bottom: 0;
}
@@ -424,7 +499,7 @@ watch(
font-weight: normal;
font-size: var(--font-size-s);
line-height: var(--font-line-height-m);
- color: var(--color-text-secondary-default);
+ color: var(--color-text-primary-default);
letter-spacing: 0;
margin-top: var(--scale-size-16);
}
@@ -433,7 +508,7 @@ watch(
font-size: var(--font-size-s);
font-style: italic;
line-height: var(--font-line-height-m);
- color: var(--color-text-secondary-default);
+ color: var(--color-text-primary-default);
margin-left: var(--scale-size-20);
position: relative;
margin-top: var(--scale-size-16);
@@ -458,7 +533,7 @@ watch(
font-weight: normal;
font-size: var(--font-size-s);
line-height: var(--font-line-height-m);
- color: var(--color-text-secondary-default);
+ color: var(--color-text-primary-default);
margin-bottom: var(--scale-size-4);
}
@@ -588,4 +663,29 @@ watch(
pointer-events: all;
transform: scale(1, 1);
}
+
+.mt-text-editor--disabled .mt-text-editor__content {
+ background-color: var(--color-background-primary-disabled);
+}
+
+:deep(.mt-text-editor__content-editor p.is-editor-empty:first-child::before) {
+ color: var(--color-text-secondary-default);
+ content: attr(data-placeholder);
+ float: left;
+ height: 0;
+ pointer-events: none;
+}
+
+.mt-text-editor--error .mt-text-editor__box {
+ border-color: var(--color-icon-critical-default);
+}
+
+.mt-text-editor--error .mt-text-editor__content {
+ background-color: var(--color-background-critical-dark);
+}
+
+.mt-text-editor--error label {
+ color: var(--color-text-critical-default);
+}
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 78dd818e8..7df8bc6d4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -327,6 +327,9 @@ importers:
'@tiptap/extension-ordered-list':
specifier: ^2.10.0
version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))
+ '@tiptap/extension-placeholder':
+ specifier: ^2.10.4
+ version: 2.10.4(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)
'@tiptap/extension-subscript':
specifier: ^2.9.1
version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))
@@ -3843,6 +3846,12 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
+ '@tiptap/extension-placeholder@2.10.4':
+ resolution: {integrity: sha512-leWG4xP7cvddR6alGZS7yojOh9941bxehgAeQDLlEisaJcNa2Od5Vbap2zipjc5sXMxZakQVChL27oH1wWhHkQ==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+ '@tiptap/pm': ^2.7.0
+
'@tiptap/extension-strike@2.10.3':
resolution: {integrity: sha512-jYoPy6F6njYp3txF3u23bgdRy/S5ATcWDO9LPZLHSeikwQfJ47nqb+EUNo5M8jIOgFBTn4MEbhuZ6OGyhnxopA==}
peerDependencies:
@@ -17529,7 +17538,7 @@ snapshots:
node-fetch: 2.7.0(encoding@0.1.13)
picomatch: 2.3.1
pkg-dir: 5.0.0
- prettier-fallback: prettier@3.2.5
+ prettier-fallback: prettier@3.3.3
pretty-hrtime: 1.0.3
resolve-from: 5.0.0
semver: 7.6.2
@@ -18096,6 +18105,11 @@ snapshots:
dependencies:
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
+ '@tiptap/extension-placeholder@2.10.4(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)':
+ dependencies:
+ '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
+ '@tiptap/pm': 2.10.3
+
'@tiptap/extension-strike@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))':
dependencies:
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
@@ -19743,7 +19757,7 @@ snapshots:
dependencies:
'@vueuse/core': 10.11.0(vue@3.5.13(typescript@5.6.2))
'@vueuse/shared': 10.11.0(vue@3.5.13(typescript@5.6.2))
- vue-demi: 0.14.8(vue@3.5.13(typescript@5.6.2))
+ vue-demi: 0.14.10(vue@3.5.13(typescript@5.6.2))
optionalDependencies:
axios: 1.7.2
change-case: 4.1.2
@@ -21447,10 +21461,6 @@ snapshots:
dependencies:
ms: 2.0.0
- debug@3.2.7:
- dependencies:
- ms: 2.1.3
-
debug@3.2.7(supports-color@5.5.0):
dependencies:
ms: 2.1.3
@@ -26457,7 +26467,7 @@ snapshots:
portfinder@1.0.32:
dependencies:
async: 2.6.4
- debug: 3.2.7
+ debug: 3.2.7(supports-color@5.5.0)
mkdirp: 0.5.6
transitivePeerDependencies:
- supports-color
@@ -30021,6 +30031,10 @@ snapshots:
dependencies:
vue: 3.5.13(typescript@5.2.2)
+ vue-demi@0.14.10(vue@3.5.13(typescript@5.6.2)):
+ dependencies:
+ vue: 3.5.13(typescript@5.6.2)
+
vue-demi@0.14.8(vue@3.5.13(typescript@5.2.2)):
dependencies:
vue: 3.5.13(typescript@5.2.2)