Deutsch
Medien
Der Editor übernimmt die Auswahl: Durchsuchen an Bildfeldern, Video-Thumbnails und Bildfeldern benutzerdefinierter Blöcke, Drag-and-Drop-Upload, Zuschnitt, Ordner, Suche. Der Speicher liegt bei Ihnen.
onRequestMedia ist eine eigene Schnittstelle — eine UI-Überschreibung für ein Host-Widget (Bynder, Cloudinary, ein eigenes Modal). Das ist nicht dieser Speicher. Sind beide gesetzt, hat der Callback Vorrang und das eingebaute Modal öffnet sich nie. Siehe Bilder.
Schnellstart
Der mitgelieferte browserlokale Provider braucht kein Backend:
js
import { init, createLocalStorageMediaProvider } from '@templatical/editor';
const editor = await init({
container: '#editor',
media: createLocalStorageMediaProvider(),
});Einträge landen im localStorage unter templatical:media. create speichert die Datei als Data-URL — ein Modell in Demo-Größe. localStorage-Kontingente liegen typischerweise bei etwa 5 MB, und ein paar große Bilder füllen das. Ordner, Ersetzen, Import, Verwendung, häufig verwendet und Kontingent sind false. Für alles jenseits von Demos, Prototypen und einem einzelnen Gerät liefern Sie einen Provider.
Installation
Nur für die eigenständige Nutzung
Installieren Sie @templatical/media-library nicht neben dem Editor. init({ media }) und Clouds Speicher enthalten Durchsuchen bereits als Lazy-Chunk; Vue wird nicht installiert. Dieses Paket ist das eigenständige SDK — init(), useMediaLibrary oder MediaLibraryModal in Ihrer eigenen Vue-App.
bash
npm install @templatical/media-librarybash
pnpm add @templatical/media-librarybash
yarn add @templatical/media-librarybash
bun add @templatical/media-libraryDieses Paket hat vue als Peer. Es entfällt, wenn:
- Sie
onRequestMediaohnemedia-Schlüssel übergeben — das Host-Widget mountet das Modal nie - Sie
mediaganz weglassen — Bildfelder bleiben URL-only - Sie die Provider-Methoden selbst aufrufen — das sind gewöhnliche Funktionen
Laden Sie den Editor über das CDN?
Dann gibt es nichts zu installieren. Der CDN-Editor enthält denselben Chunk, den auch der npm-Editor mitliefert.
Der Vertrag
media akzeptiert jedes Objekt, das MediaProvider implementiert. list ist eine Methode; jedes andere Mitglied ist entweder eine Funktion oder false:
ts
interface MediaProvider {
list(params?: MediaListParams): Promise<MediaListPage>;
create: false | ((input: MediaCreateInput) => Promise<MediaAsset>);
update: false | ((id: string, patch: MediaAssetPatch) => Promise<MediaAsset>);
delete: false | ((ids: string[]) => Promise<void>);
folders: false | MediaFoldersProvider;
replace: false | ((id: string, file: File) => Promise<MediaAsset>);
importFromUrl:
| false
| ((url: string, folderId?: string | null, templateId?: string) => Promise<MediaAsset>);
checkUsage: false | ((ids: string[]) => Promise<Record<string, MediaUsageInfo>>);
frequentlyUsed: false | (() => Promise<MediaAsset[]>);
storage: false | (() => Promise<MediaStorageInfo | null>);
}
interface MediaFoldersProvider {
list(): Promise<MediaFolder[]>; // FLACH; die Oberfläche baut den Baum über parentId
create: false | ((input: MediaFolderInput) => Promise<MediaFolder>);
update: false | ((id: string, patch: { name: string }) => Promise<MediaFolder>);
delete: false | ((id: string) => Promise<void>);
move: false | ((ids: string[], folderId: string | null) => Promise<MediaAsset[]>);
}false bedeutet, dass die aktuelle Person diese Aktion nicht ausführen darf; der Editor blendet das Bedienelement aus.
list kann nicht false sein: ohne es hätte die Auswahl nichts zu zeigen. Suche, Ordner, Kategorie und Cursor werden bei jedem Listing mitgeschickt — Galerien wachsen über eine Antwort hinaus. Ein Provider, der alles auf einmal zurückgibt, lässt nextCursor weg.
delete und checkUsage sind gesammelt: Das Raster ist Mehrfachauswahl. folders ist verschachtelt, sodass eine Galerie ohne Ordner einmal folders: false schreibt; folders.list() gibt ein flaches Array zurück, und die Oberfläche baut den Baum über parentId.
Der Zuschnitt geschieht im Client. Einen Zuschnitt persistieren Sie, indem Sie create oder replace mit der entstandenen File aufrufen. Es gibt keine Crop-Methode am Provider und keinen Fortschritts-Callback: create nimmt eine File und löst mit dem Asset auf.
Bestätigen setzt immer asset.url ein. Das Raster verwendet thumbnailUrl, ersatzweise url. Auf MediaAsset gibt es keine Conversion-Menge (small / medium / large) — gehört ein Derivat in die E-Mail, setzen Sie diese URL auf url.
Die Datenstruktur
ts
interface MediaAsset {
id: string; // vom Provider zugewiesen, von create() zurückgegeben
url: string; // landet auf dem Block
alt?: string;
filename?: string;
mimeType?: string;
width?: number;
height?: number;
size?: number; // Bytes
thumbnailUrl?: string; // Raster; fällt auf url zurück
folderId?: string | null;
canUpdate?: boolean; // optional — fehlt = erlaubt; false verbietet
canDelete?: boolean;
createdAt?: string; // optional — nur Anzeige, ohne Einfluss auf die Reihenfolge
updatedAt?: string;
}
interface MediaListParams {
search?: string;
cursor?: string;
folderId?: string | null;
category?: MediaCategory; // "images" | "documents" | "videos" | "audio"
templateId?: string; // wenn eine Vorlage geladen ist; Cloud ignoriert
}
interface MediaListPage {
items: MediaAsset[];
nextCursor?: string;
}
interface MediaCreateInput {
file: File;
folderId?: string | null;
alt?: string;
filename?: string;
templateId?: string;
}- Die
idkommt vom Provider. Der Editor erzeugt nie eine eigene, sondern verwendet, wascreate()zurückgibt. templateIdist opportunistisch. Wird beilist/create/importFromUrlübergeben, wenn eine Vorlage geladen ist; auf einer leeren Arbeitsfläche weggelassen. Medien sind nicht daran gebunden. Cloud ignoriert es; ein CMS, das eine Galerie pro Vorlage abgrenzt, liest es hier.- Die Reihenfolge kommt vom Provider. Der Editor stellt die Reihenfolge von
list()dar und sortiert nie um. - Zeitstempel dienen nur der Anzeige. Lassen Sie beide weg, entfällt die Angabe.
maxFileSize/mimeTypesam Provider sind eine clientseitige Vorprüfung, keine Sicherheitsgrenze — das Backend muss zusätzlich durchsetzen.
ts
interface MediaOptions {
maxFileSize?: number; // Bytes
mimeTypes?: Partial<Record<MediaCategory, string[]>>;
onCreated?: (asset: MediaAsset) => void;
onUpdated?: (asset: MediaAsset) => void;
onDeleted?: (asset: MediaAsset) => void;
}MediaProvider erweitert MediaOptions, sodass ein Objekt Speicher und Events trägt.
Berechtigungen steuern
Eine ganze Fähigkeit vorenthalten, indem Sie false statt einer Funktion übergeben. Der Editor blendet aus, was er nicht kann — keine Upload-Zone und kein Drop, wenn create aus ist, kein Bearbeiten bei update: false, kein Löschen bei delete: false, kein Ordnerbaum bei folders: false.
ts
const media: MediaProvider = {
list: ({ search, cursor, templateId }) =>
myCms.page({ search, cursor, templateId }),
create: ({ file }) => myCms.upload(file),
update: false,
delete: false,
folders: false,
replace: false,
importFromUrl: false,
checkUsage: false,
frequentlyUsed: false,
storage: false,
};Einen einzelnen Eintrag ausnehmen, indem Sie canUpdate / canDelete mitliefern. Fehlt der Wert, ist die Aktion erlaubt — setzen Sie sie also nur bei den Ausnahmen. Die beiden Hebel greifen nur in einer Richtung zusammen: canUpdate: true kann ein vom Provider als false übergebenes update nicht wieder aktivieren.
Eine schreibgeschützte Bibliothek
Setzen Sie jede Mutation auf false, erhalten Sie eine kuratierte Galerie, die Nutzer durchsehen, durchsuchen und auswählen, aber nie verändern können:
ts
const media: MediaProvider = {
list: async () => {
const res = await fetch('/api/media');
return json(res);
},
create: false,
update: false,
delete: false,
folders: false,
replace: false,
importFromUrl: false,
checkUsage: false,
frequentlyUsed: false,
storage: false,
};Das Auswählen funktioniert weiterhin — es kopiert { url, alt } auf die Arbeitsfläche, und den Provider erreicht dabei nichts. Drop nicht: Drop braucht create. list ist das einzige Mitglied, das sich nicht abschalten lässt.
Keine Sicherheitsgrenze
Ein ausgeblendetes Element verhindert, dass der Editor die Aktion anbietet — es hält niemanden auf, der es darauf anlegt. Berechtigungen müssen zusätzlich serverseitig erzwungen werden: Die Provider-Methoden laufen im Browser der Nutzenden — auch dafür, wer ein Asset in einer im Team geteilten Bibliothek lesen, ändern oder löschen darf.
Fehlerbehandlung
Jede Methode kann ablehnen. Der Editor meldet den Fehler über onError und lässt seine Liste im Speicher unverändert — ein fehlgeschlagenes Löschen lässt ein Asset also nicht aus der Oberfläche verschwinden. Das Modal bleibt offen. Provider-Meldungen sind für die Person lesbar: Sie landen wortgleich in der Oberfläche.
Ein fehlgeschlagenes create stellt nichts voran. importFromUrl behält einen Inline-Fehler am Feld.
Im Editor
Die Oberfläche ist eine Auswahl. Es gibt keine Leiste und kein „Medien verwalten"-Chrome.
Durchsuchen — ein Bildfeld (und das Video-Thumbnail und ein Bildfeld eines benutzerdefinierten Blocks) zeigt eine Durchsuchen-Schaltfläche, sobald
mediaoderonRequestMediakonfiguriert ist. Mit einemmedia-Provider und ohne Callback öffnet der Klick das Bibliotheks-Modal. MitonRequestMedialäuft der Callback stattdessen — das Modal wird nie gemountet. Bildfelder und das Video-Thumbnail übergebenaccept: ["images"]; benutzerdefinierte Blöcke können andere Kategorien übergeben. Das Modal zwingtlist({ category })auf diese Menge und blendet andere Tabs aus.Bestätigen — Bestätigen oder Doppelklick übernimmt ein in der Vorschau gezeigtes Asset, das zu
acceptpasst, als{ url: asset.url, alt: asset.alt }. Mehrfachauswahl gilt nur für gesammeltes Löschen und Verschieben. Schließen, Escape oder Backdrop gibtnullzurück.Drop — eine Bilddatei auf einen Bildblock oder ein Bildfeld ziehen:
Konfiguration Drop onRequestMediaja — context.filesProvider, createist eine Funktionja — provider.create({ file, templateId? })Provider, create: falsenein keines von beiden nein Die Drop-Zone filtert zuvor auf MIME-Typen
image/. Größe und Typ danach sindMediaOptions, dann der Server. Geben Sie keineblob:-URL zurück:URL.createObjectURL(file)ist sitzungslokal und bricht den Export.Verzögertes Laden — lassen Sie
mediaweg, wird keine Bibliotheks-Oberfläche geladen.onRequestMediaallein mountet das Modal nie.
Standardmäßig deaktiviert
Lassen Sie media weg, bleiben Bildfelder URL-only, sofern Sie nicht onRequestMedia übergeben haben.
Events
ts
media: {
// ...list, create, update, delete, …
onCreated: (asset) => {},
onUpdated: (asset) => {},
onDeleted: (asset) => {},
}Jedes löst aus, sobald die zugehörige Mutation auflöst, mit dem gespeicherten Asset:
onCreatednachcreateoderimportFromUrl. Ein Drop ruftcreateauf und löst dies aus, ohne das Modal zu öffnen und ohne eine Listenzeile voranzustellen — die URL landet auf dem Block.onUpdatednachupdateoderreplace.onDeletednachdelete.
Es gibt keine medienbezogene Liste auf Editor-Ebene. Die eigene Listing des Modals (wenn es offen ist) stellt bei Erfolg voran, ersetzt und filtert; ein Drop berührt diese Listing nie.
onDeleted trägt das Asset, keine ID
delete löst zu nichts auf, daher erhält der Handler den Eintrag, den das Modal unmittelbar vor dem Entfernen aus seiner geladenen Listing erfasst hat.
Ein Löschen außerhalb der geladenen Listing löst kein Event aus
Der erfasste Eintrag muss bereits in der Listing des Modals vorhanden sein. Das Löschen einer ID, die diese Listing nie gehalten hat, löscht weiterhin erfolgreich; es gibt dann aber nichts, das an onDeleted übergeben werden könnte, weshalb es nicht auslöst.
Eine Handler-Funktion, die einen Fehler wirft, wird abgefangen und an onError gemeldet — sie lässt das auslösende Erstellen, Aktualisieren oder Löschen nie fehlschlagen.
Headless-Nutzung
Es gibt kein useMedia in @templatical/core. Der Medienzustand ist an das Modal gebunden; die Provider-Methoden sind die Headless-API. Rufen Sie sie aus Ihrer eigenen Oberfläche auf, oder steuern Sie die Zustandsmaschine des mitgelieferten Modals:
ts
import { useMediaLibrary } from '@templatical/media-library';
const {
items, // Ref<MediaAsset[]>
isLoading, // Ref<boolean>
hasMore, // Ref<boolean>
loadItems, // () => Promise<void>
loadMore, // () => Promise<void>
uploadFile, // (file: File) => Promise<MediaAsset | null>
confirmDelete, // () => Promise<void>
} = useMediaLibrary({
provider,
onError: (error) => {
/* behandeln */
},
});Die Liste bleibt nach jedem erfolgreichen Aufruf synchron — beim Erstellen vorangestellt, beim Aktualisieren ersetzt, beim Löschen entfernt — und Fehler werden an onError gemeldet, ohne die Liste bei einem Fehlschlag zu verändern. Eine veraltete list- / loadMore-Antwort wird verworfen.
Sie nutzen Templatical Cloud? Cloud implementiert diesen Vertrag ohne jede Konfiguration — siehe Medienbibliothek auf Cloud.
Eigene Implementierung
Eine minimale REST-Implementierung:
ts
import { init } from '@templatical/editor';
import type { MediaProvider } from '@templatical/editor';
const json = async (res: Response) => {
if (!res.ok) throw new Error(`Medienanfrage fehlgeschlagen: ${res.status}`);
return res.json();
};
const media: MediaProvider = {
list: async (params) => {
const query = new URLSearchParams();
if (params?.search) query.set('search', params.search);
if (params?.cursor) query.set('cursor', params.cursor);
if (params?.folderId) query.set('folderId', params.folderId);
if (params?.category) query.set('category', params.category);
if (params?.templateId) query.set('templateId', params.templateId);
const res = await fetch(`/api/media?${query}`);
return json(res);
},
create: async (input) => {
const body = new FormData();
body.append('file', input.file);
if (input.folderId) body.append('folderId', input.folderId);
if (input.templateId) body.append('templateId', input.templateId);
const res = await fetch('/api/media', { method: 'POST', body });
return json(res);
},
update: async (id, patch) => {
const res = await fetch(`/api/media/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
});
return json(res);
},
delete: async (ids) => {
const res = await fetch('/api/media', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
});
if (!res.ok) throw new Error(`Löschen fehlgeschlagen: ${res.status}`);
},
folders: false,
replace: false,
importFromUrl: false,
checkUsage: false,
frequentlyUsed: false,
storage: false,
};
await init({ container: '#editor', media });Eine CMS-Galerie, die listet und hochlädt und sonst nichts, hat dieselbe Form wie das Berechtigungsbeispiel oben. Geteilte und vorlagenspezifische Galerien führen Sie in list({ templateId }) zusammen.