feat(editor): implement US-EDITOR-UX with global block selection, redesigned slash menu (favorites & preview), contextual placeholders, smart paste extended, turn into and undo/redo toasts
This commit is contained in:
60
memento-note/lib/editor/block-selection-extension.ts
Normal file
60
memento-note/lib/editor/block-selection-extension.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey, NodeSelection } from '@tiptap/pm/state'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
/**
|
||||
* Extension ProseMirror/TipTap qui ajoute la classe CSS 'block-selected'
|
||||
* aux blocs de premier niveau inclus ou traversés par la sélection active.
|
||||
*/
|
||||
export const BlockSelectionExtension = Extension.create({
|
||||
name: 'blockSelection',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('blockSelection'),
|
||||
props: {
|
||||
decorations(state) {
|
||||
const { from, to, empty } = state.selection
|
||||
if (empty) return DecorationSet.empty
|
||||
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
// Parcourir uniquement les nœuds de premier niveau (enfants directs de doc)
|
||||
state.doc.forEach((node, offset) => {
|
||||
const start = offset
|
||||
const end = offset + node.nodeSize
|
||||
|
||||
// Vérifier si la plage de sélection [from, to] intersecte le bloc [start, end]
|
||||
const intersects = Math.max(start, from) < Math.min(end, to)
|
||||
if (intersects) {
|
||||
// Il faut s'assurer que c'est un nœud de bloc pour appliquer Decoration.node
|
||||
if (node.isBlock) {
|
||||
try {
|
||||
decorations.push(
|
||||
Decoration.node(start, end, {
|
||||
class: 'block-selected',
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
// Fallback en cas d'erreur sur des structures de nœuds particulières
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isNodeSel = state.selection instanceof NodeSelection
|
||||
|
||||
// Si la sélection s'étend sur plusieurs blocs ou si c'est une NodeSelection explicite,
|
||||
// on renvoie l'ensemble des décorations pour coloration visuelle.
|
||||
if (decorations.length > 1 || (decorations.length === 1 && isNodeSel)) {
|
||||
return DecorationSet.create(state.doc, decorations)
|
||||
}
|
||||
|
||||
return DecorationSet.empty
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
27
memento-note/lib/editor/turn-into-shortcut-extension.ts
Normal file
27
memento-note/lib/editor/turn-into-shortcut-extension.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
|
||||
/**
|
||||
* Extension TipTap qui permet de transformer instantanément le bloc sous le curseur
|
||||
* en cyclant entre Titre 1, Titre 2, Titre 3 et Paragraphe normal via le raccourci clavier Cmd+Shift+H (ou Ctrl+Shift+H).
|
||||
*/
|
||||
export const TurnIntoShortcutExtension = Extension.create({
|
||||
name: 'turnIntoShortcut',
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-Shift-h': () => {
|
||||
const { editor } = this
|
||||
|
||||
if (editor.isActive('heading', { level: 1 })) {
|
||||
return editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
} else if (editor.isActive('heading', { level: 2 })) {
|
||||
return editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
} else if (editor.isActive('heading', { level: 3 })) {
|
||||
return editor.chain().focus().setParagraph().run()
|
||||
} else {
|
||||
return editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
48
memento-note/lib/editor/undo-redo-feedback-extension.ts
Normal file
48
memento-note/lib/editor/undo-redo-feedback-extension.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
/**
|
||||
* Extension TipTap qui intercepte les commandes d'annulation (Undo) et de rétablissement (Redo)
|
||||
* pour lever un toast discret (2 secondes) de confirmation, en indiquant dynamiquement le raccourci de l'action inverse selon l'OS.
|
||||
*/
|
||||
export const UndoRedoFeedbackExtension = Extension.create({
|
||||
name: 'undoRedoFeedback',
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
const isMac = typeof window !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent)
|
||||
const modKey = isMac ? '⌘' : 'Ctrl'
|
||||
|
||||
return {
|
||||
'Mod-z': () => {
|
||||
const success = this.editor.commands.undo()
|
||||
if (success) {
|
||||
toast.info('Action annulée', {
|
||||
description: `Faites ${modKey}+Maj+Z pour rétablir.`,
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
return success
|
||||
},
|
||||
'Mod-y': () => {
|
||||
const success = this.editor.commands.redo()
|
||||
if (success) {
|
||||
toast.info('Action rétablie', {
|
||||
description: `Faites ${modKey}+Z pour annuler.`,
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
return success
|
||||
},
|
||||
'Mod-Shift-z': () => {
|
||||
const success = this.editor.commands.redo()
|
||||
if (success) {
|
||||
toast.info('Action rétablie', {
|
||||
description: `Faites ${modKey}+Z pour annuler.`,
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
return success
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user