├── .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 |
14 | 15 | 16 | 17 |
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 |
22 | 23 |
24 | 25 |
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 | --------------------------------------------------------------------------------