Comandi e blocchi del modulo
Il modello
Il modulo non tocca lo schema dell'editor. Ha due strumenti:
- Comandi
/(propcomandi): voci del menu che, scelte, eseguono una funzione del modulo. - Blocchi personalizzati: un tipo di blocco generico che l'editor conserva (
tipo+dati) e che il modulo disegna con lo slotblocco-personalizzato.
Un comando come /Kanban Elementi li usa insieme: inserisce un blocco kanban con i suoi dati, e lo slot lo mostra come una vista Kanban vera.
ComandoPowerbox
interface ComandoPowerbox {
id: string
etichetta: string
descrizione?: string // seconda riga della voce
icona?: string // classe PrimeIcons, es. 'pi pi-objects-column' (default 'pi pi-bolt')
parole?: string[] // altre parole che trovano il comando
inserisci: (contesto: ContestoComando) => void | Promise<void>
}
I comandi del modulo seguono quelli dell'editor nel menu. Il filtro cerca ogni parola digitata in etichetta, descrizione e parole, senza badare a maiuscole e accenti: /bach trova un comando con parole: ['bacheca'].
Quando inserisci viene chiamato, il / e la ricerca sono già stati tolti dal testo e il cursore è dove stavano. inserisci può essere asincrono (per esempio per aprire un dialogo di scelta): un errore viene scritto in console con l'id del comando e non interrompe l'editor.
ContestoComando
inserisci riceve gli strumenti per modificare il contenuto senza conoscere Tiptap:
| Strumento | Descrizione |
|---|---|
inserisciBlocco(tipo, dati?) | Inserisce un blocco personalizzato al cursore |
inserisciLink({ etichetta, href }) | Inserisce un link; un indirizzo non sicuro viene ignorato, www.… diventa https://www.… |
chiediLink() | Apre il dialogo del link, con la ricerca di risolviLink |
editor | Istanza Tiptap (Editor di @tiptap/core), per ciò che gli strumenti non coprono |
Esempio: i comandi del modulo Knowledge
import type { ComandoPowerbox } from '@pzeta/vue-wiki'
export function comandiKnowledge(idarticolo: number): ComandoPowerbox[] {
const vista = (id: string, etichetta: string, icona: string, parole: string[] = []) => ({
id,
etichetta,
icona,
parole,
inserisci: ({ inserisciBlocco }) => inserisciBlocco(id, { idarticolo }),
}) satisfies ComandoPowerbox
return [
vista('indice', 'Indice', 'pi pi-list-check', ['figli']),
vista('kanban', 'Kanban Elementi', 'pi pi-objects-column', ['bacheca']),
vista('schede', 'Schede Elementi', 'pi pi-th-large'),
vista('elenco', 'Elenco Elementi', 'pi pi-list'),
vista('calendario', 'Calendario Elementi', 'pi pi-calendar'),
{
id: 'articolo',
etichetta: 'Articolo',
descrizione: 'Link a un altro articolo',
icona: 'pi pi-link',
inserisci: ({ chiediLink }) => chiediLink(),
},
]
}
/Articolo riusa il dialogo del link: con risolviLink che cerca gli articoli, l'utente scrive un pezzo del titolo e sceglie dai risultati.
/Sezione Richiudibile e i callout non vanno aggiunti: sono comandi dell'editor. Per /Appunti basta una citazione o un callout, salvo che il modulo voglia un blocco suo.Blocchi personalizzati
Un blocco personalizzato è un atomo: si seleziona, si sposta trascinandolo e si cancella come un'unità, ma il suo contenuto non si modifica con il cursore. Nel contenuto salvato è:
<div data-blocco="kanban" data-dati="{"idarticolo":12}" class="my-3"></div>
L'editor conserva tipo e dati (un oggetto JSON; dati illeggibili diventano {}) senza interpretarli.
Lo slot blocco-personalizzato
Lo slot riceve BloccoPersonalizzatoProps:
| Prop | Tipo | Descrizione |
|---|---|---|
tipo | string | Tipo del blocco (data-blocco) |
dati | Record<string, unknown> | Dati del blocco (data-dati) |
readonly | boolean | Lo stato di sola lettura dell'editor |
aggiorna | (dati) => void | Sostituisce i dati del blocco (per esempio dopo una scelta dell'utente) |
elimina | () => void | Toglie il blocco dal contenuto |
In sola lettura aggiorna ed elimina non fanno nulla: il contenuto non cambia.
<WikiEditor v-model="contenuto" :comandi="comandi">
<template #blocco-personalizzato="{ tipo, dati, readonly, aggiorna }">
<IndiceFigli v-if="tipo === 'indice'" :id-articolo="Number(dati.idarticolo)" />
<VistaKanban
v-else-if="tipo === 'kanban'"
:id-articolo="Number(dati.idarticolo)"
:filtri="dati.filtri"
:readonly="readonly"
@update:filtri="(filtri) => aggiorna({ ...dati, filtri })"
/>
</template>
</WikiEditor>
Lo slot viene reso nel contesto dell'applicazione: router, provide/inject e componenti registrati globalmente funzionano come altrove. Un blocco selezionato ha un bordo evidenziato.
Senza slot, o per un tipo che lo slot non gestisce, il blocco resta nel contenuto e l'editor mostra un segnaposto («Blocco «kanban»»): il contenuto non si perde.
Prova
Due comandi del modulo, /Indice e /Kanban Elementi (trovato anche da /bacheca), e uno slot che disegna i loro blocchi con dati fissi. Il contenuto parte con un indice già inserito.
Inserire un blocco da codice
Fuori dal menu /, il metodo esposto inserisciBlocco fa lo stesso:
<script setup lang="ts">
import { ref } from 'vue'
import { WikiEditor } from '@pzeta/vue-wiki'
const wiki = ref<InstanceType<typeof WikiEditor> | null>(null)
const aggiungiIndice = () => wiki.value?.inserisciBlocco('indice', { idarticolo: 12 })
</script>
<template>
<WikiEditor ref="wiki" v-model="contenuto" />
</template>
Mostrare i blocchi fuori dall'editor
Un <div data-blocco> mostrato con v-html resta vuoto: chi lo disegna è lo slot. Per una pagina in lettura che contiene blocchi del modulo conviene quindi <WikiEditor readonly> con lo stesso slot, invece di v-html. Vedi Contenuto HTML.
WikiEditor
Editor delle pagine wiki basato su Tiptap 3, con barre degli strumenti, menu dei comandi /, blocchi del modulo, immagini, link risolti e commenti ancorati al testo.
Commenti ancorati
Come WikiEditor ancora un commento al testo selezionato, ne evidenzia gli aperti e li segna nel margine destro, in modifica e in sola lettura.