Skip to content
This repository has been archived by the owner on Dec 23, 2023. It is now read-only.

Seperation of tool configuration from plugin #86

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ stats.json
.DS_Store
npm-debug.log
.idea
.yarn
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
111 changes: 83 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,6 @@
<img src="https://github.com/melishev/strapi-plugin-editor-js/blob/master/.github/assets/strapi-plugin-editorjs.gif">
<br>

## 🍀 Supported official add-ons

- [x] Paragraph Tool (default)
- [x] [Embed Tool](https://github.com/editor-js/embed)
- [x] [Table tool](https://github.com/editor-js/table)
- [x] [List Tool](https://github.com/editor-js/list)
- [x] [Warning Tool](https://github.com/editor-js/warning)
- [x] [Code Tool](https://github.com/editor-js/code)
- [x] [Link Tool](https://github.com/editor-js/link)
- [x] [Image Tool](https://github.com/editor-js/image)
- [x] [Raw HTML Tool](https://github.com/editor-js/raw)
- [x] [Heading Tool](https://github.com/editor-js/header)
- [x] [Quote Tool](https://github.com/editor-js/quote)
- [x] [Marker Tool](https://github.com/editor-js/marker)
- [x] [Checklist Tool](https://github.com/editor-js/checklist)
- [x] [Delimiter Tool](https://github.com/editor-js/delimiter)
- [x] [InlineCode Tool](https://github.com/editor-js/inline-code)
- [ ] [Personality Tool](https://github.com/editor-js/personality)
- [ ] [Attaches Tool](https://github.com/editor-js/attaches)

<br>

#### All of the above add-ons (if added) work initially when the plugin is loaded. You can also customize the add-ons available in your application using the instructions below.

<br>

## 🤟🏻 Getting Started

```bash
Expand All @@ -49,7 +23,32 @@ yarn add strapi-plugin-react-editorjs
npm install strapi-plugin-react-editorjs
```

In order for Strapi to show the Link Tool thumbnails correctly, you will need to edit the 'strapi::security' line in ./config/middlewares.js. Change that line to the following (do this at your own risk).
## Configuration

### Strapi Config

in `config/plugins.(ts|js)`

```js
module.exports = ({ env }) => ({
// ...
"editorjs": {
config: {
toolpack: 'editorjs-default-toolpack'
}
}
})
```

#### Configuration Object

| Property | Description | Type | Default |
| - | - | - | - |
| toolpack | The name of the toolpack package to load | string | `'editorjs-default-toolpack` |

### CSP

In order for Strapi to show the Link Tool thumbnails correctly, you will need to edit the 'strapi::security' line in ``./config/middlewares.js``. Change that line to the following (do this at your own risk).

```js
module.exports = [
Expand All @@ -68,7 +67,63 @@ module.exports = [
];
```

<br>
### CORS

**NOTE - This is currently unconfirmed as a requirement and needs more testing**

HTTP Requests generated by the EditorJS Link tool send the 'X-Requested-With' header, which by default Strapi is not configured to handle correctly for Cross Origin Requests (tested with 4.11). This requires the following configuration changes.


in `config/middlewares.(ts|js)`

```js
module.exports = [
// ...
{
name: 'strapi::cors',
config: {
headers: ['Content-Type', 'Authorization', 'Origin', 'Accept', 'X-Requested-With']
},
},
// ...
];
```


## 🔧 Toolpacks

A toolpack is an NPM package which pre-builds all of the EditorJS tools used by the editor and is requested by the admin panel when the editor is used.

This seperation of the tools from the plugin means that selection and configuration of tools are seperate from the plugin itself.

The default toolpack includes the following standard tools, pre-configured for use with Strapi

- [x] Paragraph Tool (default)
- [x] [Embed Tool](https://github.com/editor-js/embed)
- [x] [Table tool](https://github.com/editor-js/table)
- [x] [List Tool](https://github.com/editor-js/list)
- [x] [Warning Tool](https://github.com/editor-js/warning)
- [x] [Code Tool](https://github.com/editor-js/code)
- [x] [Link Tool](https://github.com/editor-js/link)
- [x] [Image Tool](https://github.com/editor-js/image)
- [x] [Raw HTML Tool](https://github.com/editor-js/raw)
- [x] [Heading Tool](https://github.com/editor-js/header)
- [x] [Quote Tool](https://github.com/editor-js/quote)
- [x] [Marker Tool](https://github.com/editor-js/marker)
- [x] [Checklist Tool](https://github.com/editor-js/checklist)
- [x] [Delimiter Tool](https://github.com/editor-js/delimiter)
- [x] [InlineCode Tool](https://github.com/editor-js/inline-code)

The toolpack also includes the Strapi Image tool, which allows for the insertion of an image from the Strapi Media Library.

### Custom Toolpacks

If a user wants to create a custom toolpack, it's recommended to start by cloning to default toolpack and modifying from there.

https://github.com/xenobytezero/editorjs-default-toolpack


The repository Readme has details about how to use it.

## ⚙️ How to extend/develop this plugin (optional)

Expand Down
157 changes: 122 additions & 35 deletions admin/src/components/editorjs/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,86 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import EditorJs from 'react-editor-js';
import requiredTools from './requiredTools';
import customTools from '../../config/customTools';

import MediaLibAdapter from '../medialib/adapter'
import { createReactEditorJS } from 'react-editor-js'
import MediaLibComponent from '../medialib/component';
import {changeFunc, getToggleFunc} from '../medialib/utils';

import { useFetchClient, auth } from '@strapi/helper-plugin';
import { Loader } from '@strapi/design-system';
import { Typography as Typ } from '@strapi/design-system';
import { Flex } from '@strapi/design-system';
import { EmptyStateLayout } from '@strapi/design-system';
import pluginId from '../../pluginId';

const EditorJs = createReactEditorJS();

const Editor = ({ onChange, name, value }) => {

const [editorInstance, setEditorInstance] = useState();
const [mediaLibBlockIndex, setMediaLibBlockIndex] = useState(-1);
const [isMediaLibOpen, setIsMediaLibOpen] = useState(false);
const fetchClient = useFetchClient();

const [editorInstance, setEditorInstance] = useState();
const [mediaLibBlockIndex, setMediaLibBlockIndex] = useState(-1);
const [isMediaLibOpen, setIsMediaLibOpen] = useState(false);
const [toolpack, setToolpack] = useState(null);
const [tools, setTools] = useState(null);
const [toolpackError, setToolpackError] = useState(null);

const createEjsObject = () => {
const ejs = {
pluginEndpoint: `${strapi.backendURL}/${pluginId}`,
authToken: auth.getToken(),
fetchClient,
mediaLib: {
toggle: mediaLibToggleFunc
}
}
return ejs;
}

useEffect(() => {
// check if the toolpack on the server is valid

fetchClient.get(
`${strapi.backendURL}/${pluginId}/toolpackValid`,
// we want to check the response rather than just throw
{validateStatus: () => true}
)
.then((resp) => {

// if it's valid, load the toolpack
if (resp.status === 200) {
return import(/*webpackIgnore: true*/`${strapi.backendURL}/${pluginId}/toolpack`);

// if it's not valid, the reason is in the body
} else if (resp.status === 400) {
throw new Error(resp.data)

// for something unexpected, then throw an unexpected error
} else {
throw new Error('Unexpected Error.');
}
})
.then(module => {
try {
const toolpackCreator = module.default;
const tp = toolpackCreator(createEjsObject());
setToolpack(tp);
setToolpackError(null);
} catch(err) {
throw new Error(`Failed to hydrate toolpack - ${err.message}`)
}
})
.catch((err) => {
setToolpackError(err.message);
})

}, [])

useEffect(() => {
if (tools !== null) { return; }
if (toolpack === null) { return; }
setTools({...toolpack})
}, [toolpack])


const mediaLibToggleFunc = useCallback(getToggleFunc({
openStateSetter: setIsMediaLibOpen,
Expand All @@ -29,38 +97,57 @@ const Editor = ({ onChange, name, value }) => {
mediaLibToggleFunc();
}, [mediaLibBlockIndex, editorInstance]);

const customImageTool = {
mediaLib: {
class: MediaLibAdapter,
config: {
mediaLibToggleFunc
}
const renderEditor = (actualEditor) => {

if (toolpackError !== null) {
return <>
<EmptyStateLayout
content="Failed to load Toolpack"
action={<Typ textAlign="center" variant="pi">{toolpackError}</Typ>}
/>
</>
} else if (tools === null) {
return <>
<Flex alignItems='center' justifyContent='center' direction='column' paddingTop={6} paddingBottom={6}>
<Loader small/>
<Typ variant="epsilon">Loading Toolpack...</Typ>
</Flex>
</>
} else {
return actualEditor();
}

}


return (
<>
<div style={{ border: `1px solid rgb(227, 233, 243)`, borderRadius: `2px`, marginTop: `4px` }}>
<EditorJs
// data={JSON.parse(value)}
// enableReInitialize={true}
onReady={(api) => {
if(value && JSON.parse(value).blocks.length) {
api.blocks.render(JSON.parse(value))
}
document.querySelector('[data-tool="image"]').remove()
}}
onChange={(api, newData) => {
if (!newData.blocks.length) {
newData = null;
onChange({ target: { name, value: newData } });
} else {
onChange({ target: { name, value: JSON.stringify(newData) } });
}
}}
tools={{...requiredTools, ...customTools, ...customImageTool}}
instanceRef={instance => setEditorInstance(instance)}
/>

{renderEditor(() => <>
<EditorJs
onChange={(api, ev) => {
api.saver.save().then(newData => {
if (!newData.blocks.length) {
onChange({ target: { name, value: null } });
} else {
onChange({ target: { name, value: JSON.stringify(newData) } });
}
});
}}
tools={tools}
onInitialize={editor => {
const api = editor.dangerouslyLowLevelInstance;
api.isReady.then(() => {
setEditorInstance(api);
if(value && JSON.parse(value).blocks.length) {
api.render(JSON.parse(value))
}
})
}}
/>
</>)}

</div>
<MediaLibComponent
isOpen={isMediaLibOpen}
Expand Down
4 changes: 2 additions & 2 deletions admin/src/components/editorjs/requiredTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ const requiredTools = {
"Authorization": `Bearer ${auth.getToken()}`
},
endpoints: {
byUrl: `/api/${PluginId}/image/byUrl`,
byUrl: `${process.env.STRAPI_ADMIN_BACKEND_URL}/${PluginId}/image/byUrl`,
},
uploader: {
async uploadByFile(file) {
const formData = new FormData();
formData.append("data", JSON.stringify({}));
formData.append("files.image", file);

const {data} = await axios.post(`/api/${PluginId}/image/byFile`, formData, {
const {data} = await axios.post(`${process.env.STRAPI_ADMIN_BACKEND_URL}/${PluginId}/image/byFile`, formData, {
headers: {
"Authorization": `Bearer ${auth.getToken()}`
}
Expand Down
2 changes: 1 addition & 1 deletion admin/src/components/medialib/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export default class MediaLibAdapter {

static get toolbox() {
return {
title: 'Image',
title: 'Strapi Image',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="15" viewBox="0 0 336 276"><path d="M291 150.242V79c0-18.778-15.222-34-34-34H79c-18.778 0-34 15.222-34 34v42.264l67.179-44.192 80.398 71.614 56.686-29.14L291 150.242zm-.345 51.622l-42.3-30.246-56.3 29.884-80.773-66.925L45 174.187V197c0 18.778 15.222 34 34 34h178c17.126 0 31.295-12.663 33.655-29.136zM79 0h178c43.63 0 79 35.37 79 79v118c0 43.63-35.37 79-79 79H79c-43.63 0-79-35.37-79-79V79C0 35.37 35.37 0 79 0z"/></svg>'
}
}
Expand Down
Loading