├── .gitignore
├── src
├── deps.d.ts
├── interfaces
│ ├── fetch-handler.ts
│ ├── block.ts
│ ├── media-upload.ts
│ ├── block-editor-state.ts
│ └── editor-settings.ts
├── env.ts
├── index.ts
├── store
│ ├── index.ts
│ ├── selectors.ts
│ ├── actions.ts
│ └── reducer.ts
├── wordpress.ts
├── keyboard-shortcuts
│ └── index.ts
├── components
│ ├── Notices.tsx
│ ├── Sidebar.tsx
│ ├── InserterToggle.tsx
│ ├── Header.tsx
│ ├── KeyboardShortcuts.tsx
│ ├── BlockEditor.tsx
│ └── Editor.tsx
├── lib
│ ├── default-settings.ts
│ ├── bind-input.ts
│ └── blocks.ts
└── styles.scss
├── README.md
├── tsconfig.json
├── playground
├── index.html
└── main.js
├── versions.sh
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /node_modules/
3 | /dist/
4 | /build/
--------------------------------------------------------------------------------
/src/deps.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@wordpress/components'
2 | declare module '@wordpress/data'
--------------------------------------------------------------------------------
/src/interfaces/fetch-handler.ts:
--------------------------------------------------------------------------------
1 | export {
2 | FetchHandler,
3 | APIFetchOptions,
4 | APIFetchMiddleware
5 | } from "@wordpress/api-fetch";
6 |
--------------------------------------------------------------------------------
/src/interfaces/block.ts:
--------------------------------------------------------------------------------
1 | export default interface Block {
2 | clientId: string | null,
3 | attributes: any,
4 | innerBlocks: Block[],
5 | isValid: boolean,
6 | name: string
7 | }
--------------------------------------------------------------------------------
/src/interfaces/media-upload.ts:
--------------------------------------------------------------------------------
1 | export default interface MediaUpload {
2 | allowedTypes: string[],
3 | filesList: FileList,
4 | onError: (message: string) => void,
5 | onFileChange: () => void
6 | }
--------------------------------------------------------------------------------
/src/env.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | window.process = {
3 | env: {
4 | FORCE_REDUCED_MOTION: false,
5 | GUTENBERG_PHASE: 2,
6 | COMPONENT_SYSTEM_PHASE: 1
7 | }
8 | }
9 |
10 | window.wp = {}
11 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | //import './styles.scss'
2 | import './env'
3 |
4 | export * as wordpress from './wordpress'
5 | export { registerBlockType } from '@wordpress/blocks'
6 | export { initializeEditor, removeEditor, Editor } from './components/Editor'
7 |
--------------------------------------------------------------------------------
/src/interfaces/block-editor-state.ts:
--------------------------------------------------------------------------------
1 | import Block from "./block";
2 |
3 | export interface BlocksState {
4 | past: Block[][],
5 | current: Block[],
6 | future: Block[][]
7 | }
8 |
9 | export default interface BlockEditorState {
10 | blocks: BlocksState
11 | }
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createReduxStore, register } from '@wordpress/data'
2 |
3 | import actions from './actions'
4 | import reducer from './reducer'
5 | import selectors from './selectors'
6 |
7 | const store = createReduxStore('block-editor', {
8 | reducer,
9 | actions,
10 | selectors
11 | })
12 |
13 | register(store)
--------------------------------------------------------------------------------
/src/wordpress.ts:
--------------------------------------------------------------------------------
1 | export * as blockEditor from '@wordpress/block-editor'
2 | export * as blocks from '@wordpress/blocks'
3 | export * as components from '@wordpress/components'
4 | export * as data from '@wordpress/data'
5 | export * as element from '@wordpress/element'
6 | export * as hooks from '@wordpress/hooks'
7 | export { default as serverSideRender } from '@wordpress/server-side-render'
8 |
--------------------------------------------------------------------------------
/src/store/selectors.ts:
--------------------------------------------------------------------------------
1 | import Block from "../interfaces/block"
2 | import BlockEditorState from "../interfaces/block-editor-state"
3 |
4 | const selectors = {
5 | getBlocks: (state: BlockEditorState): Block[] => state.blocks.current,
6 | canUndo: (state: BlockEditorState): boolean => state.blocks.past.length > 0,
7 | canRedo: (state: BlockEditorState): boolean => state.blocks.future.length > 0
8 | }
9 |
10 | export default selectors
--------------------------------------------------------------------------------
/src/keyboard-shortcuts/index.ts:
--------------------------------------------------------------------------------
1 | import { useSelect, register } from '@wordpress/data'
2 | import { store } from '@wordpress/keyboard-shortcuts'
3 | import { useKeyboardShortcut } from '@wordpress/compose'
4 |
5 | register(store)
6 |
7 | const useShortcut = (name, callback, options = {}) => {
8 | const shortcuts = useSelect(function (select) {
9 | return select(store).getAllShortcutRawKeyCombinations(name);
10 | }, [name]);
11 | useKeyboardShortcut(shortcuts, callback, options);
12 | }
13 |
14 | export { store, useShortcut }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Block Editor
2 |
3 | This package is a Work In Progress. It aims to seperate the Javascript frontend from [Laraberg](https://github.com/VanOns/laraberg) so it can be maintained seperately, and maybe serve as a starting point for other backend implementations.
4 |
5 | ## Usage
6 |
7 | To use the editor simply create a input or textarea element and use it to initalize it like this:
8 |
9 | ```js
10 | import { initializeEditor } from 'mauricewijnia/block-editor'
11 |
12 | const element = document.querySelector('#content')
13 | initializeEditor(element)
14 | ```
--------------------------------------------------------------------------------
/src/components/Notices.tsx:
--------------------------------------------------------------------------------
1 | import { createElement } from '@wordpress/element'
2 | import { useSelect, useDispatch } from '@wordpress/data'
3 | import { NoticeList } from '@wordpress/components'
4 |
5 | export default function Notices() {
6 | const notices = useSelect((select) => select('core/notices').getNotices())
7 | const { removeNotice } = useDispatch('core/notices')
8 |
9 | return (
10 |
16 | );
17 | }
--------------------------------------------------------------------------------
/src/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { createElement } from '@wordpress/element'
2 | import { createSlotFill, Panel } from '@wordpress/components'
3 |
4 | const { Slot, Fill } = createSlotFill(
5 | 'StandAloneBlockEditorSidebarInspector'
6 | )
7 |
8 | const Sidebar = () => {
9 | return (
10 |
18 | );
19 | };
20 |
21 | Sidebar.Fill = Fill
22 |
23 | export default Sidebar
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
4 | "outDir": "dist",
5 | "sourceMap": true,
6 | "declaration": true,
7 | "strict": true,
8 | "noImplicitReturns": true,
9 | "noImplicitAny": false,
10 | "module": "esnext",
11 | "target": "es2015",
12 | "moduleResolution": "node",
13 | "esModuleInterop": true,
14 | "allowJs": true,
15 | "jsx": "react",
16 | "jsxFactory": "createElement"
17 | },
18 | "include": [
19 | "./src/**/*"
20 | ],
21 | "exclude": [
22 | "./dist"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/InserterToggle.tsx:
--------------------------------------------------------------------------------
1 | import { createElement } from '@wordpress/element'
2 | import { ToolbarButton } from '@wordpress/components'
3 | import { plus as plusIcon } from '@wordpress/icons'
4 |
5 | interface InserterToggleProps {
6 | onToggle: () => void,
7 | isOpen: boolean,
8 | toggleProps: any
9 | }
10 |
11 | const InserterToggle = ({onToggle, isOpen, toggleProps}: InserterToggleProps) => {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export default InserterToggle
25 |
--------------------------------------------------------------------------------
/src/lib/default-settings.ts:
--------------------------------------------------------------------------------
1 | import EditorSettings from "../interfaces/editor-settings";
2 |
3 | const defaultSettings: EditorSettings = {
4 | // Laraberg settings
5 | height: '500px',
6 | mediaUpload: undefined,
7 | disabledCoreBlocks: [
8 | 'core/embed',
9 | 'core/freeform',
10 | 'core/shortcode',
11 | 'core/archives',
12 | 'core/tag-cloud',
13 | 'core/block',
14 | 'core/rss',
15 | 'core/search',
16 | 'core/calendar',
17 | 'core/categories',
18 | 'core/more',
19 | 'core/nextpage'
20 | ],
21 |
22 | // WordPress settings
23 | alignWide: true,
24 | supportsLayout: false,
25 | }
26 |
27 | export default defaultSettings
28 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { ToolbarButton, createSlotFill } from '@wordpress/components'
2 | import { createElement } from '@wordpress/element'
3 | import { cog as cogIcon } from '@wordpress/icons'
4 |
5 | const { Slot, Fill } = createSlotFill(
6 | 'HeaderToolbar'
7 | );
8 |
9 | interface HeaderProps {
10 | toggleSidebar: () => void,
11 | sidebarOpen: boolean
12 | }
13 |
14 | const Header = ({ toggleSidebar, sidebarOpen }: HeaderProps) => {
15 | return (
16 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | Header.Fill = Fill;
27 |
28 | export default Header;
29 |
--------------------------------------------------------------------------------
/src/interfaces/editor-settings.ts:
--------------------------------------------------------------------------------
1 | import MediaUpload from "./media-upload";
2 | import { FetchHandler } from './fetch-handler'
3 |
4 | export interface Color {
5 | name: string,
6 | slug: string,
7 | color: string
8 | }
9 |
10 | export interface Gradient {
11 | name: string,
12 | slug: string,
13 | gradient: string
14 | }
15 |
16 | export interface FontSize {
17 | name: string,
18 | slug: string,
19 | size: number
20 | }
21 |
22 | export default interface EditorSettings {
23 | // Laraberg settings
24 | height?: string,
25 | mediaUpload?: (upload: MediaUpload) => void,
26 | fetchHandler?: FetchHandler,
27 | disabledCoreBlocks?: string[],
28 |
29 | // WordPress settings
30 | alignWide?: boolean,
31 | supportsLayout?: boolean,
32 | maxWidth?: number,
33 | imageEditing?: boolean,
34 |
35 | colors?: Color[],
36 | gradients?: Gradient[],
37 | fontSizes?: FontSize[],
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | BlockEditor Playground
7 |
17 |
18 |
19 |
20 |
Edit
21 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/store/actions.ts:
--------------------------------------------------------------------------------
1 | import Block from "../interfaces/block";
2 |
3 | export const SET_BLOCKS = 'SET_BLOCKS'
4 | export const DUPLICATE_BLOCKS = 'DUPLICATE_BLOCKS'
5 | export const REMOVE_BLOCK = 'REMOVE_BLOCK'
6 | export const REMOVE_BLOCKS = 'REMOVE_BLOCKS'
7 | export const UNDO = 'UNDO'
8 | export const REDO = 'REDO'
9 |
10 | const actions = {
11 | setBlocks: (blocks: Block[]) => {
12 | return {
13 | type: SET_BLOCKS,
14 | blocks
15 | }
16 | },
17 | duplicateBlocks: (blockIds: string[]) => {
18 | return {
19 | type: DUPLICATE_BLOCKS,
20 | blockIds
21 | }
22 | },
23 | removeBlock: (blockId: string) => {
24 | return {
25 | type: REMOVE_BLOCKS,
26 | blockIds: [blockId]
27 | }
28 | },
29 | removeBlocks: (blockIds: string[]) => {
30 | return {
31 | type: REMOVE_BLOCKS,
32 | blockIds
33 | }
34 | },
35 | undo: () => {
36 | return { type: UNDO }
37 | },
38 | redo: () => {
39 | return { type: REDO }
40 | }
41 | }
42 |
43 | export default actions
--------------------------------------------------------------------------------
/src/lib/bind-input.ts:
--------------------------------------------------------------------------------
1 | class BindInput {
2 | element: HTMLInputElement|HTMLTextAreaElement
3 |
4 | constructor(element: HTMLInputElement|HTMLTextAreaElement) {
5 | if (!['INPUT', 'TEXTAREA'].includes(element.tagName)) {
6 | throw new Error('[BlockEditor] provided element should be an input or textarea element')
7 | }
8 |
9 | this.element = element
10 | }
11 |
12 | getValue = (): string|null => {
13 | switch(this.element.tagName) {
14 | case 'INPUT': return this.element.value
15 | case 'TEXTAREA': return this.element.innerText
16 | default: return null;
17 | }
18 | }
19 |
20 | setValue = (value: string) => {
21 | switch(this.element.tagName) {
22 | case 'INPUT':
23 | this.element.value = value
24 | break;
25 | case 'TEXTAREA':
26 | this.element.innerText = value
27 | }
28 |
29 | this.element.dispatchEvent(new Event('change'))
30 | }
31 |
32 | getElement(): HTMLInputElement|HTMLTextAreaElement {
33 | return this.element
34 | }
35 | }
36 |
37 | export default BindInput
38 |
--------------------------------------------------------------------------------
/versions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ###########################################################################################
4 | # This scripts extracts the Gutenberg package versions from a Gutenberg release directory #
5 | ###########################################################################################
6 |
7 | GUTENBERG_DIR=$1
8 | PACKAGES_DIR="$GUTENBERG_DIR/packages"
9 | PACKAGES=(
10 | "api-fetch"
11 | "base-styles"
12 | "block-editor"
13 | "block-library"
14 | "blocks"
15 | "components"
16 | "data"
17 | "element"
18 | "format-library"
19 | "hooks"
20 | "keyboard-shortcuts"
21 | "server-side-render"
22 | )
23 |
24 | if [[ ! -d $PACKAGES_DIR ]]; then
25 | echo 'Directory does not exist';
26 | exit 1
27 | fi
28 |
29 | cd $PACKAGES_DIR
30 |
31 | MISSING_PACKAGES=()
32 | for PACKAGE in ${PACKAGES[@]}; do
33 | FILE="$PACKAGE/package.json"
34 | if [[ -f $FILE ]]; then
35 | VERSION=$(cat $FILE | egrep -o '"version": (".*")' | egrep -o '\d+\.\d+\.\d+')
36 | PACKAGE_VERSION="\"@wordpress/$PACKAGE\": \"~$VERSION\","
37 | echo $PACKAGE_VERSION
38 | else
39 | MISSING_PACKAGES+=($PACKAGE)
40 | fi
41 | done
42 |
43 | for PACKAGE in ${MISSING_PACKAGES[@]}; do
44 | echo "Package '$PACKAGE' was not found."
45 | done
46 |
47 | exit 0
48 |
--------------------------------------------------------------------------------
/playground/main.js:
--------------------------------------------------------------------------------
1 | import '../src/styles.scss'
2 | import * as BlockEditor from '../src/index'
3 |
4 | const { hooks } = BlockEditor.wordpress
5 |
6 | hooks.addFilter('blocks.registerBlockType', 'block-editor', (settings, blockName) => {
7 | return settings
8 | })
9 |
10 | document.addEventListener('block-editor/init', e => {
11 | console.log(e)
12 | })
13 |
14 | const form = document.getElementById('form')
15 | form.addEventListener('submit', (e) => {
16 | e.preventDefault()
17 | console.log('submit', e)
18 | })
19 |
20 | const element = document.getElementById('content');
21 | element.addEventListener('change', (e) => {
22 | console.log(e.target.value)
23 | })
24 |
25 | const settings = {
26 | mediaUpload: ({filesList, onFileChange}) => {
27 | const files = Array.from(filesList).map(window.URL.createObjectURL)
28 |
29 | onFileChange(files)
30 |
31 | setTimeout(() => {
32 | const uploadedFiles = Array.from(filesList).map(file => {
33 | return {
34 | id: file.name,
35 | name: file.name,
36 | url: `https://dummyimage.com/600x400/000/fff&text=${file.name}`
37 | }
38 | })
39 | onFileChange(uploadedFiles)
40 | }, 1000)
41 | }
42 | }
43 | BlockEditor.initializeEditor(element, settings);
44 |
45 |
--------------------------------------------------------------------------------
/src/components/KeyboardShortcuts.tsx:
--------------------------------------------------------------------------------
1 | import { __ } from '@wordpress/i18n'
2 | import { createElement, useEffect, useCallback } from '@wordpress/element'
3 | import { useDispatch } from '@wordpress/data'
4 |
5 | import { useShortcut, store } from '@wordpress/keyboard-shortcuts'
6 |
7 | const KeyboardShortcuts = () => {
8 | const { undo, redo } = useDispatch('block-editor')
9 |
10 | useShortcut(
11 | 'block-editor/undo',
12 | useCallback((event: Event) => {
13 | event.preventDefault()
14 | undo()
15 | }, [undo]),
16 | { bindGlobal: true }
17 | )
18 |
19 | useShortcut(
20 | 'block-editor/redo',
21 | useCallback((event: Event) => {
22 | event.preventDefault()
23 | redo()
24 | }, [redo]),
25 | { bindGlobal: true }
26 | )
27 |
28 | return null
29 | }
30 |
31 | const KeyboardShortcutsRegister = () => {
32 | const { registerShortcut } = useDispatch(store)
33 |
34 | useEffect(() => {
35 | registerShortcut({
36 | name: 'block-editor/undo',
37 | category: 'global',
38 | description: __('Undo'),
39 | keyCombination: {
40 | modifier: 'primary',
41 | character: 'z',
42 | },
43 | })
44 |
45 | registerShortcut({
46 | name: 'block-editor/redo',
47 | category: 'global',
48 | description: __('Redo'),
49 | keyCombination: {
50 | modifier: 'primaryShift',
51 | character: 'z',
52 | },
53 | })
54 | }, [registerShortcut])
55 |
56 | return null
57 | }
58 |
59 | KeyboardShortcuts.Register = KeyboardShortcutsRegister
60 |
61 | export default KeyboardShortcuts
62 |
--------------------------------------------------------------------------------
/src/store/reducer.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuid } from 'uuid'
2 | import Block from "../interfaces/block";
3 | import BlockEditorState from "../interfaces/block-editor-state";
4 | import { DUPLICATE_BLOCKS, REDO, REMOVE_BLOCKS, SET_BLOCKS, UNDO } from "./actions";
5 |
6 | const DEFAULT_STATE: BlockEditorState = {
7 | blocks: {
8 | past: [],
9 | current: [],
10 | future: []
11 | }
12 | }
13 |
14 | export default function reducer (state = DEFAULT_STATE, action) {
15 | switch (action.type) {
16 | case SET_BLOCKS:
17 | return {
18 | ...state,
19 | blocks: {
20 | current: action.blocks,
21 | past: [
22 | ...state.blocks.past.slice(-19),
23 | state.blocks.current
24 | ],
25 | future: []
26 | }
27 | }
28 | case UNDO:
29 | if (state.blocks.past.length === 0) return state
30 | const past = state.blocks.past
31 | const undoCurrent = past.pop()
32 | return {
33 | ...state,
34 | blocks: {
35 | past,
36 | current: undoCurrent,
37 | future: [state.blocks.current, ...state.blocks.future]
38 | }
39 | }
40 | case REDO:
41 | if (state.blocks.future.length === 0) return state
42 | const future = state.blocks.future
43 | const redoCurrent = future.shift()
44 | return {
45 | ...state,
46 | blocks: {
47 | past: [...state.blocks.past, state.blocks.current],
48 | current: redoCurrent,
49 | future
50 | }
51 | }
52 | }
53 |
54 | return state
55 | }
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@van-ons/block-editor",
3 | "version": "1.0.0",
4 | "description": "A standalone implementation of the WordPress Block Editor",
5 | "main": "dist/index.js",
6 | "module": "dist/index.js",
7 | "keywords": [
8 | "block",
9 | "editor",
10 | "gutenberg",
11 | "block-editor"
12 | ],
13 | "author": "Maurice Wijnia",
14 | "license": "GPL-2.0-or-later",
15 | "types": "dist/index.d.ts",
16 | "files": [
17 | "dist/"
18 | ],
19 | "scripts": {
20 | "clean": "rm -rf dist",
21 | "test": "echo \"Error: no test specified\" && exit 1",
22 | "watch": "npm run build:ts -- --watch & npm run build:sass -- --watch",
23 | "start": "vite playground",
24 | "prebuild": "npm run clean",
25 | "build": "npm run build:ts && npm run build:sass",
26 | "build:sass": "sass src/styles.scss dist/styles.css",
27 | "build:ts": "tsc"
28 | },
29 | "dependencies": {
30 | "@wordpress/api-fetch": "^6.6.0",
31 | "@wordpress/base-styles": "^4.5.0",
32 | "@wordpress/block-editor": "^9.1.0",
33 | "@wordpress/block-library": "^7.6.0",
34 | "@wordpress/blocks": "^11.8.0",
35 | "@wordpress/components": "^19.11.0",
36 | "@wordpress/data": "^6.9.0",
37 | "@wordpress/element": "^4.7.0",
38 | "@wordpress/format-library": "^3.7.0",
39 | "@wordpress/hooks": "^3.9.0",
40 | "@wordpress/keyboard-shortcuts": "^3.7.0",
41 | "@wordpress/server-side-render": "^3.7.0",
42 | "axios": "^0.21.1",
43 | "uuid": "^8.3.2"
44 | },
45 | "devDependencies": {
46 | "css-loader": "^6.5.1",
47 | "css-minimizer-webpack-plugin": "^3.1.4",
48 | "mini-css-extract-plugin": "^2.4.5",
49 | "sass": "^1.43.4",
50 | "sass-loader": "^12.3.0",
51 | "ts-loader": "^9.2.6",
52 | "typescript": "^4.5.2",
53 | "vite": "^2.9.9",
54 | "webpack": "^5.64.2",
55 | "webpack-bundle-analyzer": "^4.5.0",
56 | "webpack-cli": "^4.9.1",
57 | "webpack-dev-server": "^4.6.0",
58 | "webpack-merge": "^5.8.0"
59 | },
60 | "peerDependencies": {
61 | "react": "~17.0.2",
62 | "react-dom": "~17.0.2"
63 | },
64 | "repository": {
65 | "type": "git",
66 | "url": "git+https://github.com/VanOns/block-editor.git"
67 | },
68 | "bugs": {
69 | "url": "https://github.com/VanOns/block-editor/issues"
70 | },
71 | "homepage": "https://github.com/VanOns/block-editor#readme"
72 | }
73 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @use "sass:math";
2 |
3 | @import "../node_modules/@wordpress/base-styles/animations";
4 | @import "../node_modules/@wordpress/base-styles/breakpoints";
5 | @import "../node_modules/@wordpress/base-styles/colors";
6 | @import "../node_modules/@wordpress/base-styles/mixins";
7 | @import "../node_modules/@wordpress/base-styles/default-custom-properties";
8 | @import "../node_modules/@wordpress/base-styles/variables";
9 | @import "../node_modules/@wordpress/base-styles/z-index";
10 |
11 |
12 | @import "../node_modules/@wordpress/components/src/style.scss";
13 | @import "../node_modules/@wordpress/block-editor/src/style.scss";
14 | @import "../node_modules/@wordpress/block-library/src/style.scss";
15 | @import "../node_modules/@wordpress/block-library/src/theme.scss";
16 | @import "../node_modules/@wordpress/block-library/src/editor.scss";
17 | @import "../node_modules/@wordpress/format-library/src/style.scss";
18 |
19 | $sidebar-width: 300px;
20 | $border-color: #e0e0e0;
21 | $test: 1;
22 |
23 | .block-editor-container {
24 | position: relative;
25 | border: solid 1px $border-color;
26 | border-radius: 2px;
27 |
28 | input, textarea {
29 | box-sizing: border-box;
30 | }
31 |
32 | .block-editor {
33 | display: flex;
34 | flex-direction: column;
35 | }
36 |
37 | .block-editor__header {
38 | grid-area: header;
39 | display: flex;
40 | justify-content: space-between;
41 | padding: .5rem;
42 | border-bottom: solid 1px $border-color;
43 | }
44 |
45 | .block-editor__header-toolbar {
46 | display: flex;
47 | }
48 |
49 | .block-editor__content {
50 | display: flex;
51 | }
52 |
53 | .block-editor__editor {
54 | flex: 1;
55 | padding: 1rem;
56 | overflow-y: auto;
57 | }
58 |
59 | .block-editor__sidebar {
60 | flex: 0 0 $sidebar-width;
61 | overflow-y: auto;
62 | border-left: 1px solid $border-color;
63 |
64 | .components-panel {
65 | box-sizing: content-box;
66 | height: 100%;
67 | position: relative;
68 | border: none;
69 | }
70 | }
71 |
72 | .block-editor-inserter__menu {
73 | background-color: #FFFFFF;
74 | }
75 |
76 | .block-editor-inserter__popover {
77 | .components-popover__content {
78 | background-color: #FFFFFF;
79 | max-height: 400px !important;
80 | }
81 |
82 | .block-editor-inserter__menu {
83 | margin: -12px -8px;
84 | }
85 | }
86 |
87 | iframe, img {
88 | border: none;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/BlockEditor.tsx:
--------------------------------------------------------------------------------
1 | import { createElement, useRef } from '@wordpress/element'
2 | import {
3 | BlockEditorProvider,
4 | BlockInspector,
5 | BlockList,
6 | BlockTools,
7 | Inserter,
8 | ObserveTyping,
9 | WritingFlow,
10 | BlockEditorKeyboardShortcuts,
11 | } from '@wordpress/block-editor'
12 | import { ToolbarButton, Popover } from '@wordpress/components'
13 | import { undo as undoIcon, redo as redoIcon } from '@wordpress/icons'
14 |
15 | import Header from './Header'
16 | import Sidebar from './Sidebar'
17 | import InserterToggle from './InserterToggle'
18 | import EditorSettings from '../interfaces/editor-settings'
19 | import Block from '../interfaces/block'
20 | import Notices from "./Notices"
21 |
22 | import '@wordpress/format-library'
23 |
24 | interface BlockEditorProps {
25 | settings: EditorSettings,
26 | blocks: Block[],
27 | onChange: (blocks: Block[]) => void,
28 | undo?: () => void,
29 | redo?: () => void,
30 | canUndo?: boolean,
31 | canRedo?: boolean
32 | }
33 |
34 | const BlockEditor = ({ settings, onChange, blocks, undo, redo, canUndo, canRedo }: BlockEditorProps) => {
35 | const inputTimeout = useRef(null)
36 |
37 | const handleInput = (blocks: Block[]) => {
38 | if (inputTimeout.current) {
39 | clearTimeout(inputTimeout.current)
40 | }
41 |
42 | inputTimeout.current = setTimeout(() => {
43 | onChange(blocks)
44 | }, 500)
45 | }
46 |
47 | const handleChange = (blocks: Block[]) => {
48 | if (inputTimeout.current) {
49 | clearTimeout(inputTimeout.current)
50 | }
51 |
52 | onChange(blocks)
53 | }
54 |
55 | return (
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default BlockEditor;
89 |
--------------------------------------------------------------------------------
/src/lib/blocks.ts:
--------------------------------------------------------------------------------
1 | import { getBlockTypes } from '@wordpress/blocks'
2 | import { registerCoreBlocks } from '@wordpress/block-library'
3 |
4 | import * as paragraph from '@wordpress/block-library/build-module/paragraph'
5 | import * as image from '@wordpress/block-library/build/image'
6 | import * as heading from '@wordpress/block-library/build/heading'
7 | import * as quote from '@wordpress/block-library/build/quote'
8 | import * as gallery from '@wordpress/block-library/build/gallery'
9 | import * as audio from '@wordpress/block-library/build/audio'
10 | import * as buttons from '@wordpress/block-library/build/buttons'
11 | import * as button from '@wordpress/block-library/build/button'
12 | import * as code from '@wordpress/block-library/build/code'
13 | import * as columns from '@wordpress/block-library/build/columns'
14 | import * as column from '@wordpress/block-library/build/column'
15 | import * as cover from '@wordpress/block-library/build/cover'
16 | import * as file from '@wordpress/block-library/build/file'
17 | import * as html from '@wordpress/block-library/build/html'
18 | import * as mediaText from '@wordpress/block-library/build/media-text'
19 | import * as list from '@wordpress/block-library/build/list'
20 | import * as missing from '@wordpress/block-library/build/missing'
21 |
22 | import * as preformatted from '@wordpress/block-library/build/preformatted'
23 | import * as pullquote from '@wordpress/block-library/build/pullquote'
24 | import * as group from '@wordpress/block-library/build/group'
25 | import * as separator from '@wordpress/block-library/build/separator'
26 | import * as spacer from '@wordpress/block-library/build/spacer'
27 | import * as table from '@wordpress/block-library/build/table'
28 | import * as textColumns from '@wordpress/block-library/build/text-columns'
29 | import * as verse from '@wordpress/block-library/build/verse'
30 | import * as video from '@wordpress/block-library/build/video'
31 | import * as socialLinks from '@wordpress/block-library/build/social-links'
32 | import * as socialLink from '@wordpress/block-library/build/social-link'
33 |
34 | import * as embed from '@wordpress/block-library/build/embed'
35 | import * as classic from '@wordpress/block-library/build/freeform'
36 | import * as archives from '@wordpress/block-library/build/archives'
37 | import * as more from '@wordpress/block-library/build/more'
38 | import * as nextpage from '@wordpress/block-library/build/nextpage'
39 | import * as calendar from '@wordpress/block-library/build/calendar'
40 | import * as categories from '@wordpress/block-library/build/categories'
41 | import * as reusableBlock from '@wordpress/block-library/build/block'
42 | import * as rss from '@wordpress/block-library/build/rss'
43 | import * as search from '@wordpress/block-library/build/search'
44 | import * as shortcode from '@wordpress/block-library/build/shortcode'
45 | import * as tagCloud from '@wordpress/block-library/build/tag-cloud'
46 |
47 | /**
48 | * Register all supported core blocks that are not registered yet and are not disabled in the settings
49 | *
50 | * @param disabledCoreBlocks
51 | */
52 | function registerBlocks(disabledCoreBlocks: string[] = []) {
53 | registerCoreBlocks(
54 | filterRegisteredBlocks(
55 | getCoreBlocks(disabledCoreBlocks)
56 | )
57 | )
58 | }
59 |
60 | /**
61 | * Remove blocks that are already registered from an array of blocks
62 | *
63 | * @param blocks
64 | */
65 | function filterRegisteredBlocks(blocks: any[]) {
66 | const registredBlockNames = getBlockTypes().map(b => b.name)
67 | return blocks.filter(b => !registredBlockNames.includes(b.name))
68 | }
69 |
70 | /**
71 | * Get all supported core blocks except for the ones disabled through settings
72 | *
73 | * @param disabledCoreBlocks
74 | */
75 | export const getCoreBlocks = (disabledCoreBlocks: string[] = []) => {
76 | return CORE_BLOCKS.filter(b => !disabledCoreBlocks.includes(b.name))
77 | }
78 |
79 | const CORE_BLOCKS = [
80 | // Common blocks are grouped at the top to prioritize their display
81 | // in various contexts — like the inserter and auto-complete components.
82 | paragraph,
83 | image,
84 | heading,
85 | gallery,
86 | list,
87 | quote,
88 |
89 | // Register all remaining core blocks.
90 | audio,
91 | button,
92 | buttons,
93 | code,
94 | columns,
95 | column,
96 | cover,
97 | file,
98 | group,
99 | html,
100 | mediaText,
101 | missing,
102 | preformatted,
103 | pullquote,
104 | separator,
105 | socialLinks,
106 | socialLink,
107 | spacer,
108 | table,
109 | textColumns,
110 | verse,
111 | video,
112 |
113 | embed,
114 | classic,
115 | shortcode,
116 | archives,
117 | tagCloud,
118 | reusableBlock,
119 | rss,
120 | search,
121 | calendar,
122 | categories,
123 | more,
124 | nextpage,
125 | ]
126 |
127 | export { registerBlocks }
128 |
--------------------------------------------------------------------------------
/src/components/Editor.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, render, createElement, StrictMode, unmountComponentAtNode } from '@wordpress/element'
2 | import apiFetch from '@wordpress/api-fetch'
3 | import { SlotFillProvider } from '@wordpress/components'
4 | import { parse, serialize } from '@wordpress/blocks'
5 | import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'
6 | import { doAction, applyFilters } from "@wordpress/hooks"
7 |
8 | import '../store'
9 | import { registerBlocks } from '../lib/blocks'
10 | import BlockEditor from './BlockEditor'
11 | import Header from './Header'
12 | import Sidebar from './Sidebar'
13 | import BindInput from '../lib/bind-input'
14 | import EditorSettings from '../interfaces/editor-settings'
15 | import { select, dispatch, useSelect, useDispatch } from '@wordpress/data'
16 | import defaultSettings from '../lib/default-settings'
17 | import KeyboardShortcuts from './KeyboardShortcuts'
18 |
19 |
20 | export interface EditorProps {
21 | settings: EditorSettings,
22 | onChange: (value: string) => void,
23 | input?: HTMLInputElement|HTMLTextAreaElement,
24 | value?: string,
25 | }
26 |
27 | const Editor = ({ settings, onChange, input, value }: EditorProps) => {
28 | const [sidebarOpen, setSidebarOpen] = useState(true)
29 | const { setBlocks, undo, redo } = useDispatch('block-editor')
30 |
31 | const { blocks, canUndo, canRedo } = useSelect(select => {
32 | return {
33 | blocks: select('block-editor').getBlocks(),
34 | canUndo: select('block-editor').canUndo(),
35 | canRedo: select('block-editor').canRedo()
36 | }
37 | })
38 |
39 | useEffect(() => {
40 | registerBlocks(settings.disabledCoreBlocks)
41 |
42 | input?.form?.addEventListener('submit', preventSubmit)
43 |
44 | if (settings.fetchHandler) {
45 | apiFetch.setFetchHandler(settings.fetchHandler)
46 | }
47 |
48 | /**
49 | * Cleanup
50 | */
51 | return () => {
52 | input?.form?.removeEventListener('submit', preventSubmit)
53 | }
54 | }, [])
55 |
56 | useEffect(() => {
57 | if (value) {
58 | setBlocks(parse(value))
59 | }
60 | }, [value]);
61 |
62 | useEffect(() => {
63 | onChange(serialize(blocks))
64 | }, [blocks])
65 |
66 | const toggleSidebar = () => {
67 | setSidebarOpen(!sidebarOpen)
68 | }
69 |
70 | return (
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
84 |
93 |
94 | {sidebarOpen && }
95 |
96 |
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | const removeEditor = (element: HTMLInputElement | HTMLTextAreaElement) => {
104 | dispatch('block-editor').setBlocks([])
105 | dispatch('core/blocks').removeBlockTypes(
106 | select('core/blocks').getBlockTypes().map(b => b.name)
107 | )
108 |
109 | const container = element.parentNode?.querySelector('.block-editor-container')
110 | if (container) {
111 | unmountComponentAtNode(container)
112 | container.remove()
113 | }
114 | }
115 |
116 | const initializeEditor = (element: HTMLInputElement | HTMLTextAreaElement, settings: EditorSettings = {}) => {
117 | const input = new BindInput(element)
118 |
119 | const container = document.createElement('div')
120 | container.classList.add('block-editor-container')
121 | input.getElement().insertAdjacentElement('afterend', container)
122 | input.getElement().style.display = 'none';
123 |
124 | doAction('blockEditor.beforeInit', container)
125 |
126 | render(
127 | ,
133 | container
134 | )
135 |
136 | doAction('blockEditor.afterInit', container)
137 | }
138 |
139 | const preventSubmit = (event: SubmitEvent) => {
140 | if (event.submitter?.matches('.block-editor *')) {
141 | event.preventDefault()
142 | event.stopPropagation()
143 | event.stopImmediatePropagation()
144 | }
145 | }
146 |
147 | export { initializeEditor, removeEditor, Editor }
148 |
--------------------------------------------------------------------------------