Kodemo Editor#

The Kodemo Editor is a React component that can be used to create or edit Kodemo documents. It includes includes a WYSIWYG text editor, tools for creating subjects and everything else needed to author documents.

You can use this component to host the editor on your own domain.

Installation#

To use the editor, start by installing @kodemo/editor from npm using your favorite package manager.

Rendering the Editor#

Now that we have access to the @kodemo/editor package, let's start setting up our editor. We're creating a new editor with no props here which means the editor will default to covering the full viewport and saving changes to local storage.

The KodemoEditor supports all of the same config options as the KodemoPlayer. If you'd like to learn more about those options, including how to adjust the size of the editor, see Config Options.

Store#

The editor requires a store in order to load and persist documents. If no store is provide, the editor will create its own LocalStorageStore which persists changes locally in the browser.

If you want to provide your own store, you can pass it to the editor using the store prop. In our example we are creating an instance of the default store (LocalStorageStore) and passing it to the editor.

Custom Store#

To control where the editor loads and saves data you can build your own custom store.

Your custom store needs to extend the Store class and implement two methods; load and save.

  • The load method is used to fetch your document from storage.
  • The save method is used to persist document changes to storage.

When saving, you can persist the current JSON document as a whole by calling getDocument and sending the resulting data to storage.

Sending the whole document each time there's a change gets slow when persisting large documents so we also provide JSON patches for more granular updates. These patches are available via the store's unsavedPatches property. The list of unsaved patches is automatically cleared after each save.

Learn more about JSON patches at jsonpatch.com.

Image Uploads#

If you want editors to be able to upload images you will need to provide an upload handler. This is the function that will be responsible for uploading images from the editor to your asset server.

The upload function needs to match the type ImageUploadFunction and return an Asset.

Listening for Changes#

If you want to stay updated when the document is edited you can bind a handler to onChange.

When a change is detected you can use the getDocument API method to read the current document JSON.

npm i @kodemo/editor

# or

yarn add @kodemo/editor
import React from 'react';
import ReactDOM from 'react-dom/client';
import KodemoEditor, { Store, LocalStorageStore } from '@kodemo/editor';

function App() {
const store = React.useRef<Store>(new LocalStorageStore());

return (
<KodemoEditor
store={store.current}>
</KodemoEditor>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
import React from 'react';
import ReactDOM from 'react-dom/client';
import KodemoEditor, { LocalStorageStore } from '@kodemo/editor';

function App() {
const ref = React.useRef(null);

const handleContentChange = React.useCallback(() => {
// Log the document JSON
console.log(ref.current.getDocument());
}, []);

return (
<KodemoEditor
ref={ref}
onChange={handleContentChange}>
</KodemoEditor>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
import React from 'react';
import ReactDOM from 'react-dom/client';
import KodemoEditor from '@kodemo/editor';

function App() {
return (
<KodemoEditor></KodemoEditor>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
import React from 'react';
import ReactDOM from 'react-dom/client';
import KodemoEditor, {
ImageUploadFunction,
Asset,
AssetData
} from '@kodemo/editor';

class ImageAsset implements Asset {
constructor(data: AssetData) {
this.data = data;
}

toJSON(): AssetData {
return this.data;
}
}

const uploadImage: ImageUploadFunction = async (fileData: string | ArrayBuffer): Promise<Asset> => {
return fetch('https://.../upload-endpoint', {
method: 'POST',
body: fileData
})
.then((response) => response.json())
.then((json) => new ImageAsset({
type: 'image',
url: json.url,
width: json.width,
height: json.height
}))
}

function App() {
return (
<KodemoEditor
uploadImage={uploadImage}>
import React from 'react';
import ReactDOM from 'react-dom/client';
import KodemoEditor, { Store, LocalStorageStore } from '@kodemo/editor';
import { KodemoDocument } from '@kodemo/player';

export class CustomStore extends Store {
async load(): Promise<KodemoDocument | undefined> {
// Load your document from the store
const doc: KodemoDocument = await fetch(...);

return super.load(doc);
}

async save(): Promise<boolean> {
// Option 1. Save the whole document
const doc = this.getDocument();

// Option 2. Save only the parts that changed using JSON patches
this.unsavedPatches.forEach((patch) => {
const { op, path, value } = patch;

switch (op) {
case 'add': // Add new value at path
case 'replace': // Replace value at path
case 'remove': // Remove value at path
}
});

return super.save();
}
}


function App() {
const store = React.useRef<Store>(new CustomStore());

import { Store } from '@kodemo/editor';
import { KodemoDocument } from '@kodemo/player';

export class CustomStore extends Store {
async load(): Promise<KodemoDocument | undefined> {
// Load your document from the store
const doc: KodemoDocument = await fetch(...);

return super.load(doc);
}

async save(): Promise<boolean> {
// Option 1. Save the whole document
const doc = this.getDocument();

// Option 2. Save only the parts that changed using JSON patches
this.unsavedPatches.forEach((patch) => {
const { op, path, value } = patch;

switch (op) {
case 'add':
// Add new value at path
break;
case 'replace':
// Replace value at path
break;
case 'remove':
// Remove value at path
break;
}
});

return super.save();
}
}

import { Store } from '@kodemo/editor';
import { KodemoDocument } from '@kodemo/player';

const STORAGE_KEY: string = 'ko-document';

export class LocalStorageStore extends Store {
async load(): Promise<KodemoDocument | undefined> {
let storedData: string | null = localStorage.getItem(STORAGE_KEY);
let parsedData: KodemoDocument | undefined;

if (typeof storedData === 'string') {
try {
parsedData = JSON.parse(storedData);
} catch (error) {
console.warn('Unable to parse JSON document.');
}
}

return super.load(parsedData || undefined);
}

async save(): Promise<boolean> {
super.save();

try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.getDocument()));
return super.save();
}
catch {
return false;
}
}
}