Skip to content

Instantly share code, notes, and snippets.

@kmelve
Created September 14, 2019 11:56
Show Gist options
  • Save kmelve/5c8eb803382d44ddc5e6c91d28e99551 to your computer and use it in GitHub Desktop.
Save kmelve/5c8eb803382d44ddc5e6c91d28e99551 to your computer and use it in GitHub Desktop.
Custom portable text / block editor for Sanity with markdown paste and stats
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')
}
.infoBar {
padding: 0.5em 0;
display: flex;
@nest & > div + div {
padding-left: 0.5rem;
}
}
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