Compare commits
58 Commits
v1.4.9
...
7dec45b374
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dec45b374 | |||
| ea92acff3a | |||
| a3421c02e9 | |||
| 5563d89743 | |||
| 640ff90187 | |||
| 2eb7a5247a | |||
| 3336ff0c69 | |||
| da3a4cb349 | |||
| 0ddae4dd70 | |||
| 23210e6868 | |||
| 1c0fcd24e3 | |||
| d74f3acc97 | |||
| 014a057196 | |||
| 73483b0573 | |||
| 4855923008 | |||
| fc844078a6 | |||
| 02495245a5 | |||
| 330fb2130b | |||
| 5acefc1d59 | |||
| e77bf49146 | |||
| f59f866354 | |||
| 660c3787fd | |||
| e9741ff38d | |||
| 32608c8f71 | |||
| e1965db04e | |||
| 0ad344bab9 | |||
| 96719be78d | |||
| b90baec571 | |||
| 384f86a3b3 | |||
| e8ddf4e083 | |||
| 7ee64289a8 | |||
| f09f8a91ac | |||
| bcadd46ce2 | |||
| e76337502a | |||
| 968b7087b5 | |||
| 3deba3f369 | |||
| cf46ab0c85 | |||
| 09cc3edf6f | |||
| c95a3657c0 | |||
| 9843f4d032 | |||
| 9d9b9c9dc4 | |||
| 187ef52865 | |||
| 9925f1ced4 | |||
| ded414ba1a | |||
| 11d60e687b | |||
| d3038994c3 | |||
| 0d350e12c6 | |||
| c6acaace27 | |||
| 927c7c3c70 | |||
| bf0aa92497 | |||
| 88dd76a0e4 | |||
| cc04114f89 | |||
| f456ea4ddf | |||
| 77364daa67 | |||
| 1ab7b2427a | |||
| 82ecc9cfe2 | |||
| 65d9060e26 | |||
| ec4c157226 |
@@ -134,7 +134,9 @@ Zone de texte multiligne avec compteur et redimensionnement.
|
||||
|
||||
## MalioInputRichText
|
||||
|
||||
Éditeur de texte riche basé sur **TipTap v3** + **StarterKit** + **tiptap-markdown**. Toolbar avec gras, italique, barré, titres H2/H3, listes, citation, code, code-block, lien, undo/redo. Sortie en markdown (par défaut) ou HTML.
|
||||
Éditeur de texte riche basé sur **TipTap v3** + **StarterKit** + **tiptap-markdown** + **TextStyle/Color/Highlight**. Toolbar avec gras, italique, barré, titres H2/H3, listes, citation, code, code-block, lien, **couleur du texte**, **surlignage**, undo/redo. Sortie en HTML (par défaut) ou markdown.
|
||||
|
||||
> Couleurs et surlignages ne sont **pas persistés en markdown**. Pour les conserver au save/reload, utiliser `output-format="html"`.
|
||||
|
||||
| Prop | Type | Défaut | Description |
|
||||
|------|------|--------|-------------|
|
||||
@@ -149,7 +151,7 @@ Zone de texte multiligne avec compteur et redimensionnement.
|
||||
| `hint` | `string` | `''` | Message d'aide |
|
||||
| `error` | `string` | `''` | Message d'erreur |
|
||||
| `success` | `string` | `''` | Message de succès |
|
||||
| `outputFormat` | `'markdown' \| 'html'` | `'markdown'` | Format émis dans `update:modelValue` |
|
||||
| `outputFormat` | `'markdown' \| 'html'` | `'html'` | Format émis dans `update:modelValue` |
|
||||
| `groupClass` | `string` | `''` | Classes CSS conteneur (twMerge) |
|
||||
| `labelClass` | `string` | `''` | Classes CSS label (twMerge) |
|
||||
| `editorClass` | `string` | `''` | Classes CSS wrapper éditeur (twMerge) |
|
||||
@@ -159,7 +161,7 @@ Zone de texte multiligne avec compteur et redimensionnement.
|
||||
```vue
|
||||
<MalioInputRichText v-model="note" label="Note" placeholder="Écrire ici…" />
|
||||
<MalioInputRichText v-model="cr" label="Compte-rendu" error="Trop court" />
|
||||
<MalioInputRichText v-model="article" label="Article" output-format="html" min-height="240px" />
|
||||
<MalioInputRichText v-model="article" label="Article" min-height="240px" />
|
||||
<MalioInputRichText :model-value="content" :editable="false" />
|
||||
```
|
||||
|
||||
|
||||
@@ -61,10 +61,42 @@ describe('MalioInputRichText', () => {
|
||||
expect(wrapper.find('button[title="Gras"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[title="Italique"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[title="Lien"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[title="Couleur du texte"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[title="Surlignage"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[title="Annuler"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[title="Rétablir"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('opens and closes the text color palette', async () => {
|
||||
const wrapper = await mountComponent({modelValue: ''})
|
||||
|
||||
expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(false)
|
||||
|
||||
await wrapper.get('button[title="Couleur du texte"]').trigger('click')
|
||||
expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('button[title="Couleur du texte"]').trigger('click')
|
||||
expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('opens the highlight palette and closes the color palette', async () => {
|
||||
const wrapper = await mountComponent({modelValue: ''})
|
||||
|
||||
await wrapper.get('button[title="Couleur du texte"]').trigger('click')
|
||||
expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(true)
|
||||
|
||||
await wrapper.get('button[title="Surlignage"]').trigger('click')
|
||||
expect(wrapper.find('[aria-label="Palette de surlignage"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('disables color and highlight buttons when readonly', async () => {
|
||||
const wrapper = await mountComponent({readonly: true, modelValue: ''})
|
||||
|
||||
expect(wrapper.get('button[title="Couleur du texte"]').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.get('button[title="Surlignage"]').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('does not render the toolbar in readonly display mode (editable=false)', async () => {
|
||||
const wrapper = await mountComponent({editable: false, modelValue: '**hi**'})
|
||||
|
||||
|
||||
@@ -46,6 +46,110 @@
|
||||
|
||||
<span class="mx-1 h-5 w-px bg-m-border" aria-hidden="true" />
|
||||
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-8 flex-col items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="colorPickerOpen ? 'bg-m-primary/15 text-m-primary' : ''"
|
||||
title="Couleur du texte"
|
||||
aria-label="Couleur du texte"
|
||||
:aria-expanded="colorPickerOpen"
|
||||
:disabled="disabled || readonly"
|
||||
@mousedown.prevent
|
||||
@click="toggleColorPicker"
|
||||
>
|
||||
<IconifyIcon icon="mdi:format-color-text" :width="18" :height="18" />
|
||||
<span
|
||||
class="-mt-0.5 block h-1 w-4 rounded-sm"
|
||||
:style="{ backgroundColor: currentTextColor ?? 'transparent' }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-if="colorPickerOpen"
|
||||
class="absolute left-0 top-full z-10 mt-1 flex w-44 flex-col gap-2 rounded-md border border-m-border bg-white p-2 shadow-lg"
|
||||
role="dialog"
|
||||
aria-label="Palette couleur du texte"
|
||||
>
|
||||
<div class="grid grid-cols-4 gap-1">
|
||||
<button
|
||||
v-for="swatch in textColorSwatches"
|
||||
:key="swatch.value"
|
||||
type="button"
|
||||
class="h-7 w-7 rounded border border-m-border transition-transform hover:scale-110"
|
||||
:class="currentTextColor === swatch.value ? 'ring-2 ring-m-primary ring-offset-1' : ''"
|
||||
:style="{ backgroundColor: swatch.value }"
|
||||
:title="swatch.label"
|
||||
:aria-label="swatch.label"
|
||||
@mousedown.prevent
|
||||
@click="applyTextColor(swatch.value)"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center gap-1 rounded border border-m-border px-2 py-1 text-xs text-m-text transition-colors hover:bg-m-bg"
|
||||
@mousedown.prevent
|
||||
@click="applyTextColor(null)"
|
||||
>
|
||||
<IconifyIcon icon="mdi:format-color-marker-cancel" :width="14" :height="14" />
|
||||
Aucune couleur
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-8 flex-col items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="highlightPickerOpen ? 'bg-m-primary/15 text-m-primary' : ''"
|
||||
title="Surlignage"
|
||||
aria-label="Surlignage"
|
||||
:aria-expanded="highlightPickerOpen"
|
||||
:disabled="disabled || readonly"
|
||||
@mousedown.prevent
|
||||
@click="toggleHighlightPicker"
|
||||
>
|
||||
<IconifyIcon icon="mdi:marker" :width="18" :height="18" />
|
||||
<span
|
||||
class="-mt-0.5 block h-1 w-4 rounded-sm"
|
||||
:style="{ backgroundColor: currentHighlightColor ?? 'transparent' }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-if="highlightPickerOpen"
|
||||
class="absolute left-0 top-full z-10 mt-1 flex w-44 flex-col gap-2 rounded-md border border-m-border bg-white p-2 shadow-lg"
|
||||
role="dialog"
|
||||
aria-label="Palette de surlignage"
|
||||
>
|
||||
<div class="grid grid-cols-4 gap-1">
|
||||
<button
|
||||
v-for="swatch in highlightSwatches"
|
||||
:key="swatch.value"
|
||||
type="button"
|
||||
class="h-7 w-7 rounded border border-m-border transition-transform hover:scale-110"
|
||||
:class="currentHighlightColor === swatch.value ? 'ring-2 ring-m-primary ring-offset-1' : ''"
|
||||
:style="{ backgroundColor: swatch.value }"
|
||||
:title="swatch.label"
|
||||
:aria-label="swatch.label"
|
||||
@mousedown.prevent
|
||||
@click="applyHighlight(swatch.value)"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center gap-1 rounded border border-m-border px-2 py-1 text-xs text-m-text transition-colors hover:bg-m-bg"
|
||||
@mousedown.prevent
|
||||
@click="applyHighlight(null)"
|
||||
>
|
||||
<IconifyIcon icon="mdi:format-color-marker-cancel" :width="14" :height="14" />
|
||||
Aucun surlignage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="mx-1 h-5 w-px bg-m-border" aria-hidden="true" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-8 items-center justify-center rounded text-m-text transition-colors hover:bg-m-primary/10 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
@@ -97,11 +201,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, shallowRef, useId, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, useId, watch } from 'vue'
|
||||
import { Icon as IconifyIcon } from '@iconify/vue'
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { TextStyle } from '@tiptap/extension-text-style'
|
||||
import Color from '@tiptap/extension-color'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import { Markdown } from 'tiptap-markdown'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
@@ -139,7 +246,7 @@ const props = withDefaults(
|
||||
hint: '',
|
||||
error: '',
|
||||
success: '',
|
||||
outputFormat: 'markdown',
|
||||
outputFormat: 'html',
|
||||
groupClass: '',
|
||||
labelClass: '',
|
||||
editorClass: '',
|
||||
@@ -207,9 +314,18 @@ const mergedReadonlyClass = computed(() =>
|
||||
|
||||
const focusEditor = () => {
|
||||
if (isInteractionLocked.value) return
|
||||
closePickers()
|
||||
editor.value?.commands.focus()
|
||||
}
|
||||
|
||||
const htmlPattern = /<\/?[a-z][\s\S]*>/i
|
||||
|
||||
const normalizeEditorInput = (value: string | null | undefined): string => {
|
||||
const content = (value ?? '').replace(/\r\n?/g, '\n')
|
||||
if (htmlPattern.test(content)) return content
|
||||
return content.split('\n').join('\n\n').replace(/\n{3,}/g, '\n\n')
|
||||
}
|
||||
|
||||
const promptForLink = () => {
|
||||
if (!editor.value) return
|
||||
const previous = editor.value.getAttributes('link').href as string | undefined
|
||||
@@ -222,6 +338,78 @@ const promptForLink = () => {
|
||||
editor.value.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}
|
||||
|
||||
type ColorSwatch = { label: string; value: string }
|
||||
|
||||
const textColorSwatches: ColorSwatch[] = [
|
||||
{ label: 'Rouge', value: '#bf2600' },
|
||||
{ label: 'Orange', value: '#ff8b00' },
|
||||
{ label: 'Jaune', value: '#ffc400' },
|
||||
{ label: 'Vert', value: '#00875a' },
|
||||
{ label: 'Turquoise', value: '#00a3bf' },
|
||||
{ label: 'Bleu', value: '#0747a6' },
|
||||
{ label: 'Violet', value: '#5243aa' },
|
||||
{ label: 'Gris', value: '#42526e' },
|
||||
]
|
||||
|
||||
const highlightSwatches: ColorSwatch[] = [
|
||||
{ label: 'Rouge', value: '#fdd0c8' },
|
||||
{ label: 'Orange', value: '#ffe2c2' },
|
||||
{ label: 'Jaune', value: '#fff0b3' },
|
||||
{ label: 'Vert', value: '#c6edd0' },
|
||||
{ label: 'Turquoise', value: '#c1ecf0' },
|
||||
{ label: 'Bleu', value: '#cce0ff' },
|
||||
{ label: 'Violet', value: '#dfd8fa' },
|
||||
{ label: 'Gris', value: '#dfe1e6' },
|
||||
]
|
||||
|
||||
const colorPickerOpen = ref(false)
|
||||
const highlightPickerOpen = ref(false)
|
||||
|
||||
const closePickers = () => {
|
||||
colorPickerOpen.value = false
|
||||
highlightPickerOpen.value = false
|
||||
}
|
||||
|
||||
const toggleColorPicker = () => {
|
||||
highlightPickerOpen.value = false
|
||||
colorPickerOpen.value = !colorPickerOpen.value
|
||||
}
|
||||
|
||||
const toggleHighlightPicker = () => {
|
||||
colorPickerOpen.value = false
|
||||
highlightPickerOpen.value = !highlightPickerOpen.value
|
||||
}
|
||||
|
||||
const applyTextColor = (value: string | null) => {
|
||||
if (!editor.value) return
|
||||
if (value === null) {
|
||||
editor.value.chain().focus().unsetColor().run()
|
||||
} else {
|
||||
editor.value.chain().focus().setColor(value).run()
|
||||
}
|
||||
colorPickerOpen.value = false
|
||||
}
|
||||
|
||||
const applyHighlight = (value: string | null) => {
|
||||
if (!editor.value) return
|
||||
if (value === null) {
|
||||
editor.value.chain().focus().unsetHighlight().run()
|
||||
} else {
|
||||
editor.value.chain().focus().setHighlight({ color: value }).run()
|
||||
}
|
||||
highlightPickerOpen.value = false
|
||||
}
|
||||
|
||||
const currentTextColor = computed(() => {
|
||||
const attrs = editor.value?.getAttributes('textStyle') as { color?: string } | undefined
|
||||
return attrs?.color ?? null
|
||||
})
|
||||
|
||||
const currentHighlightColor = computed(() => {
|
||||
const attrs = editor.value?.getAttributes('highlight') as { color?: string } | undefined
|
||||
return attrs?.color ?? null
|
||||
})
|
||||
|
||||
const toolbarButtons = computed(() => {
|
||||
const e = editor.value
|
||||
return [
|
||||
@@ -242,13 +430,34 @@ const toolbarButtons = computed(() => {
|
||||
const getCurrentValue = (): string => {
|
||||
if (!editor.value) return ''
|
||||
if (props.outputFormat === 'html') return editor.value.getHTML()
|
||||
const storage = editor.value.storage.markdown as { getMarkdown: () => string } | undefined
|
||||
return storage ? storage.getMarkdown() : editor.value.getHTML()
|
||||
const storage = (editor.value.storage as unknown as Record<string, { getMarkdown?: () => string } | undefined>).markdown
|
||||
return storage?.getMarkdown ? storage.getMarkdown() : editor.value.getHTML()
|
||||
}
|
||||
|
||||
const handleDocumentMousedown = (event: MouseEvent) => {
|
||||
if (!colorPickerOpen.value && !highlightPickerOpen.value) return
|
||||
const target = event.target as Node | null
|
||||
if (!target) return
|
||||
const popovers = document.querySelectorAll(`#${editorId.value} [role="dialog"]`)
|
||||
const triggers = document.querySelectorAll(`#${editorId.value} [aria-expanded]`)
|
||||
for (const node of [...popovers, ...triggers]) {
|
||||
if (node.contains(target)) return
|
||||
}
|
||||
closePickers()
|
||||
}
|
||||
|
||||
const handleDocumentKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && (colorPickerOpen.value || highlightPickerOpen.value)) {
|
||||
closePickers()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousedown', handleDocumentMousedown)
|
||||
document.addEventListener('keydown', handleDocumentKeydown)
|
||||
|
||||
editor.value = new Editor({
|
||||
content: props.modelValue ?? '',
|
||||
content: normalizeEditorInput(props.modelValue),
|
||||
editable: props.editable && !props.disabled && !props.readonly,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
@@ -259,11 +468,14 @@ onMounted(() => {
|
||||
HTMLAttributes: { rel: 'noopener noreferrer nofollow', target: '_blank' },
|
||||
},
|
||||
}),
|
||||
TextStyle,
|
||||
Color.configure({ types: ['textStyle'] }),
|
||||
Highlight.configure({ multicolor: true }),
|
||||
Placeholder.configure({
|
||||
placeholder: props.placeholder,
|
||||
}),
|
||||
Markdown.configure({
|
||||
html: false,
|
||||
html: true,
|
||||
tightLists: true,
|
||||
bulletListMarker: '-',
|
||||
linkify: true,
|
||||
@@ -290,20 +502,22 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousedown', handleDocumentMousedown)
|
||||
document.removeEventListener('keydown', handleDocumentKeydown)
|
||||
editor.value?.destroy()
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (incoming) => {
|
||||
if (!editor.value) return
|
||||
if ((incoming ?? '') === getCurrentValue()) return
|
||||
editor.value.commands.setContent(incoming ?? '', { emitUpdate: false })
|
||||
editor.value.commands.setContent(normalizeEditorInput(incoming), { emitUpdate: false })
|
||||
})
|
||||
|
||||
watch(() => [props.editable, props.disabled, props.readonly], () => {
|
||||
editor.value?.setEditable(props.editable && !props.disabled && !props.readonly)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.malio-rich-text :deep(.ProseMirror) {
|
||||
outline: none;
|
||||
@@ -323,4 +537,38 @@ watch(() => [props.editable, props.disabled, props.readonly], () => {
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
.malio-rich-text :deep(h2) {
|
||||
margin: 0.75rem 0 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 700;
|
||||
color: rgb(var(--m-text));
|
||||
}
|
||||
.malio-rich-text :deep(h3) {
|
||||
margin: 0.65rem 0 0.4rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: rgb(var(--m-text));
|
||||
}
|
||||
.malio-rich-text :deep(p) {
|
||||
margin: 0.45rem 0;
|
||||
}
|
||||
.malio-rich-text :deep(ul),
|
||||
.malio-rich-text :deep(ol) {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.malio-rich-text :deep(ul) {
|
||||
list-style: disc;
|
||||
}
|
||||
.malio-rich-text :deep(ol) {
|
||||
list-style: decimal;
|
||||
}
|
||||
.malio-rich-text :deep(blockquote) {
|
||||
margin: 0.75rem 0;
|
||||
border-left: 3px solid rgb(var(--m-border));
|
||||
padding-left: 0.75rem;
|
||||
color: rgb(var(--m-muted));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,6 +72,17 @@
|
||||
min-height="200px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4 lg:col-span-2">
|
||||
<h2 class="mb-4 text-xl font-bold">Couleurs & surlignage</h2>
|
||||
<MalioInputRichText
|
||||
v-model="colorValue"
|
||||
label="Note colorée"
|
||||
output-format="html"
|
||||
min-height="180px"
|
||||
hint="Tester les boutons couleur du texte et surlignage (palettes Jira-like)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -80,7 +91,7 @@
|
||||
# MalioInputRichText
|
||||
|
||||
Éditeur de texte riche basé sur **TipTap v3** + **StarterKit** + **tiptap-markdown**.
|
||||
Sortie en **markdown** (par défaut) ou en **HTML**. Aligné sur le thème Malio
|
||||
Sortie en **HTML** (par défaut) ou en **markdown**. Aligné sur le thème Malio
|
||||
(couleurs `m-*`, icônes `mdi:*`, états error / success / hint).
|
||||
|
||||
------------------------------------------------------------------------
|
||||
@@ -144,10 +155,10 @@ Sortie en **markdown** (par défaut) ou en **HTML**. Aligné sur le thème Malio
|
||||
### outputFormat
|
||||
|
||||
- Type: `'markdown' | 'html'`
|
||||
- Défaut: `'markdown'`
|
||||
- Défaut: `'html'`
|
||||
- Description: Format émis dans `update:modelValue`.
|
||||
- `markdown` : utilise `tiptap-markdown` (`getMarkdown()`).
|
||||
- `html` : utilise `editor.getHTML()`.
|
||||
- `markdown` : utilise `tiptap-markdown` (`getMarkdown()`).
|
||||
|
||||
### groupClass / labelClass / editorClass
|
||||
|
||||
@@ -166,8 +177,15 @@ Boutons (icônes `mdi:*`) :
|
||||
- Citation
|
||||
- Code inline, Bloc de code
|
||||
- Lien (prompt URL ; vide pour retirer)
|
||||
- Couleur du texte (palette de 8 swatches + reset)
|
||||
- Surlignage (palette de 8 swatches + reset)
|
||||
- Annuler / Rétablir
|
||||
|
||||
Les palettes couleur/surlignage s'ouvrent en popover sous leur bouton.
|
||||
Fermeture : clic sur un swatch, clic en dehors, ou touche **Échap**.
|
||||
|
||||
> Les couleurs et surlignages ne sont **pas persistés en markdown** (spec Markdown ne couvre pas la couleur). Pour préserver les couleurs au save/reload, utiliser `output-format="html"`.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Accessibilité
|
||||
@@ -199,4 +217,5 @@ const successValue = ref('Tout est bon de mon côté.')
|
||||
const disabledValue = ref('Contenu indisponible.')
|
||||
const readonlyValue = ref('## Compte-rendu\n\n- Point 1\n- Point 2\n\n> Citation importante')
|
||||
const htmlValue = ref('<p>Contenu <strong>riche</strong>.</p>')
|
||||
const colorValue = ref('<p>Sélectionner du texte puis utiliser les boutons <span style="color: #bf2600">couleur</span> ou <mark data-color="#fff0b3" style="background-color: #fff0b3">surlignage</mark>.</p>')
|
||||
</script>
|
||||
|
||||
43
package-lock.json
generated
43
package-lock.json
generated
@@ -10,7 +10,10 @@
|
||||
"dependencies": {
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@tiptap/extension-color": "^3.22.5",
|
||||
"@tiptap/extension-highlight": "^3.22.5",
|
||||
"@tiptap/extension-placeholder": "^3.22.5",
|
||||
"@tiptap/extension-text-style": "^3.22.5",
|
||||
"@tiptap/pm": "^3.22.5",
|
||||
"@tiptap/starter-kit": "^3.22.5",
|
||||
"@tiptap/vue-3": "^3.22.5",
|
||||
@@ -5142,6 +5145,19 @@
|
||||
"@tiptap/pm": "3.22.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-color": {
|
||||
"version": "3.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.22.5.tgz",
|
||||
"integrity": "sha512-4aTygOUlTFBYCvJy67SeKVdXCQw7du3Rj+N5ZutVnDnrpfzUBWsO7f+I+iDS8eMQFbWxVFLlWxGMcTbjtk1a+Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-text-style": "3.22.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "3.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.5.tgz",
|
||||
@@ -5223,6 +5239,19 @@
|
||||
"@tiptap/core": "3.22.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-highlight": {
|
||||
"version": "3.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.22.5.tgz",
|
||||
"integrity": "sha512-byWruAOKcqRN0OuzVSKqLLrced3M9AZaR2pD1BV3aUZHzMzeBjLBfByh8s4lExH2Z547xQUdHHnUflBQ572I5A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.22.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||
"version": "3.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.5.tgz",
|
||||
@@ -5373,6 +5402,20 @@
|
||||
"@tiptap/core": "3.22.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text-style": {
|
||||
"version": "3.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.22.5.tgz",
|
||||
"integrity": "sha512-jt63jy8YbhZJUGMxTUzeivLhowGtFp6YbCFrrmZJ7G6IHu8X8LJzO81ksz5nT5l8DKpldGwnINUfA6iE91JIAg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.22.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "3.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.5.tgz",
|
||||
|
||||
@@ -42,7 +42,10 @@
|
||||
"dependencies": {
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@tiptap/extension-color": "^3.22.5",
|
||||
"@tiptap/extension-highlight": "^3.22.5",
|
||||
"@tiptap/extension-placeholder": "^3.22.5",
|
||||
"@tiptap/extension-text-style": "^3.22.5",
|
||||
"@tiptap/pm": "^3.22.5",
|
||||
"@tiptap/starter-kit": "^3.22.5",
|
||||
"@tiptap/vue-3": "^3.22.5",
|
||||
|
||||
Reference in New Issue
Block a user