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

  1. Lorem

  2. 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 @@ @@ -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)