UNPKG

@accordproject/markdown-editor

Version:

A rich text editor that can read and write markdown text. Based on Slate.js.

626 lines (522 loc) 23.1 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _taggedTemplateLiteral2 = _interopRequireDefault(require("@babel/runtime/helpers/taggedTemplateLiteral")); var _react = _interopRequireWildcard(require("react")); var _reactDom = require("react-dom"); var _propTypes = _interopRequireDefault(require("prop-types")); var _styledComponents = _interopRequireDefault(require("styled-components")); var _semanticUiReact = require("semantic-ui-react"); var action = _interopRequireWildcard(require("./toolbarMethods")); var styles = _interopRequireWildcard(require("./toolbarStyles")); var tips = _interopRequireWildcard(require("./toolbarTooltip")); var CONST = _interopRequireWildcard(require("../constants")); var _LinkComponent = _interopRequireDefault(require("./LinkComponent")); var boldIcon = _interopRequireWildcard(require("../icons/bold")); var italicIcon = _interopRequireWildcard(require("../icons/italic")); var codeIcon = _interopRequireWildcard(require("../icons/code")); var quoteIcon = _interopRequireWildcard(require("../icons/open-quote")); var oListIcon = _interopRequireWildcard(require("../icons/OL")); var uListIcon = _interopRequireWildcard(require("../icons/UL")); var hyperlinkIcon = _interopRequireWildcard(require("../icons/hyperlink")); var undoIcon = _interopRequireWildcard(require("../icons/navigation-left")); var redoIcon = _interopRequireWildcard(require("../icons/navigation-right")); require("./toolbar.css"); function _templateObject4() { const data = (0, _taggedTemplateLiteral2.default)(["\nwhite-space : nowrap;\noverflow : hidden;\ntext-overflow : ellipsis;\nmax-width : 250px;\n"]); _templateObject4 = function _templateObject4() { return data; }; return data; } function _templateObject3() { const data = (0, _taggedTemplateLiteral2.default)(["\n box-sizing: border-box;\n height: 24px;\n width: 1px;\n border: 1px solid ", ";\n top: 10px;\n place-self: center;\n"]); _templateObject3 = function _templateObject3() { return data; }; return data; } function _templateObject2() { const data = (0, _taggedTemplateLiteral2.default)(["\n width: ", ";\n height: ", ";\n place-self: center;\n user-select: none !important;\n cursor: pointer;\n background-color: ", ";\n padding: ", ";\n border-radius: 5px;\n &:hover {\n background-color: ", ";\n }\n"]); _templateObject2 = function _templateObject2() { return data; }; return data; } function _templateObject() { const data = (0, _taggedTemplateLiteral2.default)(["\n position: relative;\n justify-self: center;\n width: 450px;\n background-color: ", " !important;\n"]); _templateObject = function _templateObject() { return data; }; return data; } const StyledToolbar = _styledComponents.default.div(_templateObject(), props => props.background || '#FFF'); const ToolbarIcon = _styledComponents.default.svg(_templateObject2(), props => props.width, props => props.height, props => props.background, props => props.padding, props => styles.buttonBgActive(props.hoverColor)); const VertDivider = _styledComponents.default.div(_templateObject3(), props => props.color || '#EFEFEF'); const PopupLinkWrapper = _styledComponents.default.p(_templateObject4()); /** * Object constructor for dropdown styling * @param {*} input * @return {*} a new object */ function DropdownStyle(input) { this.color = styles.buttonSymbolActive(input); this.alignSelf = 'center'; } const DropdownHeader1 = { fontSize: '25px', lineHeight: '23px', fontWeight: 'bold', color: '#122330' }; const DropdownHeader2 = { fontSize: '20px', lineHeight: '20px', fontWeight: 'bold', color: '#122330' }; const DropdownHeader3 = { fontSize: '16px', lineHeight: '16px', fontWeight: 'bold', color: '#122330' }; /* eslint-disable react/no-find-dom-node */ class FormatToolbar extends _react.default.Component { constructor() { super(); this.state = { openSetLink: false }; this.linkButtonRef = (0, _react.createRef)(); this.hyperlinkInputRef = (0, _react.createRef)(); this.onMouseDown = this.onMouseDown.bind(this); this.submitLinkForm = this.submitLinkForm.bind(this); this.removeLinkForm = this.removeLinkForm.bind(this); this.closeSetLinkForm = this.closeSetLinkForm.bind(this); this.renderLinkSetForm = this.renderLinkSetForm.bind(this); } componentDidMount() { document.addEventListener('mousedown', this.onMouseDown); } componentDidUpdate(prevProps, prevState) { // If the form is just opened, focus the Url input field if (!prevState.openSetLink && this.state.openSetLink) { this.hyperlinkInputRef.current.focus(); } // If the form is just closed, reset the values // We are not using controlled form as it will make things // more complex, thats why using DOM api if (prevState.openSetLink && !this.state.openSetLink) { const formNode = this.setLinkForm; if (formNode) formNode.reset(); // Focus back the editor this.props.editor.focus(); } } componentWillUnmount() { document.removeEventListener('mousedown', this.onMouseDown); } /** * Hides link popup conditionally */ onMouseDown(e) { /* We check if the link popup is currently open AND the click is occured somewhere other than the link popup, then, we close the popup. If we do not do that, the link popup remains opened while the user drags the mouse to select the text and gets closed once the selection finishes (i.e onmouseup) which is quite snappy. (Google docs link popup also works this way) */ const isLinkPopupOpened = this.state.openSetLink; if (isLinkPopupOpened) { // Find the link popup DOM element const popup = this.setLinkFormPopup; if (!popup) return; // Make sure the clicked element is not the popup or the // child of the popup const clickedOutsideLinkPopup = e.target !== popup && !popup.contains(e.target); if (clickedOutsideLinkPopup) { // Close the link this.closeSetLinkForm(); } } } /** * When a mark button is clicked, toggle undo or redo. */ onClickHistory(event, action) { var _this = this; return (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() { var editor; return _regenerator.default.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: editor = _this.props.editor; event.preventDefault(); if (!(action === 'undo')) { _context.next = 7; break; } _context.next = 5; return editor.undo(); case 5: _context.next = 9; break; case 7: _context.next = 9; return editor.redo(); case 9: if (editor.props.editorProps.onUndoOrRedo) editor.props.editorProps.onUndoOrRedo(editor); case 10: case "end": return _context.stop(); } }, _callee); }))(); } /** * When a mark button is clicked, toggle the current mark. */ onClickMark(event, type) { const _this$props = this.props, editor = _this$props.editor, pluginManager = _this$props.pluginManager, lockText = _this$props.lockText; if (!lockText || pluginManager.isEditable(editor, type)) { event.preventDefault(); editor.toggleMark(type); } } /** * When a link button is clicked, if the selection has a link in it, remove the link. * Otherwise, show the set link form. */ onClickLinkButton() { const _this$props2 = this.props, editor = _this$props2.editor, pluginManager = _this$props2.pluginManager, lockText = _this$props2.lockText; if (!lockText || pluginManager.isEditable(editor, 'hyperlink')) { const hasLinksBool = action.hasLinks(editor); const isOnlyLinkBool = action.isOnlyLink(editor); if (hasLinksBool && !isOnlyLinkBool) return; if (hasLinksBool && isOnlyLinkBool) { editor.unwrapInline('link'); return; } this.toggleSetLinkForm(); } } /** * When a block button is clicked, toggle the block type. */ /* eslint no-unused-expressions: 0 */ onClickBlock(event, type) { const _this$props3 = this.props, editor = _this$props3.editor, pluginManager = _this$props3.pluginManager, lockText = _this$props3.lockText; const value = editor.value; if (!lockText || pluginManager.isEditable(editor, type)) { event.preventDefault(); if (action.isClickBlockQuote(type)) { action.isSelectionList(value) ? action.transformListToBlockQuote(editor, type, value) : action.transformPtoBQSwap(editor, type); } else if (action.isClickHeading(type)) { action.hasBlock(editor, type) ? editor.setBlocks(CONST.PARAGRAPH) : editor.setBlocks(type); } else if (action.isSelectionList(value)) { action.currentList(value).type === type ? action.transformListToParagraph(editor, type) : action.transformListSwap(editor, type, value); } else if (action.isSelectionInput(value, CONST.BLOCK_QUOTE)) { action.transformBlockQuoteToList(editor, type); } else { action.transformParagraphToList(editor, type); } } } /** * Toggle the state of set link form. */ toggleSetLinkForm() { this.setState({ openSetLink: !this.state.openSetLink }); } /** * Close set link form. */ closeSetLinkForm() { this.setState({ openSetLink: false }); } /** * Apply the update to link and clear the link form. */ submitLinkForm(event, isLink) { action.applyLinkUpdate(event, this.props.editor, isLink); this.closeSetLinkForm(); this.setLinkForm.reset(); this.props.editor.focus(); } /** * Remove the link inline from the text */ removeLinkForm(event) { action.removeLink(event, this.props.editor); } /** * Render a mark-toggling toolbar button. */ renderMarkButton(type, label, icon, hi, wi, pa, vBox, classInput) { const _this$props4 = this.props, editor = _this$props4.editor, editorProps = _this$props4.editorProps; const isActive = action.hasMark(editor, type); const fillActivity = isActive ? styles.buttonSymbolActive(editorProps.BUTTON_SYMBOL_ACTIVE) : styles.buttonSymbolInactive(editorProps.BUTTON_SYMBOL_INACTIVE); const bgActivity = isActive ? styles.buttonBgActive(editorProps.BUTTON_BACKGROUND_ACTIVE) : styles.buttonBgInactiveInactive(editorProps.BUTTON_BACKGROUND_INACTIVE); const style = { borderRadius: '5px', backgroundColor: styles.tooltipBg(editorProps.TOOLTIP_BACKGROUND), color: styles.tooltipColor(editorProps.TOOLTIP) }; return _react.default.createElement(_semanticUiReact.Popup, { content: label, style: style, position: "bottom center", trigger: _react.default.createElement(ToolbarIcon, { viewBox: vBox, "aria-label": type, background: bgActivity, width: wi, height: hi, padding: pa, className: classInput, hoverColor: editorProps.BUTTON_BACKGROUND_HOVER, onMouseDown: e => e.preventDefault(), onClick: event => this.onClickMark(event, type) }, icon(fillActivity)) }); } /** * Render a block modifying button */ renderBlockButton(type, icon, hi, wi, pa, vBox, classInput, props) { const _this$props5 = this.props, editor = _this$props5.editor, editorProps = _this$props5.editorProps; const value = editor.value; const isActive = action.isSelectionInput(value, type); const fillActivity = isActive ? styles.buttonSymbolActive(editorProps.BUTTON_SYMBOL_ACTIVE) : styles.buttonSymbolInactive(editorProps.BUTTON_SYMBOL_INACTIVE); const bgActivity = isActive ? styles.buttonBgActive(editorProps.BUTTON_BACKGROUND_ACTIVE) : styles.buttonBgInactiveInactive(editorProps.BUTTON_BACKGROUND_INACTIVE); const style = { borderRadius: '5px', backgroundColor: styles.tooltipBg(editorProps.TOOLTIP_BACKGROUND), color: styles.tooltipColor(editorProps.TOOLTIP) }; return _react.default.createElement(_semanticUiReact.Popup, { content: tips.identifyBlock(type), style: style, position: "bottom center", trigger: _react.default.createElement(ToolbarIcon, (0, _extends2.default)({ viewBox: vBox, "aria-label": type, background: bgActivity, width: wi, height: hi, padding: pa, className: classInput }, props, { onClick: event => this.onClickBlock(event, type, props) }), icon(fillActivity)) }); } /** * Render form in popup to set the link. */ renderLinkSetForm() { const _calculateLinkPopupPo = (0, _LinkComponent.default)(this.props.editor, this.state.openSetLink, this.setLinkFormPopup), popupPosition = _calculateLinkPopupPo.popupPosition, popupStyle = _calculateLinkPopupPo.popupStyle; const value = this.props.editor.value; const document = value.document, selection = value.selection; const isLinkBool = action.hasLinks(this.props.editor); const selectedInlineHref = document.getClosestInline(selection.anchor.path); const selectedText = this.props.editor.value.document.getFragmentAtRange(this.props.editor.value.selection).text; return _react.default.createElement(_semanticUiReact.Ref, { innerRef: node => { this.setLinkFormPopup = node; } }, _react.default.createElement(_semanticUiReact.Popup, { context: this.linkButtonRef, content: _react.default.createElement(_semanticUiReact.Ref, { innerRef: node => { this.setLinkForm = node; } }, _react.default.createElement(_semanticUiReact.Form, { onSubmit: event => this.submitLinkForm(event, isLinkBool) }, _react.default.createElement(_semanticUiReact.Form.Field, null, _react.default.createElement("label", null, "Link Text"), _react.default.createElement(_semanticUiReact.Input, { placeholder: "Text", name: "text", defaultValue: isLinkBool && !selectedText ? this.props.editor.value.focusText.text : this.props.editor.value.fragment.text })), _react.default.createElement(_semanticUiReact.Form.Field, null, _react.default.createElement("label", null, "Link URL"), _react.default.createElement(_semanticUiReact.Input, { ref: this.hyperlinkInputRef, placeholder: 'http://example.com', defaultValue: isLinkBool && action.isOnlyLink(this.props.editor) && selectedInlineHref ? selectedInlineHref.data.get('href') : '', name: "url" })), isLinkBool && action.isOnlyLink(this.props.editor) && selectedInlineHref && _react.default.createElement(PopupLinkWrapper, null, _react.default.createElement("a", { href: selectedInlineHref.data.get('href'), target: "_blank" }, selectedInlineHref.data.get('href'))), _react.default.createElement(_semanticUiReact.Form.Field, null, _react.default.createElement(_semanticUiReact.Button, { secondary: true, floated: "right", disabled: !isLinkBool, onMouseDown: this.removeLinkForm }, "Remove"), _react.default.createElement(_semanticUiReact.Button, { primary: true, floated: "right", type: "submit" }, "Apply")))), onClose: this.closeSetLinkForm, on: "click", open: true // Keep it open always. We toggle only visibility so we can calculate its rect , position: popupPosition, style: popupStyle })); } /** * Render a link-toggling toolbar button. */ renderLinkButtonNew(type, icon, hi, wi, pa, vBox, classInput) { const _this$props6 = this.props, editor = _this$props6.editor, editorProps = _this$props6.editorProps; const isActive = action.hasLinks(editor); const fillActivity = isActive ? '#2587DA' : styles.buttonSymbolInactive(editorProps.BUTTON_SYMBOL_INACTIVE); const bgActivity = isActive ? styles.buttonBgActive(editorProps.BUTTON_BACKGROUND_ACTIVE) : styles.buttonBgInactiveInactive(editorProps.BUTTON_BACKGROUND_INACTIVE); const style = { borderRadius: '5px', backgroundColor: styles.tooltipBg(editorProps.TOOLTIP_BACKGROUND), color: styles.tooltipColor(editorProps.TOOLTIP) }; return _react.default.createElement(_semanticUiReact.Popup, { content: "Insert a link", style: style, position: "bottom center", trigger: _react.default.createElement(ToolbarIcon, { ref: this.linkButtonRef, "aria-label": type, background: bgActivity, width: wi, height: hi, padding: pa, viewBox: vBox, className: classInput, onClick: e => { e.preventDefault(); this.onClickLinkButton(); } }, icon(fillActivity)) }); } /** * Render a history-toggling toolbar button. */ renderHistoryButton(type, label, icon, hi, wi, pa, vBox, classInput) { const _this$props7 = this.props, editor = _this$props7.editor, editorProps = _this$props7.editorProps; const style = { borderRadius: '5px', backgroundColor: styles.tooltipBg(editorProps.TOOLTIP_BACKGROUND), color: styles.tooltipColor(editorProps.TOOLTIP) }; return _react.default.createElement(_semanticUiReact.Popup, { content: label, style: style, position: "bottom center", trigger: _react.default.createElement(ToolbarIcon, { "aria-label": type, background: styles.buttonBgInactiveInactive(editorProps.BUTTON_BACKGROUND_INACTIVE), width: wi, height: hi, padding: pa, viewBox: vBox, className: classInput, onClick: event => this.onClickHistory(event, type, editor) }, icon(styles.buttonSymbolInactive(editorProps.BUTTON_SYMBOL_INACTIVE))) }); } render() { const _this$props8 = this.props, pluginManager = _this$props8.pluginManager, editor = _this$props8.editor, editorProps = _this$props8.editorProps; const root = window.document.querySelector('#slate-toolbar-wrapper-id'); if (!root || this.props.editor.props.readOnly) { return null; } return (0, _reactDom.createPortal)(_react.default.createElement(StyledToolbar, { background: editorProps.TOOLBAR_BACKGROUND, className: "format-toolbar" }, _react.default.createElement(_semanticUiReact.Dropdown, { text: "Style", className: "toolbar-0x0", openOnFocus: true, simple: true, style: new DropdownStyle(editorProps.DROPDOWN_COLOR) }, _react.default.createElement(_semanticUiReact.Dropdown.Menu, null, _react.default.createElement(_semanticUiReact.Dropdown.Item, { text: "Normal", onClick: event => this.onClickBlock(event, CONST.PARAGRAPH) }), _react.default.createElement(_semanticUiReact.Dropdown.Item, { text: "Header 1", style: DropdownHeader1, onClick: event => this.onClickBlock(event, 'heading_one') }), _react.default.createElement(_semanticUiReact.Dropdown.Item, { text: "Header 2", style: DropdownHeader2, onClick: event => this.onClickBlock(event, 'heading_two') }), _react.default.createElement(_semanticUiReact.Dropdown.Item, { text: "Header 3", style: DropdownHeader3, onClick: event => this.onClickBlock(event, 'heading_three') }))), _react.default.createElement(VertDivider, { color: editorProps.DIVIDER, className: "toolbar-4x0" }), this.renderMarkButton(boldIcon.type(), boldIcon.label(), boldIcon.icon, boldIcon.height(), boldIcon.width(), boldIcon.padding(), boldIcon.vBox(), 'toolbar-0x1'), this.renderMarkButton(italicIcon.type(), italicIcon.label(), italicIcon.icon, italicIcon.height(), italicIcon.width(), italicIcon.padding(), italicIcon.vBox(), 'toolbar-0x2'), _react.default.createElement(VertDivider, { color: editorProps.DIVIDER, className: "toolbar-4x1" }), this.renderMarkButton(codeIcon.type(), codeIcon.label(), codeIcon.icon, codeIcon.height(), codeIcon.width(), codeIcon.padding(), codeIcon.vBox(), 'toolbar-1x0'), this.renderBlockButton(quoteIcon.type(), quoteIcon.icon, quoteIcon.height(), quoteIcon.width(), quoteIcon.padding(), quoteIcon.vBox(), 'toolbar-1x1'), this.renderBlockButton(uListIcon.type(), uListIcon.icon, uListIcon.height(), uListIcon.width(), uListIcon.padding(), uListIcon.vBox(), 'toolbar-1x2'), this.renderBlockButton(oListIcon.type(), oListIcon.icon, oListIcon.height(), oListIcon.width(), oListIcon.padding(), oListIcon.vBox(), 'toolbar-1x3'), _react.default.createElement(VertDivider, { color: editorProps.DIVIDER, className: "toolbar-4x2" }), this.renderLinkSetForm(), this.renderLinkButtonNew(hyperlinkIcon.type(), hyperlinkIcon.icon, hyperlinkIcon.height(), hyperlinkIcon.width(), hyperlinkIcon.padding(), hyperlinkIcon.vBox(), 'toolbar-2x1'), _react.default.createElement(VertDivider, { color: editorProps.DIVIDER, className: "toolbar-4x3" }), this.renderHistoryButton(undoIcon.type(), undoIcon.label(), undoIcon.icon, undoIcon.height(), undoIcon.width(), undoIcon.padding(), undoIcon.vBox(), 'toolbar-2x2'), this.renderHistoryButton(redoIcon.type(), redoIcon.label(), redoIcon.icon, redoIcon.height(), redoIcon.width(), redoIcon.padding(), redoIcon.vBox(), 'toolbar-2x3'), pluginManager.renderToolbar(editor)), root); } } exports.default = FormatToolbar; FormatToolbar.propTypes = { editor: _propTypes.default.object.isRequired, lockText: _propTypes.default.bool.isRequired, pluginManager: _propTypes.default.object, editorProps: _propTypes.default.shape({ BUTTON_BACKGROUND_INACTIVE: _propTypes.default.string, BUTTON_BACKGROUND_ACTIVE: _propTypes.default.string, BUTTON_BACKGROUND_HOVER: _propTypes.default.string, BUTTON_SYMBOL_INACTIVE: _propTypes.default.string, BUTTON_SYMBOL_ACTIVE: _propTypes.default.string, DROPDOWN_COLOR: _propTypes.default.string, TOOLBAR_BACKGROUND: _propTypes.default.string, TOOLTIP_BACKGROUND: _propTypes.default.string, TOOLTIP: _propTypes.default.string, DIVIDER: _propTypes.default.string }) };