UNPKG

@octopusdeploy/design-system-components

Version:
173 lines (172 loc) • 9.23 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MarkdownEditor = MarkdownEditor; const jsx_runtime_1 = require("react/jsx-runtime"); const css_1 = require("@emotion/css"); const design_system_icons_1 = require("@octopusdeploy/design-system-icons"); const design_system_tokens_1 = require("@octopusdeploy/design-system-tokens"); const react_1 = require("react"); const Button_1 = require("../../Button"); const Link_1 = require("../../Link"); const Note_1 = require("../Note"); const Text_1 = require("../Text"); const MarkdownProvider_1 = require("./MarkdownProvider"); function MarkdownEditor({ onChange, onValidate, hideMarkdownToggle, restrictHeight, label, error, accessibleName, value, disabled, placeholder, textInputRef, controlsInitiallyVisible = false, autoFocus = false, bottomNoteText = null, }) { const { inputRef, actions: markdownActions } = useTextInputMarkdownActions(value); const handleChange = (value) => { const valid = onValidate?.(value) ?? true; if (valid) { onChange?.(value); } }; //We currently treat the visibility for the markdown controls as a non-controlled prop. Ideally this //would be controlled, however, that would also mean that every consumer has to manage this state. const [showMarkdownControls, setShowMarkdownControls] = (0, react_1.useState)(controlsInitiallyVisible); const markdownTitle = `${showMarkdownControls ? "Hide" : "Show"} markdown controls`; const showMarkdownControlsToggle = hideMarkdownToggle ? null : ((0, jsx_runtime_1.jsx)(Button_1.Button, { label: markdownTitle, importance: "quiet", onClick: (e) => { e.preventDefault(); setShowMarkdownControls((prev) => !prev); } })); return ((0, jsx_runtime_1.jsxs)("div", { className: (0, css_1.cx)(styles.markdownEditorContainer, restrictHeight && styles.restrictedHeight), children: [(0, jsx_runtime_1.jsxs)("div", { className: (0, css_1.cx)({ [styles.markdownEditor]: showMarkdownControls }), children: [showMarkdownControls && ((0, jsx_runtime_1.jsx)("div", { className: styles.toolbar, children: markdownActions.map(({ type, action }, index) => ((0, jsx_runtime_1.jsx)(MarkdownEditorButton, { type: type, onClick: action }, `${index}_${type}`))) })), (0, jsx_runtime_1.jsx)(Text_1.Text, { textInputRef: (text) => { if (textInputRef) { textInputRef(text); } inputRef.current = text; }, value: value ? value : "", onChange: handleChange, error: error, label: label, placeholder: placeholder, multiline: true, accessibleName: accessibleName, minRows: showMarkdownControls ? 3 : 2, showBorder: false, className: styles.inputContainer, hideUnderline: true, autoFocus: autoFocus, disabled: disabled })] }), bottomNoteText && (0, jsx_runtime_1.jsx)(Note_1.Note, { children: bottomNoteText }), showMarkdownControls ? ((0, jsx_runtime_1.jsxs)("div", { className: styles.markdownControlsContainer, children: [showMarkdownControlsToggle, (0, jsx_runtime_1.jsx)(Link_1.ExternalLink, { href: "MasteringMarkdown", children: "Markdown instructions" })] })) : (showMarkdownControlsToggle)] })); } function useTextInputMarkdownActions(value) { const inputRef = (0, react_1.useRef)(null); const getSelection = (0, react_1.useCallback)(() => { const selection = inputRef.current?.getSelection(); return selection ?? { start: null, end: null }; }, []); const setInputState = (value, selectionStart, selectionEnd) => { const selection = { start: selectionStart, end: selectionEnd, }; inputRef.current?.setValueAndSelection(selection, value); inputRef.current?.focus(); }; const getInputState = () => { const selection = getSelection(); return new MarkdownProvider_1.MarkdownProvider(value ? value : "", selection.start ?? 0, selection.end ?? 0); }; const bold = () => { getInputState().insertBefore("**").insertAfter("**").emptySelectionText("Bold text").apply(setInputState); }; const italic = () => { getInputState().insertBefore("_").insertAfter("_").emptySelectionText("Italic text").apply(setInputState); }; const bullet = () => { getInputState().surroundWithNewlines().insertBefore("- ").emptySelectionText("List item").apply(setInputState); }; const number = () => { getInputState().surroundWithNewlines().insertBefore("1. ").emptySelectionText("List item").apply(setInputState); }; const quote = () => { getInputState().surroundWithNewlines().insertBefore("> ").insertAfterNewlineInSelection("> ").emptySelectionText("Quoted text").apply(setInputState); }; const code = () => { getInputState().surroundWithNewlines().insertBefore("```\n").insertAfter("\n```").emptySelectionText("Code").apply(setInputState); }; const link = () => { const url = prompt("Please enter a url for the link:", "http://"); if (url) { getInputState().insertBefore("[").insertAfter(`](${url})`).emptySelectionText("Enter link description here").apply(setInputState); } }; const image = () => { const url = prompt("Please enter a url for the image:", "http://"); if (url) { getInputState().insertBefore("![").insertAfter(`](${url})`).emptySelectionText("Enter image description here").apply(setInputState); } }; return { inputRef, actions: [ { type: "Bold", action: bold }, { type: "Bulleted List", action: bullet }, { type: "Code", action: code }, { type: "Link", action: link }, { type: "Numeric list", action: number }, { type: "Italic", action: italic }, { type: "Image", action: image }, { type: "Quotes", action: quote }, ], }; } const styles = { markdownEditorContainer: (0, css_1.css)({ position: "relative", width: "100%", }), markdownEditor: (0, css_1.css)({ outline: `1px solid ${design_system_tokens_1.themeTokens.color.textField.border.default}`, marginBottom: design_system_tokens_1.space[8], "&:hover": { outline: `2px solid ${design_system_tokens_1.themeTokens.color.textField.border.hover}`, }, "&:has(div.MuiInputBase-root.MuiInput-root.Mui-focused)": { outline: `2px solid ${design_system_tokens_1.themeTokens.color.textField.border.active}`, }, "& > div:has(div.MuiInputBase-root.MuiInput-root)": { padding: "10px 10px 0 10px", }, "& .MuiInput-underline::before, & .MuiInput-underline:hover:not(.Mui-disabled)::before": { borderBottom: "none", }, "& .MuiInput-underline::after": { borderBottom: "none", }, "& label.MuiFormLabel-root.MuiInputLabel-root.MuiInputLabel-formControl": { transform: "translate(0, 8px) scale(1)", "&.Mui-focused": { display: "none", }, "&.MuiFormLabel-filled": { display: "none", }, }, "& .MuiInputBase-root.MuiInput-root.MuiInputBase-formControl.MuiInput-formControl.MuiInputBase-multiline.MuiInput-multiline": { margin: 0, padding: 0, }, }), markdownControlsContainer: (0, css_1.css)({ display: "flex", flexDirection: "row", alignItems: "baseline", button: { marginRight: design_system_tokens_1.space[8], }, }), restrictedHeight: (0, css_1.css)({ maxHeight: "15rem", }), toolbar: (0, css_1.css)({ color: design_system_tokens_1.themeTokens.color.icon.secondary, display: "flex", flexDirection: "row", justifyContent: "flex-end", backgroundColor: design_system_tokens_1.themeTokens.color.background.secondary.default, padding: `0 ${design_system_tokens_1.space[8]}`, gap: design_system_tokens_1.space[12], }), inputContainer: (0, css_1.css)({ width: "100%", }), }; const markdownEditorIconMap = { "Bulleted List": (0, jsx_runtime_1.jsx)(design_system_icons_1.ListIcon, { size: 20 }), Bold: (0, jsx_runtime_1.jsx)(design_system_icons_1.BoldIcon, { size: 20 }), Code: (0, jsx_runtime_1.jsx)(design_system_icons_1.CodeIcon, { size: 20 }), Italic: (0, jsx_runtime_1.jsx)(design_system_icons_1.ItalicIcon, { size: 20 }), Link: (0, jsx_runtime_1.jsx)(design_system_icons_1.LinkIcon, { size: 20 }), "Numeric list": (0, jsx_runtime_1.jsx)(design_system_icons_1.ListOlIcon, { size: 20 }), Image: (0, jsx_runtime_1.jsx)(design_system_icons_1.ImageIcon, { size: 20 }), Quotes: (0, jsx_runtime_1.jsx)(design_system_icons_1.QuotesIcon, { size: 20 }), }; function MarkdownEditorButton({ onClick, type }) { return (0, jsx_runtime_1.jsx)(Button_1.Button, { importance: "ghost", onClick: onClick, accessibleName: type, icon: markdownEditorIconMap[type], "aria-hidden": "true", title: type }); }