Created
September 14, 2019 11:56
-
-
Save kmelve/5c8eb803382d44ddc5e6c91d28e99551 to your computer and use it in GitHub Desktop.
Custom portable text / block editor for Sanity with markdown paste and stats
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component, Fragment } from 'react' | |
import { BlockEditor } from 'part:@sanity/form-builder' | |
import Switch from 'part:@sanity/components/toggles/switch' | |
import css from './BlockEditor.module.css' | |
import { handlePaste } from './handlePaste' | |
export default class CustomEditor extends Component { | |
state = { | |
customPaste: false | |
} | |
handleCustomPaste = () => { | |
this.setState({customPaste: !this.state.customPaste}) | |
} | |
render() { | |
const { value = [] } = this.props | |
const { customPaste = false } = this.state | |
const wordsPerMinute = 200 | |
const plainText = blocksToText(value) | |
const wordTokens = plainText.split(/\w+/g).filter(Boolean) | |
const characterCount = plainText.length | |
const wordCount = wordTokens.length | |
const readingTime = Math.ceil(wordCount / wordsPerMinute) | |
return ( | |
<div> | |
<BlockEditor onPaste={customPaste ? handlePaste : undefined} {...this.props} /> | |
<div className={css.infoBar}> | |
<div>🔠{characterCount} 🚾{wordCount} ⏱{readingTime} min{' '}</div> | |
<div><Switch label={`Markdown paste (${customPaste ? 'on' : 'off'})`} onChange={this.handleCustomPaste} checked={customPaste} /></div> | |
</div> | |
</div> | |
) | |
} | |
} | |
const defaults = { nonTextBehavior: 'remove' } | |
function blocksToText(blocks, opts = {}) { | |
const options = Object.assign({}, defaults, opts) | |
return blocks | |
.map(block => { | |
if (block._type !== 'block' || !block.children) { | |
return options.nonTextBehavior === 'remove' | |
? '' | |
: `[${block._type} block]` | |
} | |
return block.children.map(child => child.text).join('') | |
}) | |
.join('\n\n') | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.infoBar { | |
padding: 0.5em 0; | |
display: flex; | |
@nest & > div + div { | |
padding-left: 0.5rem; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import unified from 'unified' | |
import markdown from 'remark-parse' | |
import html from 'remark-html' | |
import blockTools from '@sanity/block-tools' | |
export async function handlePaste (input) { | |
const { event, type, path } = input | |
const text = event.clipboardData.getData('text/plain') | |
const json = event.clipboardData.getData('application/json') | |
if (text) { | |
const {contents} = await convertMDtoPortableText(text) | |
const patch = convertHTMLtoPortableText(contents, type) | |
return patch | |
} | |
// return undefined to let the defaults do the work | |
return undefined | |
} | |
async function convertMDtoPortableText (markdownContent) { | |
const PT = await unified() | |
.use(markdown) | |
.use(html) | |
.process(markdownContent) | |
return PT | |
} | |
function convertHTMLtoPortableText (html, type, path) { | |
const hasCodeType = type.of.map(({ name }) => name).includes('code') | |
if (!hasCodeType) { | |
console.log( | |
'Run `sanity install @sanity/code-input, and add `type: "code"` to your schema.' | |
) | |
} | |
if (html && hasCodeType) { | |
const blocks = blockTools.htmlToBlocks(html, type, { | |
rules: [ | |
{ | |
deserialize (el, next, block) { | |
/** | |
* `el` and `next` is DOM Elements | |
* learn all about them: | |
* https://developer.mozilla.org/en-US/docs/Web/API/Element | |
**/ | |
if ( | |
!el || | |
!el.children || | |
(el.tagName && el.tagName.toLowerCase() !== 'pre') | |
) { | |
return undefined | |
} | |
const code = el.children[0] | |
const childNodes = | |
code && code.tagName.toLowerCase() === 'code' | |
? code.childNodes | |
: el.childNodes | |
let text = '' | |
childNodes.forEach(node => { | |
text += node.textContent | |
}) | |
/** | |
* Return this as an own block (via block helper function), | |
* instead of appending it to a default block's children | |
*/ | |
return block({ | |
_type: 'code', | |
code: text | |
}) | |
} | |
} | |
] | |
}) | |
// return an insert patch | |
return { insert: blocks, path } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment