├── .gitignore
├── .storybook
├── main.js
├── preview-body.html
└── preview-head.html
├── ISSUES.md
├── README.md
├── docs
├── Issues.md
├── awesome-editor.md
├── basic.gif
├── code.gif
├── content.md
├── drag-block.gif
├── drag.md
├── funcs.md
├── hooks.md
├── image.gif
├── mouseup-drop.md
├── plugin.md
├── selection.md
└── tips.md
├── examples
├── basic
│ ├── index.js
│ └── public
│ │ └── index.html
└── next
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── pages
│ ├── _app.js
│ ├── _document.js
│ ├── api
│ │ └── hello.js
│ └── index.js
│ ├── public
│ ├── favicon.ico
│ └── vercel.svg
│ ├── styles
│ ├── Draft.css
│ ├── Home.module.css
│ └── globals.css
│ └── yarn.lock
├── mock
├── dndHelper.js
└── nested.js
├── package-lock.json
├── package.json
├── src
├── Context.ts
├── Editor.tsx
├── components
│ ├── button
│ │ ├── Blockquote.tsx
│ │ ├── Bold.tsx
│ │ ├── BulletedList.tsx
│ │ ├── CodeBlock.tsx
│ │ ├── Dragger.tsx
│ │ ├── H1.tsx
│ │ ├── H2.tsx
│ │ ├── H3.tsx
│ │ ├── H4.tsx
│ │ ├── ImageAlignCenter.tsx
│ │ ├── ImageAlignLeft.tsx
│ │ ├── ImageAlignLeftFillContent.tsx
│ │ ├── ImageAlignRightFillContent.tsx
│ │ ├── ImageFillWidth.tsx
│ │ ├── ImageInsetCenter.tsx
│ │ ├── ImageOutsetCenter.tsx
│ │ ├── InlineCode.tsx
│ │ ├── Italic.tsx
│ │ ├── Link.tsx
│ │ ├── NumberedList.tsx
│ │ ├── Plus.tsx
│ │ ├── Selectable.tsx
│ │ ├── StrikeThrough.tsx
│ │ ├── Underline.tsx
│ │ ├── Unlink.tsx
│ │ └── utils
│ │ │ ├── action.css
│ │ │ ├── withAction.tsx
│ │ │ └── withFillColor.tsx
│ ├── image-toolbar
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── image
│ │ ├── index.tsx
│ │ ├── resizable.js
│ │ └── styles
│ │ │ └── index.css
│ ├── inline-toolbar
│ │ ├── Divider.tsx
│ │ ├── InputBar.tsx
│ │ ├── StyleControls.tsx
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── link-span
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── link
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── sidebar
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── style-controls
│ │ ├── BlockStyleControls.tsx
│ │ ├── InlineStyleControls.tsx
│ │ ├── StyleControlButton.tsx
│ │ ├── index.tsx
│ │ └── styles
│ │ │ └── index.css
│ └── title
│ │ ├── index.tsx
│ │ └── styles
│ │ └── index.css
├── constants.ts
├── createEditor.tsx
├── decoratorComposer.js
├── decorators
│ ├── color
│ │ └── index.js
│ └── prism
│ │ ├── index.tsx
│ │ ├── multiple.ts
│ │ └── theme
│ │ ├── editor.css
│ │ ├── prism-solarizedlight.css
│ │ └── prism.css
├── editor
│ ├── DocEditor.ts
│ └── ModernEditor.ts
├── helpers
│ └── clamp.ts
├── hooks
│ ├── styles
│ │ ├── useFocus.css
│ │ └── useResize.css
│ ├── useAlignment.ts
│ ├── useFocus.ts
│ └── useResize.ts
├── index.tsx
├── plugins
│ ├── AddImagePlugin.ts
│ ├── BlockStyleFnPlugin.ts
│ ├── CustomStyleMapPlugin.ts
│ ├── DefaultHandleKeyCommandPlugin.ts
│ ├── FinalNewLinePlugin.ts
│ ├── HandleDroppedFilesPlugin.ts
│ ├── InlineToolbarPlugin.ts
│ ├── LinkDecorator.ts
│ ├── LinkSpanDecoratorPlugin.ts
│ ├── PlaceholderPlugin.ts
│ ├── SelectionChangePlugin.ts
│ ├── StateFilterPlugin.ts
│ ├── StyleControlPlugin.ts
│ ├── UpdateBlockDepthData.ts
│ ├── block-render-map-plugin
│ │ ├── CodeBlock.tsx
│ │ ├── NextDiv.tsx
│ │ ├── index.tsx
│ │ ├── nextDiv.css
│ │ └── styles.css
│ ├── blockStyleFnPlugin.css
│ ├── dnd-plugin
│ │ ├── configNest.ts
│ │ ├── index-placeholder.js
│ │ ├── index.js
│ │ └── test-placeholder.js
│ ├── dnd
│ │ ├── Container.ts
│ │ ├── Dragger.ts
│ │ ├── README.md
│ │ ├── closest.ts
│ │ ├── collision.ts
│ │ ├── configs
│ │ │ ├── resolveConfig.ts
│ │ │ └── resolveDndConfig.ts
│ │ ├── dom.ts
│ │ ├── find.ts
│ │ ├── index.ts
│ │ ├── key.ts
│ │ ├── middleware
│ │ │ ├── onMove
│ │ │ │ ├── addIntermediateCtxValue.ts
│ │ │ │ ├── effects
│ │ │ │ │ ├── DndEffects.ts
│ │ │ │ │ ├── EffectsManager.ts
│ │ │ │ │ ├── handleEnterContainer.ts
│ │ │ │ │ ├── handleEnterHomeContainer.ts
│ │ │ │ │ ├── handleEnterOtherContainer.ts
│ │ │ │ │ ├── handleImpactContainerEffect.ts
│ │ │ │ │ ├── handleImpactDraggerEffect.ts
│ │ │ │ │ ├── handleLeaveContainer.ts
│ │ │ │ │ ├── handleLeaveHomeContainer.ts
│ │ │ │ │ ├── handleLeaveOtherContainer.ts
│ │ │ │ │ ├── handleReorder.ts
│ │ │ │ │ ├── handleReorderOnHomeContainer.ts
│ │ │ │ │ ├── handleReorderOnOtherContainer.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── removeIntermediateCtxValue.ts
│ │ │ │ └── syncCopyPosition.ts
│ │ │ ├── onStart
│ │ │ │ ├── attemptToCreateClone.ts
│ │ │ │ ├── getDimensions.ts
│ │ │ │ ├── getDimensionsNested.ts
│ │ │ │ └── validateContainers.ts
│ │ │ └── shared
│ │ │ │ ├── getContainer.ts
│ │ │ │ └── getImpactRawInfo.ts
│ │ ├── mutationHandler.ts
│ │ ├── reporter.ts
│ │ ├── sensors
│ │ │ ├── mouse.ts
│ │ │ └── utils.ts
│ │ ├── setAttributes.ts
│ │ ├── structure
│ │ │ └── SortedItems.ts
│ │ └── utils.ts
│ └── sidebar-plugin
│ │ ├── createAddOn.ts
│ │ ├── index.ts
│ │ └── styles.css
├── style.css
├── types
│ ├── button.ts
│ ├── components.ts
│ ├── dnd.ts
│ ├── draft-js
│ │ └── index.ts
│ ├── editor.ts
│ ├── hooks.ts
│ ├── imageToolbar.ts
│ ├── index.ts
│ ├── inlineToolBar.ts
│ ├── plugin.ts
│ ├── rect.ts
│ ├── sidebar.ts
│ ├── util.ts
│ └── withEditor.ts
├── utils
│ ├── block
│ │ ├── appendChild.ts
│ │ ├── blockMutationUtil.ts
│ │ ├── blockUtil.ts
│ │ ├── contains.ts
│ │ ├── createEmptyBlock.ts
│ │ ├── createEmptyBlockNode.ts
│ │ ├── findRootNode.ts
│ │ ├── findRootNodeSibling.ts
│ │ ├── flattenBlocks.ts
│ │ ├── horizontalTransfer.ts
│ │ ├── insertBlockAfter.ts
│ │ ├── insertBlockBefore.ts
│ │ ├── insertChild.ts
│ │ ├── removeBlock.ts
│ │ ├── removeBlockWithClear.ts
│ │ ├── resetSibling.ts
│ │ ├── transfer.ts
│ │ ├── updateBlockMapLinks.ts
│ │ ├── verticalTransfer.ts
│ │ └── wrapBlock.ts
│ ├── compareArray.ts
│ ├── contentBlock.ts
│ ├── createEntity.ts
│ ├── draft-js
│ │ ├── decorateKeyCommandHandler.ts
│ │ └── lib
│ │ │ ├── DraftModifier.ts
│ │ │ ├── NestedRichTextEditorUtil.ts
│ │ │ ├── keyCommandPlainBackspace.ts
│ │ │ ├── removeRangeFromContentState.ts
│ │ │ └── removeTextWithStrategy.ts
│ ├── editorState.ts
│ ├── event
│ │ └── bindEvents.ts
│ ├── findNode.ts
│ ├── getInlineToolbarInlineInfo.ts
│ ├── getSelectionBlockTypes.ts
│ ├── infoLog.ts
│ ├── isBlockFocused.ts
│ ├── keyHelper.ts
│ ├── moveSelectionToEnd.ts
│ ├── noop.ts
│ ├── rect
│ │ ├── findBlockContainsPoint.ts
│ │ ├── getBoundingRectWithSafeArea.ts
│ │ ├── getRootNode.ts
│ │ ├── getSelectionBlockRect.ts
│ │ ├── getSelectionRect.ts
│ │ └── getSelectionRectRelativeToOffsetParent.ts
│ ├── setSelectionToBlock.ts
│ └── throttle.ts
└── withEditor.tsx
├── stories
└── Editor.stories.tsx
├── tsconfig.json
└── tsdx.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | node_modules
5 | .cache
6 | dist
7 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | stories: ['../stories/**/*.stories.(ts|tsx)'],
5 | addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-docs'],
6 | webpackFinal: async (config) => {
7 | config.module.rules.push({
8 | test: /\.(ts|tsx)$/,
9 | use: [
10 | {
11 | loader: require.resolve('ts-loader'),
12 | options: {
13 | transpileOnly: true,
14 | },
15 | },
16 | {
17 | loader: require.resolve('react-docgen-typescript-loader'),
18 | },
19 | ],
20 | });
21 |
22 | // https://storybook.js.org/docs/configurations/custom-webpack-config/
23 | config.module.rules.splice(6, 1)
24 |
25 | // https://github.com/webpack/webpack/issues/10843
26 | config.module.rules.unshift({
27 | test: /\.css$/i,
28 | use: ['style-loader', 'css-loader'],
29 | // include: path.resolve(__dirname, '../src'),
30 | });
31 |
32 | config.resolve.extensions.push('.ts', '.tsx', '.css', 'js');
33 |
34 | return config;
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/.storybook/preview-body.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ISSUES.md:
--------------------------------------------------------------------------------
1 | # ISSUES
2 |
3 | ## InlineToolbar
4 |
5 | [] 当用户点击`link`以后,没有输入任何东西;点击其它区域使`inlineBar`消失,这个时候刚刚选中的部分背景色应该移除
6 |
7 | ## Layout design need to refer to..
8 |
9 | [What's New?](https://www.notion.so/What-s-New-157765353f2c4705bd45474e5ba8b46c)
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-tapable-editor
2 |
3 | _A pluginable, intuitive medium/notion like rich text editor_
4 |
5 | The original idea is to build an easy used rich text editor. `react-tapable-editor` is built on [draft-js](https://github.com/facebook/draft-js), and its plugin system is besed on [tapable](https://github.com/webpack/tapable) which is famous as the engine of [webpack](https://github.com/webpack/webpack).
6 |
7 | ## Features
8 |
9 | ### BlockStyle
10 |
11 | - [x] header
12 | - [x] quotation
13 | - [x] list
14 | - [x] quotation
15 |
16 | 
17 |
18 | #### code block
19 |
20 | - [x] highlight with prism
21 | - [x] copy from vscode, style could be preserved.
22 | - [ ] copy from github, code will suppress into one line.
23 |
24 | 
25 |
26 | #### Image
27 |
28 | 
29 |
30 | ### Experimental feature
31 |
32 | #### Drag and drop block
33 |
34 | 
35 |
36 | [drag to make layout design](./docs/drag.md)
37 |
38 | #### TODO consider smooth reflow...
39 |
40 | ## How to start
41 |
42 | ```bash
43 | $ npm install
44 | $ npm run storybook
45 | ```
46 |
47 | ## FAQ
48 |
49 | ### why choose draft-js
50 |
51 | - [awesome-draft-js](https://github.com/nikgraf/awesome-draft-js)
52 | - [Why Wagtail’s new editor is built with Draft.js](https://wagtail.io/blog/why-wagtail-new-editor-is-built-with-draft-js/)
53 | - [Rethinking rich text pipelines with Draft.js](https://wagtail.io/blog/rethinking-rich-text-pipelines-with-draft-js/)
54 |
--------------------------------------------------------------------------------
/docs/Issues.md:
--------------------------------------------------------------------------------
1 | # Issues
2 |
3 | ## onSelect && onFocus
4 |
5 | 之所以会出现这个问题是在进行`focus` decorator开发时,如果说当前已经处于`blur`状态的话,这个时候,如果用户鼠标点击文中的位置;如果说,此次的点击和bur时的selection一致的话,它只会触发`onFocus`;但是如果说不一致,它首先触发`onFocus`然后再触发`onSelect`将光标移动到它应该在的位置
6 |
7 | ## copy and paste
8 |
9 | [Clicking on styling button steals focus from editor, sometimes doesn't apply style to document #696](https://github.com/facebook/draft-js/issues/696)
10 |
11 | [Copy/paste between editors #787](https://github.com/facebook/draft-js/issues/787)
12 |
13 | [editor.props.stripPastedStyles](https://github.com/facebook/draft-js/blob/4c4465f6c05b6dbb9eb769f98e659f917bbdc0f6/src/component/handlers/edit/editOnPaste.js#L111)
14 |
15 | ## filter
16 |
17 | [Rethinking rich text pipelines with Draft.js](https://wagtail.io/blog/rethinking-rich-text-pipelines-with-draft-js/)
18 |
19 | [draftjs-filters](https://github.com/thibaudcolas/draftjs-filters)
20 |
21 | [Why Wagtail’s new editor is built with Draft.js](https://wagtail.io/blog/why-wagtail-new-editor-is-built-with-draft-js/)
22 |
23 | [draftail](https://github.com/springload/draftail)
24 |
25 | ## create block
26 |
27 | DraftEditorContent-core.react.js
28 |
29 |
30 | ### createNestBlockPlugin
31 | 用来创建blockKey
32 | generateRandomKey
33 |
34 | 在AtomicBlockUtils有使用到。。。
35 | 参考:splitBlockInContentState;正常触发`split-block`时,它会调用`splitBlockInContentState`这个方法;现在通过拦截的方式对split-block进行细化。
36 | insertTextInContentState
37 |
38 | ### DraftOffsetKey
39 |
40 | 其中会引入`blockKey`
41 |
42 | ### experimental
43 |
44 | `DraftEditorBlockNode.js` -> `DraftEditorNode.js` ->
--------------------------------------------------------------------------------
/docs/awesome-editor.md:
--------------------------------------------------------------------------------
1 | # awesome editor
2 |
3 | - [ProseMirror's view component](https://github.com/ProseMirror/prosemirror-view)
4 |
--------------------------------------------------------------------------------
/docs/basic.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryuever/react-tapable-editor/90b77c88e8b7ca8a2efd17a6b5a0a384df333e23/docs/basic.gif
--------------------------------------------------------------------------------
/docs/code.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryuever/react-tapable-editor/90b77c88e8b7ca8a2efd17a6b5a0a384df333e23/docs/code.gif
--------------------------------------------------------------------------------
/docs/content.md:
--------------------------------------------------------------------------------
1 | ## drag and drop block
2 |
3 | Drag and drop block is an experimental feature. It is highly inspired by notion blog style. Normally, the paragraph is placed one after another, However, if we can redesign the style with sibling paragraph, it will be more readable and attractive.
4 |
--------------------------------------------------------------------------------
/docs/drag-block.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryuever/react-tapable-editor/90b77c88e8b7ca8a2efd17a6b5a0a384df333e23/docs/drag-block.gif
--------------------------------------------------------------------------------
/docs/funcs.md:
--------------------------------------------------------------------------------
1 |
2 | # findRangeImmutable
3 |
4 | # modifyInlineStyle
5 |
6 | # getTextAfterNearestEntity
7 |
8 | # DraftModifier => applyEntity => removeEntitiesAtEdges
9 |
10 | [How to remove an entity? #182](https://github.com/facebook/draft-js/issues/182)
11 |
12 | # decorator
13 |
14 | [Coloring hexadecimal color codes](https://github.com/Soreine/draft-js-simpledecorator#example-coloring-hexadecimal-color-codes)
15 |
16 | [DraftDecorator.js](https://github.com/facebook/draft-js/blob/master/src/model/decorators/DraftDecorator.js)
--------------------------------------------------------------------------------
/docs/hooks.md:
--------------------------------------------------------------------------------
1 | # hooks
2 |
3 | ## waterfallResult
4 |
5 | 类似bailResult都是会返回一个result,但是它会接受所有的参数设置,最后返回一个合集的东西
6 |
7 | 1. 使用waterfall替代,你必须得提供一个tap放到最后,来接受;这样会造成值只能够在它的callback中来获取
8 | 2. bailResult的话,缩短了链路长度这种需求只能为1
--------------------------------------------------------------------------------
/docs/image.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryuever/react-tapable-editor/90b77c88e8b7ca8a2efd17a6b5a0a384df333e23/docs/image.gif
--------------------------------------------------------------------------------
/docs/mouseup-drop.md:
--------------------------------------------------------------------------------
1 | # mouse event and drop
2 |
3 | - [javascript-events— 'mouseup' not firing after mousemove](https://stackoverflow.com/questions/9506041/javascript-events-mouseup-not-firing-after-mousemove)
4 | - [mouseup is not fired after mousedown](https://stackoverflow.com/questions/39971069/mouseup-is-not-fired-after-mousedown)
5 | - [jquery-ui: immediate draggable on mousedown](https://stackoverflow.com/questions/40464357/jquery-ui-immediate-draggable-on-mousedown)
6 |
7 | ## Drag'n'Drop with mouse events
8 |
9 | [Drag'n'Drop with mouse events](https://javascript.info/mouse-drag-and-drop)
10 |
--------------------------------------------------------------------------------
/docs/plugin.md:
--------------------------------------------------------------------------------
1 | # plugin
2 |
3 | ## pending teardown
4 |
--------------------------------------------------------------------------------
/docs/selection.md:
--------------------------------------------------------------------------------
1 | # Selection
2 |
3 | 经过一系列操作的时候,经常会处理`selection`...
4 |
5 | ## methods
6 |
7 | ### Modifier.splitBlock
8 |
9 | 执行完以后,它的selection应该是放置到刚刚触发split时创造出的block的开头;
10 |
11 | 可以通过什么形式验证?
12 |
13 | ```js
14 | const newState = Modifier.split(editorState, selection)
15 | ```
16 |
17 | ## how to forceSelection
18 |
--------------------------------------------------------------------------------
/docs/tips.md:
--------------------------------------------------------------------------------
1 | # tips
2 |
3 | ## get last change type
4 |
5 | [Is there a way to distinguish what changed in onChange callback? #830](https://github.com/facebook/draft-js/issues/830)调用方法得到[EditorChangeType](https://draftjs.org/docs/api-reference-editor-change-type)
6 |
7 | ```js
8 | editorState.getLastChangeType()
9 | ```
--------------------------------------------------------------------------------
/examples/basic/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import PluginEditor from 'react-tapable-editor'
4 |
5 | ReactDOM.render(, document.getElementById('app'))
6 |
--------------------------------------------------------------------------------
/examples/basic/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | editor
4 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/next/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/examples/next/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | ## Learn More
18 |
19 | To learn more about Next.js, take a look at the following resources:
20 |
21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
23 |
24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
25 |
26 | ## Deploy on Vercel
27 |
28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
29 |
30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
31 |
--------------------------------------------------------------------------------
/examples/next/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "next": "9.5.2",
12 | "react": "16.13.1",
13 | "react-dom": "16.13.1",
14 | "react-tapable-editor": "^0.3.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/examples/next/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css'
2 | import '../styles/Draft.css'
3 | // https://nextjs.org/docs/basic-features/built-in-css-support#import-styles-from-node_modules
4 | // import 'react-tapable-editor/dist/my-custom-file-name.css'
5 |
6 | function MyApp({ Component, pageProps }) {
7 | return
8 | }
9 |
10 | export default MyApp
11 |
--------------------------------------------------------------------------------
/examples/next/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | class MyDocument extends Document {
4 | static async getInitialProps(ctx) {
5 | const initialProps = await Document.getInitialProps(ctx)
6 | return { ...initialProps }
7 | }
8 |
9 | render() {
10 | return (
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 | }
26 |
27 | export default MyDocument
--------------------------------------------------------------------------------
/examples/next/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | export default (req, res) => {
4 | res.statusCode = 200
5 | res.json({ name: 'John Doe' })
6 | }
7 |
--------------------------------------------------------------------------------
/examples/next/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import styles from '../styles/Home.module.css'
3 | // import '../styles/Draft.css'
4 | import Editor from 'react-tapable-editor'
5 |
6 | export default function Home() {
7 | return (
8 |
9 |
10 |
Create Next App
11 |
12 |
13 |
14 |
15 |
16 | {typeof document !== 'undefined' ? : null}
17 |
18 |
19 |
20 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/examples/next/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryuever/react-tapable-editor/90b77c88e8b7ca8a2efd17a6b5a0a384df333e23/examples/next/public/favicon.ico
--------------------------------------------------------------------------------
/examples/next/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/next/styles/Home.module.css:
--------------------------------------------------------------------------------
1 |
2 | .container {
3 | min-height: 100vh;
4 | padding: 0 0.5rem;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | overflow: auto;
10 | }
11 |
12 | .main {
13 | /* padding: 5rem 0; */
14 | width: 100%;
15 | height: 100%;
16 | flex: 1;
17 | display: flex;
18 | flex-direction: column;
19 | /* justify-content: center; */
20 | align-items: center;
21 | position: relative;
22 | }
23 |
24 | .footer {
25 | width: 100%;
26 | height: 100px;
27 | border-top: 1px solid #eaeaea;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | }
32 |
33 | .footer img {
34 | margin-left: 0.5rem;
35 | }
36 |
37 | .footer a {
38 | display: flex;
39 | justify-content: center;
40 | align-items: center;
41 | }
42 |
43 | .title a {
44 | color: #0070f3;
45 | text-decoration: none;
46 | }
47 |
48 | .title a:hover,
49 | .title a:focus,
50 | .title a:active {
51 | text-decoration: underline;
52 | }
53 |
54 | .title {
55 | margin: 0;
56 | line-height: 1.15;
57 | font-size: 4rem;
58 | }
59 |
60 | .title,
61 | .description {
62 | text-align: center;
63 | }
64 |
65 | .description {
66 | line-height: 1.5;
67 | font-size: 1.5rem;
68 | }
69 |
70 | .code {
71 | background: #fafafa;
72 | border-radius: 5px;
73 | padding: 0.75rem;
74 | font-size: 1.1rem;
75 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
76 | Bitstream Vera Sans Mono, Courier New, monospace;
77 | }
78 |
79 | .grid {
80 | display: flex;
81 | align-items: center;
82 | justify-content: center;
83 | flex-wrap: wrap;
84 |
85 | max-width: 800px;
86 | margin-top: 3rem;
87 | }
88 |
89 | .card {
90 | margin: 1rem;
91 | flex-basis: 45%;
92 | padding: 1.5rem;
93 | text-align: left;
94 | color: inherit;
95 | text-decoration: none;
96 | border: 1px solid #eaeaea;
97 | border-radius: 10px;
98 | transition: color 0.15s ease, border-color 0.15s ease;
99 | }
100 |
101 | .card:hover,
102 | .card:focus,
103 | .card:active {
104 | color: #0070f3;
105 | border-color: #0070f3;
106 | }
107 |
108 | .card h3 {
109 | margin: 0 0 1rem 0;
110 | font-size: 1.5rem;
111 | }
112 |
113 | .card p {
114 | margin: 0;
115 | font-size: 1.25rem;
116 | line-height: 1.5;
117 | }
118 |
119 | .logo {
120 | height: 1em;
121 | }
122 |
123 | .editor {
124 | height: 100%;
125 | width: 100%;
126 | position: 'relative'
127 | }
128 |
129 | @media (max-width: 600px) {
130 | .grid {
131 | width: 100%;
132 | flex-direction: column;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/mock/nested.js:
--------------------------------------------------------------------------------
1 | import Immutable from "immutable";
2 |
3 | const { Map } = Immutable;
4 |
5 | export default {
6 | blocks: [
7 | {
8 | key: "A",
9 | text: "",
10 | children: [
11 | {
12 | key: "B",
13 | text: "",
14 | data: new Map({ flexRow: true }),
15 | children: [
16 | { key: "C", text: "left block", children: [] },
17 | { key: "D", text: "right block", children: [] }
18 | ]
19 | },
20 | {
21 | key: "E",
22 | type: "header-one",
23 | text: "This is a tree based document!",
24 | children: []
25 | }
26 | ]
27 | }
28 | ],
29 | entityMap: {}
30 | };
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.3.0",
3 | "description": "medium like react tapable editor",
4 | "license": "MIT",
5 | "main": "dist/index.js",
6 | "typings": "dist/index.d.ts",
7 | "files": [
8 | "dist",
9 | "src"
10 | ],
11 | "engines": {
12 | "node": ">=10"
13 | },
14 | "scripts": {
15 | "start": "tsdx watch",
16 | "build": "rimraf dist/ && tsdx build --name react-tapable-editor --format esm,cjs,umd",
17 | "test": "tsdx test --passWithNoTests",
18 | "lint": "tsdx lint --fix",
19 | "prepare": "tsdx build",
20 | "storybook": "start-storybook -p 6006",
21 | "build-storybook": "build-storybook",
22 | "examples:basic": "DIR=basic EXT=js webpack-dev-server"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/ryuever/react-tapable-editor"
27 | },
28 | "peerDependencies": {
29 | "react": ">=16"
30 | },
31 | "husky": {
32 | "hooks": {
33 | "pre-commit": "tsdx lint"
34 | }
35 | },
36 | "prettier": {
37 | "printWidth": 80,
38 | "semi": true,
39 | "singleQuote": true,
40 | "trailingComma": "es5"
41 | },
42 | "name": "react-tapable-editor",
43 | "author": "youchao liu",
44 | "module": "dist/react-tapable-editor.esm.js",
45 | "dependencies": {
46 | "@types/classnames": "^2.2.10",
47 | "@types/draft-js": "^0.10.43",
48 | "@types/immutable": "^3.8.7",
49 | "@types/invariant": "^2.2.33",
50 | "@types/prismjs": "^1.16.1",
51 | "classnames": "^2.2.6",
52 | "draft-js": "^0.11.0",
53 | "immutable": "^4.0.0-rc.12",
54 | "invariant": "^2.2.4",
55 | "prismjs": "^1.21.0",
56 | "sabar": "^0.5.4"
57 | },
58 | "devDependencies": {
59 | "@babel/core": "^7.10.4",
60 | "@storybook/addon-actions": "^5.3.19",
61 | "@storybook/addon-docs": "^5.3.19",
62 | "@storybook/addon-info": "^5.3.19",
63 | "@storybook/addon-links": "^5.3.19",
64 | "@storybook/addons": "^5.3.19",
65 | "@storybook/react": "^5.3.19",
66 | "@types/react": "^16.9.42",
67 | "@types/react-dom": "^16.9.8",
68 | "babel-loader": "^8.1.0",
69 | "css-loader": "^3.6.0",
70 | "eslint": "^6.8.0",
71 | "eslint-config-airbnb": "^18.0.1",
72 | "eslint-config-prettier": "^6.10.0",
73 | "eslint-plugin-import": "^2.20.1",
74 | "eslint-plugin-jsx-a11y": "^6.2.3",
75 | "eslint-plugin-prettier": "^3.1.2",
76 | "eslint-plugin-react": "^7.18.3",
77 | "eslint-plugin-react-hooks": "^1.7.0",
78 | "husky": "^4.2.5",
79 | "postcss-loader": "^3.0.0",
80 | "react": "^16.13.1",
81 | "react-docgen-typescript-loader": "^3.7.2",
82 | "react-dom": "^16.13.1",
83 | "react-is": "^16.13.1",
84 | "rollup-plugin-postcss": "^3.1.3",
85 | "style-loader": "^1.2.1",
86 | "ts-loader": "^8.0.0",
87 | "tsdx": "^0.13.2",
88 | "tslib": "^2.0.0",
89 | "typescript": "^3.9.6"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import { GetEditor } from './types';
3 |
4 | export default createContext(null);
5 |
--------------------------------------------------------------------------------
/src/components/button/Blockquote.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const Blockquote: ComponentType = ({ fill }) => {
7 | return (
8 |
12 | );
13 | };
14 |
15 | export default withFillColor(withAction(Blockquote));
16 |
--------------------------------------------------------------------------------
/src/components/button/Bold.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const Bold: ComponentType = ({ fill }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default withFillColor(withAction(Bold));
15 |
--------------------------------------------------------------------------------
/src/components/button/BulletedList.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const BulletedList: ComponentType = ({ fill }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default withFillColor(withAction(BulletedList));
15 |
--------------------------------------------------------------------------------
/src/components/button/CodeBlock.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const CodeBlock: ComponentType = ({ fill }) => {
7 | return (
8 |
15 | );
16 | };
17 |
18 | export default withFillColor(withAction(CodeBlock));
19 |
--------------------------------------------------------------------------------
/src/components/button/Dragger.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Dragger = () => {
4 | return (
5 |
13 | );
14 | };
15 |
16 | export default Dragger;
17 |
--------------------------------------------------------------------------------
/src/components/button/H1.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const H1: ComponentType = ({ fill }) => {
7 | return (
8 |
14 | );
15 | };
16 |
17 | export default withFillColor(withAction(H1));
18 |
--------------------------------------------------------------------------------
/src/components/button/H2.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const H2: ComponentType = ({ fill }) => {
7 | return (
8 |
14 | );
15 | };
16 |
17 | export default withFillColor(withAction(H2));
18 |
--------------------------------------------------------------------------------
/src/components/button/H3.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const H3: ComponentType = ({ fill }) => {
7 | return (
8 |
13 | );
14 | };
15 |
16 | export default withFillColor(withAction(H3));
17 |
--------------------------------------------------------------------------------
/src/components/button/H4.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const H4: ComponentType = ({ fill }) => {
7 | return (
8 |
14 | );
15 | };
16 |
17 | export default withFillColor(withAction(H4));
18 |
--------------------------------------------------------------------------------
/src/components/button/ImageAlignCenter.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const ImageAlignCenter: ComponentType = ({ fill }) => {
7 | return (
8 |
12 | );
13 | };
14 |
15 | // export default withAction(ImageAlignCenter)
16 | export default withFillColor(withAction(ImageAlignCenter));
17 |
18 | // interface FooProps {
19 | // foo: string;
20 | // }
21 |
22 | // interface BarProps {
23 | // bar: string;
24 | // }
25 |
26 | // export const withFoo = (
27 | // Component: React.ComponentType
28 | // ): React.ComponentType> => props => (
29 | //
30 | // );
31 |
32 | // export const withBar = (
33 | // Component: React.ComponentType
34 | // ): React.ComponentType> => props => (
35 | //
36 | // );
37 |
38 | // const WrappedComponent = ({ bar, foo, more }) => {
39 | // console.log(more);
40 | // console.log(bar);
41 | // console.log(foo);
42 | // return woohoo
;
43 | // };
44 |
45 | // const EnhancedComponent = withFoo(withBar(WrappedComponent));
46 |
--------------------------------------------------------------------------------
/src/components/button/ImageAlignLeft.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const ImageAlignLeft: ComponentType = ({ fill }) => {
7 | return (
8 |
12 | );
13 | };
14 |
15 | export default withFillColor(withAction(ImageAlignLeft));
16 |
--------------------------------------------------------------------------------
/src/components/button/ImageAlignLeftFillContent.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const ImageAlignLeftFillContent: ComponentType = ({ fill }) => {
7 | return (
8 |
12 | );
13 | };
14 |
15 | export default withFillColor(withAction(ImageAlignLeftFillContent));
16 |
--------------------------------------------------------------------------------
/src/components/button/ImageAlignRightFillContent.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const ImageAlignRightFillContent: ComponentType = ({ fill }) => {
7 | return (
8 |
12 | );
13 | };
14 |
15 | export default withFillColor(withAction(ImageAlignRightFillContent));
16 |
--------------------------------------------------------------------------------
/src/components/button/ImageFillWidth.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const ImageFillWidth: ComponentType = ({ fill }) => {
7 | return (
8 |
14 | );
15 | };
16 |
17 | export default withFillColor(withAction(ImageFillWidth));
18 |
--------------------------------------------------------------------------------
/src/components/button/ImageInsetCenter.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const ImageInsetCenter: ComponentType = ({ fill }) => {
7 | return (
8 |
14 | );
15 | };
16 |
17 | export default withFillColor(withAction(ImageInsetCenter));
18 |
--------------------------------------------------------------------------------
/src/components/button/ImageOutsetCenter.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const ImageOutsetCenter: ComponentType = ({ fill }) => {
7 | return (
8 |
14 | );
15 | };
16 |
17 | export default withFillColor(withAction(ImageOutsetCenter));
18 |
--------------------------------------------------------------------------------
/src/components/button/InlineCode.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const InlineCode: ComponentType = ({ fill }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default withFillColor(withAction(InlineCode));
15 |
--------------------------------------------------------------------------------
/src/components/button/Italic.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const Italic: ComponentType = ({ fill }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default withFillColor(withAction(Italic));
15 |
--------------------------------------------------------------------------------
/src/components/button/Link.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const Link: ComponentType = ({ fill }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default withFillColor(withAction(Link));
15 |
--------------------------------------------------------------------------------
/src/components/button/NumberedList.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const NumberedList: ComponentType = ({ fill }) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default withFillColor(withAction(NumberedList));
15 |
--------------------------------------------------------------------------------
/src/components/button/Plus.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Plus = () => {
4 | return (
5 |
13 | );
14 | };
15 |
16 | export default Plus;
17 |
--------------------------------------------------------------------------------
/src/components/button/Selectable.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withAction from './utils/withAction';
3 | import { ButtonProps } from '../../types';
4 |
5 | const Selectable: ComponentType = () => {
6 | return (
7 |
10 | );
11 | };
12 |
13 | export default withAction(Selectable);
14 |
--------------------------------------------------------------------------------
/src/components/button/StrikeThrough.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const StrikeThrough: ComponentType = ({ fill }) => {
7 | return (
8 |
13 | );
14 | };
15 |
16 | export default withFillColor(withAction(StrikeThrough));
17 |
--------------------------------------------------------------------------------
/src/components/button/Underline.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | const Underline: ComponentType = ({ fill }) => {
7 | return (
8 |
16 | );
17 | };
18 |
19 | export default withFillColor(withAction(Underline));
20 |
--------------------------------------------------------------------------------
/src/components/button/Unlink.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 | import withFillColor from './utils/withFillColor';
3 | import withAction from './utils/withAction';
4 | import { ButtonProps } from '../../types';
5 |
6 | // https://www.iconfont.cn/collections/detail?cid=11790
7 |
8 | const Unlink: ComponentType = ({ fill }) => {
9 | return (
10 |
19 | );
20 | };
21 |
22 | export default withFillColor(withAction(Unlink));
23 |
--------------------------------------------------------------------------------
/src/components/button/utils/action.css:
--------------------------------------------------------------------------------
1 | .icon-wrapper {
2 | display: flex;
3 | height: 44px;
4 | align-items: center;
5 | justify-content: center;
6 | width: 35px;
7 | }
8 |
9 | .icon-wrapper:hover {
10 | cursor: pointer;
11 | background-color: rgba(238, 238, 238, 0.1);
12 | }
13 |
14 | .icon-button {
15 | background: rgba(0,0,0,0);
16 | padding: 0px;
17 | border: none;
18 | }
19 | .icon-button:focus {
20 | outline: 0;
21 | }
--------------------------------------------------------------------------------
/src/components/button/utils/withAction.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, ComponentType } from 'react';
2 | import { WithActionProps, ButtonProps } from '../../../types';
3 | import './action.css';
4 |
5 | export default (
6 | WrappedComponent: ComponentType>
7 | ): ComponentType => (props: Props) => {
8 | const { onClick, ...rest } = props;
9 | const handleClick = useCallback(() => {
10 | if (typeof onClick === 'function') onClick();
11 | }, [onClick]);
12 | // 之所以要有这个handler,因为在点击`inlineToolbar`上的按钮时,它会清掉selection;
13 | // 有人发现是`onMouseDown`造成的问题;具体参考
14 | // https://github.com/facebook/draft-js/issues/696#issuecomment-302903086
15 | const onMouseDownHandler = useCallback(e => {
16 | e.preventDefault();
17 | }, []);
18 |
19 | return (
20 |
27 | );
28 | };
29 |
30 | // //
31 |
32 | // function withAction (
33 | // WrappedComponent: ComponentType & {
34 | // onClick: () => void
35 | // }>
36 | // ): FC> {
37 | // return (props: T) => {
38 | // const { onClick, ...rest } = props;
39 | // const handleClick = useCallback(() => {
40 | // if (typeof onClick === 'function') onClick();
41 | // }, [onClick]);
42 | // // 之所以要有这个handler,因为在点击`inlineToolbar`上的按钮时,它会清掉selection;
43 | // // 有人发现是`onMouseDown`造成的问题;具体参考
44 | // // https://github.com/facebook/draft-js/issues/696#issuecomment-302903086
45 | // const onMouseDownHandler = useCallback(e => {
46 | // e.preventDefault();
47 | // }, []);
48 |
49 | // return (
50 | //
57 | // );
58 | // }
59 |
--------------------------------------------------------------------------------
/src/components/button/utils/withFillColor.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from 'react';
2 |
3 | import {
4 | ButtonProps,
5 | WithFillColorProps,
6 | WithActionProps,
7 | } from '../../../types';
8 |
9 | export default <
10 | Props extends WithFillColorProps & WithActionProps & ButtonProps
11 | >(
12 | WrappedComponent: ComponentType>
13 | ): ComponentType> =>
14 | // memo(
15 | (props: Omit) => {
16 | const { active, ...rest } = props;
17 | const fill = active ? '#34e79a' : '#fff';
18 |
19 | return (
20 | )} fill={fill} />
21 | );
22 | };
23 | // (next: Omit, prev: Omit) => next.active === prev.active
24 | // );
25 |
--------------------------------------------------------------------------------
/src/components/image-toolbar/styles.css:
--------------------------------------------------------------------------------
1 | .image-toolbar {
2 | position: absolute;
3 | display: none;
4 | visibility: hidden;
5 | z-index: 1;
6 | }
7 |
8 | .image-toolbar-inner {
9 | position: relative;
10 | background-image: linear-gradient(to bottom,rgba(49,49,47,.99),#262625);
11 | background-repeat: repeat-x;
12 | -webkit-border-radius: 5px;
13 | border-radius: 5px;
14 | padding: 0 10px;
15 | visibility: visible;
16 | }
17 |
18 | .image-toolbar-action-group {
19 | height: 44px;
20 | display: flex;
21 | flex-direction: row;
22 | }
23 |
24 | .arrow-down {
25 | width: 0;
26 | height: 0;
27 | border-left: 10px solid transparent;
28 | border-right: 10px solid transparent;
29 | border-top: 10px solid #262625;
30 |
31 | position: absolute;
32 | left: 50%;
33 | bottom: -10px;
34 | transform: translateX(-10px);
35 | }
--------------------------------------------------------------------------------
/src/components/image/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, FC, useEffect, useRef, RefObject } from 'react';
2 | import './styles/index.css';
3 | import useFocus from '../../hooks/useFocus';
4 | import useResize from '../../hooks/useResize';
5 | import useAlignment from '../../hooks/useAlignment';
6 | import { ImageProps } from '../../types';
7 |
8 | const Image: FC = props => {
9 | // createRef does not work. Because the `nodeRef` value is not updated
10 | // even if after useEffect triggered.
11 | const ref = useRef() as RefObject;
12 | const [, setIsRefReady] = useState(false);
13 |
14 | useEffect(() => {
15 | setIsRefReady(true);
16 | }, []);
17 |
18 | const { block, contentState } = props;
19 | useFocus({ nodeRef: ref, props });
20 | useResize({ nodeRef: ref, props });
21 | useAlignment({ nodeRef: ref, props });
22 |
23 | const meta = contentState.getEntity(block.getEntityAt(0)).getData();
24 | const { src } = meta;
25 |
26 | return (
27 |
28 |

29 |
30 | );
31 | };
32 |
33 | export default Image;
34 |
--------------------------------------------------------------------------------
/src/components/image/resizable.js:
--------------------------------------------------------------------------------
1 | // https://techcrunch.com/2019/12/02/why-notion-is-staying-small-as-its-valuation-gets-bigger/
2 | // imageToolbar是决定它的相对位置;同时会影响到它进行resize时的可选择方向
3 | // 1. 如果说当前是 align-center的话,resize的时候要确保一直居中
4 | // 2. 如果说当前是 align-left的话,只能够向左
5 | // 3. 如果说当前是 align-right的话,只能够向右
6 |
--------------------------------------------------------------------------------
/src/components/image/styles/index.css:
--------------------------------------------------------------------------------
1 | .image-wrapper {
2 | width: 900px;
3 | margin-left: auto;
4 | margin-right: auto;
5 |
6 | /* constraint image max size */
7 | max-width: 100%;
8 | }
9 |
10 | .image {
11 | width: 100%;
12 | margin: auto;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/inline-toolbar/Divider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default React.memo(
4 | () => ,
5 | () => true
6 | );
7 |
--------------------------------------------------------------------------------
/src/components/inline-toolbar/InputBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { createRef, useEffect, useCallback, FC } from 'react';
2 | import { EditorState } from 'draft-js';
3 | import Divider from './Divider';
4 |
5 | import Link from '../button/Link';
6 | import Unlink from '../button/Unlink';
7 |
8 | import { createLinkAtSelection } from '../../utils/createEntity';
9 | import { InputBarProps } from '../../types';
10 |
11 | const InputBar: FC = ({ getEditor }) => {
12 | const inputRef = createRef();
13 | const { hooks } = getEditor();
14 |
15 | useEffect(() => {
16 | if (inputRef.current) inputRef.current.focus();
17 | }, [inputRef]);
18 |
19 | const submit = useCallback(
20 | (value: string) => {
21 | const { editorState, hooks } = getEditor();
22 | const newState = createLinkAtSelection(editorState, value);
23 | const currentContent = newState.getCurrentContent();
24 | const selection = newState.getSelection();
25 | const focusOffset = selection.getFocusOffset();
26 | const focusKey = selection.getFocusKey();
27 |
28 | // 通过下面的方式,并不能够将cursor放置在刚刚的selection末尾
29 | // const nextState = EditorState.set(newState, {
30 | // currentContent: currentContent.merge({
31 | // selectionAfter: currentContent.getSelectionAfter().merge({
32 | // hasFocus: true,
33 | // anchorOffset: focusOffset,
34 | // anchorKey: focusKey,
35 | // })
36 | // })
37 | // })
38 | // hooks.setState.call(nextState)
39 |
40 | // 当用户输入完以后,指针是放置在selection的后面
41 | const nextState = EditorState.forceSelection(
42 | newState,
43 | currentContent.getSelectionAfter().merge({
44 | anchorOffset: focusOffset,
45 | anchorKey: focusKey,
46 | })
47 | );
48 |
49 | hooks.setState.call(nextState);
50 | },
51 | [getEditor]
52 | );
53 |
54 | const onKeyDownHandler = useCallback(
55 | e => {
56 | const { key } = e;
57 | if (key === 'Enter') {
58 | hooks.cleanUpLinkClickState.call();
59 | e.preventDefault();
60 | const inputValue = e.target.value;
61 | submit(inputValue);
62 | hooks.hideInlineToolbar.call();
63 | } else if (key === 'Escape') {
64 | e.preventDefault();
65 | hooks.hideInlineToolbar.call();
66 | }
67 | },
68 | [hooks.cleanUpLinkClickState, hooks.hideInlineToolbar, submit]
69 | );
70 |
71 | return (
72 |
73 |
79 |
80 |
81 | {}} />
82 | {}} />
83 |
84 |
85 | );
86 | };
87 |
88 | export default InputBar;
89 |
--------------------------------------------------------------------------------
/src/components/inline-toolbar/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect, useCallback, FC } from 'react';
2 | import Immutable from 'immutable';
3 | import withEditor from '../../withEditor';
4 | import StyleControls from './StyleControls';
5 | import InputBar from './InputBar';
6 | import getSelectionBlockTypes from '../../utils/getSelectionBlockTypes';
7 | import getInlineToolbarInlineInfo from '../../utils/getInlineToolbarInlineInfo';
8 | import { InlineToolbarProps, InlineToolbarStateValues } from '../../types';
9 |
10 | import './styles.css';
11 |
12 | const InlineToolbar: FC = props => {
13 | const { forwardRef, getEditor } = props;
14 | const [value, setValue] = useState({
15 | styles: Immutable.OrderedSet(),
16 | blockTypes: [],
17 | inDisplayMode: true,
18 | hasLink: false,
19 | } as InlineToolbarStateValues);
20 | const inDisplayModeRef = useRef(true);
21 |
22 | useEffect(() => {
23 | const { hooks } = getEditor();
24 | hooks.inlineBarChange.tap('InlineToolbar', (editorState, visibility) => {
25 | const nextValue = {
26 | inDisplayMode:
27 | visibility === 'hidden' ? true : inDisplayModeRef.current,
28 | } as InlineToolbarStateValues;
29 |
30 | if (editorState) {
31 | const { styles, hasLink } = getInlineToolbarInlineInfo(editorState);
32 | if (styles) {
33 | nextValue.styles = styles as any;
34 | }
35 | nextValue.hasLink = hasLink;
36 | nextValue.blockTypes = getSelectionBlockTypes(editorState);
37 | } else {
38 | nextValue.styles = Immutable.OrderedSet();
39 | nextValue.blockTypes = [];
40 | nextValue.hasLink = false;
41 | }
42 |
43 | inDisplayModeRef.current = nextValue.inDisplayMode;
44 |
45 | setValue(nextValue);
46 | });
47 | }, [getEditor]);
48 |
49 | const { styles, blockTypes, inDisplayMode, hasLink } = value;
50 |
51 | const toggleDisplayMode = useCallback(() => {
52 | setValue({
53 | ...value,
54 | inDisplayMode: !inDisplayMode,
55 | });
56 | inDisplayModeRef.current = !inDisplayMode;
57 | }, [inDisplayMode, value]);
58 |
59 | return (
60 |
61 | {inDisplayMode && (
62 |
69 | )}
70 | {!inDisplayMode &&
}
71 |
72 |
73 | );
74 | };
75 |
76 | const MemoToolbar = React.memo(
77 | props => {
78 | return ;
79 | },
80 | () => true
81 | );
82 |
83 | export default withEditor(MemoToolbar);
84 |
--------------------------------------------------------------------------------
/src/components/inline-toolbar/styles.css:
--------------------------------------------------------------------------------
1 | .inline-toolbar {
2 | position: absolute;
3 | display: none;
4 | visibility: hidden;
5 | z-index: 1;
6 | width: 485px;
7 | }
8 |
9 | .inline-toolbar-inner {
10 | position: relative;
11 | background-image: linear-gradient(to bottom,rgba(49,49,47,.99),#262625);
12 | background-repeat: repeat-x;
13 | -webkit-border-radius: 5px;
14 | border-radius: 5px;
15 | padding: 0 10px;
16 | visibility: visible;
17 | }
18 |
19 | .inline-toolbar-action-group {
20 | height: 44px;
21 | display: flex;
22 | flex-direction: row;
23 | }
24 |
25 | .divider {
26 | width: 1px;
27 | height: 30px;
28 | margin: 7px 2px;
29 | background-color: #666;
30 | }
31 |
32 | .arrow-down {
33 | width: 0;
34 | height: 0;
35 | border-left: 10px solid transparent;
36 | border-right: 10px solid transparent;
37 | border-top: 10px solid #262625;
38 |
39 | position: absolute;
40 | left: 50%;
41 | bottom: -10px;
42 | transform: translateX(-10px);
43 | }
44 |
45 | .inline-toolbar-link-inner {
46 | position: relative;
47 | background-image: linear-gradient(to bottom,rgba(49,49,47,.99),#262625);
48 | background-repeat: repeat-x;
49 | -webkit-border-radius: 5px;
50 | border-radius: 5px;
51 | padding: 0 10px;
52 | visibility: visible;
53 | display: flex;
54 | flex-direction: row;
55 | }
56 |
57 | .inline-link-input {
58 | outline: 0;
59 | height: 44px;
60 | border: none;
61 | flex: auto;
62 | background-image: linear-gradient(to bottom,rgba(49,49,47,.99),#262625);
63 | color: #fff;
64 | font-size: 16px;
65 | }
66 |
67 | .link-action-group {
68 | width: 70px;
69 | height: 44px;
70 | display: flex;
71 | flex-direction: row;
72 | }
--------------------------------------------------------------------------------
/src/components/link-span/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { LinkSpanProps } from '../../types';
3 | import './styles.css';
4 |
5 | export default (props: LinkSpanProps) => {
6 | return {props.children};
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/link-span/styles.css:
--------------------------------------------------------------------------------
1 | .link_span {
2 | background-color: #dee0e3;
3 | }
--------------------------------------------------------------------------------
/src/components/link/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { LinkProps } from '../../types';
3 | import './styles.css';
4 |
5 | const Link: FC = (props: LinkProps) => {
6 | const { contentState, entityKey, children } = props;
7 | const entity = contentState.getEntity(entityKey);
8 | const data = entity.getData();
9 | const { url } = data;
10 |
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export default Link;
19 |
--------------------------------------------------------------------------------
/src/components/link/styles.css:
--------------------------------------------------------------------------------
1 | .decorator_link {
2 | color: #3370ff;
3 | text-decoration: none;
4 | }
--------------------------------------------------------------------------------
/src/components/sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import classes from 'classnames';
3 | import Plus from '../button/Plus';
4 | import Dragger from '../button/Dragger';
5 | import { SidebarProps } from '../../types';
6 | import './styles.css';
7 |
8 | const Sidebar = (props: SidebarProps) => {
9 | const { forwardRef } = props;
10 | const containerStyleRef = useRef(classes('container'));
11 |
12 | return (
13 |
21 | );
22 | };
23 |
24 | export default Sidebar;
25 |
--------------------------------------------------------------------------------
/src/components/sidebar/styles.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: center;
5 | align-items: center;
6 | height: 24px;
7 | position: absolute;
8 | top: 1000px;
9 | bottom: 1000px;
10 | opacity: 0;
11 | }
12 |
13 | .plus {
14 | height: 24px;
15 | width: 24px;
16 | display: flex;
17 | justify-content: center;
18 | align-items: center;
19 | border-radius: 4px;
20 | }
21 |
22 | .dragger {
23 | width: 18px;
24 | height: 24px;
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | border-radius: 4px;
29 | }
30 |
31 | .dragger:hover,
32 | .plus:hover {
33 | background-color: #eee;
34 | cursor: pointer;
35 | transition: background 120ms ease-in 0s;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/style-controls/BlockStyleControls.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { GetEditor } from '../../types';
3 |
4 | import StyleControlButton from './StyleControlButton';
5 |
6 | const BLOCK_TYPES = [
7 | { label: 'H1', style: 'header-one' },
8 | // {label: 'H2', style: 'header-two'},
9 | // {label: 'H3', style: 'header-three'},
10 | // {label: 'H4', style: 'header-four'},
11 | // {label: 'H5', style: 'header-five'},
12 | // {label: 'H6', style: 'header-six'},
13 | { label: 'Blockquote', style: 'blockquote' },
14 | { label: 'UL', style: 'unordered-list-item' },
15 | { label: 'OL', style: 'ordered-list-item' },
16 | { label: 'Code Block', style: 'code-block' },
17 | ];
18 |
19 | const BlockStyleControls = ({ getEditor }: { getEditor: GetEditor }) => {
20 | const { editorState } = getEditor();
21 | const toggleBlockStyleControl = useCallback(
22 | blockType => {
23 | // 这个地方需要用最新的`editorState`;如果不进行这一步的话,editorState
24 | // 会是比较老的。
25 | const { editorState: latestEditorState, hooks } = getEditor();
26 | hooks.toggleWaterfallBlockType.call(null, latestEditorState, blockType);
27 | },
28 | [getEditor]
29 | );
30 |
31 | const selection = editorState.getSelection();
32 | const blockType = editorState
33 | .getCurrentContent()
34 | .getBlockForKey(selection.getStartKey())
35 | .getType();
36 |
37 | return (
38 |
39 | {BLOCK_TYPES.map(type => (
40 |
47 | ))}
48 |
49 | );
50 | };
51 |
52 | export default BlockStyleControls;
53 |
--------------------------------------------------------------------------------
/src/components/style-controls/InlineStyleControls.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { GetEditor } from '../../types';
3 | import StyleControlButton from './StyleControlButton';
4 |
5 | const INLINE_STYLES = [
6 | { label: 'Bold', style: 'BOLD' },
7 | { label: 'Italic', style: 'ITALIC' },
8 | { label: 'Underline', style: 'UNDERLINE' },
9 | { label: 'Monospace', style: 'CODE' },
10 | ];
11 |
12 | // 1. 当切换到输入中文的时候,如果说这个时候点击`inline style button`的话,
13 | // 它是不会生效到下一个中文字符的
14 | // 2. 当换行的时候,上一个block最后字符的`inline-style`无法自动应用到新的一行
15 | // (新的一行如果说是输入中文)
16 |
17 | const InlineStyleControls = ({ getEditor }: { getEditor: GetEditor }) => {
18 | const { editorState } = getEditor();
19 | let currentStyle = editorState.getCurrentInlineStyle();
20 | const contentState = editorState.getCurrentContent();
21 | const selection = editorState.getSelection();
22 | const handleToggle = useCallback(
23 | inlineStyle => {
24 | const { hooks } = getEditor();
25 | hooks.toggleInlineStyle.call(inlineStyle);
26 | },
27 | [getEditor]
28 | );
29 |
30 | // 主要是为了解决当输入中文的时候,会出现`active inline style`被清空的现象;
31 | if (!currentStyle.size && selection.isCollapsed()) {
32 | const block = contentState.getBlockForKey(selection.getAnchorKey());
33 | const startOffset = selection.getStartOffset();
34 |
35 | const chars = block.getCharacterList();
36 | const length = chars.size;
37 |
38 | if (length < startOffset) {
39 | currentStyle = block.getInlineStyleAt(length - 1);
40 | }
41 | }
42 |
43 | return (
44 |
45 | {INLINE_STYLES.map(({ label, style }) => (
46 |
53 | ))}
54 |
55 | );
56 | };
57 |
58 | export default InlineStyleControls;
59 |
--------------------------------------------------------------------------------
/src/components/style-controls/StyleControlButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import classnames from 'classnames';
3 | import { StyleControlButtonProps, Label } from '../../types';
4 |
5 | const StyleControlButton = ({
6 | label,
7 | active,
8 | onToggle,
9 | style,
10 | }: StyleControlButtonProps) => {
11 | const handleMouseDown = useCallback(
12 | e => {
13 | e.preventDefault();
14 | if (typeof onToggle === 'function') {
15 | onToggle(style);
16 | }
17 | },
18 | [onToggle, style]
19 | );
20 |
21 | const cx = classnames({
22 | 'miuffy-style-button': true,
23 | 'miuffy-active-button': active,
24 | });
25 |
26 | return (
27 |
33 | );
34 | };
35 |
36 | export default StyleControlButton;
37 |
--------------------------------------------------------------------------------
/src/components/style-controls/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BlockStyleControls from './BlockStyleControls';
3 | import InlineStyleControls from './InlineStyleControls';
4 | import { GetEditor } from '../../types';
5 | import './styles/index.css';
6 |
7 | const StyleControls = ({ getEditor }: { getEditor: GetEditor }) => (
8 |
13 | );
14 |
15 | export default StyleControls;
16 |
--------------------------------------------------------------------------------
/src/components/style-controls/styles/index.css:
--------------------------------------------------------------------------------
1 | .miuffy-style-button {
2 | min-width: 32px;
3 | height: 32px;
4 | font-size: 12px;
5 | line-height: 32px;
6 | border: none;
7 | padding: 0 8px;
8 | background: transparent;
9 | border: 1px solid transparent;
10 | margin-right: 5px;
11 | }
12 |
13 | .miuffy-style-button:hover {
14 | background-color: #f5f5f5;
15 | transition: all 300ms ease-in;
16 | box-sizing: border-box;
17 | border-radius: 3px;
18 | cursor: pointer;
19 | }
20 |
21 | .miuffy-active-button:hover,
22 | .miuffy-active-button {
23 | background-color: #e8e8e8;
24 | box-sizing: border-box;
25 | border-radius: 3px;
26 | cursor: pointer;
27 | }
28 |
29 | .miuffy-editor-controls {
30 | display: flex;
31 | flex-direction: row;
32 | position: fixed;
33 | top: 0;
34 | right: 0;
35 | left: 0;
36 | padding-top: 5px;
37 | padding-left: 40px;
38 | background: #fff;
39 | border-bottom: 1px solid #eee;
40 | z-index: 100;
41 | padding-bottom: 5px;
42 | }
43 |
44 | .delimiter {
45 | width: 1px;
46 | background: #eee;
47 | margin: 5px;
48 | }
--------------------------------------------------------------------------------
/src/components/title/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import withEditor from '../../withEditor';
3 |
4 | import './styles/index.css';
5 |
6 | const Title = () => (
7 |
8 |
9 |
10 | );
11 |
12 | export default withEditor(Title);
13 |
--------------------------------------------------------------------------------
/src/components/title/styles/index.css:
--------------------------------------------------------------------------------
1 | .article-title {
2 | height: 70px;
3 | border-bottom: 1px solid #eee;
4 | margin-bottom: 32px;
5 | }
6 |
7 | .title-input {
8 | height: 50px;
9 | line-height: 50px;
10 | font-size: 32px;
11 | width: 100%;
12 | border: none;
13 | color: #262626;
14 | font-weight: 700;
15 | }
16 | /* 消除title输入时,边框border highlight */
17 | .title-input:focus {
18 | outline: none !important;
19 | }
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const rootSelector: string = '.DraftEditor-root';
2 |
--------------------------------------------------------------------------------
/src/decoratorComposer.js:
--------------------------------------------------------------------------------
1 | function decorateComposer(...funcs) {
2 | if (!funcs.length) return args => args;
3 | // if (funcs.length === 1) return (...args) =>
4 | // funcs[0].apply(this, [decorateComposer.getEditor, ...args])
5 |
6 | if (funcs.length === 1) {
7 | return funcs[0];
8 | }
9 | return funcs.reduce((a, b) => (...args) => {
10 | console.log('args : ', a);
11 | // return a(b(...args))
12 | return a(b(args[0].bind(this, 3))).bind(this, 4);
13 | });
14 | // return funcs.reduce((a, b) => (...args) => a(b.apply(this, [decorateComposer.getEditor, ...args])))
15 | }
16 |
17 | export default decorateComposer;
18 |
--------------------------------------------------------------------------------
/src/decorators/color/index.js:
--------------------------------------------------------------------------------
1 | // coloring hexadecimal
2 | // https://github.com/Soreine/draft-js-simpledecorator#example-coloring-hexadecimal-color-codes
3 |
--------------------------------------------------------------------------------
/src/decorators/prism/multiple.ts:
--------------------------------------------------------------------------------
1 | import Immutable, { List } from 'immutable';
2 | import { ContentBlock } from 'draft-js';
3 | import { DraftDecoratorType, ContentNodeState } from '../../types';
4 |
5 | const KEY_SEPARATOR = '-';
6 |
7 | interface MultiDecoratorType {
8 | decorators: List;
9 | }
10 |
11 | // https://stackoverflow.com/questions/52431074/how-to-solve-this-implicitly-has-type-any-when-typescript-checking-classic
12 | function MultiDecorator(
13 | this: MultiDecoratorType,
14 | decorators: DraftDecoratorType[]
15 | ) {
16 | this.decorators = List(decorators);
17 | }
18 |
19 | /**
20 | Return list of decoration IDs per character
21 |
22 | @param {ContentBlock}
23 | @return {List}
24 | */
25 | MultiDecorator.prototype.getDecorations = function(
26 | block: ContentBlock,
27 | contentState: ContentNodeState
28 | ) {
29 | const decorations = Array(block.getText().length).fill(null);
30 |
31 | this.decorators.forEach(function(decorator: DraftDecoratorType, i: number) {
32 | const _decorations = decorator.getDecorations(block, contentState);
33 |
34 | _decorations.forEach(function(key, offset) {
35 | if (!key) {
36 | return;
37 | }
38 |
39 | key = i + KEY_SEPARATOR + key;
40 |
41 | decorations[offset] = key;
42 | });
43 | });
44 |
45 | return Immutable.List(decorations);
46 | };
47 |
48 | /**
49 | Return component to render a decoration
50 |
51 | @param {String}
52 | @return {Function}
53 | */
54 | MultiDecorator.prototype.getComponentForKey = function(key: string) {
55 | const decorator = this.getDecoratorForKey(key);
56 | return decorator.getComponentForKey(this.getInnerKey(key));
57 | };
58 |
59 | /**
60 | Return props to render a decoration
61 |
62 | @param {String}
63 | @return {Object}
64 | */
65 | MultiDecorator.prototype.getPropsForKey = function(key: string) {
66 | const decorator = this.getDecoratorForKey(key);
67 | return decorator.getPropsForKey(this.getInnerKey(key));
68 | };
69 |
70 | /**
71 | Return a decorator for a specific key
72 |
73 | @param {String}
74 | @return {Decorator}
75 | */
76 | MultiDecorator.prototype.getDecoratorForKey = function(key: string) {
77 | const parts = key.split(KEY_SEPARATOR);
78 | const index = Number(parts[0]);
79 |
80 | return this.decorators.get(index);
81 | };
82 |
83 | /**
84 | Return inner key for a decorator
85 |
86 | @param {String}
87 | @return {String}
88 | */
89 | MultiDecorator.prototype.getInnerKey = function(key: string) {
90 | const parts = key.split(KEY_SEPARATOR);
91 | return parts.slice(1).join(KEY_SEPARATOR);
92 | };
93 |
94 | export default MultiDecorator;
95 |
--------------------------------------------------------------------------------
/src/decorators/prism/theme/editor.css:
--------------------------------------------------------------------------------
1 | /* .DraftEditor-root {
2 | background: #fff;
3 | border: 1px solid #ddd;
4 | font-family: 'Georgia', serif;
5 | font-size: 14px;
6 | padding: 15px;
7 | }
8 |
9 | .DraftEditor-editor {
10 | border-top: 1px solid #ddd;
11 | cursor: text;
12 | font-size: 16px;
13 | margin-top: 10px;
14 | }
15 |
16 | .DraftEditor-editor .public-DraftEditorPlaceholder-root,
17 | .DraftEditor-editor .public-DraftEditor-content {
18 | margin: 0 -15px -15px;
19 | padding: 15px;
20 | }
21 |
22 | .DraftEditor-editor .public-DraftEditor-content {
23 | min-height: 100px;
24 | }
25 |
26 | .DraftEditor-hidePlaceholder .public-DraftEditorPlaceholder-root {
27 | display: none;
28 | }
29 |
30 | .DraftEditor-editor .DraftEditor-blockquote {
31 | border-left: 5px solid #eee;
32 | color: #666;
33 | font-family: 'Hoefler Text', 'Georgia', serif;
34 | font-style: italic;
35 | margin: 16px 0;
36 | padding: 10px 20px;
37 | } */
38 |
39 | /* .DraftEditor-root .public-DraftStyleDefault-pre {
40 | background-color: rgba(0, 0, 0, 0.05);
41 | font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace;
42 | font-size: 16px;
43 | padding: 20px;
44 | } */
45 |
46 | /* .DraftEditor-controls {
47 | font-family: 'Helvetica', sans-serif;
48 | font-size: 14px;
49 | margin-bottom: 5px;
50 | user-select: none;
51 | }
52 |
53 | .DraftEditor-styleButton {
54 | color: #999;
55 | cursor: pointer;
56 | margin-right: 16px;
57 | padding: 2px 0;
58 | display: inline-block;
59 | }
60 |
61 | .DraftEditor-activeButton {
62 | color: #5890ff;
63 | } */
64 |
--------------------------------------------------------------------------------
/src/editor/DocEditor.ts:
--------------------------------------------------------------------------------
1 | import BlockStyleFnPlugin from '../plugins/BlockStyleFnPlugin';
2 | import SelectionChangePlugin from '../plugins/SelectionChangePlugin';
3 | import CustomStyleMapPlugin from '../plugins/CustomStyleMapPlugin';
4 | import BlockRenderMapPlugin from '../plugins/block-render-map-plugin';
5 | // import StyleControlPlugin from "../plugins/StyleControlPlugin";
6 |
7 | import AddImagePlugin from '../plugins/AddImagePlugin';
8 | import DefaultHandleKeyCommandPlugin from '../plugins/DefaultHandleKeyCommandPlugin';
9 |
10 | import InlineToolbarPlugin from '../plugins/InlineToolbarPlugin';
11 | import LinkSpanDecoratorPlugin from '../plugins/LinkSpanDecoratorPlugin';
12 | import LinkDecoratorPlugin from '../plugins/LinkDecorator';
13 | import SidebarPlugin from '../plugins/sidebar-plugin';
14 |
15 | import StateFilterPlugin from '../plugins/StateFilterPlugin';
16 |
17 | import DNDPlugin from '../plugins/dnd-plugin/configNest';
18 |
19 | import FinalNewLinePlugin from '../plugins/FinalNewLinePlugin';
20 |
21 | import UpdateBlockDepthData from '../plugins/UpdateBlockDepthData';
22 |
23 | import createEditor from '../createEditor';
24 |
25 | const defaultPlugins = [
26 | // @ts-ignore
27 | new BlockStyleFnPlugin(),
28 | // @ts-ignore
29 | new SelectionChangePlugin(),
30 | // @ts-ignore
31 | new CustomStyleMapPlugin(),
32 | // @ts-ignore
33 | new BlockRenderMapPlugin(),
34 |
35 | // @ts-ignore
36 | new AddImagePlugin(),
37 | // new HandleDroppedFilesPlugin(),
38 |
39 | // 对于keyCommand的一个兜底行为
40 | // @ts-ignore
41 | new DefaultHandleKeyCommandPlugin(),
42 |
43 | // @ts-ignore
44 | // new StyleControlPlugin(),
45 |
46 | // @ts-ignore
47 | new InlineToolbarPlugin(),
48 | // @ts-ignore
49 | new LinkSpanDecoratorPlugin(),
50 | // @ts-ignore
51 | new LinkDecoratorPlugin(),
52 | // @ts-ignore
53 | new SidebarPlugin(),
54 |
55 | // @ts-ignore
56 | new StateFilterPlugin(),
57 |
58 | // @ts-ignore
59 | new DNDPlugin(),
60 |
61 | // @ts-ignore
62 | new FinalNewLinePlugin(),
63 |
64 | // @ts-ignore
65 | new UpdateBlockDepthData(),
66 | ];
67 |
68 | const DocEditor = createEditor(defaultPlugins);
69 |
70 | export default DocEditor;
71 |
--------------------------------------------------------------------------------
/src/editor/ModernEditor.ts:
--------------------------------------------------------------------------------
1 | import BlockStyleFnPlugin from '../plugins/BlockStyleFnPlugin';
2 | import SelectionChangePlugin from '../plugins/SelectionChangePlugin';
3 | import CustomStyleMapPlugin from '../plugins/CustomStyleMapPlugin';
4 | import BlockRenderMapPlugin from '../plugins/block-render-map-plugin';
5 | import HandleDroppedFilesPlugin from '../plugins/HandleDroppedFilesPlugin';
6 | import AddImagePlugin from '../plugins/AddImagePlugin';
7 | import DefaultHandleKeyCommandPlugin from '../plugins/DefaultHandleKeyCommandPlugin';
8 |
9 | import InlineToolbarPlugin from '../plugins/InlineToolbarPlugin';
10 | import LinkSpanDecoratorPlugin from '../plugins/LinkSpanDecoratorPlugin';
11 | import LinkDecoratorPlugin from '../plugins/LinkDecorator';
12 | import SidebarPlugin from '../plugins/sidebar-plugin';
13 |
14 | import StateFilterPlugin from '../plugins/StateFilterPlugin';
15 |
16 | import DNDPlugin from '../plugins/dnd-plugin/configNest';
17 |
18 | import FinalNewLinePlugin from '../plugins/FinalNewLinePlugin';
19 |
20 | import UpdateBlockDepthData from '../plugins/UpdateBlockDepthData';
21 |
22 | import createEditor from '../createEditor';
23 |
24 | const defaultPlugins = [
25 | // @ts-ignore
26 | new BlockStyleFnPlugin(),
27 | // @ts-ignore
28 | new SelectionChangePlugin(),
29 | // @ts-ignore
30 | new CustomStyleMapPlugin(),
31 | // @ts-ignore
32 | new BlockRenderMapPlugin(),
33 |
34 | // @ts-ignore
35 | new HandleDroppedFilesPlugin(),
36 | // @ts-ignore
37 | new AddImagePlugin(),
38 |
39 | // 对于keyCommand的一个兜底行为
40 | // @ts-ignore
41 | new DefaultHandleKeyCommandPlugin(),
42 |
43 | // @ts-ignore
44 | new InlineToolbarPlugin(),
45 | // @ts-ignore
46 | new LinkSpanDecoratorPlugin(),
47 | // @ts-ignore
48 | new LinkDecoratorPlugin(),
49 | // @ts-ignore
50 | new SidebarPlugin(),
51 |
52 | // @ts-ignore
53 | new StateFilterPlugin(),
54 |
55 | // @ts-ignore
56 | new DNDPlugin(),
57 |
58 | // @ts-ignore
59 | new FinalNewLinePlugin(),
60 |
61 | // @ts-ignore
62 | new UpdateBlockDepthData(),
63 | ];
64 |
65 | const ModernEditor = createEditor(defaultPlugins);
66 |
67 | export default ModernEditor;
68 |
--------------------------------------------------------------------------------
/src/helpers/clamp.ts:
--------------------------------------------------------------------------------
1 | export default (value: number, min: number, max: number) => {
2 | if (value < min) return min;
3 | if (value > max) return max;
4 | return value;
5 | };
6 |
--------------------------------------------------------------------------------
/src/hooks/styles/useFocus.css:
--------------------------------------------------------------------------------
1 | .focused_atomic_active {
2 | box-shadow: 0 0 0 3px #03a87c;
3 | }
4 |
5 | .focused_atomic:hover {
6 | box-shadow: 0 0 0 3px #34e79a;
7 | }
8 |
--------------------------------------------------------------------------------
/src/hooks/styles/useResize.css:
--------------------------------------------------------------------------------
1 | .resizable-component {
2 | position: relative;
3 | user-select: none;
4 | }
5 |
6 | .left-bar-wrapper {
7 | position: absolute;
8 | top: 0;
9 | bottom: 0;
10 | left: 0;
11 | width: 20px;
12 | cursor: col-resize;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | transition: opacity 150ms ease;
17 | opacity: 0;
18 | }
19 |
20 | .bar {
21 | width: 5px;
22 | height: 20%;
23 | max-height: 100px;
24 | background-color: gray;
25 | border-radius: 10px;
26 | }
27 |
28 | .right-bar-wrapper {
29 | position: absolute;
30 | top: 0;
31 | right: 0;
32 | bottom: 0;
33 | width: 20px;
34 | cursor: col-resize;
35 | display: flex;
36 | align-items: center;
37 | justify-content: center;
38 | transition: opacity 150ms ease;
39 | opacity: 0;
40 | }
41 |
42 | .bar-visible {
43 | opacity: 1;
44 | }
45 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ModernEditor from './editor/ModernEditor';
2 |
3 | export default ModernEditor;
4 |
--------------------------------------------------------------------------------
/src/plugins/AddImagePlugin.ts:
--------------------------------------------------------------------------------
1 | import { AtomicBlockUtils, EditorState } from 'draft-js';
2 | import Image from '../components/image';
3 | import { GetEditor } from '../types';
4 |
5 | const DecoratedImage = Image;
6 | function AddImagePlugin() {
7 | this.apply = (getEditor: GetEditor) => {
8 | const { hooks } = getEditor();
9 |
10 | hooks.addImage.tap(
11 | 'AddImagePlugin',
12 | (
13 | editorState: EditorState,
14 | file: {
15 | src: string;
16 | }
17 | ) => {
18 | const { src } = file || {};
19 | if (!src) return;
20 | const entityType = 'IMAGE';
21 | const contentState = editorState.getCurrentContent();
22 | const contentStateWithEntity = contentState.createEntity(
23 | entityType,
24 | 'IMMUTABLE',
25 | {
26 | src,
27 | alignment: 'center',
28 | resizeLayout: {
29 | width: '900px',
30 | },
31 | }
32 | );
33 | const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
34 | const newEditorState = AtomicBlockUtils.insertAtomicBlock(
35 | editorState,
36 | entityKey,
37 | ' '
38 | );
39 |
40 | hooks.setState.call(newEditorState);
41 | }
42 | );
43 |
44 | // 函数触发的时机,是否可以将alignment属性设置到props
45 | hooks.blockRendererFn.tap('AddImagePlugin', (contentBlock, editorState) => {
46 | if (contentBlock && contentBlock.getType() === 'atomic') {
47 | const contentState = editorState.getCurrentContent();
48 | const entity = contentBlock.getEntityAt(0);
49 | if (!entity) return null;
50 | const entityState = contentState.getEntity(entity);
51 | const type = entityState.getType();
52 | const data = entityState.getData();
53 | if (type === 'IMAGE') {
54 | const { alignment, resizeLayout } = data;
55 |
56 | return {
57 | component: DecoratedImage,
58 | editable: false,
59 | props: {
60 | getEditor,
61 | alignment,
62 | resizeLayout,
63 | },
64 | };
65 | }
66 | }
67 | return null;
68 | });
69 | };
70 | }
71 |
72 | export default AddImagePlugin;
73 |
--------------------------------------------------------------------------------
/src/plugins/BlockStyleFnPlugin.ts:
--------------------------------------------------------------------------------
1 | import classes from 'classnames';
2 | import { GetEditor } from '../types';
3 | import './blockStyleFnPlugin.css';
4 |
5 | function BlockStyleFnPlugin() {
6 | this.apply = (getEditor: GetEditor) => {
7 | const { hooks } = getEditor();
8 | hooks.blockStyleFn.tap('BlockStyleFnPlugin', (...props) => {
9 | const block = props[0];
10 | const cls = [];
11 | const blockData = block.getData();
12 | const depth = blockData.get('depth') || 0;
13 |
14 | const isDataWrapper = blockData.get('data-wrapper');
15 | const dataDirection = blockData.get('data-direction');
16 |
17 | if (block.getChildKeys().size && isDataWrapper) {
18 | cls.push(`data-wrapper-${dataDirection}`);
19 | }
20 |
21 | cls.push(`block-level-${depth}`);
22 |
23 | switch (block.getType()) {
24 | // 控制比如说,最后渲染出来的引用,它的class是
25 | case 'blockquote':
26 | cls.push('miuffy-blockquote');
27 | break;
28 | case 'unstyled':
29 | cls.push('miuffy-paragraph');
30 | break;
31 | case 'unordered-list-item':
32 | cls.push('miuffy-unordered-list-item');
33 | break;
34 | case 'atomic':
35 | const { editorState } = getEditor();
36 | const contentState = editorState.getCurrentContent();
37 | const entity = block.getEntityAt(0);
38 | if (!entity) return null;
39 | const entityState = contentState.getEntity(entity);
40 | const type = entityState.getType();
41 | const data = entityState.getData();
42 | if (type === 'IMAGE') {
43 | const { alignment } = data;
44 |
45 | switch (alignment) {
46 | case 'center':
47 | cls.push('figure-image-center');
48 | break;
49 | case 'right':
50 | cls.push('figure-image-right');
51 | break;
52 | case 'left':
53 | cls.push('figure-image-left');
54 | break;
55 | case 'leftFill':
56 | cls.push('figure-image-left-fill');
57 | break;
58 | case 'rightFill':
59 | cls.push('figure-image-right-fill');
60 | break;
61 | default:
62 | cls.push('figure-image');
63 | }
64 | }
65 | break;
66 | default:
67 | // ...
68 | }
69 | return classes(cls);
70 | });
71 | };
72 | }
73 |
74 | export default BlockStyleFnPlugin;
75 |
--------------------------------------------------------------------------------
/src/plugins/CustomStyleMapPlugin.ts:
--------------------------------------------------------------------------------
1 | import { GetEditor } from '../types';
2 |
3 | function CustomStyleMapPlugin() {
4 | this.apply = (getEditor: GetEditor) => {
5 | const { hooks } = getEditor();
6 | hooks.createCustomStyleMap.tap('CustomStyleMapPlugin', () => ({
7 | CODE: {
8 | backgroundColor: 'rgba(0, 0, 0, 0.05)',
9 | fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
10 | fontSize: 16,
11 | padding: 2,
12 | },
13 | // https://draftjs.org/docs/advanced-topics-inline-styles/#mapping-a-style-string-to-css
14 | 'STRIKE-THROUGH': {
15 | textDecoration: 'line-through',
16 | },
17 | }));
18 | };
19 | }
20 |
21 | export default CustomStyleMapPlugin;
22 |
--------------------------------------------------------------------------------
/src/plugins/DefaultHandleKeyCommandPlugin.ts:
--------------------------------------------------------------------------------
1 | // 解决的问题
2 | // 1. 首先判断`hasText()`;如果说开头是一个`\u200B`字符的话,很大程度就是为了inlineStyle
3 | // 2. toggleInlineStyle 和 `blockStyle`
4 | // 3. 对atomic entity怎么处理?
5 |
6 | // https://github.com/facebook/draft-js/blob/master/examples/draft-0-9-1/rich/rich.html#L52
7 | // https://github.com/facebook/draft-js/blob/master/src/model/modifier/RichTextEditorUtil.js#L54
8 | import { EditorState, DraftEditorCommand } from 'draft-js';
9 | import { GetEditor } from '../types';
10 |
11 | // @ts-ignore
12 | import decorateKeyCommandHandler from '../utils/draft-js/decorateKeyCommandHandler';
13 |
14 | function DefaultHandleKeyCommandPlugin() {
15 | this.apply = (getEditor: GetEditor) => {
16 | const { hooks } = getEditor();
17 | hooks.handleKeyCommand.tap(
18 | 'HandleBackspaceOnStartOfBlockPlugin',
19 | (command: DraftEditorCommand, editorState: EditorState) => {
20 | // https://github.com/facebook/draft-js/blob/master/examples/draft-0-10-0/playground/src/DraftJsRichEditorExample.js#L26
21 | const newState = decorateKeyCommandHandler(editorState, command);
22 | if (newState) {
23 | hooks.setState.call(newState);
24 | return 'handled';
25 | }
26 | return;
27 | }
28 | );
29 | };
30 | }
31 |
32 | export default DefaultHandleKeyCommandPlugin;
33 |
--------------------------------------------------------------------------------
/src/plugins/FinalNewLinePlugin.ts:
--------------------------------------------------------------------------------
1 | import { GetEditor, ContentBlockNode, BlockNodeMap } from '../types';
2 | import createEmptyBlockNode from '../utils/block/createEmptyBlockNode';
3 | import { EditorState } from 'draft-js';
4 |
5 | function FinalNewLinePlugin() {
6 | this.apply = (getEditor: GetEditor) => {
7 | const { hooks } = getEditor();
8 |
9 | hooks.finalNewLine.tap('finalNewLine', (editorState: EditorState) => {
10 | const currentState = editorState.getCurrentContent();
11 | const blockMap = currentState.getBlockMap() as BlockNodeMap;
12 | const last = blockMap.last();
13 |
14 | const type = last.getType();
15 | const parent = last.getParentKey();
16 |
17 | if (
18 | parent /** the last block is not the top-most component */ ||
19 | type !== 'unstyled'
20 | ) {
21 | const emptyBlock = createEmptyBlockNode();
22 | const key = emptyBlock.getKey();
23 |
24 | const lastBlockWithNullParent = blockMap
25 | .reverse()
26 | .toSeq()
27 | .skipUntil(function(block) {
28 | return !block.getParentKey();
29 | })
30 | .first();
31 |
32 | if (lastBlockWithNullParent) {
33 | const newBlock = emptyBlock.merge({
34 | prevSibling: lastBlockWithNullParent.getKey(),
35 | });
36 | let newBlockMap = blockMap.set(
37 | lastBlockWithNullParent.getKey(),
38 | lastBlockWithNullParent.merge({
39 | nextSibling: key,
40 | })
41 | );
42 |
43 | newBlockMap = newBlockMap
44 | .toSeq()
45 | .concat([[key, newBlock]])
46 | .toOrderedMap();
47 | const newContent = (currentState as any).set('blockMap', newBlockMap);
48 | return EditorState.push(editorState, newContent, 'insert-characters');
49 | }
50 | }
51 |
52 | return editorState;
53 | });
54 | };
55 | }
56 |
57 | export default FinalNewLinePlugin;
58 |
--------------------------------------------------------------------------------
/src/plugins/HandleDroppedFilesPlugin.ts:
--------------------------------------------------------------------------------
1 | import { EditorState } from 'draft-js';
2 | import { GetEditor } from '../types';
3 |
4 | const readFile = (file: File) => {
5 | const { type: fileType, lastModified, name, size, type } = file;
6 |
7 | if (!fileType.startsWith('image/')) return Promise.resolve();
8 | const reader = new FileReader();
9 |
10 | return new Promise((resolve, reject) => {
11 | reader.onload = () => {
12 | const src = reader.result;
13 | resolve({
14 | src,
15 | name,
16 | size,
17 | type,
18 | lastModified,
19 | });
20 | };
21 |
22 | reader.onerror = () => {
23 | reject();
24 | };
25 |
26 | reader.readAsDataURL(file);
27 | });
28 | };
29 |
30 | function HandleDroppedFilesPlugin() {
31 | this.apply = (getEditor: GetEditor) => {
32 | const { hooks } = getEditor();
33 | hooks.handleDroppedFiles.tap(
34 | 'HandleDroppedFilesPlugin',
35 | (editorState: EditorState, _, files: FileList) => {
36 | const jobs = [] as Promise[];
37 | files.forEach(file => {
38 | jobs.push(readFile(file));
39 | });
40 | Promise.all(jobs).then(result => {
41 | result.forEach(file => {
42 | hooks.addImage.call(editorState, file);
43 | });
44 | });
45 | }
46 | );
47 | };
48 | }
49 |
50 | export default HandleDroppedFilesPlugin;
51 |
--------------------------------------------------------------------------------
/src/plugins/LinkDecorator.ts:
--------------------------------------------------------------------------------
1 | import Link from '../components/link';
2 | import { GetEditor, DraftNodeDecoratorStrategy, DecoratorPair } from '../types';
3 |
4 | function LinkDecorator() {
5 | this.apply = (getEditor: GetEditor) => {
6 | const { hooks } = getEditor();
7 |
8 | hooks.updateDecorator.tap(
9 | 'LinkDecorator',
10 | (pairs: DecoratorPair[] = []) => {
11 | const strategy: DraftNodeDecoratorStrategy = (
12 | contentBlock,
13 | cb,
14 | contentState
15 | ) => {
16 | if (!contentState) return;
17 | contentBlock.findEntityRanges(character => {
18 | const entityKey = character.getEntity();
19 | if (!entityKey) return false;
20 | const entityType = contentState.getEntity(entityKey).getType();
21 | return entityType === 'LINK';
22 | }, cb);
23 | };
24 |
25 | return pairs.concat({
26 | strategy,
27 | component: Link,
28 | });
29 | }
30 | );
31 | };
32 | }
33 |
34 | export default LinkDecorator;
35 |
--------------------------------------------------------------------------------
/src/plugins/LinkSpanDecoratorPlugin.ts:
--------------------------------------------------------------------------------
1 | import LinkSpan from '../components/link-span';
2 | import { GetEditor, DraftNodeDecoratorStrategy, DecoratorPair } from '../types';
3 |
4 | function LinkSpanDecoratorPlugin() {
5 | this.apply = (getEditor: GetEditor) => {
6 | const { hooks } = getEditor();
7 |
8 | hooks.updateDecorator.tap(
9 | 'LinkSpanDecoratorPlugin',
10 | (pairs: DecoratorPair[] = []) => {
11 | const strategy: DraftNodeDecoratorStrategy = (
12 | contentBlock,
13 | cb,
14 | contentState
15 | ) => {
16 | if (!contentState) return;
17 | contentBlock.findEntityRanges(character => {
18 | const entityKey = character.getEntity();
19 |
20 | if (!entityKey) return false;
21 | const entityType = contentState.getEntity(entityKey).getType();
22 |
23 | return entityType === 'LINK_SPAN';
24 | }, cb);
25 | };
26 |
27 | return pairs.concat({
28 | strategy,
29 | component: LinkSpan,
30 | });
31 | }
32 | );
33 | };
34 | }
35 |
36 | export default LinkSpanDecoratorPlugin;
37 |
--------------------------------------------------------------------------------
/src/plugins/UpdateBlockDepthData.ts:
--------------------------------------------------------------------------------
1 | import { GetEditor, BlockNodeMap } from '../types';
2 | import { EditorState } from 'draft-js';
3 |
4 | /**
5 | * depth is used for `blockStyleFnPlugin`
6 | */
7 | const UpdateBlockDepthData = function() {
8 | this.apply = (getEditor: GetEditor) => {
9 | const { hooks } = getEditor();
10 | hooks.updateBlockDepthData.tap(
11 | 'updateBlockDepthData',
12 | (editorState: EditorState) => {
13 | const currentState = editorState.getCurrentContent();
14 | const blockMap = currentState.getBlockMap() as BlockNodeMap;
15 | const selectionState = editorState.getSelection();
16 | const depthMap = Object.create(null);
17 | blockMap.toSeq().forEach(block => {
18 | const key = block.getKey();
19 | const parentKey = block.getParentKey();
20 | depthMap[key] = {
21 | directParent: parentKey,
22 | parents: [],
23 | depth: 0,
24 | };
25 | });
26 |
27 | for (let key in depthMap) {
28 | const value = depthMap[key];
29 | let parent = value.directParent;
30 |
31 | while (parent) {
32 | value.parents.push(parent);
33 | parent = depthMap[parent].directParent;
34 | }
35 |
36 | value.depth = value.parents.length;
37 | }
38 |
39 | const newBlocks = blockMap
40 | .toSeq()
41 | .map(block => {
42 | const data = block.getData();
43 | const key = block.getKey();
44 | const newDepth = depthMap[key].depth;
45 | const oldDepth = data.get('depth');
46 | if (newDepth === oldDepth) return block;
47 | return block.merge({
48 | data: data.merge({ depth: depthMap[key].depth }),
49 | });
50 | })
51 | .toOrderedMap();
52 |
53 | // refer to https://github.com/facebook/draft-js/blob/master/src/model/transaction/modifyBlockForContentState.js#L37
54 | const newContent = (currentState as any).merge({
55 | blockMap: newBlocks,
56 | selectionBefore: selectionState,
57 | selectionAfter: selectionState,
58 | });
59 |
60 | // TODO: why selection
61 | // The following code will cause selection issues...
62 |
63 | // const newBlockMap = blockMap.toSeq().map(block => {
64 | // const data = block.getData();
65 | // const key = block.getKey();
66 | // const newDepth = depthMap[key].depth;
67 | // const oldDepth = data.get('depth');
68 | // if (newDepth === oldDepth) return block;
69 |
70 | // return block.merge({
71 | // data: data.merge({ depth: depthMap[key].depth }),
72 | // });
73 | // }).toOrderedMap();
74 |
75 | // // Note: set ``
76 | // const newContent = (currentState as any).set('blockMap', newBlockMap);
77 |
78 | return EditorState.push(editorState, newContent, 'change-block-data');
79 | }
80 | );
81 | };
82 | };
83 |
84 | export default UpdateBlockDepthData;
85 |
--------------------------------------------------------------------------------
/src/plugins/block-render-map-plugin/CodeBlock.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactChild, FC } from 'react';
2 | import './styles.css';
3 |
4 | const CodeWrapper: FC<{ children?: ReactChild }> = props => {
5 | const { children } = props;
6 |
7 | return {children}
;
8 | };
9 |
10 | export default CodeWrapper;
11 |
--------------------------------------------------------------------------------
/src/plugins/block-render-map-plugin/NextDiv.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactChild } from 'react';
2 | // import classes from "classnames";
3 | import './nextDiv.css';
4 | // import useFocus from '../../hooks/useFocus'
5 |
6 | const NextDiv: FC<{ children?: ReactChild }> = props => {
7 | const { children } = props;
8 |
9 | return <>{children}>;
10 | };
11 |
12 | export default NextDiv;
13 |
--------------------------------------------------------------------------------
/src/plugins/block-render-map-plugin/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DefaultDraftBlockRenderMap } from 'draft-js';
3 | import Immutable from 'immutable';
4 | import classNames from 'classnames';
5 |
6 | import CodeBlock from './CodeBlock';
7 | import NextDiv from './NextDiv';
8 | import { GetEditor } from '../../types';
9 |
10 | const UL_WRAP = ;
11 | const OL_WRAP =
;
12 |
13 | function BlockRenderMapPlugin() {
14 | this.apply = (getEditor: GetEditor) => {
15 | const { hooks } = getEditor();
16 | hooks.createBlockRenderMap.tap('BlockRenderMapPlugin', () => {
17 | const newBlockRenderMap = Immutable.Map({
18 | 'header-two': {
19 | element: 'h2',
20 | },
21 |
22 | 'unordered-list-item': {
23 | element: 'li',
24 | wrapper: UL_WRAP,
25 | },
26 |
27 | 'ordered-list-item': {
28 | element: 'li',
29 | wrapper: OL_WRAP,
30 | },
31 |
32 | 'code-block': {
33 | element: 'pre',
34 | wrapper: ,
35 | },
36 |
37 | unstyled: {
38 | element: 'div',
39 | wrapper: ,
40 | },
41 | });
42 |
43 | return DefaultDraftBlockRenderMap.merge(newBlockRenderMap);
44 | });
45 | };
46 | }
47 |
48 | export default BlockRenderMapPlugin;
49 |
--------------------------------------------------------------------------------
/src/plugins/block-render-map-plugin/nextDiv.css:
--------------------------------------------------------------------------------
1 | .next-unstyled {
2 | position: relative;
3 | }
4 |
--------------------------------------------------------------------------------
/src/plugins/block-render-map-plugin/styles.css:
--------------------------------------------------------------------------------
1 | .code-mirror {
2 | background-color: #f5f6f7;
3 | /* background-color: rgba(0, 0, 0, 0.05); */
4 | font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace;
5 | font-size: 16px;
6 | padding: 20px;
7 | }
8 |
9 | .code-mirror pre {
10 | margin: 6px 0;
11 | }
--------------------------------------------------------------------------------
/src/plugins/blockStyleFnPlugin.css:
--------------------------------------------------------------------------------
1 | .figure-image-right {
2 | float: right;
3 |
4 | /* 需要指定`position`否则figure-image无法被点中 */
5 | position: relative;
6 | clear: both;
7 | outline: 0;
8 | margin: 0;
9 | }
10 |
11 | .figure-image-right-fill {
12 | float: right;
13 | margin-right: -150px;
14 |
15 | /* 需要指定`position`否则figure-image无法被点中 */
16 | position: relative;
17 | clear: both;
18 | outline: 0;
19 | z-index: 1;
20 | margin: 0;
21 | }
22 |
23 | .figure-image-left {
24 | float: left;
25 |
26 | /* 需要指定`position`否则figure-image无法被点中 */
27 | position: relative;
28 | clear: both;
29 | outline: 0;
30 | z-index: 1;
31 | margin: 0;
32 | }
33 |
34 | .figure-image-left-fill {
35 | float: left;
36 | margin-left: -150px;
37 |
38 | /* 需要指定`position`否则figure-image无法被点中 */
39 | position: relative;
40 | clear: both;
41 | outline: 0;
42 | z-index: 1;
43 | margin: 0;
44 | }
45 |
46 | .figure-image-center {
47 | /* 需要指定`position`否则figure-image无法被点中 */
48 | position: relative;
49 | clear: both;
50 | outline: 0;
51 | margin-left: auto;
52 | margin-right: auto;
53 | box-sizing: border-box;
54 | }
55 |
56 | /* .display-flex >div {
57 | display: flex;
58 | flex-direction: row;
59 | }
60 |
61 | .display-flex >div >div[data-block="true"]{
62 | width: 50%;
63 | } */
64 |
65 | .data-wrapper-column > div {
66 | display: flex;
67 | flex-direction: column;
68 | }
69 |
70 | .data-wrapper-row > div {
71 | display: flex;
72 | flex-direction: row;
73 | }
74 |
--------------------------------------------------------------------------------
/src/plugins/dnd/Container.ts:
--------------------------------------------------------------------------------
1 | import { containerKeyExtractor } from './key';
2 | import SortedItems from './structure/SortedItems';
3 | import { orientationToAxis, axisMeasure } from './utils';
4 | import Dragger from './Dragger';
5 | import {
6 | ResultDNDConfig,
7 | ResultConfig,
8 | Containers,
9 | ContainerDimension,
10 | } from '../../types';
11 |
12 | class Container {
13 | public id: string;
14 | public el: HTMLElement;
15 | public containers: Containers;
16 | public children: SortedItems;
17 | public dndConfig: ResultDNDConfig;
18 | public containerConfig: ResultConfig;
19 | public dimension: ContainerDimension;
20 | public parentContainer: Container | null;
21 |
22 | constructor({
23 | el,
24 | containers,
25 | dndConfig,
26 | containerConfig,
27 | }: {
28 | el: HTMLElement;
29 | containers: Containers;
30 | dndConfig: ResultDNDConfig;
31 | containerConfig: ResultConfig;
32 | }) {
33 | this.el = el;
34 | this.id = containerKeyExtractor();
35 | this.containers = containers;
36 | this.children = new SortedItems({
37 | sorter: this.sorter.bind(this),
38 | });
39 | this.dndConfig = dndConfig;
40 | this.containerConfig = containerConfig;
41 |
42 | this.containers[this.id] = this;
43 | this.dimension = {} as any;
44 | this.parentContainer = null;
45 | }
46 |
47 | sorter(a: Dragger, b: Dragger): number {
48 | const { orientation } = this.containerConfig;
49 | const axis = orientationToAxis[orientation];
50 | const [minProperty] = axisMeasure[axis];
51 | const aValue = a.dimension![minProperty] || 0;
52 | const bValue = b.dimension![minProperty] || 0;
53 | return aValue - bValue;
54 | }
55 |
56 | // used for transition
57 | addDirectChild(dragger: Dragger) {
58 | this.children.add(dragger);
59 | dragger.container = this;
60 | dragger._teardown = () => {
61 | const index = this.children.findIndex(dragger);
62 | if (index !== -1) this.children.splice(index, 1);
63 | };
64 | }
65 |
66 | cleanup() {
67 | delete this.containers[this.id];
68 | }
69 | }
70 |
71 | export default Container;
72 |
--------------------------------------------------------------------------------
/src/plugins/dnd/Dragger.ts:
--------------------------------------------------------------------------------
1 | import { draggerKeyExtractor } from './key';
2 | import Container from './Container';
3 | import { DraggerDimension } from '../../types';
4 |
5 | class Dragger {
6 | public id: string;
7 | public container: Container;
8 | public el: HTMLElement;
9 | public _teardown: { (): void } | null;
10 | public dimension: DraggerDimension;
11 |
12 | constructor({ el, container }: { el: HTMLElement; container: Container }) {
13 | this.container = container;
14 | this.el = el;
15 | this._teardown = null;
16 | this.id = draggerKeyExtractor();
17 | this.dimension = {} as DraggerDimension;
18 | }
19 |
20 | teardown() {
21 | if (!this.container) return;
22 | if (typeof this._teardown === 'function') this._teardown();
23 | }
24 | }
25 |
26 | export default Dragger;
27 |
--------------------------------------------------------------------------------
/src/plugins/dnd/closest.ts:
--------------------------------------------------------------------------------
1 | import { find } from './find';
2 |
3 | const supportedMatchesName = (() => {
4 | const base = 'matches';
5 |
6 | // Server side rendering
7 | if (typeof document === 'undefined') {
8 | return base;
9 | }
10 |
11 | // See https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
12 | const candidates = [base, 'msMatchesSelector', 'webkitMatchesSelector'];
13 |
14 | const value = find(candidates, name => name in Element.prototype);
15 |
16 | return value || base;
17 | })();
18 |
19 | function closestPonyfill(
20 | el: HTMLElement | null,
21 | selector: string
22 | ): HTMLElement | null {
23 | if (el == null) {
24 | return null;
25 | }
26 |
27 | // Element.prototype.matches is supported in ie11 with a different name
28 | // https://caniuse.com/#feat=matchesselector
29 | // $FlowFixMe - dynamic property
30 | if ((el as any)[supportedMatchesName](selector)) {
31 | return el;
32 | }
33 |
34 | // recursively look up the tree
35 | return closestPonyfill(el.parentElement, selector);
36 | }
37 |
38 | // current element will be excluded from search result
39 | export const exclusiveClosest = (
40 | el: HTMLElement,
41 | selector: string
42 | ): HTMLElement | null => {
43 | const parent = el.parentNode as HTMLElement;
44 | if (parent) return closest(parent, selector);
45 | return null;
46 | };
47 |
48 | export default function closest(
49 | el: HTMLElement | null,
50 | selector: string
51 | ): HTMLElement | null {
52 | if (!el) return null;
53 | // Using native closest for maximum speed where we can
54 | if (el.closest) {
55 | return el.closest(selector);
56 | }
57 | // ie11: damn you!
58 | return closestPonyfill(el, selector);
59 | }
60 |
--------------------------------------------------------------------------------
/src/plugins/dnd/configs/resolveConfig.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Config,
3 | DNDConfig,
4 | ResultConfig,
5 | Orientation,
6 | DefaultConfig,
7 | } from '../../../types';
8 |
9 | const defaultConfig: DefaultConfig = {
10 | orientation: Orientation.Vertical,
11 | };
12 |
13 | const reservedKeys = [
14 | 'orientation',
15 | 'draggerHandlerSelector',
16 | 'containerSelector',
17 | 'draggerSelector',
18 | 'shouldAcceptDragger',
19 | 'containerEffect',
20 | 'draggerEffect',
21 | 'impactDraggerEffect',
22 | 'impactContainerEffect',
23 | ];
24 |
25 | export default (configs: Config[], props: DNDConfig): ResultConfig[] => {
26 | return configs.map(config => {
27 | const next = {
28 | ...props,
29 | ...defaultConfig,
30 | ...config,
31 | } as ResultConfig;
32 | const o = {} as ResultConfig;
33 |
34 | for (const key in next) {
35 | if (reservedKeys.indexOf(key) !== -1) {
36 | o[key] = next[key];
37 | }
38 | }
39 |
40 | return o;
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/src/plugins/dnd/configs/resolveDndConfig.ts:
--------------------------------------------------------------------------------
1 | import { Mode, DNDConfig, ResultDNDConfig } from '../../../types';
2 |
3 | const defaultDndConfig = {
4 | mode: Mode.Fluid,
5 | collisionPadding: 10,
6 | withPlaceholder: true,
7 | isNested: true,
8 | onDrop: () => {},
9 | };
10 |
11 | const reservedKeys = [
12 | 'mode',
13 | 'collisionPadding',
14 | 'withPlaceholder',
15 | 'isNested',
16 | 'onDrop',
17 | ];
18 |
19 | export default (props: DNDConfig): ResultDNDConfig => {
20 | const next: ResultDNDConfig = {
21 | ...defaultDndConfig,
22 | ...props,
23 | };
24 | const o = {} as ResultDNDConfig;
25 |
26 | for (const key in next) {
27 | if (reservedKeys.indexOf(key) !== -1) {
28 | o[key] = next[key];
29 | }
30 | }
31 |
32 | return o;
33 | };
34 |
--------------------------------------------------------------------------------
/src/plugins/dnd/dom.ts:
--------------------------------------------------------------------------------
1 | import { intersect, within } from './collision';
2 | import { ResultConfig, Point } from '../../types';
3 |
4 | // https://stackoverflow.com/questions/384286/how-do-you-check-if-a-javascript-object-is-a-dom-object
5 | export const isElement = (el: any): boolean =>
6 | el instanceof Element || el instanceof HTMLDocument;
7 |
8 | export const matchesDragger = (el: any, configs: ResultConfig[]) => {
9 | if (!isElement(el)) return -1;
10 |
11 | const len = configs.length;
12 | for (let i = 0; i < len; i++) {
13 | const config = configs[i];
14 | const { draggerSelector } = config;
15 |
16 | if (el.matches(draggerSelector)) {
17 | return config;
18 | }
19 | }
20 |
21 | return -1;
22 | };
23 |
24 | export const matchesContainer = (el: any, configs: ResultConfig[]) => {
25 | if (!isElement(el)) return -1;
26 |
27 | const len = configs.length;
28 | for (let i = 0; i < len; i++) {
29 | const config = configs[i];
30 | const { containerSelector } = config;
31 | if (el.matches(containerSelector)) {
32 | return config;
33 | }
34 | }
35 |
36 | return -1;
37 | };
38 |
39 | export const getViewport = () => {
40 | const { innerHeight, innerWidth } = window;
41 | const { clientHeight, clientWidth } = document.documentElement;
42 |
43 | return {
44 | top: 0,
45 | right: innerWidth || clientWidth,
46 | bottom: innerHeight || clientHeight,
47 | left: 0,
48 | };
49 | };
50 |
51 | // https://stackoverflow.com/a/7557433/2006805
52 | export const isElementInViewport = (el: HTMLElement) => {
53 | const rect = el.getBoundingClientRect();
54 | const viewport = getViewport();
55 |
56 | return (
57 | rect.top >= viewport.top &&
58 | rect.left >= viewport.left &&
59 | rect.bottom <= viewport.bottom &&
60 | rect.right <= viewport.right
61 | );
62 | };
63 |
64 | export const isElementVisibleInViewport = (el: HTMLElement) => {
65 | const viewport = getViewport();
66 | const rect = el.getBoundingClientRect();
67 | return intersect(viewport, rect);
68 | };
69 |
70 | // Has rect cache.. If container's rect is not update.
71 | // It is recommended to use this method
72 | export const withinElement = (el: HTMLElement) => {
73 | const rect = el.getBoundingClientRect();
74 | return (point: Point) => within(rect, point);
75 | };
76 |
--------------------------------------------------------------------------------
/src/plugins/dnd/key.ts:
--------------------------------------------------------------------------------
1 | let containerIndex = 0;
2 | let draggerIndex = 0;
3 |
4 | export const containerKeyExtractor = () => `ctx_${containerIndex++}`;
5 | export const draggerKeyExtractor = () => `dragger_${draggerIndex++}`;
6 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/addIntermediateCtxValue.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'sabar';
2 | import { OnMoveHandleContext, OnMoveOperation } from '../../../../types';
3 |
4 | const addIntermediateCtxValue = (ctx: object, actions: Action) => {
5 | const context = ctx as OnMoveHandleContext;
6 | context.action = {
7 | operation: OnMoveOperation.OnStart,
8 | isHomeContainerFocused: false,
9 | effectsManager: null,
10 | };
11 |
12 | actions.next();
13 | };
14 |
15 | export default addIntermediateCtxValue;
16 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/DndEffects.ts:
--------------------------------------------------------------------------------
1 | import EffectsManager from './EffectsManager';
2 |
3 | export default class DndEffects {
4 | private children: {
5 | [key: string]: EffectsManager;
6 | };
7 |
8 | constructor() {
9 | this.children = {};
10 | }
11 |
12 | find(id: string) {
13 | return this.children[id];
14 | }
15 |
16 | add(effectsManager: EffectsManager) {
17 | const { id } = effectsManager;
18 | this.children[id] = effectsManager;
19 | return () => {
20 | delete this.children[id];
21 | };
22 | }
23 |
24 | remove(effectsManager: EffectsManager) {
25 | const { id } = effectsManager;
26 | if (this.children[id]) {
27 | this.children[id].teardown();
28 | delete this.children[id];
29 | }
30 | }
31 |
32 | teardown() {
33 | for (const id in this.children) {
34 | const child = this.children[id];
35 | child.teardown();
36 | }
37 | }
38 |
39 | partialTeardown() {
40 | for (const id in this.children) {
41 | const child = this.children[id];
42 | if (!child.isHomeContainerEffects()) child.teardown();
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/EffectsManager.ts:
--------------------------------------------------------------------------------
1 | import { isFunction } from '../../../utils';
2 | import Dragger from '../../../Dragger';
3 | import Container from '../../../Container';
4 | import {
5 | ImpactDraggerEffect,
6 | DraggerEffect,
7 | ContainerEffect,
8 | } from '../../../../../types';
9 |
10 | export default class EffectsManager {
11 | private dragger: Dragger;
12 | private impactContainer: Container;
13 | public id: string;
14 | public impactDraggerEffects: ImpactDraggerEffect[];
15 | public impactContainerEffects: ContainerEffect[];
16 | public upstreamDraggersEffects: DraggerEffect[];
17 | public downstreamDraggersEffects: DraggerEffect[];
18 |
19 | constructor({
20 | dragger,
21 | impactContainer,
22 | }: {
23 | dragger: Dragger;
24 | impactContainer: Container;
25 | }) {
26 | this.dragger = dragger;
27 | this.impactContainer = impactContainer;
28 | this.id = impactContainer.id;
29 |
30 | this.impactDraggerEffects = [];
31 | this.impactContainerEffects = [];
32 | this.upstreamDraggersEffects = [];
33 | this.downstreamDraggersEffects = [];
34 | }
35 |
36 | isHomeContainerEffects() {
37 | const { container } = this.dragger;
38 | return container.id === this.impactContainer.id;
39 | }
40 |
41 | assertRun(fn: any) {
42 | if (isFunction(fn)) fn();
43 | }
44 |
45 | clearImpactContainerEffects() {
46 | this.impactContainerEffects.forEach(({ teardown }) =>
47 | this.assertRun(teardown)
48 | );
49 | this.impactContainerEffects = [];
50 | }
51 |
52 | clearImpactDraggerEffects() {
53 | this.impactDraggerEffects.forEach(({ teardown }) =>
54 | this.assertRun(teardown)
55 | );
56 | this.impactDraggerEffects = [];
57 | }
58 |
59 | clearDownstreamEffects() {
60 | this.downstreamDraggersEffects.forEach(({ teardown }) =>
61 | this.assertRun(teardown)
62 | );
63 | this.downstreamDraggersEffects = [];
64 | }
65 |
66 | clearUpstreamEffects() {
67 | this.upstreamDraggersEffects.forEach(({ teardown }) =>
68 | this.assertRun(teardown)
69 | );
70 | this.upstreamDraggersEffects = [];
71 | }
72 |
73 | teardown() {
74 | this.clearImpactContainerEffects();
75 | this.clearImpactDraggerEffects();
76 | this.clearUpstreamEffects();
77 | this.clearDownstreamEffects();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleEnterContainer.ts:
--------------------------------------------------------------------------------
1 | import EffectsManager from './EffectsManager';
2 | import report from '../../../reporter';
3 | import Container from '../../../Container';
4 | import {
5 | OnMoveHandleContext,
6 | OnMoveArgs,
7 | OnMoveOperation,
8 | } from '../../../../../types';
9 | import { Action } from 'sabar';
10 |
11 | const handleEnterContainer = (args: any, ctx: object, actions: Action) => {
12 | const { lifeUpDragger, isHomeContainer, prevImpact } = args as OnMoveArgs;
13 | const context = ctx as OnMoveHandleContext;
14 | const { impactRawInfo, dndEffects } = context;
15 |
16 | const prevImpactVContainer = prevImpact.impactVContainer;
17 | const currentImpactVContainer = impactRawInfo.impactVContainer;
18 |
19 | console.log('prev ', prevImpactVContainer, currentImpactVContainer);
20 |
21 | if (
22 | (!prevImpactVContainer && currentImpactVContainer) ||
23 | (prevImpactVContainer &&
24 | currentImpactVContainer &&
25 | prevImpactVContainer.id !== currentImpactVContainer.id)
26 | ) {
27 | let effectsManager = dndEffects.find(currentImpactVContainer.id);
28 | const { impactVContainer } = impactRawInfo;
29 |
30 | if (!effectsManager) {
31 | effectsManager = new EffectsManager({
32 | dragger: lifeUpDragger,
33 | impactContainer: impactVContainer as Container,
34 | });
35 |
36 | dndEffects.add(effectsManager);
37 | }
38 |
39 | report.logEnterContainer(currentImpactVContainer);
40 |
41 | context.action = {
42 | operation: OnMoveOperation.OnEnter,
43 | isHomeContainerFocused: isHomeContainer(currentImpactVContainer),
44 | effectsManager,
45 | };
46 | context.impact = {
47 | impactVContainer: currentImpactVContainer,
48 | index: null,
49 | };
50 | }
51 |
52 | actions.next();
53 | };
54 |
55 | export default handleEnterContainer;
56 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleEnterOtherContainer.ts:
--------------------------------------------------------------------------------
1 | import { orientationToMeasure } from '../../../utils';
2 | import { Action } from 'sabar';
3 | import { OnMoveHandleContext } from 'types';
4 | import Container from '../../../Container';
5 |
6 | const handleEnterOtherContainer = (ctx: object, actions: Action) => {
7 | const context = ctx as OnMoveHandleContext;
8 | const {
9 | impactRawInfo,
10 | action: { operation, isHomeContainerFocused, effectsManager },
11 | } = context;
12 |
13 | if (operation !== 'onEnter' || isHomeContainerFocused) {
14 | actions.next();
15 | return;
16 | }
17 |
18 | const {
19 | impactVContainer,
20 | impactPosition,
21 | candidateVDraggerIndex,
22 | } = impactRawInfo;
23 |
24 | const {
25 | containerConfig: { containerEffect, draggerEffect, orientation },
26 | children,
27 | } = impactVContainer as Container;
28 |
29 | const measure = orientationToMeasure(orientation);
30 | const positionIndex = measure.indexOf(impactPosition as string);
31 |
32 | if (typeof containerEffect === 'function') {
33 | // const teardown = containerEffect({
34 | // el: impactVContainer!.el,
35 | // });
36 | // effectsManager!.impactContainerEffects.push({
37 | // teardown,
38 | // vContainer: impactVContainer as Container,
39 | // });
40 | }
41 |
42 | if (typeof draggerEffect !== 'function') {
43 | actions.next();
44 | return;
45 | }
46 |
47 | let initialValue = candidateVDraggerIndex || 0;
48 |
49 | if (positionIndex === 1) {
50 | initialValue += 1;
51 | }
52 |
53 | const impact = {
54 | index: initialValue,
55 | impactVContainer: impactVContainer as Container,
56 | };
57 |
58 | const len = children.getSize();
59 |
60 | for (let i = initialValue; i < len; i++) {
61 | const vDragger = children.getItem(i);
62 | const isHighlight = i === initialValue;
63 | const teardown = draggerEffect({
64 | placedPosition: (isHighlight ? impactPosition : measure[0]) as any,
65 | shouldMove: !isHighlight || !positionIndex,
66 | downstream: !isHighlight || !positionIndex,
67 | el: vDragger.el,
68 | dimension: vDragger.dimension.rect,
69 | isHighlight,
70 | });
71 | effectsManager!.downstreamDraggersEffects.push({ teardown, vDragger });
72 | }
73 |
74 | context.impact = impact;
75 |
76 | actions.next();
77 | };
78 |
79 | export default handleEnterOtherContainer;
80 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleImpactContainerEffect.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'sabar';
2 | import { OnMoveHandleContext, OnMoveArgs } from 'types';
3 | import { generateContainerEffectKey } from './utils';
4 |
5 | const handleImpactContainerEffect = (
6 | args: any,
7 | ctx: object,
8 | actions: Action
9 | ) => {
10 | const { prevImpact } = args as OnMoveArgs;
11 | const context = ctx as OnMoveHandleContext;
12 | const {
13 | dndEffects,
14 | impactRawInfo,
15 | action: { operation, isHomeContainerFocused, effectsManager },
16 | } = context;
17 |
18 | if (operation === 'onLeave') {
19 | const { impactVContainer } = prevImpact;
20 | const effectsManager = dndEffects.find(impactVContainer!.id);
21 | effectsManager.clearImpactContainerEffects();
22 | }
23 |
24 | const { impactVContainer } = impactRawInfo;
25 |
26 | if (
27 | operation === 'onEnter' &&
28 | !isHomeContainerFocused &&
29 | effectsManager &&
30 | impactVContainer
31 | ) {
32 | const {
33 | containerConfig: { impactContainerEffect },
34 | } = impactVContainer;
35 | if (typeof impactContainerEffect === 'function') {
36 | const effectKey = generateContainerEffectKey(impactVContainer!, 'active');
37 | const index = effectsManager.impactContainerEffects.findIndex(
38 | ({ key }) => key === effectKey
39 | );
40 |
41 | if (index === -1) {
42 | const teardown = impactContainerEffect({
43 | container: impactVContainer.el,
44 | isHighlight: true,
45 | });
46 |
47 | effectsManager.impactContainerEffects.push({
48 | teardown,
49 | vContainer: impactVContainer,
50 | key: effectKey,
51 | });
52 | }
53 | }
54 | }
55 |
56 | actions.next();
57 | };
58 |
59 | export default handleImpactContainerEffect;
60 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleImpactDraggerEffect.ts:
--------------------------------------------------------------------------------
1 | import { orientationToMeasure } from '../../../utils';
2 | import { generateDraggerEffectKey } from './utils';
3 | import Dragger from '../../../Dragger';
4 | import { Action } from 'sabar';
5 | import { OnMoveHandleContext, OnMoveArgs } from 'types';
6 |
7 | const handleImpactDraggerEffect = (args: any, ctx: object, actions: Action) => {
8 | const { liftUpVDragger } = args as OnMoveArgs;
9 | const context = ctx as OnMoveHandleContext;
10 | const {
11 | impactRawInfo,
12 | dndEffects,
13 | dndConfig: { withPlaceholder },
14 | } = context;
15 |
16 | const { impactVContainer, impactPosition, candidateVDragger } = impactRawInfo;
17 |
18 | if (withPlaceholder || !impactVContainer) {
19 | actions.next();
20 | return;
21 | }
22 |
23 | const {
24 | containerConfig: { orientation, impactDraggerEffect },
25 | } = impactVContainer;
26 |
27 | const effectsManager = dndEffects.find(impactVContainer.id);
28 |
29 | const measure = orientationToMeasure(orientation);
30 | const positionIndex = measure.indexOf(impactPosition as string);
31 |
32 | if (typeof impactDraggerEffect === 'function') {
33 | const effectKey = generateDraggerEffectKey(
34 | impactVContainer,
35 | candidateVDragger as Dragger,
36 | impactPosition as any
37 | );
38 | const index = effectsManager.impactDraggerEffects.findIndex(
39 | ({ key }) => key === effectKey
40 | );
41 |
42 | if (index === -1) {
43 | effectsManager.clearImpactDraggerEffects();
44 |
45 | const teardown = impactDraggerEffect({
46 | dragger: liftUpVDragger.el,
47 | container: impactVContainer.el,
48 | candidateDragger: (candidateVDragger as Dragger).el,
49 | shouldMove: !positionIndex,
50 | downstream: !positionIndex,
51 | placedPosition: impactPosition as any,
52 | dimension: (candidateVDragger as Dragger).dimension.rect,
53 | isHighlight: true,
54 | });
55 |
56 | effectsManager.impactDraggerEffects.push({
57 | teardown,
58 | vDragger: candidateVDragger as Dragger,
59 | key: effectKey,
60 | });
61 | }
62 |
63 | context.output = {
64 | dragger: liftUpVDragger.el,
65 | candidateDragger: (candidateVDragger as Dragger).el,
66 | container: impactVContainer.el,
67 | placedPosition: impactPosition as any,
68 | };
69 | }
70 |
71 | actions.next();
72 | };
73 |
74 | export default handleImpactDraggerEffect;
75 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleLeaveContainer.ts:
--------------------------------------------------------------------------------
1 | import report from '../../../reporter';
2 | import {
3 | OnMoveHandleContext,
4 | OnMoveArgs,
5 | OnMoveOperation,
6 | } from '../../../../../types';
7 | import { Action } from 'sabar';
8 |
9 | const handleLeaveContainer = (args: any, ctx: object, actions: Action) => {
10 | const { isHomeContainer, prevImpact } = args as OnMoveArgs;
11 | const context = ctx as OnMoveHandleContext;
12 | const { impactRawInfo, dndEffects } = context;
13 |
14 | const prevImpactVContainer = prevImpact.impactVContainer;
15 | const currentImpactVContainer = impactRawInfo.impactVContainer;
16 |
17 | if (!prevImpactVContainer && currentImpactVContainer) {
18 | actions.next();
19 | return;
20 | }
21 |
22 | if (
23 | (prevImpactVContainer && !currentImpactVContainer) ||
24 | (prevImpactVContainer &&
25 | currentImpactVContainer &&
26 | prevImpactVContainer.id !== currentImpactVContainer.id)
27 | ) {
28 | const effectsManager = dndEffects.find(prevImpactVContainer.id);
29 | report.logLeaveContainer(prevImpactVContainer);
30 |
31 | context.action = {
32 | operation: OnMoveOperation.OnLeave,
33 | isHomeContainerFocused: isHomeContainer(prevImpactVContainer),
34 | effectsManager,
35 | };
36 | }
37 |
38 | actions.next();
39 | };
40 |
41 | export default handleLeaveContainer;
42 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleLeaveHomeContainer.ts:
--------------------------------------------------------------------------------
1 | import { orientationToMeasure } from '../../../utils';
2 | import { OnMoveArgs, OnMoveHandleContext } from '../../../../../types';
3 | import { Action } from 'sabar';
4 |
5 | const handleLeaveHomeContainer = (args: any, ctx: object, actions: Action) => {
6 | const { prevImpact, liftUpVDraggerIndex } = args as OnMoveArgs;
7 | const context = ctx as OnMoveHandleContext;
8 | const {
9 | dndConfig: { withPlaceholder },
10 | action: { operation, isHomeContainerFocused, effectsManager },
11 | } = context;
12 |
13 | if (operation !== 'onLeave' || !isHomeContainerFocused) {
14 | actions.next();
15 | return;
16 | }
17 |
18 | if (!withPlaceholder) {
19 | effectsManager!.teardown();
20 | actions.next();
21 | return;
22 | }
23 |
24 | const { index, impactVContainer } = prevImpact;
25 |
26 | if (!index || !impactVContainer) {
27 | actions.next();
28 | }
29 |
30 | const {
31 | children,
32 | containerConfig: { orientation, draggerEffect },
33 | } = impactVContainer!;
34 | // Don't care prev index, reset all the effects on the items before
35 | // `liftUpVDraggerIndex`
36 | effectsManager!.clearDownstreamEffects();
37 | if (typeof draggerEffect === 'function') {
38 | const upstreamStartIndex = Math.max(liftUpVDraggerIndex + 1, index!);
39 | const len = children.getSize();
40 |
41 | for (let i = upstreamStartIndex; i < len; i++) {
42 | const vDragger = children.getItem(i);
43 | const measure = orientationToMeasure(orientation);
44 | const teardown = draggerEffect({
45 | el: vDragger.el,
46 | shouldMove: true,
47 | placedPosition: measure[0],
48 | downstream: false,
49 | dimension: vDragger.dimension.rect,
50 | isHighlight: false,
51 | });
52 |
53 | effectsManager!.upstreamDraggersEffects.push({
54 | teardown,
55 | vDragger,
56 | });
57 | }
58 | }
59 |
60 | actions.next();
61 | };
62 |
63 | export default handleLeaveHomeContainer;
64 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleLeaveOtherContainer.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'sabar';
2 | import { OnMoveHandleContext } from 'types';
3 |
4 | const handleLeaveOtherContainer = (ctx: object, actions: Action) => {
5 | const context = ctx as OnMoveHandleContext;
6 | const {
7 | action: { operation, isHomeContainerFocused, effectsManager },
8 | } = context;
9 | if (operation !== 'onLeave' || isHomeContainerFocused) {
10 | actions.next();
11 | return;
12 | }
13 |
14 | effectsManager!.teardown();
15 |
16 | actions.next();
17 | };
18 |
19 | export default handleLeaveOtherContainer;
20 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleReorder.ts:
--------------------------------------------------------------------------------
1 | import {
2 | OnMoveHandleContext,
3 | OnMoveArgs,
4 | OnMoveOperation,
5 | } from '../../../../../types';
6 | import { Action } from 'sabar';
7 |
8 | const handleReorder = (args: any, ctx: object, actions: Action) => {
9 | const { prevImpact, isHomeContainer } = args as OnMoveArgs;
10 | const context = ctx as OnMoveHandleContext;
11 | const {
12 | impactRawInfo: { impactVContainer: currentImpactVContainer },
13 | dndEffects,
14 | } = context;
15 | const { impactVContainer: prevImpactVContainer } = prevImpact;
16 |
17 | if (
18 | prevImpactVContainer &&
19 | currentImpactVContainer &&
20 | prevImpactVContainer.id === currentImpactVContainer.id
21 | ) {
22 | const effectsManager = dndEffects.find(currentImpactVContainer.id);
23 |
24 | context.action = {
25 | operation: OnMoveOperation.ReOrder,
26 | isHomeContainerFocused: isHomeContainer(currentImpactVContainer),
27 | effectsManager,
28 | };
29 |
30 | actions.next();
31 | return;
32 | }
33 |
34 | actions.next();
35 | };
36 |
37 | export default handleReorder;
38 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/handleReorderOnOtherContainer.ts:
--------------------------------------------------------------------------------
1 | import { orientationToMeasure } from '../../../utils';
2 | import { Action } from 'sabar';
3 | import {
4 | OnMoveHandleContext,
5 | Impact,
6 | OnMoveOperation,
7 | } from '../../../../../types';
8 | import Container from '../../../Container';
9 | import Dragger from '../../../Dragger';
10 |
11 | const handleReorderOnHomeContainer = (ctx: object, actions: Action) => {
12 | const context = ctx as OnMoveHandleContext;
13 | const {
14 | action: { operation, isHomeContainerFocused, effectsManager },
15 | } = context;
16 |
17 | if (operation !== OnMoveOperation.ReOrder || isHomeContainerFocused) {
18 | actions.next();
19 | return;
20 | }
21 |
22 | const {
23 | impactRawInfo: {
24 | candidateVDragger,
25 | impactVContainer,
26 | impactPosition,
27 | candidateVDraggerIndex,
28 | },
29 | } = context;
30 | const {
31 | containerConfig: { orientation, draggerEffect },
32 | } = impactVContainer as Container;
33 | const currentIndex = context.impact.index || 0;
34 |
35 | const measure = orientationToMeasure(orientation);
36 |
37 | if (typeof draggerEffect !== 'function') {
38 | actions.next();
39 | return;
40 | }
41 |
42 | const impact = {
43 | impactVContainer,
44 | index: candidateVDraggerIndex,
45 | };
46 |
47 | // move down
48 | if (currentIndex < (candidateVDraggerIndex as number)) {
49 | if (impactPosition === measure[0]) {
50 | actions.next();
51 | return;
52 | }
53 |
54 | const index = effectsManager!.downstreamDraggersEffects.findIndex(
55 | ({ vDragger }) => {
56 | return vDragger.id === (candidateVDragger as Dragger).id;
57 | }
58 | );
59 |
60 | if (index !== -1) {
61 | const { teardown } = effectsManager!.downstreamDraggersEffects[index];
62 | effectsManager!.downstreamDraggersEffects.splice(index, 1);
63 | if (typeof teardown === 'function') teardown();
64 | }
65 | }
66 |
67 | // move up
68 | if (currentIndex > (candidateVDraggerIndex as number)) {
69 | if (impactPosition === measure[1]) {
70 | actions.next();
71 | return;
72 | }
73 |
74 | const teardown = draggerEffect({
75 | el: (candidateVDragger as Dragger).el,
76 | shouldMove: true,
77 | placedPosition: measure[0],
78 | downstream: true,
79 | dimension: (candidateVDragger as Dragger).dimension.rect,
80 | isHighlight: true,
81 | });
82 |
83 | effectsManager!.downstreamDraggersEffects.push({
84 | vDragger: candidateVDragger as Dragger,
85 | teardown,
86 | });
87 | }
88 |
89 | context.impact = impact as Impact;
90 |
91 | actions.next();
92 | };
93 |
94 | export default handleReorderOnHomeContainer;
95 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/effects/utils.ts:
--------------------------------------------------------------------------------
1 | import Container from '../../../Container';
2 | import Dragger from '../../../Dragger';
3 | import { Position } from '../../../../../types';
4 |
5 | export const generateDraggerEffectKey = (
6 | vContainer: Container,
7 | impactVDragger: Dragger,
8 | placedPosition: Position
9 | ) => {
10 | return `${vContainer.id}_${impactVDragger.id}_${placedPosition}`;
11 | };
12 |
13 | export const generateContainerEffectKey = (
14 | vContainer: Container,
15 | status: string
16 | ) => {
17 | return `${vContainer.id}_${status}`;
18 | };
19 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/removeIntermediateCtxValue.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'sabar';
2 | import { OnMoveHandleContext } from '../../../../types';
3 |
4 | const removeIntermediateCtxValue = (ctx: object, actions: Action) => {
5 | const context = ctx as OnMoveHandleContext;
6 | delete context.action;
7 | actions.next();
8 | };
9 |
10 | export default removeIntermediateCtxValue;
11 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onMove/syncCopyPosition.ts:
--------------------------------------------------------------------------------
1 | import { OnMoveArgs } from '../../../../types';
2 | import { Action } from 'sabar';
3 |
4 | export default (args: any, _: object, actions: Action) => {
5 | const { impactPoint, clone } = args as OnMoveArgs;
6 | const [clientX, clientY] = impactPoint;
7 | clone.style.position = 'fixed';
8 | clone.style.top = `${clientY}px`;
9 | clone.style.left = `${clientX}px`;
10 |
11 | actions.next();
12 | };
13 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onStart/attemptToCreateClone.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Clone node should be created if there is no copy when moving
3 | */
4 |
5 | import { setCloneAttributes } from '../../setAttributes';
6 | import Dragger from '../../Dragger';
7 | import { Action } from 'sabar';
8 | import { OnStartHandlerContext } from '../../../../types';
9 |
10 | // https://stackoverflow.com/questions/1848445/duplicating-an-element-and-its-style-with-javascript
11 | // cloneNode will not preserve node style. It requires to set clone element with fixed style
12 | export default (args: any, ctx: object, actions: Action) => {
13 | const { dragger }: { dragger: Dragger } = args;
14 | const context = ctx as OnStartHandlerContext;
15 | const { el } = dragger;
16 | context.extra.clone = el.cloneNode(true) as HTMLElement;
17 | const rect = el.getBoundingClientRect();
18 | const { width, height } = rect;
19 | context.extra.clone.style.width = `${width}px`;
20 | context.extra.clone.style.height = `${height}px`;
21 | context.extra.clone.style.zIndex = '1';
22 | context.extra.clone.style.backgroundColor = 'transparent';
23 |
24 | setCloneAttributes(context.extra.clone);
25 |
26 | document.body.appendChild(context.extra.clone);
27 |
28 | actions.next();
29 | };
30 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onStart/getDimensions.ts:
--------------------------------------------------------------------------------
1 | // only resolve the style of container or dragger enclosed by container.
2 | import { isElementVisibleInViewport, withinElement } from '../../dom';
3 | import { Action } from 'sabar';
4 | import { OnStartHandlerContext } from 'types';
5 | import Container from '../../Container';
6 | import Dragger from '../../Dragger';
7 |
8 | const getDimension = (v: Container | Dragger) => {
9 | const { el } = v;
10 | const rect = el.getBoundingClientRect();
11 | return rect;
12 | };
13 |
14 | // If it is a container, subject property may be required which will indicate
15 | // whether it is visible or not.
16 | const getSubject = (el: HTMLElement) => {
17 | return {
18 | isVisible: isElementVisibleInViewport(el),
19 | };
20 | };
21 |
22 | export default (ctx: object, actions: Action) => {
23 | const context = ctx as OnStartHandlerContext;
24 | const { vDraggers, vContainers } = context;
25 | const draggerKeys = Object.keys(vDraggers);
26 | const containerKeys = Object.keys(vContainers);
27 |
28 | draggerKeys.forEach(key => {
29 | const dragger = vDraggers[key];
30 | const rect = getDimension(dragger);
31 | dragger.dimension = {
32 | rect,
33 | };
34 | });
35 |
36 | containerKeys.forEach(key => {
37 | const container = vContainers[key];
38 | const rect = getDimension(container);
39 | container.dimension = {
40 | rect,
41 | subject: getSubject(container.el),
42 | within: withinElement(container.el),
43 | };
44 | });
45 |
46 | actions.next();
47 | };
48 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onStart/getDimensionsNested.ts:
--------------------------------------------------------------------------------
1 | // Append `collisionRect` to dimension property.
2 | // `getDimension` method should be considered.
3 |
4 | import { orientationToAxis, axisMeasure } from '../../utils';
5 | import { OnStartHandlerContext } from '../../../../types';
6 | import { Action } from 'sabar';
7 |
8 | export default (ctx: object, actions: Action) => {
9 | const context = ctx as OnStartHandlerContext;
10 | const { vDraggers, dndConfig } = context;
11 | const { mode, collisionPadding = 0 } = dndConfig;
12 |
13 | if (mode !== 'nested') {
14 | actions.next();
15 | return;
16 | }
17 |
18 | // Only if under `nested` mode, `dimension` should be appended with
19 | // `collisionRect` property.
20 | for (const key in vDraggers) {
21 | const dragger = vDraggers[key];
22 | const { container, dimension } = dragger;
23 | const { rect } = dimension;
24 | const { top, right, bottom, left } = rect;
25 | const { containerConfig } = container;
26 | const { orientation } = containerConfig;
27 | const axis = orientationToAxis[orientation];
28 | const [first, second] = axisMeasure[axis];
29 |
30 | const firstCollisionRect = {
31 | top,
32 | right,
33 | bottom,
34 | left,
35 | [first]: Math.max(rect[first] - collisionPadding, 0),
36 | [second]: rect[first],
37 | };
38 |
39 | const secondCollisionRect = {
40 | top,
41 | right,
42 | bottom,
43 | left,
44 | [first]: rect[second],
45 | [second]: rect[second] + collisionPadding,
46 | };
47 |
48 | dragger.dimension = {
49 | ...dimension,
50 | firstCollisionRect,
51 | secondCollisionRect,
52 | };
53 | }
54 |
55 | actions.next();
56 | };
57 |
--------------------------------------------------------------------------------
/src/plugins/dnd/middleware/onStart/validateContainers.ts:
--------------------------------------------------------------------------------
1 | import { intersect, coincide, contains, overlapOnEdge } from '../../collision';
2 | import { Action } from 'sabar';
3 | import { OnStartHandlerContext } from 'types';
4 |
5 | /**
6 | * 1. Container should be enclosed by other container entirely.
7 | * 2. Container should be on the outer of other container.
8 | * 3. The intersection of containers is not allowed.
9 | */
10 |
11 | export default (ctx: object, actions: Action) => {
12 | const context = ctx as OnStartHandlerContext;
13 | const { vContainers } = context;
14 |
15 | const keys = Object.keys(vContainers);
16 | const len = keys.length;
17 |
18 | for (let i = 0; i < len; i++) {
19 | const containerA = vContainers[keys[i]];
20 | for (let j = i + 1; j < len; j++) {
21 | const containerB = vContainers[keys[j]];
22 | const a = containerA.dimension.rect;
23 | const b = containerB.dimension.rect;
24 |
25 | // To ensure there are spaces between containers.
26 | if (
27 | intersect(a, b) &&
28 | !coincide(a, b) &&
29 | !contains(a, b) &&
30 | !overlapOnEdge(a, b)
31 | ) {
32 | console.warn(
33 | '=======================================\n' +
34 | 'The interaction of containers is forbidden\n' +
35 | ` containerA's id: ${containerA.id}\n` +
36 | ` containerB's id: ${containerB.id}\n`
37 | );
38 | }
39 | }
40 | }
41 |
42 | actions.next();
43 | };
44 |
--------------------------------------------------------------------------------
/src/plugins/dnd/mutationHandler.ts:
--------------------------------------------------------------------------------
1 | import { matchesDragger, matchesContainer, isElement } from './dom';
2 | import DND from './index';
3 | import reporter from './reporter';
4 |
5 | const DEBUG = false;
6 |
7 | export default (ctx: DND) => (mutationList: MutationRecord[]) => {
8 | const { containers } = ctx;
9 | const { configs } = ctx;
10 |
11 | for (const mutation of mutationList) {
12 | const { addedNodes, removedNodes } = mutation;
13 | if (addedNodes.length) {
14 | let merged = [] as Node[];
15 |
16 | addedNodes.forEach(node => {
17 | if (node && isElement(node)) {
18 | try {
19 | merged.push(node);
20 | // If dom is wrapped with a new div container, Only the new parent
21 | // div will be remarked.
22 | // https://stackoverflow.com/questions/8321874/how-to-get-all-childnodes-in-js-including-all-the-grandchildren
23 | const elements = (node as any).getElementsByTagName('div');
24 | merged = [...merged, ...elements];
25 | } catch (err) {
26 | console.log('err ', err);
27 | }
28 | }
29 | });
30 |
31 | merged.forEach(node => {
32 | const containerConfig = matchesContainer(node, configs);
33 | if (containerConfig !== -1) {
34 | ctx.handleContainerElement(node as HTMLElement, containerConfig);
35 | // In case, dragger is batch updated by container..
36 | ctx.handleDraggers(node as HTMLElement);
37 | DEBUG && reporter.addContainerNode(node);
38 | }
39 |
40 | // A container could be a dragger
41 | const matchedDragger = matchesDragger(node, configs);
42 | if (matchedDragger !== -1) {
43 | ctx.handleDraggerElement(node as HTMLElement);
44 | DEBUG && reporter.addDraggerNode(node);
45 | }
46 | });
47 | }
48 |
49 | if (removedNodes.length) {
50 | removedNodes.forEach(node => {
51 | const matchedContainer = matchesContainer(node, configs);
52 |
53 | if (matchedContainer !== -1) {
54 | const containerId = (node as HTMLElement).getAttribute(
55 | 'data-container-id'
56 | );
57 | if (!containerId) return;
58 | const container = containers[containerId];
59 | DEBUG && reporter.removeContainerNode(node);
60 | if (container) container.cleanup();
61 | }
62 | });
63 | }
64 | }
65 | };
66 |
--------------------------------------------------------------------------------
/src/plugins/dnd/reporter.ts:
--------------------------------------------------------------------------------
1 | import Container from './Container';
2 | import { LoggerComponent } from '../../types';
3 |
4 | const capitalize = (s: string) => {
5 | if (typeof s !== 'string') return '';
6 | return s.charAt(0).toUpperCase() + s.slice(1);
7 | };
8 |
9 | function reporter() {
10 | this.logAddEffect = (component: LoggerComponent) => {
11 | const { id } = component;
12 | console.log(
13 | `Add effect to component %c${id}`,
14 | 'background: #222; color: red'
15 | );
16 | };
17 |
18 | this.logRemoveEffect = (component: LoggerComponent) => {
19 | const { id } = component;
20 | console.log(
21 | `Remove effect from component %c${id}`,
22 | 'background: #222; color: #bada55'
23 | );
24 | };
25 |
26 | const manipulateNode = (action: string, name: string) => (
27 | node: HTMLElement
28 | ) => {
29 | console.log(`${capitalize(action)} ${name} node`, node);
30 | };
31 |
32 | this.addContainerNode = manipulateNode('add', 'container');
33 | this.addDraggerNode = manipulateNode('add', 'dragger');
34 | this.removeContainerNode = manipulateNode('remove', 'container');
35 |
36 | this.logEnterContainer = (container: Container) => {
37 | console.log(`On enter: %c${container.id}`, 'color: #bada55');
38 | };
39 |
40 | this.logLeaveContainer = (container: Container) => {
41 | console.log(`On leave: %c${container.id}`, 'color: #bada55');
42 | };
43 | }
44 |
45 | // @ts-ignore
46 | export default new reporter();
47 |
--------------------------------------------------------------------------------
/src/plugins/dnd/sensors/utils.ts:
--------------------------------------------------------------------------------
1 | import closest from '../closest';
2 | import { ResultConfig } from '../../../types';
3 |
4 | export const hasDraggerHandlerMatched = (
5 | el: HTMLElement,
6 | configs: ResultConfig[]
7 | ) => {
8 | const len = configs.length;
9 | for (let i = 0; i < len; i++) {
10 | const { draggerHandlerSelector } = configs[i];
11 | if (closest(el, draggerHandlerSelector!)) return true;
12 | }
13 |
14 | return false;
15 | };
16 |
--------------------------------------------------------------------------------
/src/plugins/dnd/structure/SortedItems.ts:
--------------------------------------------------------------------------------
1 | import { Sorter } from '../../../types';
2 |
3 | class SortedItems {
4 | public sorter: Sorter;
5 | public items: T[];
6 |
7 | constructor({ sorter }: { sorter: Sorter }) {
8 | this.sorter = sorter;
9 | this.items = [];
10 | }
11 |
12 | add(item: T) {
13 | this.items.push(item);
14 | this.items.sort(this.sorter);
15 | }
16 |
17 | remove(item: T) {
18 | const index = this.findIndex(item);
19 | if (index !== -1) this.items.splice(index, 1);
20 | }
21 |
22 | findIndex(item: T) {
23 | const { id } = item as any;
24 | return this.items.findIndex(item => (item as any).id === id);
25 | }
26 |
27 | getSize() {
28 | return this.items.length;
29 | }
30 |
31 | getItem(index: number) {
32 | return this.items[index];
33 | }
34 |
35 | slice(...args: [number?, number?]) {
36 | return [].slice.apply(this.items, args);
37 | }
38 |
39 | splice(...args: [number, number?]) {
40 | return [].splice.apply(this.items, args as any);
41 | }
42 | }
43 |
44 | export default SortedItems;
45 |
--------------------------------------------------------------------------------
/src/plugins/dnd/utils.ts:
--------------------------------------------------------------------------------
1 | import { AxisMeasure, OrientationToAxis, Orientation } from '../../types';
2 |
3 | export const orientationToAxis: OrientationToAxis = {
4 | vertical: 'y',
5 | horizontal: 'x',
6 | };
7 |
8 | export const axisMeasure: AxisMeasure = {
9 | y: ['top', 'bottom'],
10 | x: ['left', 'right'],
11 | };
12 |
13 | export const orientationToMeasure = (orientation: Orientation): string[] => {
14 | const axis = orientationToAxis[orientation];
15 | return axisMeasure[axis];
16 | };
17 |
18 | export const axisClientMeasure = {
19 | x: 'clientX',
20 | y: 'clientY',
21 | };
22 |
23 | export const isClamped = (value: number, min: number, max: number) => {
24 | return value >= min && value <= max;
25 | };
26 |
27 | export const isFunction = (fn: any) => typeof fn === 'function';
28 |
--------------------------------------------------------------------------------
/src/plugins/sidebar-plugin/createAddOn.ts:
--------------------------------------------------------------------------------
1 | const svgNS = 'http://www.w3.org/2000/svg';
2 |
3 | const createAddOn = (nodeKey: string) => {
4 | const wrapper = document.createElement('div');
5 | wrapper.classList.add('sidebar-addon');
6 | wrapper.setAttribute('data-id', nodeKey);
7 |
8 | // add plus icon
9 | const plusWrapper = document.createElement('div');
10 | plusWrapper.classList.add('plus');
11 | const plus = document.createElementNS(svgNS, 'svg');
12 | plus.setAttributeNS(null, 'viewBox', '0 0 18 18');
13 | plus.setAttributeNS(null, 'width', '14');
14 | plus.setAttributeNS(null, 'height', '14');
15 | plus.setAttributeNS(null, 'fill', 'rgba(55, 53, 47, 0.3)');
16 | const polygon = document.createElementNS(svgNS, 'polygon');
17 | polygon.setAttributeNS(
18 | null,
19 | 'points',
20 | '17,8 10,8 10,1 8,1 8,8 1,8 1,10 8,10 8,17 10,17 10,10 17,10 '
21 | );
22 | plus.appendChild(polygon);
23 | plusWrapper.appendChild(plus);
24 |
25 | const selectableWrapper = document.createElement('div');
26 | selectableWrapper.classList.add('selectable');
27 | const selectable = document.createElementNS(svgNS, 'svg');
28 | selectable.setAttributeNS(null, 'viewBox', '0 0 10 10');
29 | selectable.setAttributeNS(null, 'width', '14');
30 | selectable.setAttributeNS(null, 'height', '14');
31 | selectable.setAttributeNS(null, 'fill', 'rgba(55, 53, 47, 0.3)');
32 | const path = document.createElementNS(svgNS, 'path');
33 | path.setAttributeNS(
34 | null,
35 | 'd',
36 | 'M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'
37 | );
38 | selectable.appendChild(path);
39 | selectableWrapper.appendChild(selectable);
40 |
41 | wrapper.appendChild(plusWrapper);
42 | wrapper.appendChild(selectableWrapper);
43 |
44 | return wrapper;
45 | };
46 |
47 | export default createAddOn;
48 |
--------------------------------------------------------------------------------
/src/plugins/sidebar-plugin/styles.css:
--------------------------------------------------------------------------------
1 | .sidebar-addon {
2 | position: absolute;
3 | top: 0;
4 | left: -47px;
5 | height: 24px;
6 | display: flex;
7 | flex-direction: row;
8 | width: 42px;
9 | align-items: center;
10 | opacity: 0;
11 | padding-right: 5px;
12 | /* padding-top: 10px; */
13 | }
14 |
15 | .sidebar-addon-visible {
16 | opacity: 1;
17 | transition: opacity 0.3s ease-in-out;
18 | margin-top: 5px;
19 | }
20 |
21 | .sidebar-addon .plus {
22 | flex: 1;
23 | display: flex;
24 | align-items: center;
25 | justify-content: center;
26 | height: 24px;
27 | }
28 |
29 | .sidebar-addon .selectable {
30 | flex: 1;
31 | display: flex;
32 | align-items: center;
33 | justify-content: center;
34 | height: 24px;
35 | }
36 |
37 | .sidebar-addon .selectable:hover {
38 | user-select: none;
39 | background: rgba(55, 53, 47, 0.08);
40 | transition: background 120ms ease-in 0s;
41 | cursor: grab;
42 | border-radius: 3px;
43 | }
44 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | overflow: hidden;
4 | }
5 |
6 | .miuffy-editor-root {
7 | width: 100%;
8 | height: 100vh;
9 | position: relative;
10 | font-family: Helvetica Neue, -apple-system, BlinkMacSystemFont, Segoe UI,
11 | PingFang SC, Roboto, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC,
12 | Myriad Pro, Hiragino Sans, Yu Gothic, Lucida Grande, sans-serif;
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | overflow: auto;
17 | /* font-family: 'Tahoma For Number', 'Chinese Quote', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; */
18 | }
19 |
20 | .miuffy-editor {
21 | max-width: 100%;
22 | background: #fff;
23 | border: none;
24 | padding: 0 96px 30vh;
25 | display: flex;
26 | flex-direction: column;
27 | align-items: center;
28 | }
29 |
30 | .miuffy-editor .article-title,
31 | .miuffy-editor div[data-contents="true"] > div
32 | /* .miuffy-editor div[data-contents="true"] > figure */
33 | {
34 | width: 900px;
35 | max-width: 100%;
36 | margin-left: auto;
37 | margin-right: auto;
38 | }
39 |
40 | div[data-contents="true"] {
41 | /* display: flex;
42 | flex-direction: column;
43 | align-items: center; */
44 | padding: 5px;
45 | }
46 |
47 | .miuffy-paragraph {
48 | min-height: 24px;
49 | line-height: 24px;
50 | white-space: normal;
51 | margin: 0;
52 | letter-spacing: 0.05em;
53 | position: relative;
54 | /* padding: 10px; */
55 | }
56 |
57 | .miuffy-blockquote {
58 | margin-top: 0px;
59 | margin-bottom: 0px;
60 | padding-left: 1em;
61 | margin-left: 0px;
62 | border-left: 3px solid #eee;
63 | opacity: 0.6;
64 | }
65 |
66 | .miuffy-draft-editor {
67 | margin: 48px 60px 0 60px;
68 | }
69 |
70 | * {
71 | box-sizing: content-box;
72 | }
73 |
74 | .block-level-0.miuffy-paragraph {
75 | padding-left: 10px;
76 | padding-right: 10px;
77 | padding-top: 5px;
78 | padding-bottom: 5px;
79 | }
80 |
81 | .block-level-1.miuffy-paragraph {
82 | padding-right: 10px;
83 | }
--------------------------------------------------------------------------------
/src/types/button.ts:
--------------------------------------------------------------------------------
1 | import { ReactChild } from 'react';
2 |
3 | export interface WithActionProps {
4 | onClick: Function;
5 | }
6 |
7 | export interface WithFillColorProps {
8 | active: boolean;
9 | }
10 |
11 | export interface ButtonProps {
12 | children?: ReactChild;
13 | fill: string;
14 | }
15 |
16 | export type IWithActionProps = {
17 | [P in keyof T]: T[P];
18 | };
19 |
--------------------------------------------------------------------------------
/src/types/components.ts:
--------------------------------------------------------------------------------
1 | import { ReactChild, RefObject } from 'react';
2 | import { ContentNodeState, BlockProps } from '.';
3 | import { ContentBlockNode } from './draft-js';
4 |
5 | export interface LinkProps {
6 | entityKey: string;
7 | contentState: ContentNodeState;
8 | children?: ReactChild;
9 | }
10 |
11 | export interface LinkSpanProps {
12 | children?: ReactChild;
13 | }
14 |
15 | export interface SidebarProps {
16 | forwardRef: RefObject;
17 | }
18 |
19 | export enum Label {
20 | H1 = 'fas fa-heading',
21 | Blockquote = 'fas fa-quote-left',
22 | UL = 'fas fa-list-ul',
23 | OL = 'fas fa-list-ol',
24 | 'Code Block' = 'fas fa-code',
25 | }
26 |
27 | // ts-hint: https://stackoverflow.com/questions/55377365/what-does-keyof-typeof-mean-in-typescript
28 | export interface StyleControlButtonProps {
29 | label: keyof typeof Label;
30 | active: boolean;
31 | onToggle: Function;
32 | style: object;
33 | }
34 |
35 | export interface ImageProps {
36 | block: ContentBlockNode;
37 | contentState: ContentNodeState;
38 | blockProps: BlockProps;
39 | }
40 |
--------------------------------------------------------------------------------
/src/types/editor.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, ReactNode, Ref } from 'react';
2 | import { DraftBlockRenderMap, DraftStyleMap, Editor } from 'draft-js';
3 | import { GetEditor } from './';
4 |
5 | export interface EditorPropsBefore {
6 | imageRef: RefObject;
7 | inlineRef: RefObject;
8 | sidebarRef: RefObject;
9 | blockRenderMap: DraftBlockRenderMap;
10 | customStyleMap: DraftStyleMap;
11 | children?: ReactNode;
12 | placeholder: string;
13 | }
14 |
15 | // ts-hint: interface extends interface...
16 | export type EditorProps = {
17 | forwardRef: Ref;
18 | } & EditorPropsBefore & {
19 | getEditor: GetEditor;
20 | };
21 |
22 | export type EditorRef = RefObject;
23 |
--------------------------------------------------------------------------------
/src/types/hooks.ts:
--------------------------------------------------------------------------------
1 | import { RefObject } from 'react';
2 | import { ContentBlockNode, GetEditor } from '.';
3 | import { ContentNodeState } from './draft-js';
4 |
5 | export interface HooksProps {
6 | nodeRef: RefObject;
7 | props: HooksComponentProps;
8 | }
9 |
10 | export interface ResizeLayout {
11 | width: string;
12 | }
13 |
14 | export interface BlockProps {
15 | getEditor: GetEditor;
16 | resizeLayout: ResizeLayout;
17 | alignment: string;
18 | }
19 |
20 | export interface HooksComponentProps {
21 | blockProps: BlockProps;
22 | block: ContentBlockNode;
23 | contentState: ContentNodeState;
24 | }
25 |
--------------------------------------------------------------------------------
/src/types/imageToolbar.ts:
--------------------------------------------------------------------------------
1 | import { RefObject } from 'react';
2 | import { GetEditor } from './plugin';
3 |
4 | export interface ImageAlignmentButtonFC {
5 | activeKey: string;
6 | clickHandler: Function;
7 | active: boolean;
8 | }
9 |
10 | export enum Alignment {
11 | Center = 'center',
12 | Right = 'right',
13 | Left = 'left',
14 | LeftFill = 'leftFill',
15 | RightFill = 'rightFill',
16 | }
17 |
18 | // ts-hint: ImageToolbarProps should be split into two parts,
19 | // or it will error in Editor.ts file
20 | export type ImageToolbarProps = {
21 | forwardRef: RefObject;
22 | } & {
23 | getEditor: GetEditor;
24 | };
25 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './plugin';
2 | export * from './editor';
3 | export * from './withEditor';
4 | export * from './imageToolbar';
5 | export * from './button';
6 |
7 | export * from './draft-js';
8 | export * from './util';
9 | export * from './rect';
10 | export * from './components';
11 | export * from './hooks';
12 | export * from './dnd';
13 | export * from './inlineToolBar';
14 | export * from './sidebar';
15 |
--------------------------------------------------------------------------------
/src/types/inlineToolBar.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from 'react';
2 | import { GetEditor } from './';
3 | import Immutable from 'immutable';
4 | import { DraftBlockType } from 'draft-js';
5 |
6 | // ts-hint: InlineToolbarProps should be split into two parts,
7 | // or it will error in Editor.ts file
8 | export type InlineToolbarProps = {
9 | forwardRef: Ref;
10 | } & {
11 | getEditor: GetEditor;
12 | };
13 |
14 | export interface InlineToolbarStateValues {
15 | styles: Immutable.OrderedSet;
16 | blockTypes: DraftBlockType[];
17 | hasLink: boolean;
18 | inDisplayMode: boolean;
19 | }
20 |
21 | export interface InputBarProps {
22 | getEditor: GetEditor;
23 | }
24 |
25 | export interface StyleControlsButtonProps {
26 | active: boolean;
27 | getEditor: GetEditor;
28 | }
29 |
30 | export interface LinkControlsProps {
31 | active: boolean;
32 | getEditor: GetEditor;
33 | handleClick: Function;
34 | }
35 |
36 | export interface StyleControlsProps {
37 | blockTypes: string[];
38 | styles: any;
39 | getEditor: GetEditor;
40 | toggleDisplayMode: Function;
41 | hasLink: boolean;
42 | }
43 |
--------------------------------------------------------------------------------
/src/types/plugin.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, ReactNode } from 'react';
2 | import { SyncHook, SyncWaterfallHook, SyncBailHook } from 'tapable';
3 | import { EditorState, Editor } from 'draft-js';
4 | import { DraftNodeDecoratorStrategy } from './draft-js';
5 |
6 | export interface Hooks {
7 | setState: SyncHook;
8 | onChange: SyncWaterfallHook;
9 | stateFilter: SyncBailHook;
10 | toggleWaterfallBlockType: SyncWaterfallHook;
11 | toggleBlockType: SyncHook;
12 | toggleInlineStyleV2: SyncHook;
13 | toggleInlineStyle: SyncHook;
14 | afterInlineStyleApplied: SyncHook;
15 | createBlockRenderMap: SyncBailHook;
16 | createCustomStyleMap: SyncBailHook;
17 | blockStyleFn: SyncBailHook;
18 | handleKeyCommand: SyncBailHook;
19 | handleDroppedFiles: SyncHook;
20 | addImage: SyncHook;
21 | blockRendererFn: SyncBailHook;
22 | createPlaceholder: SyncHook;
23 | didUpdate: SyncHook;
24 | updatePlaceholder: SyncHook;
25 | compositeDecorator: SyncWaterfallHook;
26 | keyBindingFn: SyncBailHook;
27 | syncSelectionChange: SyncHook;
28 | selectionInitChange: SyncHook;
29 | selectionCollapsedChange: SyncHook;
30 | selectionFocusChange: SyncHook;
31 | selectionMoveInnerBlock: SyncHook;
32 | selectionMoveOuterBlock: SyncHook;
33 | selectionRangeChange: SyncHook;
34 | selectionRangeSizeChange: SyncHook;
35 | selectionRangeContentChange: SyncHook;
36 | inlineBarChange: SyncHook;
37 | hideInlineToolbar: SyncHook;
38 | afterClickLinkButton: SyncHook;
39 | cleanUpLinkClickState: SyncHook;
40 | toggleImageToolbarVisible: SyncHook;
41 | updateDecorator: SyncWaterfallHook;
42 | updateDragSubscription: SyncHook;
43 | syncBlockKeys: SyncHook;
44 | prepareDragStart: SyncHook;
45 | teardownDragDrop: SyncHook;
46 | afterMounted: SyncHook;
47 | finalNewLine: SyncBailHook;
48 | updateBlockDepthData: SyncBailHook;
49 | }
50 |
51 | export interface PluginState {
52 | editorState: EditorState;
53 | }
54 |
55 | export interface PluginEditorProps {
56 | plugins: any[];
57 | }
58 |
59 | export interface PluginEditorState {
60 | editorState: EditorState;
61 | }
62 |
63 | export interface GetEditor {
64 | (): {
65 | hooks: Hooks;
66 | editorState: EditorState;
67 | editorRef: RefObject;
68 | inlineToolbarRef: RefObject;
69 | imageToolbarRef: RefObject;
70 | sidebarRef: RefObject;
71 | };
72 | }
73 |
74 | // https://stackoverflow.com/questions/50604272/typescript-property-foreach-does-not-exist-on-type-filelist
75 | // https://stackoverflow.com/questions/46349147/property-foreach-does-not-exist-on-type-nodelistof
76 | declare global {
77 | interface FileList {
78 | forEach(callback: (f: File) => void): void;
79 | }
80 | }
81 |
82 | export interface TimeoutHandler {
83 | (
84 | callback: (...args: any[]) => void,
85 | ms: number,
86 | ...args: any[]
87 | ): NodeJS.Timeout;
88 | }
89 |
90 | export interface DecoratorPair {
91 | strategy: DraftNodeDecoratorStrategy;
92 | component: ReactNode;
93 | }
94 |
--------------------------------------------------------------------------------
/src/types/rect.ts:
--------------------------------------------------------------------------------
1 | export interface CoordinateMapItem {
2 | rect: RectObject;
3 | offsetKey: string;
4 | }
5 |
6 | export interface RectObject {
7 | top: number;
8 | right: number;
9 | bottom: number;
10 | left: number;
11 | width?: number;
12 | height?: number;
13 | }
14 |
15 | export type CoordinateMap = CoordinateMapItem[];
16 |
17 | export interface PointObject {
18 | x: number;
19 | y: number;
20 | }
21 |
22 | export interface SafeArea {
23 | blockKey: string;
24 | offsetKey: string;
25 | rect: RectObject;
26 | }
27 |
--------------------------------------------------------------------------------
/src/types/sidebar.ts:
--------------------------------------------------------------------------------
1 | export interface CurrentSidebar {
2 | node: HTMLElement;
3 | teardown: () => void;
4 | child: Node;
5 | offsetKey: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/util.ts:
--------------------------------------------------------------------------------
1 | import { ContentBlockNode } from '.';
2 | import { Map } from 'immutable';
3 |
4 | export enum Position {
5 | Top = 'top',
6 | Right = 'right',
7 | Bottom = 'bottom',
8 | Left = 'left',
9 | }
10 |
11 | export enum Direction {
12 | Row = 'row',
13 | Column = 'column',
14 | }
15 |
16 | export interface TransformBlockCallback {
17 | (block: ContentBlockNode): ContentBlockNode;
18 | }
19 |
20 | // export interface WrapperProps {
21 | // flexRow: boolean;
22 | // 'data-wrapper': boolean;
23 | // 'data-direction': Direction;
24 | // }
25 |
26 | export type WrapperProps = Map<
27 | 'flexRow' | 'data-wrapper' | 'data-direction' | undefined,
28 | boolean | Direction | undefined
29 | >;
30 |
31 | // addEventListener
32 | // (type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
33 |
34 | // export interface Binding {
35 | // eventName: K;
36 | // fn: (this: Window, ev: WindowEventMap[K]) => any;
37 | // options?: boolean | AddEventListenerOptions
38 | // }
39 | export interface Binding {
40 | eventName: string;
41 | fn: (e: MouseEvent) => any;
42 | options?: AddEventListenerOptions;
43 | }
44 |
--------------------------------------------------------------------------------
/src/types/withEditor.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { GetEditor } from './';
3 |
4 | export type ReturnProps = {
5 | // ts-hint: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#mapped-types
6 | // ts-hint: https://www.typescriptlang.org/docs/handbook/advanced-types.html keyof....
7 | [P in keyof T]: T[P];
8 | } & {
9 | children?: ReactNode;
10 | };
11 |
12 | export type IWrappedComponent = ReturnProps & {
13 | getEditor: GetEditor;
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/block/appendChild.ts:
--------------------------------------------------------------------------------
1 | import { List } from 'immutable';
2 | import blockUtil from './blockUtil';
3 | import blockMutationUtil from './blockMutationUtil';
4 | import { BlockNodeMap, ContentBlockNode } from '../../types';
5 |
6 | /**
7 | * If target block has children, childBlock should be placed after all
8 | * of its children.
9 | */
10 | function appendChild(
11 | blockMap: BlockNodeMap,
12 | parentBlock: ContentBlockNode,
13 | childBlock: ContentBlockNode
14 | ) {
15 | const blocksBefore = blockUtil.blocksBefore(blockMap, parentBlock);
16 | const childBlockKey = childBlock.getKey();
17 | const parentBlockKey = parentBlock.getKey();
18 | const childrenBlocks = blockUtil.getChildrenBlocks(blockMap, parentBlock);
19 |
20 | const lastBlock = childrenBlocks.size
21 | ? childrenBlocks.last()
22 | : parentBlock;
23 | const blocksAfter = blockUtil.blocksAfter(blockMap, lastBlock);
24 |
25 | const newBlockMap = blocksBefore
26 | .concat([[parentBlock.getKey(), parentBlock]])
27 | .concat(childrenBlocks.size ? childrenBlocks.toSeq() : [])
28 | .concat([[childBlock.getKey(), childBlock]])
29 | .concat(blocksAfter)
30 | .toOrderedMap();
31 |
32 | return newBlockMap.withMutations(function(blocks) {
33 | blockMutationUtil.transformBlock(childBlockKey, blocks, function(block) {
34 | return block.merge({
35 | parent: parentBlockKey,
36 | prevSibling: childrenBlocks.size ? lastBlock.getKey() : null,
37 | });
38 | });
39 |
40 | if (childrenBlocks.size) {
41 | blockMutationUtil.transformBlock(lastBlock.getKey(), blocks, function(
42 | block
43 | ) {
44 | return block.merge({
45 | nextSibling: childBlockKey,
46 | });
47 | });
48 | }
49 |
50 | blockMutationUtil.transformBlock(parentBlockKey, blocks, function(block) {
51 | const parentChildrenList = block.getChildKeys();
52 | const newChildrenArray = parentChildrenList.toArray();
53 | newChildrenArray.push(childBlockKey);
54 |
55 | return block.merge({
56 | children: List(newChildrenArray),
57 | });
58 | });
59 | });
60 | }
61 |
62 | export default appendChild;
63 |
--------------------------------------------------------------------------------
/src/utils/block/blockMutationUtil.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BlockNodeMap,
3 | ContentBlockNode,
4 | TransformBlockCallback,
5 | } from '../../types';
6 |
7 | const BlockMutationUtil = {
8 | // moveBlockInContentState
9 | // removeRangeFromContentState.js
10 | transformBlock: function transformBlock(
11 | key: string | null,
12 | blockMap: BlockNodeMap,
13 | func: TransformBlockCallback
14 | ) {
15 | if (!key) {
16 | return;
17 | }
18 |
19 | const block = blockMap.get(key);
20 |
21 | if (!block) {
22 | return;
23 | }
24 |
25 | blockMap.set(key, func(block));
26 | },
27 |
28 | deleteFromChildrenList: function deleteFromChildrenList(
29 | block: ContentBlockNode,
30 | originalBlockKey: string
31 | ) {
32 | const parentChildrenList = block.getChildKeys();
33 | return block.merge({
34 | children: parentChildrenList.delete(
35 | parentChildrenList.indexOf(originalBlockKey)
36 | ),
37 | });
38 | },
39 | };
40 |
41 | export default BlockMutationUtil;
42 |
--------------------------------------------------------------------------------
/src/utils/block/contains.ts:
--------------------------------------------------------------------------------
1 | import { BlockNodeMap } from '../../types';
2 |
3 | const contains = (blockMap: BlockNodeMap, key1: string, key2: string) => {
4 | if (key1 === key2) return true;
5 |
6 | const block1 = blockMap.get(key1);
7 | const block2 = blockMap.get(key2);
8 |
9 | if (!block1 || !block2) return false;
10 |
11 | const childKeys = block1.getChildKeys().toArray();
12 |
13 | if (childKeys.indexOf(key2) !== -1) return true;
14 |
15 | for (let i = 0; i < childKeys.length; i++) {
16 | const key = childKeys[i];
17 | if (contains(blockMap, key, key2)) return true;
18 | }
19 |
20 | return false;
21 | };
22 |
23 | export default contains;
24 |
--------------------------------------------------------------------------------
/src/utils/block/createEmptyBlock.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import generateRandomKey from 'draft-js/lib/generateRandomKey';
3 | import { ContentBlock } from 'draft-js';
4 | import { Map, List } from 'immutable';
5 |
6 | export default () => {
7 | const blockKey = generateRandomKey();
8 | return new ContentBlock({
9 | key: blockKey,
10 | text: '',
11 | data: Map(),
12 | children: List(),
13 | type: 'unstyled',
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/utils/block/createEmptyBlockNode.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import generateRandomKey from 'draft-js/lib/generateRandomKey';
3 | // @ts-ignore
4 | import ContentBlockNode from 'draft-js/lib/ContentBlockNode';
5 | import { Map, List } from 'immutable';
6 | import { ContentBlockNode as ContentBlockNodeType } from '../../types';
7 |
8 | export default () => {
9 | const blockKey = generateRandomKey();
10 | return new ContentBlockNode({
11 | key: blockKey,
12 | text: '',
13 | data: Map(),
14 | children: List(),
15 | type: 'unstyled',
16 | }) as ContentBlockNodeType;
17 | };
18 |
--------------------------------------------------------------------------------
/src/utils/block/findRootNode.ts:
--------------------------------------------------------------------------------
1 | import { BlockNodeMap, ContentBlockNode } from '../../types';
2 |
3 | const findRootNode = (
4 | blockMap: BlockNodeMap,
5 | blockKey: string
6 | ): null | ContentBlockNode => {
7 | const block = blockMap.get(blockKey);
8 | if (!block) return null;
9 | if (!block.getParentKey()) return block;
10 |
11 | return findRootNode(blockMap, block.getParentKey());
12 | };
13 |
14 | export default findRootNode;
15 |
--------------------------------------------------------------------------------
/src/utils/block/findRootNodeSibling.ts:
--------------------------------------------------------------------------------
1 | import { BlockNodeMap } from '../../types';
2 |
3 | export default (blockMap: BlockNodeMap, blockKey: string): null | string => {
4 | const block = blockMap
5 | .toSeq()
6 | .skipUntil(block => {
7 | return block.getKey() === blockKey;
8 | })
9 | .skip(1)
10 | .find(block => {
11 | return !block.getParentKey();
12 | });
13 |
14 | if (block) return block.getKey();
15 | return null;
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/block/insertBlockAfter.ts:
--------------------------------------------------------------------------------
1 | import { List } from 'immutable';
2 | import blockUtil from './blockUtil';
3 | import blockMutationUtil from './blockMutationUtil';
4 | import { BlockNodeMap, ContentBlockNode } from '../../types';
5 |
6 | /**
7 | * If target block has children, sourceBlock should be placed after all
8 | * of its children.
9 | */
10 | function insertBlockAfter(
11 | blockMap: BlockNodeMap,
12 | targetBlock: ContentBlockNode | undefined,
13 | sourceBlock: ContentBlockNode | undefined
14 | ): BlockNodeMap {
15 | if (!sourceBlock || !targetBlock) return blockMap;
16 |
17 | const blocksBefore = blockUtil.blocksBefore(blockMap, targetBlock);
18 | const sourceBlockKey = sourceBlock.getKey();
19 | const targetBlockKey = targetBlock.getKey();
20 |
21 | const childrenBlocks = blockUtil.getChildrenBlocks(blockMap, targetBlock);
22 | const lastBlock = childrenBlocks.size
23 | ? childrenBlocks.last()
24 | : targetBlock;
25 | const blocksAfter = blockUtil.blocksAfter(blockMap, lastBlock);
26 |
27 | const newBlockMap = blocksBefore
28 | .concat([[targetBlock.getKey(), targetBlock]])
29 | .concat(childrenBlocks.size ? childrenBlocks.toSeq() : [])
30 | .concat([[sourceBlock.getKey(), sourceBlock]])
31 | .concat(blocksAfter)
32 | .toOrderedMap() as BlockNodeMap;
33 |
34 | return newBlockMap.withMutations(function(blocks) {
35 | blockMutationUtil.transformBlock(sourceBlockKey, blocks, function(block) {
36 | return block.merge({
37 | prevSibling: targetBlockKey,
38 | nextSibling: targetBlock.getNextSiblingKey(),
39 | parent: targetBlock.getParentKey(),
40 | });
41 | });
42 |
43 | blockMutationUtil.transformBlock(
44 | targetBlock.getParentKey(),
45 | blocks,
46 | function(block) {
47 | const parentChildrenList = block.getChildKeys();
48 | const newChildrenArray = parentChildrenList.toArray();
49 | newChildrenArray.push(sourceBlockKey);
50 |
51 | return block.merge({
52 | children: List(newChildrenArray),
53 | });
54 | }
55 | );
56 |
57 | blockMutationUtil.transformBlock(targetBlockKey, blocks, function(block) {
58 | return block.merge({
59 | nextSibling: sourceBlockKey,
60 | });
61 | });
62 | });
63 | }
64 |
65 | export default insertBlockAfter;
66 |
--------------------------------------------------------------------------------
/src/utils/block/insertBlockBefore.ts:
--------------------------------------------------------------------------------
1 | import { List } from 'immutable';
2 | import blockMutationUtil from './blockMutationUtil';
3 | import blockUtil from './blockUtil';
4 | import { BlockNodeMap, ContentBlockNode } from '../../types';
5 |
6 | function insertBlockBefore(
7 | blockMap: BlockNodeMap,
8 | targetBlock: ContentBlockNode | undefined,
9 | sourceBlock: ContentBlockNode | undefined
10 | ): BlockNodeMap {
11 | if (!targetBlock || !sourceBlock) return blockMap;
12 |
13 | const blocksBefore = blockUtil.blocksBefore(blockMap, targetBlock);
14 | const blocksAfter = blockUtil.blocksAfter(blockMap, targetBlock);
15 | const sourceBlockKey = sourceBlock.getKey();
16 | const targetBlockKey = targetBlock.getKey();
17 | const targetParentKey = targetBlock.getParentKey();
18 | const targetOriginPrevSibling = targetBlock.getPrevSiblingKey();
19 | const targetOriginNextSibling = targetBlock.getNextSiblingKey();
20 |
21 | const newBlockMap = blocksBefore
22 | .concat([
23 | [sourceBlockKey, sourceBlock],
24 | [targetBlockKey, targetBlock],
25 | ])
26 | .concat(blocksAfter)
27 | .toOrderedMap() as BlockNodeMap;
28 |
29 | return newBlockMap.withMutations(function(blocks) {
30 | blockMutationUtil.transformBlock(sourceBlockKey, blocks, function(block) {
31 | return block.merge({
32 | prevSibling: targetOriginPrevSibling,
33 | nextSibling: targetBlockKey,
34 | });
35 | });
36 |
37 | blockMutationUtil.transformBlock(
38 | targetBlock.getPrevSiblingKey(),
39 | blocks,
40 | function(block) {
41 | return block.merge({
42 | nextSibling: sourceBlockKey,
43 | });
44 | }
45 | );
46 |
47 | blockMutationUtil.transformBlock(targetBlockKey, blocks, function(block) {
48 | return block.merge({
49 | prevSibling: sourceBlockKey,
50 | nextSibling: targetOriginNextSibling,
51 | });
52 | });
53 |
54 | blockMutationUtil.transformBlock(targetParentKey, blocks, function(block) {
55 | const parentChildrenList = block.getChildKeys();
56 | const newChildrenArray = parentChildrenList.toArray();
57 | const index = newChildrenArray.indexOf(targetBlockKey);
58 | newChildrenArray.splice(index, 0, sourceBlockKey);
59 |
60 | return block.merge({
61 | children: List(newChildrenArray),
62 | });
63 | });
64 | });
65 | }
66 |
67 | export default insertBlockBefore;
68 |
--------------------------------------------------------------------------------
/src/utils/block/removeBlock.ts:
--------------------------------------------------------------------------------
1 | import blockUtil from './blockUtil';
2 | import blockMutationUtil from './blockMutationUtil';
3 | import { BlockNodeMap, ContentBlockNode } from '../../types';
4 |
5 | /**
6 | *
7 | * @param {*} editorState
8 | * @param {String} sourceBlockKey
9 | * @param {String} targetBlockKey
10 | * @param {String} position : one of values ['top', 'right', 'left']
11 | */
12 |
13 | function removeBlock(
14 | blockMap: BlockNodeMap,
15 | block: ContentBlockNode | string | undefined,
16 | removeParentIfHasNoChild?: boolean
17 | ): BlockNodeMap {
18 | let blockToRemove: ContentBlockNode | undefined;
19 | let blockKey: string;
20 |
21 | if (!block) {
22 | return blockMap;
23 | }
24 | if (typeof block === 'string') {
25 | blockToRemove = blockMap.get(block);
26 | blockKey = block;
27 | } else {
28 | blockToRemove = block;
29 | blockKey = block.getKey();
30 | }
31 |
32 | if (!blockToRemove) return blockMap;
33 |
34 | const blocksBefore = blockUtil.blocksBefore(blockMap, blockToRemove);
35 | const blocksAfter = blockUtil.blocksAfter(blockMap, blockToRemove);
36 | let newBlockMap = blocksBefore.concat(blocksAfter).toOrderedMap();
37 | const parentKey = blockToRemove.getParentKey();
38 |
39 | newBlockMap = newBlockMap.withMutations(function(blocks) {
40 | blockMutationUtil.transformBlock(parentKey, blocks, function(block) {
41 | return blockMutationUtil.deleteFromChildrenList(block, blockKey);
42 | });
43 | });
44 |
45 | const newParentBlock = newBlockMap.get(parentKey);
46 | const childrenSize = blockUtil.getChildrenSize(newParentBlock);
47 |
48 | if (removeParentIfHasNoChild && !childrenSize) {
49 | return removeBlock(newBlockMap, newParentBlock, removeParentIfHasNoChild);
50 | }
51 |
52 | return newBlockMap.withMutations(function(blocks) {
53 | blockMutationUtil.transformBlock(
54 | blockToRemove!.getPrevSiblingKey(),
55 | blocks,
56 | function(block) {
57 | return block.merge({
58 | nextSibling: blockToRemove!.getNextSiblingKey(),
59 | });
60 | }
61 | );
62 |
63 | blockMutationUtil.transformBlock(
64 | blockToRemove!.getNextSiblingKey(),
65 | blocks,
66 | function(block) {
67 | return block.merge({
68 | prevSibling: blockToRemove!.getPrevSiblingKey(),
69 | });
70 | }
71 | );
72 | });
73 | }
74 |
75 | export default removeBlock;
76 |
--------------------------------------------------------------------------------
/src/utils/block/removeBlockWithClear.ts:
--------------------------------------------------------------------------------
1 | import { BlockNodeMap, ContentBlockNode } from '../../types';
2 | import removeBlock from './removeBlock';
3 |
4 | /**
5 | *
6 | * @param blockMap
7 | *
8 | * 1. If current block is in `column` direction and text is empty string...
9 | */
10 | const removeBlockWithClear = (
11 | blockMap: BlockNodeMap,
12 | block: ContentBlockNode | string | undefined
13 | ) => {
14 | let blockToRemove: ContentBlockNode | undefined;
15 |
16 | if (!block) {
17 | return blockMap;
18 | }
19 | if (typeof block === 'string') {
20 | blockToRemove = blockMap.get(block);
21 | } else {
22 | blockToRemove = block;
23 | }
24 |
25 | if (!blockToRemove) return blockMap;
26 |
27 | const parentKey = blockToRemove!.getParentKey();
28 |
29 | let newBlockMap = removeBlock(blockMap, block);
30 |
31 | if (!parentKey) return newBlockMap;
32 |
33 | const parentBlock = newBlockMap.get(parentKey);
34 | const parentBlockData = parentBlock?.getData();
35 | const parentDirection = parentBlockData?.get('data-direction');
36 | const parentText = parentBlock?.getText();
37 | const parentChildrenLength = parentBlock?.getCharacterList().size;
38 |
39 | if (
40 | parentDirection === 'column' &&
41 | !parentChildrenLength &&
42 | parentText === ''
43 | ) {
44 | newBlockMap = removeBlock(newBlockMap, parentBlock);
45 | }
46 |
47 | return newBlockMap;
48 | };
49 |
50 | export default removeBlockWithClear;
51 |
--------------------------------------------------------------------------------
/src/utils/block/resetSibling.ts:
--------------------------------------------------------------------------------
1 | import { ContentBlockNode } from '../../types';
2 |
3 | export default (block: ContentBlockNode) => {
4 | return block.merge({
5 | prevSibling: null,
6 | nextSibling: null,
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/src/utils/block/transfer.ts:
--------------------------------------------------------------------------------
1 | import { EditorState } from 'draft-js';
2 | import horizontalTransfer from './horizontalTransfer';
3 | import verticalTransfer from './verticalTransfer';
4 | import { Position, ContentNodeState } from '../../types';
5 |
6 | const requiredError = (prop: EditorState | string | Position) => {
7 | throw new Error(`${prop} is required in transfer function`);
8 | };
9 |
10 | const transfer = (
11 | editorState: EditorState,
12 | sourceBlockKey: string,
13 | targetBlockKey: string,
14 | position: Position
15 | ) => {
16 | if (!editorState) requiredError('editorState');
17 | if (!sourceBlockKey) requiredError('sourceBlockKey');
18 | if (!targetBlockKey) requiredError('targetBlockKey');
19 | if (!position) requiredError('position');
20 | const currentState = editorState.getCurrentContent() as ContentNodeState;
21 |
22 | let blockMap;
23 | switch (position) {
24 | case 'top':
25 | blockMap = verticalTransfer(
26 | editorState,
27 | sourceBlockKey,
28 | targetBlockKey,
29 | Position.Top
30 | );
31 | break;
32 | case 'right':
33 | blockMap = horizontalTransfer(
34 | editorState,
35 | sourceBlockKey,
36 | targetBlockKey,
37 | Position.Right
38 | );
39 | break;
40 | case 'bottom':
41 | blockMap = verticalTransfer(
42 | editorState,
43 | sourceBlockKey,
44 | targetBlockKey,
45 | Position.Bottom
46 | );
47 | break;
48 | case 'left':
49 | blockMap = horizontalTransfer(
50 | editorState,
51 | sourceBlockKey,
52 | targetBlockKey,
53 | Position.Left
54 | );
55 | break;
56 | }
57 |
58 | return currentState.merge({
59 | blockMap,
60 | });
61 | };
62 |
63 | export default transfer;
64 |
--------------------------------------------------------------------------------
/src/utils/block/updateBlockMapLinks.ts:
--------------------------------------------------------------------------------
1 | import { ContentState } from 'draft-js';
2 | import { BlockNodeMap } from '../../types';
3 |
4 | export function findLastBlockWithNullParent(
5 | contentState: ContentState
6 | ): BlockNodeMap {
7 | const blockMap = contentState.getBlockMap() as BlockNodeMap;
8 | return blockMap
9 | .reverse()
10 | .skipUntil(block => !block.getParentKey())
11 | .take(1);
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/block/verticalTransfer.ts:
--------------------------------------------------------------------------------
1 | import { EditorState } from 'draft-js';
2 | import insertBlockBefore from './insertBlockBefore';
3 | import insertBlockAfter from './insertBlockAfter';
4 | import removeBlockWithClear from './removeBlockWithClear';
5 | import { Position, BlockNodeMap } from '../../types';
6 |
7 | const verticalTransfer = (
8 | editorState: EditorState,
9 | sourceBlockKey: string,
10 | targetBlockKey: string,
11 | position: Position
12 | ): BlockNodeMap => {
13 | const currentState = editorState.getCurrentContent();
14 | let blockMap = currentState.getBlockMap() as BlockNodeMap;
15 | const sourceBlock = blockMap.get(sourceBlockKey);
16 | blockMap = removeBlockWithClear(blockMap, sourceBlockKey);
17 | // Note: targetBlock should be placed after `removeBlockWithClear`, because
18 | // its sibling has changed after `removeBlockWithClear`; `hooks.setState` may
19 | // cause loop due to the same `prevSibling` and `nextSibling`..
20 | const targetBlock = blockMap.get(targetBlockKey);
21 |
22 | if (position === 'top') {
23 | return insertBlockBefore(blockMap, targetBlock, sourceBlock);
24 | }
25 |
26 | if (position === 'bottom') {
27 | return insertBlockAfter(blockMap, targetBlock, sourceBlock);
28 | }
29 |
30 | return blockMap;
31 | };
32 |
33 | export default verticalTransfer;
34 |
--------------------------------------------------------------------------------
/src/utils/compareArray.ts:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/3115982/how-to-check-if-two-arrays-are-equal-with-javascript
2 |
3 | export default (a: string[] = [], b: string[] = []) => {
4 | if (a === b) return [];
5 |
6 | const result = [];
7 | const aLen = a.length;
8 | const bLen = b.length;
9 |
10 | for (let i = 0; i < aLen; ++i) {
11 | const blockKey = a[i];
12 | if (b.indexOf(blockKey) === -1) {
13 | result.push({
14 | op: 'remove',
15 | blockKey,
16 | });
17 | }
18 | }
19 |
20 | for (let i = 0; i < bLen; ++i) {
21 | const blockKey = b[i];
22 | if (a.indexOf(blockKey) === -1) {
23 | result.push({
24 | op: 'add',
25 | blockKey,
26 | });
27 | }
28 | }
29 |
30 | return result;
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/contentBlock.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/facebook/draft-js/blob/master/src/model/immutable/ContentState.js#L128
2 |
3 | import { ContentBlockNode } from 'types';
4 |
5 | // `%u200B` nonWidthCharacter
6 | export const hasText = (block: ContentBlockNode) =>
7 | escape(block.getText()).replace(/%u200B/g, '').length > 0;
8 |
--------------------------------------------------------------------------------
/src/utils/createEntity.ts:
--------------------------------------------------------------------------------
1 | import { RichUtils, EditorState } from 'draft-js';
2 |
3 | const createLinkAtSelection = (editorState: EditorState, url: string) => {
4 | const contentState = editorState
5 | .getCurrentContent()
6 | .createEntity('LINK', 'MUTABLE', { url });
7 | const entityKey = contentState.getLastCreatedEntityKey();
8 |
9 | return RichUtils.toggleLink(
10 | editorState,
11 | editorState.getSelection(),
12 | entityKey
13 | );
14 | };
15 |
16 | const createLinkSpanAtSelection = (editorState: EditorState) => {
17 | const selection = editorState.getSelection();
18 | const startKey = selection.getStartKey();
19 | const startOffset = selection.getStartOffset();
20 | const endOffset = selection.getEndOffset();
21 | const currentContent = editorState.getCurrentContent();
22 | const contentBlock = currentContent.getBlockForKey(startKey);
23 |
24 | const text = contentBlock.getText().slice(startOffset, endOffset);
25 |
26 | const contentState = editorState
27 | .getCurrentContent()
28 | .createEntity('LINK_SPAN', 'MUTABLE', { text });
29 | const entityKey = contentState.getLastCreatedEntityKey();
30 |
31 | return RichUtils.toggleLink(
32 | editorState,
33 | editorState.getSelection(),
34 | entityKey
35 | );
36 | };
37 |
38 | export { createLinkAtSelection, createLinkSpanAtSelection };
39 |
--------------------------------------------------------------------------------
/src/utils/draft-js/lib/keyCommandPlainBackspace.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | /**
4 | * Copyright (c) Facebook, Inc. and its affiliates.
5 | *
6 | * This source code is licensed under the MIT license found in the
7 | * LICENSE file in the root directory of this source tree.
8 | *
9 | * @format
10 | *
11 | * @emails oncall+draft_js
12 | */
13 |
14 | var EditorState = require('draft-js/lib/EditorState');
15 |
16 | var UnicodeUtils = require('fbjs/lib/UnicodeUtils');
17 |
18 | var moveSelectionBackward = require('draft-js/lib/moveSelectionBackward');
19 |
20 | var removeTextWithStrategy = require('./removeTextWithStrategy');
21 | /**
22 | * Remove the selected range. If the cursor is collapsed, remove the preceding
23 | * character. This operation is Unicode-aware, so removing a single character
24 | * will remove a surrogate pair properly as well.
25 | */
26 |
27 | function keyCommandPlainBackspace(editorState) {
28 | var afterRemoval = removeTextWithStrategy(
29 | editorState,
30 | function(strategyState) {
31 | var selection = strategyState.getSelection();
32 | var content = strategyState.getCurrentContent();
33 | var key = selection.getAnchorKey();
34 | var offset = selection.getAnchorOffset();
35 | var charBehind = content.getBlockForKey(key).getText()[offset - 1];
36 | return moveSelectionBackward(
37 | strategyState,
38 | charBehind ? UnicodeUtils.getUTF16Length(charBehind, 0) : 1
39 | );
40 | },
41 | 'backward'
42 | );
43 |
44 | if (afterRemoval === editorState.getCurrentContent()) {
45 | return editorState;
46 | }
47 |
48 | var selection = editorState.getSelection();
49 | return EditorState.push(
50 | editorState,
51 | afterRemoval.set('selectionBefore', selection),
52 | selection.isCollapsed() ? 'backspace-character' : 'remove-range'
53 | );
54 | }
55 |
56 | module.exports = keyCommandPlainBackspace;
57 |
--------------------------------------------------------------------------------
/src/utils/draft-js/lib/removeTextWithStrategy.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | /**
3 | * Copyright (c) Facebook, Inc. and its affiliates.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | * @format
9 | *
10 | * @emails oncall+draft_js
11 | */
12 |
13 | var DraftModifier = require('./DraftModifier');
14 |
15 | var gkx = require('draft-js/lib/gkx');
16 |
17 | var experimentalTreeDataSupport = gkx('draft_tree_data_support');
18 | /**
19 | * For a collapsed selection state, remove text based on the specified strategy.
20 | * If the selection state is not collapsed, remove the entire selected range.
21 | */
22 |
23 | function removeTextWithStrategy(editorState, strategy, direction) {
24 | var selection = editorState.getSelection();
25 | var content = editorState.getCurrentContent();
26 | var target = selection;
27 | var anchorKey = selection.getAnchorKey();
28 | var focusKey = selection.getFocusKey();
29 | var anchorBlock = content.getBlockForKey(anchorKey);
30 |
31 | if (experimentalTreeDataSupport) {
32 | if (direction === 'forward') {
33 | if (anchorKey !== focusKey) {
34 | // For now we ignore forward delete across blocks,
35 | // if there is demand for this we will implement it.
36 | return content;
37 | }
38 | }
39 | }
40 |
41 | if (selection.isCollapsed()) {
42 | if (direction === 'forward') {
43 | if (editorState.isSelectionAtEndOfContent()) {
44 | return content;
45 | }
46 |
47 | if (experimentalTreeDataSupport) {
48 | var isAtEndOfBlock =
49 | selection.getAnchorOffset() ===
50 | content.getBlockForKey(anchorKey).getLength();
51 |
52 | if (isAtEndOfBlock) {
53 | var anchorBlockSibling = content.getBlockForKey(
54 | anchorBlock.nextSibling
55 | );
56 |
57 | if (!anchorBlockSibling || anchorBlockSibling.getLength() === 0) {
58 | // For now we ignore forward delete at the end of a block,
59 | // if there is demand for this we will implement it.
60 | return content;
61 | }
62 | }
63 | }
64 | } else if (editorState.isSelectionAtStartOfContent()) {
65 | return content;
66 | }
67 |
68 | target = strategy(editorState);
69 |
70 | if (target === selection) {
71 | return content;
72 | }
73 | }
74 |
75 | return DraftModifier.removeRange(content, target, direction);
76 | }
77 |
78 | module.exports = removeTextWithStrategy;
79 |
--------------------------------------------------------------------------------
/src/utils/editorState.ts:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant';
2 | import { Modifier, EditorState } from 'draft-js';
3 |
4 | // https://github.com/facebook/draft-js/blob/master/src/component/handlers/edit/commands/moveSelectionForward.js#L27
5 | // https://github.com/facebook/draft-js/blob/master/src/component/handlers/edit/commands/keyCommandMoveSelectionToStartOfBlock.js
6 | export const moveSelectionForward = (
7 | editorState: EditorState,
8 | maxDistance: number
9 | ) => {
10 | const selection = editorState.getSelection(); // Should eventually make this an invariant
11 |
12 | process.env.NODE_ENV !== 'production'
13 | ? invariant(
14 | selection.isCollapsed(),
15 | 'moveSelectionForward should only be called with a collapsed SelectionState'
16 | )
17 | : void 0;
18 | const key = selection.getStartKey();
19 | const offset = selection.getStartOffset();
20 | const content = editorState.getCurrentContent();
21 | let focusOffset;
22 | const block = content.getBlockForKey(key);
23 |
24 | if (maxDistance > block.getText().length - offset) {
25 | focusOffset = block.getText().length;
26 | } else {
27 | focusOffset = offset + maxDistance;
28 | }
29 |
30 | return selection.merge({
31 | focusOffset,
32 | anchorOffset: focusOffset,
33 | });
34 | };
35 |
36 | export const splitAtLastCharacterAndForwardSelection = (
37 | editorState: EditorState
38 | ): EditorState => {
39 | const currentContent = editorState.getCurrentContent();
40 | const selection = editorState.getSelection();
41 | const endKey = selection.getEndKey();
42 | const block = currentContent.getBlockForKey(endKey);
43 | const blockSize = block.getLength();
44 | const newCurrentState = Modifier.splitBlock(
45 | currentContent,
46 | selection.merge({
47 | anchorOffset: blockSize - 1,
48 | focusOffset: blockSize - 1,
49 | })
50 | );
51 |
52 | const nextState = EditorState.push(
53 | editorState,
54 | newCurrentState,
55 | 'split-block'
56 | );
57 | return EditorState.forceSelection(
58 | nextState,
59 | moveSelectionForward(nextState, 1)
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/src/utils/event/bindEvents.ts:
--------------------------------------------------------------------------------
1 | import { Binding } from '../../types';
2 |
3 | // https://github.com/atlassian/react-beautiful-dnd/blob/master/src/view/event-bindings/bind-events.js
4 |
5 | function getOptions(
6 | shared?: AddEventListenerOptions,
7 | fromBinding?: AddEventListenerOptions
8 | ) {
9 | return {
10 | ...shared,
11 | ...fromBinding,
12 | };
13 | }
14 |
15 | export function bindEvents(
16 | el: HTMLElement | Window | Document,
17 | bindings: Binding[] | Binding,
18 | sharedOptions?: AddEventListenerOptions
19 | ) {
20 | const empty = [] as Binding[];
21 | const nextBindings = empty.concat(bindings);
22 | const unBindings = nextBindings.map(binding => {
23 | const options = getOptions(sharedOptions, binding.options);
24 | // ts-hint: https://github.com/microsoft/TypeScript/issues/28357#issuecomment-436484705
25 | el.addEventListener(
26 | binding.eventName,
27 | binding.fn as EventListener,
28 | options
29 | );
30 |
31 | return function unbind() {
32 | el.removeEventListener(
33 | binding.eventName,
34 | binding.fn as EventListener,
35 | options
36 | );
37 | };
38 | });
39 |
40 | // Return a function to unbind events
41 | return function unbindAll() {
42 | unBindings.forEach(unbind => unbind());
43 | };
44 | }
45 |
46 | // once event triggered. it will be teardown first...
47 | export function bindEventsOnce(
48 | el: HTMLElement,
49 | bindings: Binding[] | Binding,
50 | sharedOptions?: AddEventListenerOptions
51 | ) {
52 | const empty = [] as Binding[];
53 | const nextBindings = empty.concat(bindings);
54 | const unBindings = nextBindings.map(binding => {
55 | const options = getOptions(sharedOptions, binding.options);
56 | let unbind = () => {};
57 |
58 | const wrappedFn = (e: MouseEvent) => {
59 | binding.fn.call(null, e);
60 | unbind();
61 | };
62 |
63 | el.addEventListener(binding.eventName, wrappedFn as EventListener, options);
64 |
65 | unbind = () =>
66 | el.removeEventListener(
67 | binding.eventName,
68 | wrappedFn as EventListener,
69 | options
70 | );
71 |
72 | return unbind;
73 | });
74 |
75 | // Return a function to unbind events
76 | return function unbindAll() {
77 | unBindings.forEach(unbind => unbind());
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/src/utils/findNode.ts:
--------------------------------------------------------------------------------
1 | import { generateOffsetKey } from './keyHelper';
2 |
3 | export function getNodeByBlockKey(blockKey: string): HTMLElement | null {
4 | const offsetKey = generateOffsetKey(blockKey);
5 | return document.querySelector(
6 | `[data-block="true"][data-offset-key="${offsetKey}"]`
7 | );
8 | }
9 |
10 | export const getNodeByOffsetKey = (offsetKey: string): HTMLElement | null => {
11 | return document.querySelector(
12 | `[data-block="true"][data-offset-key="${offsetKey}"]`
13 | );
14 | };
15 |
16 | // https://stackoverflow.com/questions/29937768/document-queryselector-multiple-data-attributes-in-one-element
17 | // consecutive selector should not has spaces between them
18 | export const getOffsetKeyNodeChildren = (
19 | offsetKey: string
20 | ): NodeListOf | null => {
21 | return document.querySelectorAll(
22 | `[data-block="true"][data-offset-key="${offsetKey}"] div.sidebar-addon`
23 | );
24 | };
25 |
26 | export const getSelectableNodeByOffsetKey = (
27 | offsetKey: string
28 | ): Element | null => {
29 | return document.querySelector(
30 | `[data-block="true"] [data-id="${offsetKey}"] div.selectable`
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/utils/getSelectionBlockTypes.ts:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import { EditorState, DraftBlockType, ContentBlock } from 'draft-js';
3 |
4 | const { Map } = Immutable;
5 |
6 | function getSelectionBlockTypes(editorState: EditorState): DraftBlockType[] {
7 | const contentState = editorState.getCurrentContent();
8 | const selectionState = editorState.getSelection();
9 |
10 | const startKey = selectionState.getStartKey();
11 | const endKey = selectionState.getEndKey();
12 | const blockMap = contentState.getBlockMap();
13 | const blockTypes = [] as DraftBlockType[];
14 | blockMap
15 | .toSeq()
16 | .skipUntil(function(_, k) {
17 | return k === startKey;
18 | })
19 | .takeUntil(function(_, k) {
20 | return k === endKey;
21 | })
22 | .concat(Map([[endKey, blockMap.get(endKey)]]))
23 | .forEach(block => {
24 | if (!block) return;
25 | const currentBlockType = (block as ContentBlock).getType();
26 | if (blockTypes.indexOf(currentBlockType) === -1) {
27 | blockTypes.push(currentBlockType);
28 | }
29 | });
30 |
31 | return blockTypes;
32 | }
33 |
34 | export default getSelectionBlockTypes;
35 |
--------------------------------------------------------------------------------
/src/utils/infoLog.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Intentional info-level logging for clear separation from ad-hoc console debug logging.
3 | */
4 | function infoLog(...args: any[]) {
5 | console.log("**DEBUG**", ...args); // eslint-disable-line
6 | }
7 |
8 | export default infoLog;
9 |
--------------------------------------------------------------------------------
/src/utils/isBlockFocused.ts:
--------------------------------------------------------------------------------
1 | import { EditorState } from 'draft-js';
2 | import { ContentBlockNode } from '../types';
3 |
4 | const isBlockFocused = (editorState: EditorState, block: ContentBlockNode) => {
5 | const selection = editorState.getSelection();
6 | // 首先判断当前selection是否是focused
7 | if (!selection.getHasFocus()) return false;
8 |
9 | // 主要解决的是,比如block是否被focus;所以如果说selection不是`isCollapsed`的
10 | // 话,直接返回false 不再处理
11 | if (!selection.isCollapsed()) return false;
12 |
13 | const blockKey = block.getKey();
14 | const startKey = selection.getStartKey();
15 |
16 | return blockKey === startKey;
17 | };
18 |
19 | export default isBlockFocused;
20 |
--------------------------------------------------------------------------------
/src/utils/keyHelper.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import DraftOffsetKey from 'draft-js/lib/DraftOffsetKey';
3 |
4 | export function generateOffsetKey(blockKey: string): string {
5 | return DraftOffsetKey.encode(blockKey, 0, 0);
6 | }
7 |
8 | // offsetKey: erm6t-0-0
9 | export function extractBlockKeyFromOffsetKey(offsetKey: string): string {
10 | const parts = offsetKey.split('-');
11 | return parts[0];
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/moveSelectionToEnd.ts:
--------------------------------------------------------------------------------
1 | import { EditorState, SelectionState } from 'draft-js';
2 | import { BlockNodeMap, ContentBlockNode } from 'types';
3 |
4 | /**
5 | * Returns a new EditorState where the Selection is at the end.
6 | *
7 | * This ensures to mimic the textarea behaviour where the Selection is placed at
8 | * the end. This is needed when blocks (like stickers or other media) are added
9 | * without the editor having had focus yet. It still works to place the
10 | * Selection at a specific location by clicking on the text.
11 | */
12 | const moveSelectionToEnd = (editorState: EditorState) => {
13 | const content = editorState.getCurrentContent();
14 | const blockMap = content.getBlockMap() as BlockNodeMap;
15 |
16 | const key = blockMap.last().getKey();
17 | const length = blockMap.last().getLength();
18 |
19 | const selection = new SelectionState().merge({
20 | anchorKey: key,
21 | anchorOffset: length,
22 | focusKey: key,
23 | focusOffset: length,
24 | });
25 |
26 | return EditorState.acceptSelection(editorState, selection);
27 | };
28 |
29 | export default moveSelectionToEnd;
30 |
--------------------------------------------------------------------------------
/src/utils/noop.ts:
--------------------------------------------------------------------------------
1 | export default () => {};
2 |
--------------------------------------------------------------------------------
/src/utils/rect/findBlockContainsPoint.ts:
--------------------------------------------------------------------------------
1 | import { PointObject, CoordinateMap } from '../../types';
2 | // https://stackoverflow.com/questions/18295825/determine-if-point-is-within-bounding-box
3 |
4 | export default function findBlockContainsPoint(
5 | coordinateMap: CoordinateMap,
6 | point: PointObject
7 | ) {
8 | const len = coordinateMap.length;
9 |
10 | for (let i = 0; i < len; i++) {
11 | const data = coordinateMap[i];
12 | if (!data) continue;
13 | const { rect } = data;
14 | const { top, right, bottom, left } = rect;
15 | const { x, y } = point;
16 | const falsy = left < x && x < right && y > top && y < bottom;
17 |
18 | if (falsy) return data;
19 | }
20 |
21 | return null;
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/rect/getBoundingRectWithSafeArea.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/alexreardon/css-box-model/blob/master/src/index.js
2 |
3 | import { EditorState } from 'draft-js';
4 | import { getNodeByBlockKey } from '../findNode';
5 | import { generateOffsetKey } from '../keyHelper';
6 | import { SafeArea, BlockNodeMap } from '../../types';
7 |
8 | export default function getBoundingRectWithSafeArea(
9 | editorState: EditorState,
10 | safeArea = 100
11 | ) {
12 | if (!editorState) return;
13 | const currentState = editorState.getCurrentContent();
14 | const blockMap = currentState.getBlockMap() as BlockNodeMap;
15 |
16 | // mainly, used to display sidebar
17 | const shiftLeft = [] as SafeArea[];
18 | // mainly, used to display drop direction bar..
19 | const shiftRight = [] as SafeArea[];
20 |
21 | blockMap.forEach(block => {
22 | const blockKey = block.getKey();
23 | const offsetKey = generateOffsetKey(blockKey);
24 | const node = getNodeByBlockKey(blockKey);
25 | const childrenSize = block.getChildKeys().size;
26 |
27 | // node with children should be omitted.
28 | if (!node || childrenSize) return;
29 |
30 | const { top, right, bottom, left } = node.getBoundingClientRect();
31 | // right and left both should minus `safeArea`
32 |
33 | shiftLeft.push({
34 | blockKey,
35 | offsetKey,
36 | rect: {
37 | top,
38 | right: right - safeArea,
39 | bottom,
40 | left: left - safeArea,
41 | },
42 | });
43 |
44 | shiftRight.push({
45 | blockKey,
46 | offsetKey,
47 | rect: {
48 | top,
49 | right: right + safeArea,
50 | bottom,
51 | left: left + safeArea,
52 | },
53 | });
54 | });
55 |
56 | return {
57 | shiftLeft: shiftLeft.filter(v => v),
58 | shiftRight: shiftRight.filter(v => v),
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/utils/rect/getRootNode.ts:
--------------------------------------------------------------------------------
1 | import { EditorRef } from '../../types';
2 | // https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditor.react.js#L332
3 | // https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-inline-toolbar-plugin/src/components/Toolbar/index.js#L67
4 |
5 | const getRootNode = (editorRef: EditorRef) => {
6 | if (!editorRef) return;
7 |
8 | let rootNode = (editorRef.current as any).editor;
9 |
10 | while (rootNode && rootNode.className.indexOf('DraftEditor-root') === -1) {
11 | rootNode = rootNode.parentNode as any;
12 | }
13 |
14 | return rootNode;
15 | };
16 |
17 | export default getRootNode;
18 |
--------------------------------------------------------------------------------
/src/utils/rect/getSelectionBlockRect.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/facebook/draft-js/issues/45#issuecomment-189800287
2 | // https://github.com/facebook/draft-js/issues/45#issuecomment-187964504
3 |
--------------------------------------------------------------------------------
/src/utils/rect/getSelectionRect.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/facebook/draft-js/blob/master/src/component/selection/getVisibleSelectionRect.js
2 | // https://github.com/facebook/draft-js/issues/45#issuecomment-190938047
3 |
--------------------------------------------------------------------------------
/src/utils/rect/getSelectionRectRelativeToOffsetParent.ts:
--------------------------------------------------------------------------------
1 | import { getVisibleSelectionRect } from 'draft-js';
2 | import getRootNode from './getRootNode';
3 | import { EditorRef, RectObject } from '../../types';
4 |
5 | const getSelectionRectRelativeToOffsetParent = (
6 | editorRef: EditorRef
7 | ): RectObject | undefined => {
8 | const rootNode = getRootNode(editorRef);
9 | const visibleSelectionRect = getVisibleSelectionRect(window);
10 | if (!rootNode || !visibleSelectionRect) return;
11 | const rootRect = rootNode.getBoundingClientRect();
12 | const rootOffsetTop = rootNode.offsetTop;
13 | const rootOffsetLeft = rootNode.offsetLeft;
14 | const { width, height } = visibleSelectionRect;
15 |
16 | const top = rootOffsetTop + visibleSelectionRect.top - rootRect.top;
17 | const left = rootOffsetLeft + visibleSelectionRect.left - rootRect.left;
18 |
19 | return {
20 | top,
21 | left,
22 | right: left + width,
23 | bottom: top + height,
24 | width,
25 | height,
26 | };
27 | };
28 |
29 | export default getSelectionRectRelativeToOffsetParent;
30 |
--------------------------------------------------------------------------------
/src/utils/setSelectionToBlock.ts:
--------------------------------------------------------------------------------
1 | import { SelectionState, EditorState } from 'draft-js';
2 | // @ts-ignore
3 | import DraftOffsetKey from 'draft-js/lib/DraftOffsetKey';
4 | import { ContentBlockNode } from '../types';
5 |
6 | // Set selection of editor to next/previous block
7 | export default (editorState: EditorState, contentBlock: ContentBlockNode) => {
8 | const blockKey = contentBlock.getKey();
9 | // TODO verify that always a key-0-0 exists
10 | const offsetKey = DraftOffsetKey.encode(blockKey, 0, 0);
11 | const node = document.querySelectorAll(`[data-offset-key="${offsetKey}"]`)[0];
12 | // set the native selection to the node so the caret is not in the text and
13 | // the selectionState matches the native selection
14 | const selection = window.getSelection();
15 | const range = document.createRange();
16 | range.setStart(node, 0);
17 | range.setEnd(node, 0);
18 | selection!.removeAllRanges();
19 | selection!.addRange(range);
20 |
21 | return EditorState.forceSelection(
22 | editorState,
23 | new SelectionState().merge({
24 | anchorKey: blockKey,
25 | anchorOffset: 0,
26 | focusKey: blockKey,
27 | focusOffset: 0,
28 | isBackward: false,
29 | })
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/throttle.ts:
--------------------------------------------------------------------------------
1 | // ts-hint: https://github.com/github/mini-throttle/blob/master/index.ts
2 | export default function throttle(
3 | fn: (...args: T) => unknown,
4 | timeout: number
5 | ) {
6 | let isAvailable = true;
7 |
8 | return function(...args: T) {
9 | if (!isAvailable) return;
10 | isAvailable = false;
11 | setTimeout(() => {
12 | fn(...args);
13 | isAvailable = true;
14 | }, timeout);
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/src/withEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, ComponentType, FC } from 'react';
2 | import Context from './Context';
3 | import { ReturnProps, GetEditor, IWrappedComponent } from './types';
4 |
5 | function getDisplayName(
6 | WrappedComponent: ComponentType>
7 | ) {
8 | return (
9 | WrappedComponent.displayName ||
10 | WrappedComponent.name ||
11 | 'WithEditorComponent'
12 | );
13 | }
14 |
15 | // ts-hint: refer to https://medium.com/@jrwebdev/react-higher-order-component-patterns-in-typescript-42278f7590fb
16 | // and take note on `Subtract` method...
17 | function withEditor(
18 | // ts-hint: according to WrappedComponent type. it will implicate the generic type
19 | // of `T` which exclude `getEditor` props
20 | WrappedComponent: ComponentType>
21 | ): FC> {
22 | return (props: T) => {
23 | WrappedComponent.displayName = `WrappedComponent(${getDisplayName(
24 | WrappedComponent
25 | )})`;
26 | const getEditor = useContext(Context) as GetEditor;
27 | return ;
28 | };
29 | }
30 |
31 | export default withEditor;
32 |
--------------------------------------------------------------------------------
/stories/Editor.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Editor from '../src'
3 |
4 | export default {
5 | title: 'Welcome',
6 | };
7 |
8 | // By passing optional props to this story, you can control the props of the component when
9 | // you consume the story in a test.
10 | export const Default = (props?: Partial) => ;
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types"],
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "lib": ["dom", "esnext"],
6 | "importHelpers": true,
7 | "declaration": true,
8 | "sourceMap": true,
9 | "rootDir": "./src",
10 | "strict": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "noImplicitReturns": true,
14 | "noImplicitThis": false,
15 | "noFallthroughCasesInSwitch": true,
16 | "moduleResolution": "node",
17 | "baseUrl": "./",
18 | "paths": {
19 | "@": ["./"],
20 | "*": ["src/*", "node_modules/*"]
21 | },
22 | "jsx": "react",
23 | "esModuleInterop": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tsdx.config.js:
--------------------------------------------------------------------------------
1 | const postcss = require('rollup-plugin-postcss');
2 | const path = require('path')
3 |
4 | module.exports = {
5 | rollup(config, options) {
6 | config.plugins.push(
7 | postcss({
8 | // modules: true,
9 | extract: true,
10 | // Or with custom file name
11 | extract: path.resolve('dist/my-custom-file-name.css')
12 | })
13 | );
14 | return config;
15 | },
16 | };
17 |
18 | // import path from 'path'
19 | // postcss({
20 | // extract: true,
21 | // // Or with custom file name
22 | // extract: path.resolve('dist/my-custom-file-name.css')
23 | // })
--------------------------------------------------------------------------------