import React, {useState, useMemo} from 'react' import {centerLayoutDecorator} from '../.storybook/decorators' import {Toolbar, ToolbarButton, ToolbarDivider, ToolbarButtonProps} from './toolbar' import {Typography} from '../layout/typography' import {Link} from '../data/link' import { MaterialIconFormatBold, MaterialIconFormatItalic, MaterialIconFormatUnderlined, MaterialIconFormatStrikethrough, MaterialIconLooksOneOutlined, MaterialIconLooksTwoOutlined, MaterialIconLooks3Outlined, MaterialIconFormatListBulleted, MaterialIconFormatListNumbered, MaterialIconLink } from '@karma.run/icons' import {createEditor, Node, Element, Editor} from 'slate' import { Slate, Editable, withReact, useSlate, RenderElementProps, RenderMarkProps } from 'slate-react' import {withHistory} from 'slate-history' import {withSchema, SchemaRule} from 'slate-schema' export default { component: Toolbar, title: 'Navigation|Toolbar', decorators: [centerLayoutDecorator(0.8)] } enum RichtextNodeType { H1 = 'heading-one', H2 = 'heading-two', H3 = 'heading-three', Paragraph = 'paragraph', UnorderedList = 'unordered-list', OrderedList = 'ordered-list', ListItem = 'list-item', Link = 'link' } enum RichtextMarkType { Bold = 'bold', Italic = 'italic', Underline = 'underline', Strikethrough = 'strikethrough' } function renderElement({attributes, children, element}: RenderElementProps) { switch (element.type) { case RichtextNodeType.H1: return ( {children} ) case RichtextNodeType.H2: return ( {children} ) case RichtextNodeType.H3: return ( {children} ) case RichtextNodeType.Paragraph: return ( {children} ) case RichtextNodeType.UnorderedList: return case RichtextNodeType.OrderedList: return
    {children}
case RichtextNodeType.ListItem: return
  • {children}
  • case RichtextNodeType.Link: return {children} } } function renderMark({attributes, children, mark}: RenderMarkProps) { switch (mark.type) { case RichtextMarkType.Bold: return {children} case RichtextMarkType.Italic: return {children} case RichtextMarkType.Underline: return {children} case RichtextMarkType.Strikethrough: return {children} } } const schema: SchemaRule[] = [ { for: 'node', match: 'editor', validate: { children: [ { match: [ ([node]) => node.type === RichtextNodeType.H1 || node.type === RichtextNodeType.H2 || node.type === RichtextNodeType.H3 || node.type === RichtextNodeType.UnorderedList || node.type === RichtextNodeType.OrderedList || node.type === RichtextNodeType.Paragraph ] } ] }, normalize: (editor, error) => { const {code, path} = error switch (code) { case 'child_invalid': Editor.setNodes(editor, {type: RichtextNodeType.Paragraph}, {at: path}) break } } }, { for: 'node', match: ([node]) => node.type === RichtextNodeType.UnorderedList || node.type === RichtextNodeType.OrderedList, validate: { children: [{match: [([node]) => node.type === RichtextNodeType.ListItem]}] }, normalize: (editor, error) => { const {code, path} = error switch (code) { case 'child_invalid': Editor.setNodes(editor, {type: RichtextNodeType.ListItem}, {at: path}) break } } } ] export const Default = () => { const [value, setValue] = useState(mockRichTextValue) const [hasFocus, setFocus] = useState(false) const editor = useMemo( () => withSchema(withRichText(withHistory(withReact(createEditor()))), schema), [] ) return ( setValue(nodes)}> <> setFocus(true)} onBlur={() => setFocus(false)} placeholder="Start writing..." renderElement={renderElement} renderMark={renderMark} /> ) } interface SlateBlockButtonProps extends ToolbarButtonProps { readonly blockType: RichtextNodeType } function SlateBlockButton({icon, blockType}: SlateBlockButtonProps) { const editor = useSlate() return ( { e.preventDefault() editor.exec({type: 'toggle_block', block: blockType}) }} /> ) } function SlateLinkButton() { const editor = useSlate() return ( { e.preventDefault() if (isBlockActive(editor, RichtextNodeType.Link)) { editor.exec({type: 'remove_link'}) } else { const url = window.prompt('Enter the URL of the link:') if (!url) return editor.exec({type: 'insert_link', url}) } }} /> ) } interface SlateMarkButtonProps extends ToolbarButtonProps { readonly markType: RichtextMarkType } function SlateMarkButton({icon, markType}: SlateMarkButtonProps) { const editor = useSlate() return ( { e.preventDefault() editor.exec({type: 'toggle_mark', mark: markType}) }} /> ) } function isBlockActive(editor: Editor, type: RichtextNodeType) { const {selection} = editor if (!selection) return false const match = Editor.match(editor, selection, {type}) return !!match } function isMarkActive(editor: Editor, type: RichtextMarkType) { const marks = Editor.activeMarks(editor) const isActive = marks.some(mark => mark.type === type) return isActive } function unwrapLink(editor: Editor) { Editor.unwrapNodes(editor, {match: {type: RichtextNodeType.Link}}) } function wrapLink(editor: Editor, url: string) { if (isBlockActive(editor, RichtextNodeType.Link)) { unwrapLink(editor) } const link = {type: 'link', url, children: []} Editor.wrapNodes(editor, link, {split: true}) Editor.collapse(editor, {edge: 'end'}) } function withRichText(editor: Editor): Editor { const {exec, isInline} = editor editor.isInline = node => (node.type === RichtextNodeType.Link ? true : isInline(node)) editor.exec = command => { if (command.type === 'insert_link') { const {url} = command if (editor.selection) { wrapLink(editor, url) } return } if (command.type === 'remove_link') { unwrapLink(editor) } if (command.type === 'toggle_block') { const {block: type} = command const isActive = isBlockActive(editor, type) const isListType = type === RichtextNodeType.UnorderedList || type === RichtextNodeType.OrderedList Editor.unwrapNodes(editor, {match: {type: RichtextNodeType.UnorderedList}}) Editor.unwrapNodes(editor, {match: {type: RichtextNodeType.OrderedList}}) const newType = isActive ? RichtextNodeType.Paragraph : isListType ? RichtextNodeType.ListItem : type Editor.setNodes(editor, {type: newType}) if (!isActive && isListType) { Editor.wrapNodes(editor, {type, children: []}) } return } if (command.type === 'toggle_mark') { const {mark: type} = command const isActive = isMarkActive(editor, type) const cmd = isActive ? 'remove_mark' : 'add_mark' editor.exec({type: cmd, mark: {type}}) return } exec(command) } return editor } const mockRichTextValue: Element[] = [ { type: RichtextNodeType.H1, children: [ { text: 'This is a H1', marks: [] } ] }, { type: RichtextNodeType.H2, children: [ { text: 'This is a H2', marks: [] } ] }, { type: RichtextNodeType.H3, children: [ { text: 'This is a H3', marks: [] } ] }, { type: RichtextNodeType.Paragraph, children: [ { text: "Since it's rich text, you can do things like turn a selection of text ", marks: [] }, { text: 'bold', marks: [{type: RichtextMarkType.Bold}] }, { text: ', or ', marks: [] }, { text: 'italic', marks: [{type: RichtextMarkType.Italic}] }, { text: '!', marks: [] } ] }, { type: RichtextNodeType.Paragraph, children: [ { text: 'In addition to block nodes, you can create inline nodes, like ', marks: [] }, { type: RichtextNodeType.Link, url: 'http://google.ch', children: [{text: 'links', marks: []}] }, { text: '!', marks: [] } ] }, { type: RichtextNodeType.UnorderedList, children: [ { type: RichtextNodeType.ListItem, children: [ { text: 'Bullet one', marks: [] } ] }, { type: RichtextNodeType.ListItem, children: [ { text: 'Bullet two', marks: [{type: RichtextMarkType.Bold}, {type: RichtextMarkType.Italic}] } ] } ] }, { type: RichtextNodeType.OrderedList, children: [ { type: RichtextNodeType.ListItem, children: [ { text: 'Number one', marks: [] } ] }, { type: RichtextNodeType.ListItem, children: [ { text: 'Number two', marks: [{type: RichtextMarkType.Bold}] } ] } ] } ]