diff --git a/app/components/textEditor/editor.js b/app/components/textEditor/editor.js new file mode 100644 index 00000000..f5e9fe81 --- /dev/null +++ b/app/components/textEditor/editor.js @@ -0,0 +1,127 @@ +import defaultTheme from "./theme/defaultTheme"; +import { LexicalComposer } from "@lexical/react/LexicalComposer"; +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; +import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; +import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; +import ToolbarPlugin from "./plugins/ToolbarPlugin"; +import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { TableCellNode, TableNode, TableRowNode } from "@lexical/table"; +import { ListItemNode, ListNode } from "@lexical/list"; +import { CodeHighlightNode, CodeNode } from "@lexical/code"; +import { AutoLinkNode, LinkNode } from "@lexical/link"; +import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; +import { ListPlugin } from "@lexical/react/LexicalListPlugin"; +import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; +import { TRANSFORMERS } from "@lexical/markdown"; +import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin"; +import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"; +import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin"; +import AutoLinkPlugin from "./plugins/AutoLinkPlugin"; +import styles from "../../styles/Editor.module.css" +import { $generateHtmlFromNodes } from '@lexical/html'; +import {$generateNodesFromDOM} from '@lexical/html'; +import { useEffect, } from "react"; +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {$insertNodes} from 'lexical' +import PropTypes from 'prop-types' + +function Placeholder() { + return
Enter some rich text...
; +} + + + + +const editorConfig = { + // The editor theme + theme: defaultTheme, + // Handling of errors during update + onError(error) { + throw error; + }, + // Any custom nodes go here + nodes: [ + HeadingNode, + ListNode, + ListItemNode, + QuoteNode, + CodeNode, + CodeHighlightNode, + TableNode, + TableCellNode, + TableRowNode, + AutoLinkNode, + LinkNode + ], + + +}; + + +function SetInitValue(props) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + editor.update(() => { + if(!props.value) return; + const parser = new DOMParser(); + const doc = parser.parseFromString(props.value, "text/html"); + const nodes = $generateNodesFromDOM(editor , doc); + $insertNodes(nodes); + }); + }, [props.value]); + + return null; +} + +export default function Editor({ name, value, onChange, placeholder }) { + + const on_Change = (editorState, editor) => { + editor.update(() => { + const htmlString = $generateHtmlFromNodes(editor, null); + const customFormData = { + target: { + name: name, + value: htmlString + }} + onChange(customFormData); + }); + } + + return ( + +
+ +
+ } + placeholder={ + placeholder ? placeholder : } + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + + + + + + + +
+
+
+ ); +} + +Editor.propTypes = { + name: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string, + placeholder: PropTypes.string +}; diff --git a/app/components/textEditor/plugins/AutoLinkPlugin.js b/app/components/textEditor/plugins/AutoLinkPlugin.js new file mode 100644 index 00000000..3475c917 --- /dev/null +++ b/app/components/textEditor/plugins/AutoLinkPlugin.js @@ -0,0 +1,34 @@ +import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin"; + +const URL_MATCHER = /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; + +const EMAIL_MATCHER = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/; + +const MATCHERS = [ + (text) => { + const match = URL_MATCHER.exec(text); + return ( + match && { + index: match.index, + length: match[0].length, + text: match[0], + url: match[0] + } + ); + }, + (text) => { + const match = EMAIL_MATCHER.exec(text); + return ( + match && { + index: match.index, + length: match[0].length, + text: match[0], + url: `mailto:${match[0]}` + } + ); + } +]; + +export default function PlaygroundAutoLinkPlugin() { + return ; +} diff --git a/app/components/textEditor/plugins/CodeHighlightPlugin.js b/app/components/textEditor/plugins/CodeHighlightPlugin.js new file mode 100644 index 00000000..f9318052 --- /dev/null +++ b/app/components/textEditor/plugins/CodeHighlightPlugin.js @@ -0,0 +1,11 @@ +import { registerCodeHighlighting } from "@lexical/code"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { useEffect } from "react"; + +export default function CodeHighlightPlugin() { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + return registerCodeHighlighting(editor); + }, [editor]); + return null; +} diff --git a/app/components/textEditor/plugins/ListMaxIndentLevelPlugin.js b/app/components/textEditor/plugins/ListMaxIndentLevelPlugin.js new file mode 100644 index 00000000..657535e7 --- /dev/null +++ b/app/components/textEditor/plugins/ListMaxIndentLevelPlugin.js @@ -0,0 +1,68 @@ +import { $getListDepth, $isListItemNode, $isListNode } from "@lexical/list"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { + $getSelection, + $isElementNode, + $isRangeSelection, + INDENT_CONTENT_COMMAND, + COMMAND_PRIORITY_HIGH +} from "lexical"; +import { useEffect } from "react"; + +function getElementNodesInSelection(selection) { + const nodesInSelection = selection.getNodes(); + + if (nodesInSelection.length === 0) { + return new Set([ + selection.anchor.getNode().getParentOrThrow(), + selection.focus.getNode().getParentOrThrow() + ]); + } + + return new Set( + nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow())) + ); +} + +function isIndentPermitted(maxDepth) { + const selection = $getSelection(); + + if (!$isRangeSelection(selection)) { + return false; + } + + const elementNodesInSelection = getElementNodesInSelection(selection); + + let totalDepth = 0; + + for (const elementNode of elementNodesInSelection) { + if ($isListNode(elementNode)) { + totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth); + } else if ($isListItemNode(elementNode)) { + const parent = elementNode.getParent(); + if (!$isListNode(parent)) { + throw new Error( + "ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent." + ); + } + + totalDepth = Math.max($getListDepth(parent) + 1, totalDepth); + } + } + + return totalDepth <= maxDepth; +} + +export default function ListMaxIndentLevelPlugin({ maxDepth }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerCommand( + INDENT_CONTENT_COMMAND, + () => !isIndentPermitted(maxDepth ?? 7), + COMMAND_PRIORITY_HIGH + ); + }, [editor, maxDepth]); + + return null; +} diff --git a/app/components/textEditor/plugins/ToolbarPlugin.js b/app/components/textEditor/plugins/ToolbarPlugin.js new file mode 100644 index 00000000..2c327e18 --- /dev/null +++ b/app/components/textEditor/plugins/ToolbarPlugin.js @@ -0,0 +1,438 @@ +import { + $isCodeNode, getDefaultCodeLanguage +} from "@lexical/code"; +import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; +import { + $isListNode, INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, ListNode, REMOVE_LIST_COMMAND +} from "@lexical/list"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { + $isHeadingNode +} from "@lexical/rich-text"; +import { + $isAtNodeEnd, $isParentElementRTL +} from "@lexical/selection"; +import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import { + $getSelection, + $isRangeSelection, CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, REDO_COMMAND, SELECTION_CHANGE_COMMAND, UNDO_COMMAND +} from "lexical"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +import styles from "../../../styles/Editor.module.css"; + +import { + BsArrowClockwise, BsArrowCounterclockwise, BsCode, BsLink, BsListOl, BsListUl, BsPen, BsTextCenter, BsTextLeft, BsTextRight, BsTypeBold, BsTypeItalic, BsTypeStrikethrough, BsTypeUnderline +} from "react-icons/bs"; +import DOMPurify from "dompurify"; + +const LowPriority = 1; + +function Divider() { + return
; +} + +function positionEditorElement(editor, rect) { + if (rect === null) { + editor.style.opacity = "0"; + editor.style.top = "-1000px"; + editor.style.left = "-1000px"; + } else { + editor.style.opacity = "1"; + editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`; + editor.style.left = `${rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2 + }px`; + } +} + +function FloatingLinkEditor({ editor }) { + const editorRef = useRef(null); + const inputRef = useRef(null); + const mouseDownRef = useRef(false); + const [linkUrl, setLinkUrl] = useState(""); + const [isEditMode, setEditMode] = useState(false); + const [lastSelection, setLastSelection] = useState(null); + + const updateLinkEditor = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent)) { + setLinkUrl(parent.getURL()); + } else if ($isLinkNode(node)) { + setLinkUrl(node.getURL()); + } else { + setLinkUrl(""); + } + } + const editorElem = editorRef.current; + const nativeSelection = window.getSelection(); + const activeElement = document.activeElement; + + if (editorElem === null) { + return; + } + + const rootElement = editor.getRootElement(); + if ( + selection !== null && + !nativeSelection.isCollapsed && + rootElement !== null && + rootElement.contains(nativeSelection.anchorNode) + ) { + const domRange = nativeSelection.getRangeAt(0); + let rect; + if (nativeSelection.anchorNode === rootElement) { + let inner = rootElement; + while (inner.firstElementChild != null) { + inner = inner.firstElementChild; + } + rect = inner.getBoundingClientRect(); + } else { + rect = domRange.getBoundingClientRect(); + } + + if (!mouseDownRef.current) { + positionEditorElement(editorElem, rect); + } + setLastSelection(selection); + } else if (!activeElement || activeElement.className !== styles.link_input) { + positionEditorElement(editorElem, null); + setLastSelection(null); + setEditMode(false); + setLinkUrl(""); + } + + return true; + }, [editor]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + updateLinkEditor(); + }); + }), + + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + () => { + updateLinkEditor(); + return true; + }, + LowPriority + ) + ); + }, [editor, updateLinkEditor]); + + useEffect(() => { + editor.getEditorState().read(() => { + updateLinkEditor(); + }); + + }, [editor, updateLinkEditor]); + + useEffect(() => { + if (isEditMode && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditMode]); + + return ( +
+ {isEditMode ? ( + { + setLinkUrl(event.target.value); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (lastSelection !== null) { + if (linkUrl !== "") { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl); + } + setEditMode(false); + } + } else if (event.key === "Escape") { + event.preventDefault(); + setEditMode(false); + } + }} + /> + ) : ( + <> +
+ + {DOMPurify.sanitize(linkUrl)} + + event.preventDefault()} + onClick={() => { + setEditMode(true); + }} + > + + + +
+ + )} +
+ ); +} + +function getSelectedNode(selection) { + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = selection.anchor.getNode(); + const focusNode = selection.focus.getNode(); + if (anchorNode === focusNode) { + return anchorNode; + } + const isBackward = selection.isBackward(); + if (isBackward) { + return $isAtNodeEnd(focus) ? anchorNode : focusNode; + } else { + return $isAtNodeEnd(anchor) ? focusNode : anchorNode; + } +} + +export default function ToolbarPlugin() { + const [editor] = useLexicalComposerContext(); + const toolbarRef = useRef(null); + const [canUndo, setCanUndo] = useState(false); + const [canRedo, setCanRedo] = useState(false); + const [blockType, setBlockType] = useState("paragraph"); + const [selectedElementKey, setSelectedElementKey] = useState(null); + const [isRTL, setIsRTL] = useState(false); + const [isLink, setIsLink] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + const [isUnderline, setIsUnderline] = useState(false); + const [isStrikethrough, setIsStrikethrough] = useState(false); + const [isCode, setIsCode] = useState(false); + + const updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + const element = + anchorNode.getKey() === "root" + ? anchorNode + : anchorNode.getTopLevelElementOrThrow(); + const elementKey = element.getKey(); + const elementDOM = editor.getElementByKey(elementKey); + if (elementDOM !== null) { + setSelectedElementKey(elementKey); + if ($isListNode(element)) { + const parentList = $getNearestNodeOfType(anchorNode, ListNode); + const type = parentList ? parentList.getTag() : element.getTag(); + setBlockType(type); + } else { + const type = $isHeadingNode(element) + ? element.getTag() + : element.getType(); + setBlockType(type); + if ($isCodeNode(element)) { + setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage()); + } + } + } + // Update text format + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + setIsUnderline(selection.hasFormat("underline")); + setIsStrikethrough(selection.hasFormat("strikethrough")); + setIsCode(selection.hasFormat("code")); + setIsRTL($isParentElementRTL(selection)); + + // Update links + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent) || $isLinkNode(node)) { + setIsLink(true); + } else { + setIsLink(false); + } + } + }, [editor]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, newEditor) => { + updateToolbar(); + return false; + }, + LowPriority + ), + editor.registerCommand( + CAN_UNDO_COMMAND, + (payload) => { + setCanUndo(payload); + return false; + }, + LowPriority + ), + editor.registerCommand( + CAN_REDO_COMMAND, + (payload) => { + setCanRedo(payload); + return false; + }, + LowPriority + ) + ); + }, [editor, updateToolbar]); + + const insertLink = useCallback(() => { + if (!isLink) { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); + } else { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [editor, isLink]); + + return ( +
+ { + editor.dispatchCommand(UNDO_COMMAND); + }} + className={"" + styles.toolbar_tools} + aria-label="Undo" + > + + + { + editor.dispatchCommand(REDO_COMMAND); + }} + className={"" + styles.toolbar_tools} + aria-label="Redo" + > + + + + <> + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"); + }} + className={styles.toolbar_tools + " " + (isBold ? styles.active : "")} + aria-label="Format Bold" + > + + + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"); + }} + className={styles.toolbar_tools + " " + (isItalic ? styles.active : "")} + aria-label="Format Italics" + > + + + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"); + }} + className={styles.toolbar_tools + " " + (isUnderline ? styles.active : "")} + aria-label="Format Underline" + > + + + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"); + }} + className={ + styles.toolbar_tools + " " + (isStrikethrough ? styles.active : "") + } + aria-label="Format Strikethrough" + > + + + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"); + }} + className={styles.toolbar_tools + " " + (isCode ? styles.active : "")} + aria-label="Insert Code" + > + + + + + + {isLink && + createPortal(, document.body)} + + { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "left"); + }} + className={styles.toolbar_tools} + aria-label="Left Align" + > + + + { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "center"); + }} + className={styles.toolbar_tools} + aria-label="Center Align" + > + + + { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "right"); + }} + className={"" + styles.toolbar_tools} + aria-label="Right Align" + > + + + { + (blockType === "ul") ? editor.dispatchCommand(REMOVE_LIST_COMMAND) : editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND) + } + } > + + + { + (blockType === "ol") ? editor.dispatchCommand(REMOVE_LIST_COMMAND) : editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND) + } + } > + + + +
+ ); +} diff --git a/app/components/textEditor/theme/defaultTheme.js b/app/components/textEditor/theme/defaultTheme.js new file mode 100644 index 00000000..dacc3e9a --- /dev/null +++ b/app/components/textEditor/theme/defaultTheme.js @@ -0,0 +1,10 @@ +const defaultTheme = { + ltr: 'text-left', + rtl: 'text-right', + text:{ + underline : 'text-decoration-underline', + } + }; + +export default defaultTheme; + \ No newline at end of file diff --git a/app/package.json b/app/package.json index 702c977c..5f3e7748 100644 --- a/app/package.json +++ b/app/package.json @@ -14,6 +14,7 @@ "@apollo/client": "^3.5.9", "@jitsi/react-sdk": "^1.0.0", "@jitsi/web-sdk": "^0.2.0", + "@lexical/react": "^0.8.0", "@rocket.chat/fuselage": "^0.31.3", "@rocket.chat/fuselage-hooks": "^0.31.3", "@rocket.chat/fuselage-polyfills": "^0.31.3", @@ -29,6 +30,7 @@ "gapi-cjs": "^1.0.3", "graphql": "^16.3.0", "js-cookie": "^3.0.1", + "lexical": "^0.8.0", "lodash": "^4.17.21", "marked": "^4.0.12", "next": "^12.2.5", diff --git a/app/styles/Editor.module.css b/app/styles/Editor.module.css new file mode 100644 index 00000000..0a7fc825 --- /dev/null +++ b/app/styles/Editor.module.css @@ -0,0 +1,344 @@ +.other h2 { + font-size: 18px; + color: #444; + margin-bottom: 7px; +} + +.other a { + color: #777; + text-decoration: underline; + font-size: 14px; +} + +.other ul { + padding: 0; + margin: 0; + list-style-type: none; +} + +.editor_container { + margin: 20px auto 20px auto; + border-radius: 2px; + color: #000; + position: relative; + line-height: 20px; + font-weight: 400; + text-align: left; + border-right : 1px solid black; + border-left : 1px solid black; + border-bottom: 1px solid black; +} + +.editor_inner { + background: #fff; + position: relative; +} + +.editor_input { + min-height: 150px; + resize: none; + font-size: 15px; + caret-color: rgb(5, 5, 5); + position: relative; + tab-size: 1; + outline: 0; + padding: 15px 10px; + caret-color: #444; +} + +.editor_placeholder { + color: #999; + overflow: hidden; + position: absolute; + text-overflow: ellipsis; + top: 15px; + left: 10px; + font-size: 15px; + user-select: none; + display: inline-block; + pointer-events: none; +} + +.editor_text_bold { + font-weight: bold; +} + +.editor_text_italic { + font-style: italic; +} + +.editor_text_underline { + text-decoration: underline; +} + +.editor_text_strikethrough { + text-decoration: line-through; +} + +.editor_text_underlineStrikethrough { + text-decoration: underline line-through; +} + +.editor_text_code { + background-color: rgb(240, 242, 245); + padding: 1px 0.25rem; + font-family: Menlo, Consolas, Monaco, monospace; + font-size: 94%; +} + +.editor_link { + color: rgb(33, 111, 219); + text-decoration: none; +} + +.editor_code { + background-color: rgb(240, 242, 245); + font-family: Menlo, Consolas, Monaco, monospace; + display: block; + padding: 8px 8px 8px 52px; + line-height: 1.53; + font-size: 13px; + margin: 0; + margin-top: 8px; + margin-bottom: 8px; + tab-size: 2; + /* white-space: pre; */ + overflow-x: auto; + position: relative; +} + +.editor_code:before { + content: attr(data-gutter); + position: absolute; + background-color: #eee; + left: 0; + top: 0; + border-right: 1px solid #ccc; + padding: 8px; + color: #777; + white-space: pre-wrap; + text-align: right; + min-width: 25px; +} +.editor_code:after { + content: attr(data-highlight-language); + top: 0; + right: 3px; + padding: 3px; + font-size: 10px; + text-transform: uppercase; + position: absolute; + color: rgba(0, 0, 0, 0.5); +} + +.editor_tokenComment { + color: slategray; +} + +.editor_tokenPunctuation { + color: #999; +} + +.editor_tokenProperty { + color: #905; +} + +.editor_tokenSelector { + color: #690; +} + +.editor_tokenOperator { + color: #9a6e3a; +} + +.editor_tokenAttr { + color: #07a; +} + +.editor_tokenVariable { + color: #e90; +} + +.editor_tokenFunction { + color: #dd4a68; +} + +.editor_paragraph { + margin: 0; + margin-bottom: 8px; + position: relative; +} + +.editor_paragraph:last-child { + margin-bottom: 0; +} + +.editor_heading_h1 { + font-size: 24px; + color: rgb(5, 5, 5); + font-weight: 400; + margin: 0; + margin-bottom: 12px; + padding: 0; +} + +.editor_heading_h2 { + font-size: 15px; + color: rgb(101, 103, 107); + font-weight: 700; + margin: 0; + margin-top: 10px; + padding: 0; + text-transform: uppercase; +} + +.editor_quote { + margin: 0; + margin-left: 20px; + font-size: 15px; + color: rgb(101, 103, 107); + border-left-color: rgb(206, 208, 212); + border-left-width: 4px; + border-left-style: solid; + padding-left: 16px; +} + +.editor_list_ol { + padding: 0; + margin: 0; + margin-left: 16px; +} + +.editor_list_ul { + padding: 0; + margin: 0; + margin-left: 16px; +} + +.editor_listitem { + margin: 8px 32px 8px 32px; +} + +.editor_nested_listitem { + list-style-type: none; +} + +.toolbar { + display: flex; + margin-bottom: 1px; + background: #ffffff; + border-top: 1px solid black; + border-bottom: 1px solid black; + border-radius: 2px; + vertical-align: middle; + flex-wrap: wrap; +} + +.toolbar_tools { + border: 0; + display: flex; + background: none; + padding: 8px; + cursor: pointer; + vertical-align: middle; + align-self: center; + background: white; + border-radius: 4px; + margin:2px; + margin-right : 2px; +} + +.toolbar_tools:hover { + background-color: #d7d2d2; +} +.active { + background: #9e9e9e; +} + + +.link_editor { + position: absolute; + z-index: 100; + top: -10000px; + left: -10000px; + margin-top: -6px; + max-width: 300px; + width: 100%; + opacity: 0; + background-color: #fff; + box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3); + border-radius: 8px; + transition: opacity 0.5s; +} + +.link_editor .link_input { + display: block; + width: calc(100% - 24px); + box-sizing: border-box; + margin: 8px 12px; + padding: 8px 12px; + border-radius: 15px; + background-color: #eee; + font-size: 15px; + color: rgb(5, 5, 5); + border: 0; + outline: 0; + position: relative; + font-family: inherit; +} + +.link_editor span.link_edit { + display: flex; + align-items: center; + align-self: center; + background-size: 16px; + background-position: center; + background-repeat: no-repeat; + width: 35px; + vertical-align: -0.25em; + position: absolute; + right: 0; + top: 0; + bottom: 0; + cursor: pointer; +} + +.link_editor .link_input a { + color: rgb(33, 111, 219); + text-decoration: none; + display: block; + white-space: nowrap; + overflow: hidden; + margin-right: 30px; + text-overflow: ellipsis; +} + +.link_editor .link_input a:hover { + text-decoration: underline; +} + +.link_editor .button { + width: 20px; + height: 20px; + display: inline-block; + padding: 6px; + border-radius: 8px; + cursor: pointer; + margin: 0 2px; +} + +.link_editor .button.hovered { + width: 20px; + height: 20px; + display: inline-block; + background-color: #eee; +} + +.link_editor .button i, +.actions i { + background-size: contain; + display: inline-block; + height: 20px; + width: 20px; + vertical-align: -0.25em; +} \ No newline at end of file