├── HISTORY.md ├── examples ├── event.html ├── single.html ├── controllered.html ├── plugin.html ├── simple.html ├── value.html ├── single.js ├── event.js ├── value.js ├── controllered.js ├── plugin.js └── simple.js ├── tests ├── usage.spec.js └── index.js ├── index.js ├── src ├── EditorCore │ ├── export │ │ ├── defaultInlineStyle.ts │ │ ├── isUnitlessNumber.ts │ │ ├── exportText.ts │ │ └── getHTML.ts │ ├── ConfigStore.ts │ ├── handlePastedText.ts │ ├── createPlugin.ts │ ├── customHTML2Content.ts │ └── index.tsx ├── Toolbar │ ├── ToolbarLine.tsx │ ├── index.tsx │ └── Toolbar.tsx ├── definitions │ └── draft-js.d.ts ├── index.ts └── interfaces.d.ts ├── .vscode ├── settings.json └── tasks.json ├── .editorconfig ├── .gitignore ├── type-definitions └── rc-editor-core.d.ts ├── tsconfig.json ├── .travis.yml ├── LICENSE ├── package.json ├── assets └── index.less └── README.md /HISTORY.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/event.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/single.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/controllered.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/plugin.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/value.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /tests/usage.spec.js: -------------------------------------------------------------------------------- 1 | // add spec here! 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // export this package's api 2 | export * from './src/'; 3 | -------------------------------------------------------------------------------- /src/EditorCore/export/defaultInlineStyle.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | BOLD: { 3 | 'font-weight': 'bold', 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.tsdk": "./node_modules/typescript/lib/" 4 | } -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | // do not add tests to this file, add tests to other .spec.js 2 | const req = require.context('.', false, /\.spec\.js$/); 3 | req.keys().forEach(req); 4 | -------------------------------------------------------------------------------- /src/Toolbar/ToolbarLine.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export default class ToolbarLine extends React.Component { 3 | render() { 4 | return
{this.props.children}
; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css,jsx,ts,tsx}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /src/definitions/draft-js.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as Draft from 'draft-js'; 3 | 4 | declare module 'draft-js' { 5 | export type DraftBlockRenderConfig = { 6 | element: string; 7 | wrapper?: React.ReactElement; 8 | }; 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | node_modules 21 | .cache 22 | *.css 23 | build 24 | lib 25 | es 26 | coverage 27 | typings 28 | yarn.lock 29 | package-lock.json 30 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "tsc", 6 | "jsx": "react", 7 | "isShellCommand": true, 8 | "args": ["-p", ".", "--jsx", "preserve"], 9 | "module": "commonjs", 10 | "declaration": false, 11 | "problemMatcher": "$tsc" 12 | } -------------------------------------------------------------------------------- /src/EditorCore/ConfigStore.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | export default class ConfigStore { 4 | private _store: Map; 5 | constructor() { 6 | this._store = Map(); 7 | } 8 | 9 | set(key, value) { 10 | this._store = this._store.set(key, value); 11 | } 12 | 13 | get(key) { 14 | return this._store.get(key); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /type-definitions/rc-editor-core.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace RcEditorCore { 2 | interface EditorCore { 3 | props:any; 4 | state:any; 5 | refs:any; 6 | context:any; 7 | setState():any; 8 | render():any; 9 | forceUpdate():any; 10 | } 11 | interface IEditor { 12 | new():EditorCore; 13 | } 14 | var EditorCore: IEditor; 15 | } 16 | 17 | export = RcEditorCore; 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import EditorCore from './EditorCore'; 2 | 3 | const { GetText, GetHTML } = EditorCore; 4 | const toEditorState = EditorCore.ToEditorState; 5 | 6 | const EditorCorePublic = { 7 | EditorCore, 8 | GetText, 9 | GetHTML, 10 | toEditorState, 11 | }; 12 | 13 | export { 14 | EditorCore, 15 | GetText, 16 | GetHTML, 17 | toEditorState, 18 | }; 19 | 20 | export default EditorCorePublic; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "moduleResolution": "node", 5 | "allowSyntheticDefaultImports": true, 6 | "experimentalDecorators": true, 7 | "jsx": "preserve", 8 | "noUnusedParameters": true, 9 | "noUnusedLocals": true, 10 | "target": "es6", 11 | "lib": [ 12 | "dom", 13 | "es7" 14 | ] 15 | }, 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /src/EditorCore/handlePastedText.ts: -------------------------------------------------------------------------------- 1 | export type DraftHandleValue = 'handled' | 'not-handled'; 2 | import { Modifier } from 'draft-js'; 3 | import customHTML2Content from './customHTML2Content'; 4 | 5 | export default function handlePastedText(text: string, html?: string):DraftHandleValue { 6 | if (html) { 7 | const fragment = customHTML2Content(html); 8 | const withImage = Modifier.replaceWithFragment( 9 | imageBlock, 10 | insertionTarget, 11 | fragment 12 | ); 13 | } 14 | return 'not-handled'; 15 | } 16 | -------------------------------------------------------------------------------- /examples/single.js: -------------------------------------------------------------------------------- 1 | // use jsx to render html, do not modify simple.html 2 | /* eslint-disable new-cap, no-console */ 3 | 4 | import 'rc-editor-core/assets/index.less'; 5 | import { EditorCore } from 'rc-editor-core'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | 9 | const toolbars = []; 10 | 11 | 12 | ReactDOM.render( console.log('on focus')} 16 | onBlur={() => console.log('on blur')} 17 | toolbars={toolbars} 18 | />, document.getElementById('__react-content')); 19 | -------------------------------------------------------------------------------- /src/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // interface Plugin { 4 | // decorators?: Array; 5 | // onChange: (editorState: EditorState)=> boolean; 6 | // callbacks: { 7 | // onUpArrow?: Function; 8 | // onDownArrow?: Function; 9 | // handleReturn?: Function; 10 | // handleKeyBinding?: Function; 11 | // setEditorState: (editorState: EditorState) => void; 12 | // getEditorState: () => EditorState; 13 | // }; 14 | // } 15 | // 16 | // interface EditorProps { 17 | // multiLines: boolean; 18 | // plugins: Array; 19 | // prefixCls: string; 20 | // onChange?: (editorState: EditorState) => void; 21 | // } 22 | // 23 | // interface EditorCoreState { 24 | // editorState: EditorState; 25 | // } 26 | // 27 | -------------------------------------------------------------------------------- /src/EditorCore/createPlugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Editor, 3 | EditorState, 4 | ContentState, 5 | CompositeDecorator, 6 | Entity, 7 | Modifier, 8 | getDefaultKeyBinding, 9 | KeyBindingUtil, 10 | DefaultDraftBlockRenderMap, 11 | DraftBlockRenderConfig, 12 | } from 'draft-js'; 13 | 14 | export interface Plugin { 15 | name: string; 16 | decorators?: Array; 17 | component?: Function; 18 | onChange: (editorState: EditorState)=> EditorState; 19 | customStyleFn?: Function; 20 | callbacks: { 21 | onUpArrow?: Function; 22 | onDownArrow?: Function; 23 | handleReturn?: Function; 24 | handleKeyBinding?: Function; 25 | setEditorState: (editorState: EditorState) => void; 26 | getEditorState: () => EditorState; 27 | }; 28 | config?: Object; 29 | } -------------------------------------------------------------------------------- /src/Toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EditorState} from 'draft-js'; 3 | import Toolbar from './Toolbar'; 4 | 5 | function noop(_: any): any {}; 6 | 7 | export function createToolbar(config = {}) { 8 | function editorStateChange(editorState) { 9 | // console.log('>> editorStateChange', editorState); 10 | } 11 | const callbacks = { 12 | onChange: editorStateChange, 13 | onUpArrow: noop, 14 | onDownArrow: noop, 15 | getEditorState: noop, 16 | setEditorState: noop, 17 | handleReturn: noop, 18 | }; 19 | 20 | return { 21 | name: 'toolbar', 22 | decorators: [], 23 | callbacks, 24 | onChange(editorState: EditorState): any { 25 | return callbacks.onChange ? callbacks.onChange(editorState) : editorState; 26 | }, 27 | component: Toolbar, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | notifications: 6 | email: 7 | - surgesoft@gmail.com 8 | 9 | node_js: 10 | - 6 11 | 12 | before_install: 13 | - | 14 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/' 15 | then 16 | echo "Only docs were updated, stopping build process." 17 | exit 18 | fi 19 | npm install npm@3.x -g 20 | phantomjs --version 21 | script: 22 | - | 23 | if [ "$TEST_TYPE" = test ]; then 24 | npm test 25 | else 26 | npm run $TEST_TYPE 27 | fi 28 | env: 29 | matrix: 30 | - TEST_TYPE=lint 31 | - TEST_TYPE=test 32 | - TEST_TYPE=coverage 33 | - TEST_TYPE=saucelabs 34 | 35 | 36 | matrix: 37 | allow_failures: 38 | - env: "TEST_TYPE=test" 39 | - env: "TEST_TYPE=coverage" 40 | - env: "TEST_TYPE=saucelabs" -------------------------------------------------------------------------------- /examples/event.js: -------------------------------------------------------------------------------- 1 | // use jsx to render html, do not modify event.html 2 | /* eslint-disable new-cap, no-console */ 3 | 4 | import 'rc-editor-core/assets/index.less'; 5 | import { EditorCore } from 'rc-editor-core'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | 9 | const EventPlugin = { 10 | callbacks: { 11 | setEditorState: null, 12 | getEditorState: null, 13 | onUpArrow: () => { console.log('>> onUpArrow'); }, 14 | onDownArrow: () => { console.log('>> onDownArrow '); }, 15 | onBlur: () => { console.log('>> onBlur'); }, 16 | onFocus: () => { console.log('>> onBlur prevent'); return true; }, 17 | handlePastedText: (text, html) => { console.log('>> handlePastedText', text, html); }, 18 | }, 19 | }; 20 | const plugins = [EventPlugin]; 21 | const toolbars = []; 22 | 23 | 24 | ReactDOM.render( console.log('on focus')} 27 | onBlur={() => console.log('on blur')} 28 | toolbars={toolbars} 29 | />, document.getElementById('__react-content')); 30 | -------------------------------------------------------------------------------- /src/EditorCore/export/isUnitlessNumber.ts: -------------------------------------------------------------------------------- 1 | var isUnitlessNumber = { 2 | animationIterationCount: true, 3 | borderImageOutset: true, 4 | borderImageSlice: true, 5 | borderImageWidth: true, 6 | boxFlex: true, 7 | boxFlexGroup: true, 8 | boxOrdinalGroup: true, 9 | columnCount: true, 10 | flex: true, 11 | flexGrow: true, 12 | flexPositive: true, 13 | flexShrink: true, 14 | flexNegative: true, 15 | flexOrder: true, 16 | gridRow: true, 17 | gridColumn: true, 18 | fontWeight: true, 19 | lineClamp: true, 20 | lineHeight: true, 21 | opacity: true, 22 | order: true, 23 | orphans: true, 24 | tabSize: true, 25 | widows: true, 26 | zIndex: true, 27 | zoom: true, 28 | 29 | // SVG-related properties 30 | fillOpacity: true, 31 | floodOpacity: true, 32 | stopOpacity: true, 33 | strokeDasharray: true, 34 | strokeDashoffset: true, 35 | strokeMiterlimit: true, 36 | strokeOpacity: true, 37 | strokeWidth: true 38 | }; 39 | 40 | function prefixKey(prefix, key) { 41 | return prefix + key.charAt(0).toUpperCase() + key.substring(1); 42 | } 43 | 44 | 45 | var prefixes = ['Webkit', 'ms', 'Moz', 'O']; 46 | Object.keys(isUnitlessNumber).forEach(function (prop) { 47 | prefixes.forEach(function (prefix) { 48 | isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]; 49 | }); 50 | }); 51 | 52 | export default isUnitlessNumber; -------------------------------------------------------------------------------- /src/EditorCore/export/exportText.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Entity } from "draft-js"; 2 | 3 | export function encodeContent(text: string): string { 4 | return text 5 | .split('&').join('&') 6 | .split('<').join('<') 7 | .split('>').join('>') 8 | .split('\xA0').join(' ') 9 | .split('\n').join('
' + '\n'); 10 | } 11 | 12 | export function decodeContent(text: string): string { 13 | return text 14 | .split('
' + '\n').join('\n'); 15 | } 16 | 17 | export default function exportText(editorState: EditorState, options = { encode: false}): string { 18 | const content = editorState.getCurrentContent(); 19 | const blockMap = content.getBlockMap(); 20 | const { encode } = options; 21 | return blockMap.map( block => { 22 | let resultText = ''; 23 | let lastPosition = 0; 24 | const text = block.getText(); 25 | block.findEntityRanges(function (character) { 26 | return !!character.getEntity(); 27 | }, function (start, end) { 28 | var key = block.getEntityAt(start); 29 | const entityData = content.getEntity(key).getData(); 30 | resultText += text.slice(lastPosition, start); 31 | resultText += entityData && entityData.export ? entityData.export(entityData) : text.slice(start, end ); 32 | lastPosition = end; 33 | }); 34 | resultText += text.slice(lastPosition); 35 | return encode ? encodeContent(resultText) : resultText; 36 | }).join(encode ? '
\n' : '\n'); 37 | } 38 | -------------------------------------------------------------------------------- /examples/value.js: -------------------------------------------------------------------------------- 1 | // use jsx to render html, do not modify simple.html 2 | /* eslint-disable new-cap, no-console */ 3 | 4 | import 'rc-editor-core/assets/index.less'; 5 | import { EditorCore, GetText } from 'rc-editor-core'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import BasicStyle from 'rc-editor-plugin-basic-style'; 9 | import Emoji from 'rc-editor-plugin-emoji'; 10 | import 'rc-editor-plugin-emoji/assets/index.css'; 11 | 12 | const plugins = [BasicStyle, Emoji]; 13 | const toolbars = [ 14 | ['bold', 'italic', 'underline', 'strikethrough', '|', 'superscript', 'subscript', '|', 'emoji'], 15 | ]; 16 | 17 | function editorChange(editorState) { 18 | console.log('>> editorExport:', GetText(editorState, { encode: true })); 19 | } 20 | 21 | class Editor extends React.Component { 22 | state = { 23 | defaultValue: 'hello world', 24 | }; 25 | 26 | reset = () => { 27 | this.refs.editor.Reset(); 28 | } 29 | render() { 30 | return ( 31 |
32 | 33 | editorChange(editorState)} 39 | onFocus={(ev) => { console.log('focus', ev); }} 40 | onBlur={(ev) => { console.log('blur', ev); }} 41 | /> 42 |
43 | ); 44 | } 45 | } 46 | 47 | ReactDOM.render(, document.getElementById('__react-content')); 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Draft.js software 4 | 5 | Copyright (c) 2013-present, Facebook, Inc. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name Facebook nor the names of its contributors may be used to 19 | endorse or promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /examples/controllered.js: -------------------------------------------------------------------------------- 1 | // use jsx to render html, do not modify simple.html 2 | /* eslint-disable new-cap, no-console */ 3 | 4 | import 'rc-editor-core/assets/index.less'; 5 | import { EditorCore, toEditorState } from 'rc-editor-core'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import BasicStyle from 'rc-editor-plugin-basic-style'; 9 | import Emoji from 'rc-editor-plugin-emoji'; 10 | import 'rc-editor-plugin-emoji/assets/index.css'; 11 | 12 | const plugins = [BasicStyle, Emoji]; 13 | const toolbars = [ 14 | ['bold', 'italic', 'underline', 'strikethrough', '|', 'superscript', 'subscript', '|', 'emoji'], 15 | ]; 16 | 17 | function keyDown(ev) { 18 | if (ev.keyCode === 13 && ev.ctrlKey) { 19 | return 'split-block'; 20 | } 21 | } 22 | 23 | class Editor extends React.Component { 24 | state = { 25 | defaultValue: toEditorState('hello world'), 26 | value: null, 27 | readOnly: false, 28 | }; 29 | editorChange = (editorState) => { 30 | this.setState({ 31 | value: editorState, 32 | }); 33 | } 34 | reset = () => { 35 | this.setState({ 36 | value: this.state.defaultValue, 37 | }); 38 | } 39 | toggleReadOnly = () => { 40 | this.setState({ 41 | readOnly: !this.state.readOnly, 42 | }); 43 | } 44 | render() { 45 | return (
46 | 47 | 48 | keyDown(ev)} 54 | onChange={() => {}} 55 | value={''} 56 | /> 57 |
); 58 | } 59 | } 60 | 61 | ReactDOM.render(, document.getElementById('__react-content')); 62 | -------------------------------------------------------------------------------- /src/Toolbar/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | /* tslint:disable:interface-name */ 2 | 3 | import React from 'react'; 4 | import { Map, List } from 'immutable'; 5 | import { Plugin } from '../EditorCore'; 6 | import { EditorState } from 'draft-js'; 7 | import ToolbarLine from './ToolbarLine'; 8 | 9 | export interface ToolbarProps { 10 | plugins: List; 11 | toolbars: any[]; 12 | prefixCls: string; 13 | className: string; 14 | editorState: EditorState; 15 | } 16 | 17 | function noop() {} 18 | 19 | export default class Toolbar extends React.Component { 20 | public pluginsMap: Map; 21 | constructor(props) { 22 | super(props); 23 | const map = {}; 24 | props.plugins.forEach((plugin: Plugin) => { 25 | map[plugin.name] = plugin; 26 | }); 27 | this.pluginsMap = Map(map); 28 | this.state = { 29 | editorState: props.editorState, 30 | toolbars: [], 31 | }; 32 | } 33 | public renderToolbarItem(pluginName, idx) { 34 | const element = this.pluginsMap.get(pluginName); 35 | if (element && element.component) { 36 | const { component } = element; 37 | const props = { 38 | key: `toolbar-item-${idx}`, 39 | onClick: component.props ? component.props.onClick : noop , 40 | }; 41 | if (React.isValidElement(component)) { 42 | return React.cloneElement(component, props); 43 | } 44 | return React.createElement(component, props); 45 | } 46 | return null; 47 | } 48 | public conpomentWillReceiveProps(nextProps) { 49 | this.render(); 50 | } 51 | render() { 52 | const { toolbars, prefixCls } = this.props; 53 | return ( 54 |
55 | {toolbars.map((toolbar, idx) => { 56 | const children = React.Children.map(toolbar, this.renderToolbarItem.bind(this)); 57 | return ({children}); 58 | })} 59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-editor-core", 3 | "version": "0.8.10", 4 | "description": "editor-core ui component for react", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-editor-core", 9 | "editor-core" 10 | ], 11 | "homepage": "https://github.com/react-component/editor-core", 12 | "author": "surgesoft@gmail.com", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/react-component/editor-core.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/react-component/editor-core/issues" 19 | }, 20 | "files": [ 21 | "lib", 22 | "es", 23 | "assets/*.css", 24 | "type-definitions" 25 | ], 26 | "licenses": "MIT", 27 | "main": "./lib/index", 28 | "module": "./es/index", 29 | "typings": "type-definitions/rc-editor-core.d.ts", 30 | "config": { 31 | "port": 8003 32 | }, 33 | "scripts": { 34 | "build": "rc-tools run build", 35 | "compile": "rc-tools run compile --babel-runtime", 36 | "gh-pages": "rc-tools run gh-pages", 37 | "start": "rc-tools run server", 38 | "watch": "rc-tools run watch --out-dir=../editor/node_modules/rc-editor-core/lib", 39 | "pub": "rc-tools run pub", 40 | "lint": "rc-tools run lint", 41 | "karma": "rc-tools run karma", 42 | "saucelabs": "rc-tools run saucelabs", 43 | "test": "rc-tools run test", 44 | "chrome-test": "rc-tools run chrome-test", 45 | "coverage": "rc-tools run coverage" 46 | }, 47 | "devDependencies": { 48 | "expect.js": "0.3.x", 49 | "pre-commit": "1.x", 50 | "rc-editor-plugin-basic-style": "^0.3.6", 51 | "rc-editor-plugin-emoji": "^0.2.8", 52 | "rc-editor-plugin-image": "^0.0.4", 53 | "rc-tools": "^7.0.2", 54 | "react": "^16.0.0", 55 | "react-dom": "^16.0.0", 56 | "typescript": "~2.0", 57 | "typings": "^1.0.4" 58 | }, 59 | "dependencies": { 60 | "babel-runtime": "^6.26.0", 61 | "classnames": "^2.2.5", 62 | "draft-js": "^0.10.0", 63 | "immutable": "^3.7.4", 64 | "lodash": "^4.16.5", 65 | "prop-types": "^15.5.8", 66 | "setimmediate": "^1.0.5" 67 | }, 68 | "peerDependencies": { 69 | "react": ">=15.0.0", 70 | "react-dom": ">=15.0.0" 71 | }, 72 | "pre-commit": [ 73 | "lint" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /examples/plugin.js: -------------------------------------------------------------------------------- 1 | // use jsx to render html, do not modify simple.html 2 | /* eslint-disable new-cap, no-console */ 3 | 4 | import 'rc-editor-core/assets/index.less'; 5 | import { EditorCore, GetHTML } from 'rc-editor-core'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import { RichUtils } from 'draft-js'; 9 | import 'rc-editor-plugin-emoji/assets/index.css'; 10 | 11 | const callbacks = { 12 | getEditorState: () => {}, 13 | setEditorState: () => {}, 14 | getStyleMap: () => {}, 15 | }; 16 | 17 | function toggleInlineStyle(style) { 18 | return () => { 19 | const editorState = callbacks.getEditorState(); 20 | 21 | callbacks.setEditorState( 22 | RichUtils.toggleInlineStyle(editorState, `customer-style-${style}`) 23 | ); 24 | }; 25 | } 26 | 27 | const Test = { 28 | name: 'test', 29 | callbacks, 30 | component:
31 |
red
32 |
bold
33 |
, 34 | toHtml(text, entity) { 35 | console.log('>> toHtml', entity); 36 | if (entity.getType() === 'LINK') { 37 | return `text`; 38 | } 39 | }, 40 | customStyleFn(styleSet) { 41 | return styleSet.map(style => { 42 | if (style === 'customer-style-red') { 43 | return { 44 | color: 'red', 45 | }; 46 | } 47 | if (style === 'customer-style-bold') { 48 | return { 49 | fontWeight: 'bold', 50 | }; 51 | } 52 | return {}; 53 | }).reduce(Object.assign); 54 | }, 55 | }; 56 | 57 | const plugins = [Test]; 58 | const toolbars = [['test']]; 59 | 60 | function keyDown(ev) { 61 | if (ev.keyCode === 13) { 62 | if (ev.ctrlKey) { 63 | return 'split-block'; 64 | } 65 | return true; 66 | } 67 | return false; 68 | } 69 | 70 | class EditorWrapper extends React.Component { 71 | state = { 72 | plugins: [], 73 | editorState: null, 74 | }; 75 | onChange = (editorState) => { 76 | this.setState({ 77 | editorState, 78 | }); 79 | } 80 | render() { 81 | return (
82 | keyDown(ev)} 86 | onChange={this.onChange} 87 | value={this.state.editorState} 88 | /> 89 | {this.state.editorState ? GetHTML(this.state.editorState) : null} 90 |
); 91 | } 92 | } 93 | 94 | ReactDOM.render(, document.getElementById('__react-content')); 95 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | // use jsx to render html, do not modify simple.html 2 | /* eslint-disable new-cap, no-console */ 3 | 4 | import 'rc-editor-core/assets/index.less'; 5 | import 'rc-select/assets/index.css'; 6 | import { EditorCore, GetHTML } from 'rc-editor-core'; 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import BasicStyle from 'rc-editor-plugin-basic-style'; 10 | import Emoji from 'rc-editor-plugin-emoji'; 11 | import { Entity } from 'draft-js'; 12 | import 'rc-editor-plugin-emoji/assets/index.css'; 13 | 14 | const blockPlugin = { 15 | name: 'image', 16 | callbacks: { 17 | setEditorState: null, 18 | getEditorState: null, 19 | }, 20 | blockRenderMap: { 21 | img: { 22 | element: 'img', 23 | }, 24 | paragraph: { 25 | element: 'p', 26 | }, 27 | }, 28 | }; 29 | 30 | function MediaBlock({ block }) { 31 | console.log('>> block', block); 32 | const entity = block.getEntityAt(0); 33 | if (entity) { 34 | const entityInstance = Entity.get(entity); 35 | const entityData = entityInstance.getData(); 36 | console.log('MediaBlock', entityInstance.getType(), entityData); 37 | } 38 | return MediaBlock; 39 | } 40 | 41 | const ImagePlugin = { 42 | name: 'image', 43 | callbacks: { 44 | setEditorState: null, 45 | getEditorState: null, 46 | }, 47 | blockRendererFn: (contentBlock) => { 48 | if (contentBlock.getType() === 'image-block') { 49 | return { 50 | component: MediaBlock, 51 | editable: false, 52 | }; 53 | } 54 | }, 55 | blockRenderMap: { 56 | 'image-block': { 57 | component: 'div', 58 | editable: false, 59 | }, 60 | }, 61 | }; 62 | 63 | const plugins = [blockPlugin, ImagePlugin, BasicStyle, Emoji]; 64 | const toolbars = [['fontSize', '|', 65 | 'fontColor', 66 | 'bold', 'italic', 'underline', 'strikethrough', '|', 67 | 'superscript', 'subscript', '|', 68 | 'align-justify', 'align-left', 'align-right', 'align-middle', '|', 'image']]; 69 | 70 | class EditorWithPreview extends React.Component { 71 | state = { 72 | html: '', 73 | }; 74 | editorChange = (editorState) => { 75 | console.log('Editor Change:', editorState); 76 | this.setState({ 77 | html: GetHTML(editorState), 78 | }); 79 | } 80 | focus = () => { 81 | if (this.editor) this.editor.focus(); 82 | } 83 | render() { 84 | return (
85 |
86 | 87 | this.editor = ele} 95 | /> 96 |
); 97 | } 98 | } 99 | 100 | ReactDOM.render(, document.getElementById('__react-content')); 101 | -------------------------------------------------------------------------------- /assets/index.less: -------------------------------------------------------------------------------- 1 | @iconfont-css-prefix: editor-icon; 2 | @editor-prefix: rc-editor; 3 | 4 | @font-face { 5 | font-family: 'editoricon'; 6 | src: url('//at.alicdn.com/t/font_1466643381_3569083.eot'); /* IE9*/ 7 | src: url('//at.alicdn.com/t/font_1466643381_3569083.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 8 | url('//at.alicdn.com/t/font_1466643381_3569083.woff') format('woff'), /* chrome、firefox */ 9 | url('//at.alicdn.com/t/font_1466643381_3569083.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ 10 | url('//at.alicdn.com/t/font_1466643381_3569083.svg#iconfont') format('svg'); /* iOS 4.1- */ 11 | } 12 | 13 | 14 | .iconfont-mixin() { 15 | display: inline-block; 16 | font-style: normal; 17 | vertical-align: baseline; 18 | text-align: center; 19 | text-transform: none; 20 | text-rendering: auto; 21 | line-height: 1; 22 | padding: 6px; 23 | border: transparent 1px solid; 24 | cursor: pointer; 25 | 26 | &:before { 27 | display: block; 28 | font-size: 14px; 29 | font-family: "editoricon" !important; 30 | } 31 | } 32 | 33 | .@{iconfont-css-prefix} { 34 | color: #999; 35 | .iconfont-mixin(); 36 | 37 | &:hover, &.active { 38 | color: black; 39 | border-color: #999; 40 | } 41 | } 42 | .@{iconfont-css-prefix}-split { 43 | padding: 0 4px; 44 | &:before { 45 | content: ' '; 46 | border-right: 1px solid #ccc; 47 | } 48 | } 49 | 50 | .@{iconfont-css-prefix}-bold:before { content: '\e60b';} 51 | .@{iconfont-css-prefix}-underline:before { content: '\e60c';} 52 | .@{iconfont-css-prefix}-strikethrough:before { content: '\e60d';} 53 | .@{iconfont-css-prefix}-italic:before { content: '\e600';} 54 | .@{iconfont-css-prefix}-superscript:before { content: '\e60f';} 55 | .@{iconfont-css-prefix}-emoji:before { content: '\e610';} 56 | .@{iconfont-css-prefix}-subscript:before { content: '\e60e';} 57 | .@{iconfont-css-prefix}-align-justify:before { content: '\e605';} 58 | .@{iconfont-css-prefix}-align-left:before { content: '\e602';} 59 | .@{iconfont-css-prefix}-align-middle:before { content: '\e603';} 60 | .@{iconfont-css-prefix}-align-right:before { content: '\e601';} 61 | .@{iconfont-css-prefix}-font-color:before { content: '\e606';} 62 | .@{iconfont-css-prefix}-view:before { content: '\e607';} 63 | .@{iconfont-css-prefix}-picture:before { content: '\e608';} 64 | .@{iconfont-css-prefix}-attach:before { content: '\e609';} 65 | .@{iconfont-css-prefix}-video:before { content: '\e60a';} 66 | 67 | .@{editor-prefix}-core-toolbar { 68 | background: #f4f4f4; 69 | padding: 6px; 70 | .editor-icon { 71 | } 72 | } 73 | 74 | .alignLeft { 75 | text-align: left; 76 | } 77 | 78 | .alignRight { 79 | text-align: right; 80 | } 81 | 82 | .alignMiddle { 83 | text-align: center; 84 | } 85 | 86 | .alignJustify { 87 | text-align: justify; 88 | } 89 | 90 | .@{editor-prefix}-core-editor.oneline { 91 | 92 | .DraftEditor-editorContainer { 93 | overflow-x: auto; 94 | } 95 | .public-DraftEditor-content { 96 | white-space: nowrap !important; 97 | word-wrap: break-word; 98 | } 99 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-editor-core 2 | --- 3 | 4 | React EditorCore Component 5 | 6 | 7 | [![NPM version][npm-image]][npm-url] 8 | [![build status][travis-image]][travis-url] 9 | [![Test coverage][coveralls-image]][coveralls-url] 10 | [![gemnasium deps][gemnasium-image]][gemnasium-url] 11 | [![node version][node-image]][node-url] 12 | [![npm download][download-image]][download-url] 13 | [![Sauce Test Status](https://saucelabs.com/buildstatus/rc-editor-core)](https://saucelabs.com/u/rc-editor-core) 14 | 15 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/rc-editor-core.svg)](https://saucelabs.com/u/rc-editor-core) 16 | 17 | [npm-image]: http://img.shields.io/npm/v/rc-editor-core.svg?style=flat-square 18 | [npm-url]: http://npmjs.org/package/rc-editor-core 19 | [travis-image]: https://img.shields.io/travis/react-component/editor-core.svg?style=flat-square 20 | [travis-url]: https://travis-ci.org/react-component/editor-core 21 | [coveralls-image]: https://img.shields.io/coveralls/react-component/editor-core.svg?style=flat-square 22 | [coveralls-url]: https://coveralls.io/r/react-component/editor-core?branch=master 23 | [gemnasium-image]: http://img.shields.io/gemnasium/react-component/editor-core.svg?style=flat-square 24 | [gemnasium-url]: https://gemnasium.com/react-component/editor-core 25 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square 26 | [node-url]: http://nodejs.org/download/ 27 | [download-image]: https://img.shields.io/npm/dm/rc-editor-core.svg?style=flat-square 28 | [download-url]: https://npmjs.org/package/rc-editor-core 29 | 30 | 31 | ## Browser Support 32 | 33 | | ![IE / Edge](https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/edge.png)
IE / Edge | ![Firefox](https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/firefox.png)
Firefox | ![Chrome](https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/chrome.png)
Chrome | ![Safari](https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/safari.png )
Safari | ![iOS Safari](https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/safari-ios.png)
iOS Safari | ![Chrome for Anroid](https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/chrome-android.png)
Chrome for Android | 34 | | --------- | --------- | --------- | --------- | --------- | --------- | 35 | | IE11, Edge [1, 2]| last 2 versions| last 2 versions| last 2 versions| not fully supported [3] | not fully supported [3] 36 | 37 | [1] May need a shim or a polyfill for some syntax used in Draft.js ([docs](https://draftjs.org/docs/advanced-topics-issues-and-pitfalls.html#polyfills)). 38 | 39 | [2] IME inputs have known issues in these browsers, especially Korean ([docs](https://draftjs.org/docs/advanced-topics-issues-and-pitfalls.html#ime-and-internet-explorer)). 40 | 41 | [3] There are known issues with mobile browsers, especially on Android ([docs](https://draftjs.org/docs/advanced-topics-issues-and-pitfalls.html#mobile-not-yet-supported)). 42 | 43 | ## Screenshots 44 | 45 | 46 | 47 | 48 | ## Development 49 | 50 | ``` 51 | npm install 52 | npm start 53 | ``` 54 | 55 | ## Example 56 | 57 | http://localhost:8003/examples/ 58 | 59 | 60 | online example: http://react-component.github.io/editor-core/ 61 | 62 | 63 | ## Feature 64 | 65 | * support ie8,ie8+,chrome,firefox,safari 66 | 67 | 68 | ## install 69 | 70 | 71 | [![rc-editor-core](https://nodei.co/npm/rc-editor-core.png)](https://npmjs.org/package/rc-editor-core) 72 | 73 | 74 | ## Usage 75 | 76 | ```js 77 | var EditorCore = require('rc-editor-core'); 78 | var React = require('react'); 79 | React.render(, container); 80 | ``` 81 | 82 | ## API 83 | 84 | ### props 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
nametypedefaultdescription
classNameStringadditional css class of root dom node
104 | 105 | 106 | ## Test Case 107 | 108 | ``` 109 | npm test 110 | npm run chrome-test 111 | ``` 112 | 113 | ## Coverage 114 | 115 | ``` 116 | npm run coverage 117 | ``` 118 | 119 | open coverage/ dir 120 | 121 | ## License 122 | 123 | rc-editor-core is released under the MIT license. 124 | -------------------------------------------------------------------------------- /src/EditorCore/customHTML2Content.ts: -------------------------------------------------------------------------------- 1 | import { BlockMapBuilder, BlockMap, ContentState, genKey, Entity, CharacterMetadata, ContentBlock, convertFromHTML } from 'draft-js' 2 | import toArray from 'lodash/toArray'; 3 | import DraftEntityInstance = Entity.DraftEntityInstance; 4 | import CharacterMetadataConfig = CharacterMetadata.CharacterMetadataConfig; 5 | import { List, OrderedSet, Repeat, fromJS } from 'immutable' 6 | 7 | 8 | function compose (...argument): Function { 9 | var args = arguments; 10 | var start = args.length - 1; 11 | return function() { 12 | var i = start; 13 | var result = args[start].apply(this, arguments); 14 | while (i--) result = args[i].call(this, result); 15 | return result; 16 | }; 17 | }; 18 | /* 19 | * Helpers 20 | */ 21 | 22 | // Prepares img meta data object based on img attributes 23 | const getBlockSpecForElement = (imgElement) => ({ 24 | contentType: 'image', 25 | src: imgElement.getAttribute('src'), 26 | width: imgElement.getAttribute('width'), 27 | height: imgElement.getAttribute('height'), 28 | align: imgElement.style.cssFloat, 29 | }) 30 | 31 | // Wraps meta data in HTML element which is 'understandable' by Draft, I used
. 32 | const wrapBlockSpec = (blockSpec) => { 33 | if (blockSpec == null) { 34 | return null 35 | } 36 | const tempEl = document.createElement('blockquote') 37 | // stringify meta data and insert it as text content of temp HTML element. We will later extract 38 | // and parse it. 39 | tempEl.innerText = JSON.stringify(blockSpec) 40 | return tempEl 41 | } 42 | // Replaces element with our temp element 43 | const replaceElement = (oldEl, newEl) => { 44 | if (!(newEl instanceof HTMLElement)) { 45 | return 46 | } 47 | const parentNode = oldEl.parentNode 48 | return parentNode.replaceChild(newEl, oldEl) 49 | } 50 | 51 | const elementToBlockSpecElement = compose(wrapBlockSpec, getBlockSpecForElement); 52 | 53 | const imgReplacer = (imgElement) => { 54 | return replaceElement(imgElement, elementToBlockSpecElement(imgElement)); 55 | } 56 | 57 | // creates ContentBlock based on provided spec 58 | const createContentBlock = ( blockData: DraftEntityInstance, contentState: ContentState) => { 59 | const {key, type, text, data, inlineStyles, entityData} = blockData; 60 | let blockSpec = { 61 | type: type != null ? type : 'unstyled', 62 | text: text != null ? text : '', 63 | key: key != null ? key : genKey(), 64 | data: null, 65 | characterList: List([]), 66 | }; 67 | 68 | if (data) { 69 | blockSpec.data = fromJS(data) 70 | } 71 | 72 | if (inlineStyles || entityData) { 73 | let entityKey; 74 | if (entityData) { 75 | const {type, mutability, data} = entityData; 76 | contentState.createEntity(type, mutability, data); 77 | entityKey = contentState.getLastCreatedEntityKey(); 78 | } else { 79 | entityKey = null 80 | } 81 | const style = OrderedSet(inlineStyles || []) 82 | const charData = CharacterMetadata.create({style, entityKey} as CharacterMetadataConfig) 83 | blockSpec.characterList = List(Repeat(charData, text.length)) 84 | } 85 | return new ContentBlock(blockSpec) 86 | } 87 | 88 | // takes HTML string and returns DraftJS ContentState 89 | export default function customHTML2Content(HTML, contentState: ContentState): BlockMap { 90 | let tempDoc = new DOMParser().parseFromString(HTML, 'text/html') 91 | // replace all with
elements 92 | toArray(tempDoc.querySelectorAll('img')).forEach(imgReplacer); 93 | // use DraftJS converter to do initial conversion. I don't provide DOMBuilder and 94 | // blockRenderMap arguments here since it should fall back to its default ones, which are fine 95 | let { contentBlocks } = convertFromHTML(tempDoc.body.innerHTML); 96 | // now replace
ContentBlocks with 'atomic' ones 97 | contentBlocks = contentBlocks.reduce(function (contentBlocks, block) { 98 | if (block.getType() !== 'blockquote') { 99 | return contentBlocks.concat(block) 100 | } 101 | const image = JSON.parse(block.getText()) 102 | contentState.createEntity('IMAGE-ENTITY', 'IMMUTABLE', image); 103 | const entityKey = contentState.getLastCreatedEntityKey(); 104 | const charData = CharacterMetadata.create({ entity: entityKey }); 105 | // const blockSpec = Object.assign({ type: 'atomic', text: ' ' }, { entityData }) 106 | // const atomicBlock = createContentBlock(blockSpec) 107 | // const spacerBlock = createContentBlock({}); 108 | 109 | const fragmentArray = [ 110 | new ContentBlock({ 111 | key: genKey(), 112 | type: 'image-block', 113 | text: ' ', 114 | characterList: List(Repeat(charData, charData.count())), 115 | }), 116 | new ContentBlock({ 117 | key: genKey(), 118 | type: 'unstyled', 119 | text: '', 120 | characterList: List(), 121 | }), 122 | ]; 123 | 124 | return contentBlocks.concat(fragmentArray); 125 | }, []); 126 | // console.log('>> customHTML2Content contentBlocks', contentBlocks); 127 | tempDoc = null; 128 | return BlockMapBuilder.createFromArray(contentBlocks) 129 | } 130 | -------------------------------------------------------------------------------- /src/EditorCore/export/getHTML.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, ContentState, CharacterMetadata, Entity, DefaultDraftInlineStyle } from "draft-js"; 2 | import { List, OrderedSet, is } from 'immutable'; 3 | import isUnitlessNumber from './isUnitlessNumber'; 4 | 5 | import BlockMap from 'draft-js/lib/BlockMap'; 6 | 7 | export type Style = OrderedSet; 8 | export const EMPTY_SET: Style = OrderedSet(); 9 | export const DEFAULT_ELEMENT = 'span'; 10 | export const DEFAULT_INLINE_STYLE = DefaultDraftInlineStyle; 11 | 12 | function encodeContent(text: string): string { 13 | return text 14 | .split('&').join('&') 15 | .split('<').join('<') 16 | .split('>').join('>') 17 | .split('\xA0').join(' ') 18 | .split('\n').join('
' + '\n'); 19 | } 20 | 21 | function encodeAttr(text: string): string { 22 | return text 23 | .split('&').join('&') 24 | .split('<').join('<') 25 | .split('>').join('>') 26 | .split('"').join('"'); 27 | } 28 | 29 | const VENDOR_PREFIX = /^(moz|ms|o|webkit)-/; 30 | const NUMERIC_STRING = /^\d+$/; 31 | const UPPERCASE_PATTERN = /([A-Z])/g; 32 | 33 | // Lifted from: https://github.com/facebook/react/blob/master/src/renderers/dom/shared/CSSPropertyOperations.js 34 | function processStyleName(name: string): string { 35 | return name 36 | .replace(UPPERCASE_PATTERN, '-$1') 37 | .toLowerCase() 38 | .replace(VENDOR_PREFIX, '-$1-'); 39 | } 40 | 41 | // Lifted from: https://github.com/facebook/react/blob/master/src/renderers/dom/shared/dangerousStyleValue.js 42 | function processStyleValue(name: string, value: string): string { 43 | let isNumeric; 44 | if (typeof value === 'string') { 45 | isNumeric = NUMERIC_STRING.test(value); 46 | } else { 47 | isNumeric = true; 48 | value = String(value); 49 | } 50 | 51 | if (!isNumeric || value === '0' || isUnitlessNumber[name] === true) { 52 | return value; 53 | } else { 54 | return value + 'px'; 55 | } 56 | } 57 | 58 | function getStyleText(styleObject) { 59 | if (!styleObject) { 60 | return ''; 61 | } 62 | return Object.keys(styleObject).map(name => { 63 | const styleName = processStyleName(name); 64 | const styleValue = processStyleValue(name, styleObject[name]); 65 | return `${styleName}:${styleValue}`; 66 | }).join(';'); 67 | } 68 | 69 | function getEntityContent(contentState: ContentState, entityKey, content: string): string { 70 | if (entityKey) { 71 | const entity = contentState.getEntity(entityKey); 72 | const entityData = entity.getData(); 73 | if (entityData && entityData.export) { 74 | return entityData.export(content, entityData); 75 | } 76 | } 77 | return content; 78 | } 79 | 80 | export default function GetHTML(configStore) { 81 | return function exportHtml(editorState: EditorState) { 82 | const contentState = editorState.getCurrentContent(); 83 | const blockMap:BlockMap = contentState.getBlockMap(); 84 | 85 | const customStyleMap = configStore.get('customStyleMap') || {}; 86 | const customBlockRenderMap = configStore.get('blockRenderMap') || {}; 87 | const customStyleFn = configStore.get('customStyleFn'); 88 | const toHTMLList = configStore.get('toHTMLList'); 89 | Object.assign(customStyleMap, DEFAULT_INLINE_STYLE); 90 | 91 | return blockMap.map(block => { 92 | let resultText = '
'; 93 | let closeTag = '
'; 94 | let lastPosition = 0; 95 | const text = block.getText(); 96 | const blockType = block.getType(); 97 | const blockRender = customBlockRenderMap.get(blockType); 98 | if (blockRender) { 99 | const element = typeof blockRender.element === 'function' ? blockRender.elementTag || 'div' : 'div'; 100 | resultText = `<${element || 'div'} style="${getStyleText(customBlockRenderMap.get(blockType).style || {})}">`; 101 | closeTag = ``; 102 | } 103 | 104 | const charMetaList = block.getCharacterList(); 105 | 106 | let charEntity = null; 107 | let prevCharEntity = null; 108 | let ranges = []; 109 | let rangeStart = 0; 110 | for (let i = 0, len = text.length; i < len; i++) { 111 | prevCharEntity = charEntity; 112 | let meta: CharacterMetadata = charMetaList.get(i); 113 | charEntity = meta ? meta.getEntity() : null; 114 | if (i > 0 && charEntity !== prevCharEntity) { 115 | ranges.push([ 116 | prevCharEntity, 117 | getStyleRanges( 118 | text.slice(rangeStart, i), 119 | charMetaList.slice(rangeStart, i) 120 | ), 121 | ]); 122 | rangeStart = i; 123 | } 124 | } 125 | ranges.push([ 126 | charEntity, 127 | getStyleRanges( 128 | text.slice(rangeStart), 129 | charMetaList.slice(rangeStart) 130 | ), 131 | ]); 132 | 133 | ranges.map(([ entityKey, stylePieces]) => { 134 | let element = DEFAULT_ELEMENT; 135 | const rawContent = stylePieces.map(([text]) => text).join(''); 136 | let content = stylePieces.map(([text, styleSet]) => { 137 | let encodedContent = encodeContent(text); 138 | if (styleSet.size) { 139 | let inlineStyle = {}; 140 | styleSet.forEach(item => { 141 | if (customStyleMap.hasOwnProperty(item)) { 142 | const currentStyle = customStyleMap[item]; 143 | inlineStyle = Object.assign(inlineStyle, currentStyle); 144 | } 145 | }); 146 | const customedStyle = customStyleFn(styleSet); 147 | inlineStyle = Object.assign(inlineStyle, customedStyle); 148 | return `${encodedContent}`; 149 | } 150 | return `${encodedContent}`; 151 | }).join(''); 152 | 153 | if (entityKey) { 154 | const entity = contentState.getEntity(entityKey); 155 | const entityData = entity.getData(); 156 | if (entityData && entityData.export) { 157 | resultText += entityData.export(content, entityData); 158 | } else { 159 | let HTMLText = ''; 160 | toHTMLList.forEach(toHTML => { 161 | const text = toHTML(rawContent, entity, contentState); 162 | if (text) { 163 | HTMLText = text; 164 | } 165 | }); 166 | if (HTMLText) { resultText += HTMLText } 167 | } 168 | } else { 169 | resultText += content; 170 | } 171 | }); 172 | 173 | resultText += closeTag; 174 | return resultText; 175 | }).join('\n'); 176 | } 177 | } 178 | 179 | function getStyleRanges(text: String, charMetaList: List) { 180 | let charStyle = EMPTY_SET; 181 | let prevCharStyle = EMPTY_SET; 182 | let ranges = []; 183 | let rangeStart = 0; 184 | for (let i = 0, len = text.length; i < len; i++) { 185 | prevCharStyle = charStyle; 186 | let meta = charMetaList.get(i); 187 | charStyle = meta ? meta.getStyle() : EMPTY_SET; 188 | if (i > 0 && !is(charStyle, prevCharStyle)) { 189 | ranges.push([text.slice(rangeStart, i), prevCharStyle]); 190 | rangeStart = i; 191 | } 192 | } 193 | ranges.push([text.slice(rangeStart), charStyle]); 194 | return ranges; 195 | } 196 | -------------------------------------------------------------------------------- /src/EditorCore/index.tsx: -------------------------------------------------------------------------------- 1 | /* tslint:disable:member-ordering interface-name */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import { 7 | Editor, 8 | EditorState, 9 | ContentState, 10 | CompositeDecorator, 11 | Modifier, 12 | getDefaultKeyBinding, 13 | KeyBindingUtil, 14 | DefaultDraftBlockRenderMap, 15 | DraftBlockRenderConfig, 16 | DraftInlineStyle, 17 | } from 'draft-js'; 18 | 19 | import { List, Map } from 'immutable'; 20 | import 'setimmediate'; 21 | import classnames from 'classnames'; 22 | import { createToolbar } from '../Toolbar'; 23 | import ConfigStore from './ConfigStore'; 24 | import GetHTML from './export/getHTML'; 25 | import exportText, { decodeContent } from './export/exportText'; 26 | import handlePastedText from './handlePastedText'; 27 | import customHTML2Content from './customHTML2Content'; 28 | 29 | const { hasCommandModifier } = KeyBindingUtil; 30 | 31 | function noop(): void {}; 32 | 33 | export type DraftHandleValue = 'handled' | 'not-handled'; 34 | export interface Plugin { 35 | name: string; 36 | decorators?: any[]; 37 | component?: Function; 38 | onChange: (editorState: EditorState) => EditorState; 39 | customStyleFn?: Function; 40 | blockRendererFn?: Function; 41 | callbacks: { 42 | onUpArrow?: Function; 43 | onDownArrow?: Function; 44 | handleReturn?: Function; 45 | handleKeyBinding?: Function; 46 | setEditorState: (editorState: EditorState) => void; 47 | getEditorState: () => EditorState; 48 | }; 49 | config?: Object; 50 | } 51 | 52 | export interface EditorProps { 53 | multiLines: boolean; 54 | plugins: Plugin[]; 55 | pluginConfig?: Object; 56 | prefixCls: string; 57 | onChange?: (editorState: EditorState) => EditorState; 58 | toolbars: any[]; 59 | splitLine: String; 60 | onKeyDown?: (ev: any) => boolean; 61 | defaultValue?: EditorState; 62 | placeholder?: string; 63 | onFocus?: () => void; 64 | onBlur?: () => void; 65 | style?: Object; 66 | value?: EditorState | any; 67 | readOnly?: boolean; 68 | } 69 | 70 | export interface EditorCoreState { 71 | editorState?: EditorState; 72 | inlineStyleOverride?: DraftInlineStyle; 73 | customStyleMap?: Object; 74 | customBlockStyleMap?: Object; 75 | blockRenderMap?: Map; 76 | toolbarPlugins?: List; 77 | plugins?: Plugin[]; 78 | compositeDecorator?: CompositeDecorator; 79 | } 80 | 81 | const defaultPluginConfig = { 82 | }; 83 | 84 | const focusDummyStyle = { 85 | width: 0, 86 | opacity: 0, 87 | border: 0, 88 | position: 'absolute', 89 | left: 0, 90 | top: 0, 91 | }; 92 | 93 | const toolbar = createToolbar(); 94 | const configStore = new ConfigStore(); 95 | 96 | class EditorCore extends React.Component { 97 | static ToEditorState(text: string): EditorState { 98 | const createEmptyContentState = ContentState.createFromText(decodeContent(text) || ''); 99 | const editorState = EditorState.createWithContent(createEmptyContentState); 100 | return EditorState.forceSelection(editorState, createEmptyContentState.getSelectionAfter()); 101 | } 102 | public static GetText = exportText; 103 | public static GetHTML = GetHTML(configStore); 104 | 105 | public getDefaultValue(): EditorState { 106 | const { defaultValue, value } = this.props; 107 | return value || defaultValue; 108 | } 109 | 110 | public Reset(): void { 111 | const defaultValue = this.getDefaultValue(); 112 | const contentState = defaultValue ? defaultValue.getCurrentContent() : ContentState.createFromText(''); 113 | const updatedEditorState = EditorState.push(this.state.editorState, contentState, 'remove-range'); 114 | 115 | this.setEditorState( 116 | EditorState.forceSelection(updatedEditorState, contentState.getSelectionBefore()), 117 | ); 118 | } 119 | 120 | public SetText(text: string): void { 121 | const createTextContentState = ContentState.createFromText(text || ''); 122 | const editorState = EditorState.push(this.state.editorState, createTextContentState, 'change-block-data'); 123 | this.setEditorState( 124 | EditorState.moveFocusToEnd(editorState) 125 | , true); 126 | } 127 | 128 | public state: EditorCoreState; 129 | private plugins: any; 130 | private controlledMode: boolean; 131 | private _editorWrapper: Element; 132 | private forceUpdateImmediate: number; 133 | private _focusDummy: Element; 134 | 135 | constructor(props: EditorProps) { 136 | super(props); 137 | this.plugins = List(List(props.plugins).flatten(true)); 138 | 139 | let editorState; 140 | 141 | if (props.value !== undefined) { 142 | if (props.value instanceof EditorState) { 143 | editorState = props.value || EditorState.createEmpty(); 144 | } else { 145 | editorState = EditorState.createEmpty(); 146 | } 147 | } else { 148 | editorState = EditorState.createEmpty(); 149 | } 150 | 151 | editorState = this.generatorDefaultValue(editorState); 152 | 153 | this.state = { 154 | plugins: this.reloadPlugins(), 155 | editorState, 156 | customStyleMap: {}, 157 | customBlockStyleMap: {}, 158 | compositeDecorator: null, 159 | }; 160 | 161 | if (props.value !== undefined) { 162 | this.controlledMode = true; 163 | } 164 | } 165 | 166 | refs: { 167 | [str: string]: any; 168 | editor?: any; 169 | }; 170 | 171 | public static defaultProps = { 172 | multiLines: true, 173 | plugins: [], 174 | prefixCls: 'rc-editor-core', 175 | pluginConfig: {}, 176 | toolbars: [], 177 | spilitLine: 'enter', 178 | }; 179 | 180 | public static childContextTypes = { 181 | getEditorState: PropTypes.func, 182 | setEditorState: PropTypes.func, 183 | }; 184 | 185 | public getChildContext() { 186 | return { 187 | getEditorState: this.getEditorState, 188 | setEditorState: this.setEditorState, 189 | }; 190 | } 191 | 192 | public reloadPlugins(): Plugin[] { 193 | return this.plugins && this.plugins.size ? this.plugins.map((plugin: Plugin) => { 194 | // 如果插件有 callbacks 方法,则认为插件已经加载。 195 | if (plugin.callbacks) { 196 | return plugin; 197 | } 198 | // 如果插件有 constructor 方法,则构造插件 199 | if (plugin.hasOwnProperty('constructor')) { 200 | const pluginConfig = Object.assign(this.props.pluginConfig, plugin.config || {}, defaultPluginConfig); 201 | return plugin.constructor(pluginConfig); 202 | } 203 | // else 无效插件 204 | console.warn('>> 插件: [', plugin.name , '] 无效。插件或许已经过期。'); 205 | return false; 206 | }).filter(plugin => plugin).toArray() : []; 207 | } 208 | 209 | public componentWillMount(): void { 210 | const plugins = this.initPlugins().concat([toolbar]); 211 | const customStyleMap = {}; 212 | const customBlockStyleMap = {}; 213 | 214 | let customBlockRenderMap: Map = Map(DefaultDraftBlockRenderMap); 215 | let toHTMLList: List = List([]); 216 | 217 | // initialize compositeDecorator 218 | const compositeDecorator = new CompositeDecorator( 219 | plugins.filter(plugin => plugin.decorators !== undefined) 220 | .map(plugin => plugin.decorators) 221 | .reduce((prev, curr) => prev.concat(curr), []), 222 | ); 223 | 224 | // initialize Toolbar 225 | const toolbarPlugins = List(plugins.filter(plugin => !!plugin.component && plugin.name !== 'toolbar')); 226 | 227 | // load inline styles... 228 | plugins.forEach( plugin => { 229 | const { styleMap, blockStyleMap, blockRenderMap, toHtml } = plugin; 230 | if (styleMap) { 231 | for (const key in styleMap) { 232 | if (styleMap.hasOwnProperty(key)) { 233 | customStyleMap[key] = styleMap[key]; 234 | } 235 | } 236 | } 237 | if (blockStyleMap) { 238 | for (const key in blockStyleMap) { 239 | if (blockStyleMap.hasOwnProperty(key)) { 240 | customBlockStyleMap[key] = blockStyleMap[key]; 241 | customBlockRenderMap = customBlockRenderMap.set(key, { 242 | element: null, 243 | }); 244 | } 245 | } 246 | } 247 | 248 | if (toHtml) { 249 | toHTMLList = toHTMLList.push(toHtml); 250 | } 251 | 252 | if (blockRenderMap) { 253 | for (const key in blockRenderMap) { 254 | if (blockRenderMap.hasOwnProperty(key)) { 255 | customBlockRenderMap = customBlockRenderMap.set(key, blockRenderMap[key]); 256 | } 257 | } 258 | } 259 | }); 260 | configStore.set('customStyleMap', customStyleMap); 261 | configStore.set('customBlockStyleMap', customBlockStyleMap); 262 | configStore.set('blockRenderMap', customBlockRenderMap); 263 | configStore.set('customStyleFn', this.customStyleFn.bind(this)); 264 | configStore.set('toHTMLList', toHTMLList); 265 | 266 | this.setState({ 267 | toolbarPlugins, 268 | compositeDecorator, 269 | }); 270 | 271 | this.setEditorState(EditorState.set(this.state.editorState, 272 | { decorator: compositeDecorator }, 273 | ), false, false); 274 | 275 | } 276 | public componentWillReceiveProps(nextProps) { 277 | if (this.forceUpdateImmediate) { 278 | this.cancelForceUpdateImmediate(); 279 | } 280 | if (this.controlledMode) { 281 | const decorators = nextProps.value.getDecorator(); 282 | 283 | const editorState = decorators ? 284 | nextProps.value : 285 | EditorState.set(nextProps.value, 286 | { decorator: this.state.compositeDecorator }, 287 | ); 288 | this.setState({ 289 | editorState, 290 | }); 291 | } 292 | } 293 | public componentWillUnmount() { 294 | this.cancelForceUpdateImmediate(); 295 | } 296 | 297 | private cancelForceUpdateImmediate = () => { 298 | clearImmediate(this.forceUpdateImmediate); 299 | this.forceUpdateImmediate = null; 300 | } 301 | // 处理 value 302 | generatorDefaultValue(editorState: EditorState): EditorState { 303 | const defaultValue = this.getDefaultValue(); 304 | if (defaultValue) { 305 | return defaultValue; 306 | } 307 | return editorState; 308 | } 309 | 310 | public getStyleMap(): Object { 311 | return configStore.get('customStyleMap'); 312 | } 313 | public setStyleMap(customStyleMap): void { 314 | configStore.set('customStyleMap', customStyleMap); 315 | this.render(); 316 | } 317 | 318 | public initPlugins(): any[] { 319 | const enableCallbacks = ['focus', 'getEditorState', 'setEditorState', 'getStyleMap', 'setStyleMap']; 320 | return this.getPlugins().map(plugin => { 321 | enableCallbacks.forEach( callbackName => { 322 | if (plugin.callbacks.hasOwnProperty(callbackName)) { 323 | plugin.callbacks[callbackName] = this[callbackName].bind(this); 324 | } 325 | }); 326 | 327 | return plugin; 328 | }); 329 | } 330 | 331 | private focusEditor(ev) { 332 | this.refs.editor.focus(ev); 333 | if (this.props.readOnly) { 334 | this._focusDummy.focus(); 335 | } 336 | if (this.props.onFocus) { 337 | this.props.onFocus(ev); 338 | } 339 | } 340 | 341 | private _focus(ev): void { 342 | if (!ev || !ev.nativeEvent || !ev.nativeEvent.target) { 343 | return; 344 | } 345 | 346 | if (document.activeElement 347 | && document.activeElement.getAttribute('contenteditable') === 'true' 348 | ) { 349 | return; 350 | } 351 | 352 | return this.focus(ev); 353 | } 354 | 355 | public focus(ev): void { 356 | const event = ev && ev.nativeEvent; 357 | if (event && event.target === this._editorWrapper) { 358 | const { editorState } = this.state; 359 | const selection = editorState.getSelection(); 360 | if (!selection.getHasFocus()) { 361 | if (selection.isCollapsed()) { 362 | return this.setState({ 363 | editorState: EditorState.moveSelectionToEnd(editorState), 364 | }, () => { 365 | this.focusEditor(ev); 366 | }); 367 | } 368 | } 369 | } 370 | this.focusEditor(ev); 371 | } 372 | 373 | public getPlugins(): Plugin[] { 374 | return this.state.plugins.slice(); 375 | } 376 | 377 | public getEventHandler(): Object { 378 | const enabledEvents = [ 379 | 'onUpArrow', 380 | 'onDownArrow', 381 | 'handleReturn', 382 | 'onFocus', 383 | 'onBlur', 384 | 'onTab', 385 | 'handlePastedText', 386 | ]; 387 | const eventHandler = {}; 388 | enabledEvents.forEach(event => { 389 | eventHandler[event] = this.generatorEventHandler(event); 390 | }); 391 | return eventHandler; 392 | } 393 | 394 | getEditorState(needFocus = false): EditorState { 395 | if (needFocus) { 396 | this.refs.editor.focus(); 397 | } 398 | return this.state.editorState; 399 | } 400 | 401 | setEditorState(editorState: EditorState, focusEditor: boolean = false, triggerChange: boolean = true): void { 402 | let newEditorState = editorState; 403 | 404 | this.getPlugins().forEach(plugin => { 405 | if (plugin.onChange) { 406 | const updatedEditorState = plugin.onChange(newEditorState); 407 | if (updatedEditorState) { 408 | newEditorState = updatedEditorState; 409 | } 410 | } 411 | }); 412 | 413 | if (this.props.onChange && triggerChange) { 414 | this.props.onChange(newEditorState); 415 | 416 | // close this issue https://github.com/ant-design/ant-design/issues/5788 417 | // when onChange not take any effect 418 | // `` won't rerender cause no props is changed. 419 | // add an timeout here, 420 | // if props.onChange not trigger componentWillReceiveProps, 421 | // we will force render Editor with previous editorState, 422 | if (this.controlledMode) { 423 | this.forceUpdateImmediate = setImmediate(() => this.setState({ 424 | editorState: new EditorState(this.state.editorState.getImmutable()), 425 | })); 426 | } 427 | } 428 | 429 | if (!this.controlledMode) { 430 | this.setState( 431 | { editorState: newEditorState }, 432 | focusEditor ? () => setImmediate(() => this.refs.editor.focus()) : noop, 433 | ); 434 | } 435 | } 436 | public handleKeyBinding(ev): any { 437 | if (this.props.onKeyDown) { 438 | ev.ctrlKey = hasCommandModifier(ev); 439 | const keyDownResult = this.props.onKeyDown(ev); 440 | if (keyDownResult) { 441 | return keyDownResult; 442 | } 443 | return getDefaultKeyBinding(ev); 444 | } 445 | return getDefaultKeyBinding(ev); 446 | } 447 | public handleKeyCommand(command: String): DraftHandleValue { 448 | if (this.props.multiLines) { 449 | return this.eventHandle('handleKeyBinding', command); 450 | } 451 | 452 | return command === 'split-block' ? 'handled' : 'not-handled'; 453 | } 454 | 455 | public getBlockStyle(contentBlock): String { 456 | const customBlockStyleMap = configStore.get('customBlockStyleMap'); 457 | const type = contentBlock.getType(); 458 | if (customBlockStyleMap.hasOwnProperty(type)) { 459 | return customBlockStyleMap[type]; 460 | } 461 | return ''; 462 | } 463 | 464 | public blockRendererFn(contentBlock) { 465 | let blockRenderResult = null; 466 | this.getPlugins().forEach(plugin => { 467 | if (plugin.blockRendererFn) { 468 | const result = plugin.blockRendererFn(contentBlock); 469 | if (result) { 470 | blockRenderResult = result; 471 | } 472 | } 473 | }); 474 | return blockRenderResult; 475 | } 476 | 477 | eventHandle(eventName, ...args): DraftHandleValue { 478 | const plugins = this.getPlugins(); 479 | for (let i = 0; i < plugins.length; i++) { 480 | const plugin = plugins[i]; 481 | if (plugin.callbacks[eventName] 482 | && typeof plugin.callbacks[eventName] === 'function') { 483 | const result = plugin.callbacks[eventName](...args); 484 | if (result === true) { 485 | return 'handled'; 486 | } 487 | } 488 | } 489 | return this.props.hasOwnProperty(eventName) && this.props[eventName](...args) === true ? 'handled' : 'not-handled'; 490 | } 491 | 492 | generatorEventHandler(eventName): Function { 493 | return (...args) => { 494 | return this.eventHandle(eventName, ...args); 495 | }; 496 | } 497 | 498 | customStyleFn(styleSet): Object { 499 | if (styleSet.size === 0) { 500 | return {}; 501 | } 502 | 503 | const plugins = this.getPlugins(); 504 | const resultStyle = {}; 505 | for (let i = 0; i < plugins.length; i++) { 506 | if (plugins[i].customStyleFn) { 507 | const styled = plugins[i].customStyleFn(styleSet); 508 | if (styled) { 509 | Object.assign(resultStyle, styled); 510 | } 511 | } 512 | } 513 | return resultStyle; 514 | } 515 | handlePastedText = (text: string, html: string): DraftHandleValue => { 516 | const { editorState } = this.state; 517 | if (html) { 518 | const contentState = editorState.getCurrentContent(); 519 | const selection = editorState.getSelection(); 520 | 521 | const fragment = customHTML2Content(html, contentState); 522 | const pastedContent = Modifier.replaceWithFragment( 523 | contentState, 524 | selection, 525 | fragment, 526 | ); 527 | 528 | const newContent = pastedContent.merge({ 529 | selectionBefore: selection, 530 | selectionAfter: pastedContent.getSelectionAfter().set('hasFocus', true), 531 | }); 532 | 533 | this.setEditorState( 534 | EditorState.push(editorState, newContent as ContentState, 'insert-fragment'), 535 | true, 536 | ); 537 | return 'handled'; 538 | } 539 | return 'not-handled'; 540 | } 541 | render() { 542 | const { prefixCls, toolbars, style, readOnly, multiLines } = this.props; 543 | const { editorState, toolbarPlugins } = this.state; 544 | const customStyleMap = configStore.get('customStyleMap'); 545 | const blockRenderMap = configStore.get('blockRenderMap'); 546 | const eventHandler = this.getEventHandler(); 547 | const Toolbar = toolbar.component; 548 | 549 | const cls = classnames({ 550 | [`${prefixCls}-editor`]: true, 551 | readonly: readOnly, 552 | oneline: !multiLines, 553 | }); 554 | 555 | return (
560 | 567 |
this._editorWrapper = ele} 570 | style={style} 571 | onClick={(ev) => ev.preventDefault()} 572 | > 573 | 588 | {readOnly ? 589 | this._focusDummy = ele } onBlur={eventHandler.onBlur}/> : 590 | null 591 | } 592 | {this.props.children} 593 |
594 |
); 595 | } 596 | } 597 | 598 | export default EditorCore; 599 | --------------------------------------------------------------------------------