import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import * as React from 'react'; import { useIsMobile } from '@strapi/admin/strapi-admin'; import { Box, Flex, IconButton, useComposedRefs } from '@strapi/design-system'; import { Drag } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { Transforms, Editor, Range } from 'slate'; import { Editable, ReactEditor } from 'slate-react'; import { styled, css } from 'styled-components'; import { ItemTypes } from '../../../../../constants/dragAndDrop.mjs'; import { DIRECTIONS, useDragAndDrop } from '../../../../../hooks/useDragAndDrop.mjs'; import { getTranslation } from '../../../../../utils/translations.mjs'; import { decorateCode } from './Blocks/Code.mjs'; import { useBlocksEditorContext } from './BlocksEditor.mjs'; import { useConversionModal } from './BlocksToolbar.mjs'; import { getEntries } from './utils/types.mjs'; const StyledEditable = styled(Editable)` // The outline style is set on the wrapper with :focus-within outline: none; display: flex; flex-direction: column; gap: ${({ theme })=>theme.spaces[3]}; height: 100%; // For fullscreen align input in the center with fixed width width: ${(props)=>props.$isExpandedMode ? '512px' : '100%'}; margin: auto; font-size: 1.6rem; ${({ theme })=>theme.breakpoints.medium} { font-size: 1.4rem; } > *:last-child { padding-bottom: ${({ theme })=>theme.spaces[3]}; } `; const Wrapper = styled(Box)` position: ${({ $isOverDropTarget })=>$isOverDropTarget && 'relative'}; `; const DropPlaceholder = styled(Box)` position: absolute; right: 0; // Show drop placeholder 8px above or below the drop target ${({ dragDirection, theme, placeholderMargin })=>css` top: ${dragDirection === DIRECTIONS.UPWARD && `-${theme.spaces[placeholderMargin]}`}; bottom: ${dragDirection === DIRECTIONS.DOWNWARD && `-${theme.spaces[placeholderMargin]}`}; `} `; const DragItem = styled(Flex)` // Style each block rendered using renderElement() & > [data-slate-node='element'] { width: 100%; opacity: inherit; } // Set the visibility of drag button [role='button'] { visibility: ${(props)=>props.$dragVisibility}; opacity: inherit; } &[aria-disabled='true'] { user-drag: none; } `; const DragIconButton = styled(IconButton)` user-select: none; display: flex; align-items: center; justify-content: center; border: none; border-radius: ${({ theme })=>theme.borderRadius}; padding-left: ${({ theme })=>theme.spaces[0]}; padding-right: ${({ theme })=>theme.spaces[0]}; padding-top: ${({ theme })=>theme.spaces[1]}; padding-bottom: ${({ theme })=>theme.spaces[1]}; visibility: hidden; cursor: grab; opacity: inherit; margin-top: ${(props)=>props.$dragHandleTopMargin ?? 0}; &:hover { background: ${({ theme })=>theme.colors.neutral100}; } &:active { cursor: grabbing; background: ${({ theme })=>theme.colors.neutral150}; } &[aria-disabled='true'] { visibility: hidden; } svg { min-width: ${({ theme })=>theme.spaces[3]}; path { fill: ${({ theme })=>theme.colors.neutral500}; } } `; const DragAndDropElement = ({ children, index, setDragDirection, dragDirection, dragHandleTopMargin })=>{ const { editor, disabled, name: name1, setLiveText } = useBlocksEditorContext('DragAndDropElement'); const { formatMessage } = useIntl(); const [dragVisibility, setDragVisibility] = React.useState('hidden'); const isDragAndDropEnabled = !disabled; const handleMoveBlock = React.useCallback((newIndex, currentIndex)=>{ Transforms.moveNodes(editor, { at: currentIndex, to: newIndex }); // Add 1 to the index for the live text message const currentIndexPosition = [ currentIndex[0] + 1, ...currentIndex.slice(1) ]; const newIndexPosition = [ newIndex[0] + 1, ...newIndex.slice(1) ]; setLiveText(formatMessage({ id: getTranslation('components.Blocks.dnd.reorder'), defaultMessage: '{item}, moved. New position in the editor: {position}.' }, { item: `${name1}.${currentIndexPosition.join(',')}`, position: `${newIndexPosition.join(',')} of ${editor.children.length}` })); }, [ editor, formatMessage, name1, setLiveText ]); const [{ handlerId, isDragging, isOverDropTarget, direction }, blockRef, dropRef, dragRef] = useDragAndDrop(isDragAndDropEnabled, { type: `${ItemTypes.BLOCKS}_${name1}`, index, item: { index, displayedValue: children }, onDropItem (currentIndex, newIndex) { if (newIndex) handleMoveBlock(newIndex, currentIndex); } }); const composedBoxRefs = useComposedRefs(blockRef, dropRef); // Set Drag direction before loosing state while dragging React.useEffect(()=>{ if (direction) { setDragDirection(direction); } }, [ direction, setDragDirection ]); // On selection change hide drag handle React.useEffect(()=>{ setDragVisibility('hidden'); }, [ editor.selection ]); return /*#__PURE__*/ jsxs(Wrapper, { ref: composedBoxRefs, $isOverDropTarget: isOverDropTarget, children: [ isOverDropTarget && /*#__PURE__*/ jsx(DropPlaceholder, { borderStyle: "solid", borderColor: "secondary200", borderWidth: "2px", width: "calc(100% - 24px)", marginLeft: "auto", dragDirection: dragDirection, // For list items placeholder reduce the margin around placeholderMargin: children.props.as && children.props.as === 'li' ? 1 : 2 }), isDragging ? /*#__PURE__*/ jsx(CloneDragItem, { dragHandleTopMargin: dragHandleTopMargin, children: children }) : /*#__PURE__*/ jsxs(DragItem, { ref: dragRef, "data-handler-id": handlerId, gap: 2, paddingLeft: 2, alignItems: "start", onDragStart: isDragAndDropEnabled ? (event)=>{ const target = event.target; const currentTarget = event.currentTarget; // Dragging action should only trigger drag event when button is dragged, however update styles on the whole dragItem. if (target.getAttribute('role') !== 'button') { event.preventDefault(); } else { // Setting styles using dragging state is not working, so set it on current target element as nodes get dragged currentTarget.style.opacity = '0.5'; } } : undefined, onDragEnd: isDragAndDropEnabled ? (event)=>{ const currentTarget = event.currentTarget; currentTarget.style.opacity = '1'; } : undefined, onMouseEnter: ()=>setDragVisibility('visible'), onFocusCapture: ()=>setDragVisibility('visible'), onBlurCapture: ()=>setDragVisibility('hidden'), onMouseLeave: ()=>setDragVisibility('hidden'), "aria-disabled": disabled, $dragVisibility: dragVisibility, children: [ /*#__PURE__*/ jsx(DragIconButton, { tag: "div", contentEditable: false, role: "button", tabIndex: 0, withTooltip: false, label: formatMessage({ id: getTranslation('components.DragHandle-label'), defaultMessage: 'Drag' }), onClick: (e)=>e.stopPropagation(), "aria-disabled": disabled, disabled: disabled, draggable: isDragAndDropEnabled, // For some blocks top margin added to drag handle to align at the text level $dragHandleTopMargin: dragHandleTopMargin, children: /*#__PURE__*/ jsx(Drag, { color: "primary500" }) }), children ] }) ] }); }; // To prevent applying opacity to the original item being dragged, display a cloned element without opacity. const CloneDragItem = ({ children, dragHandleTopMargin })=>{ const { formatMessage } = useIntl(); return /*#__PURE__*/ jsxs(DragItem, { gap: 2, paddingLeft: 2, alignItems: "start", $dragVisibility: "visible", children: [ /*#__PURE__*/ jsx(DragIconButton, { tag: "div", role: "button", withTooltip: false, label: formatMessage({ id: getTranslation('components.DragHandle-label'), defaultMessage: 'Drag' }), $dragHandleTopMargin: dragHandleTopMargin, children: /*#__PURE__*/ jsx(Drag, { color: "neutral600" }) }), children ] }); }; const baseRenderLeaf = (props, modifiers)=>{ // Recursively wrap the children for each active modifier const wrappedChildren = getEntries(modifiers).reduce((currentChildren, modifierEntry)=>{ const [name1, modifier] = modifierEntry; if (props.leaf[name1]) { return modifier.renderLeaf(currentChildren); } return currentChildren; }, props.children); return /*#__PURE__*/ jsx("span", { ...props.attributes, className: props.leaf.className, children: wrappedChildren }); }; const baseRenderElement = ({ props, blocks, editor, dragDirection, setDragDirection, isMobile })=>{ const { element } = props; const blockMatch = Object.values(blocks).find((block)=>block?.matchNode(element)); const block = blockMatch || blocks.paragraph; if (!block) { return /*#__PURE__*/ jsx(Fragment, {}); } const nodePath = ReactEditor.findPath(editor, element); const isDraggable = block.isDraggable?.(element) ?? true; if (!isDraggable || isMobile) { return block.renderElement(props); } return /*#__PURE__*/ jsx(DragAndDropElement, { index: nodePath, setDragDirection: setDragDirection, dragDirection: dragDirection, dragHandleTopMargin: block.dragHandleTopMargin, children: block.renderElement(props) }); }; const dragNoop = ()=>true; const BlocksContent = ({ placeholder, ariaLabelId })=>{ const { editor, disabled, blocks, modifiers, setLiveText, isExpandedMode, flushPendingFormSync } = useBlocksEditorContext('BlocksContent'); const isMobile = useIsMobile(); const blocksRef = React.useRef(null); const { formatMessage } = useIntl(); const [dragDirection, setDragDirection] = React.useState(null); const { modalElement, handleConversionResult } = useConversionModal(); // Create renderLeaf function based on the modifiers store const renderLeaf = React.useCallback((props)=>baseRenderLeaf(props, modifiers), [ modifiers ]); const handleMoveBlocks = (editor, event)=>{ if (!editor.selection) return; const start = Range.start(editor.selection); const currentIndex = [ start.path[0] ]; let newIndexPosition = 0; if (event.key === 'ArrowUp') { newIndexPosition = currentIndex[0] > 0 ? currentIndex[0] - 1 : currentIndex[0]; } else { newIndexPosition = currentIndex[0] < editor.children.length - 1 ? currentIndex[0] + 1 : currentIndex[0]; } const newIndex = [ newIndexPosition ]; if (newIndexPosition !== currentIndex[0]) { Transforms.moveNodes(editor, { at: currentIndex, to: newIndex }); setLiveText(formatMessage({ id: getTranslation('components.Blocks.dnd.reorder'), defaultMessage: '{item}, moved. New position in the editor: {position}.' }, { item: `${name}.${currentIndex[0] + 1}`, position: `${newIndex[0] + 1} of ${editor.children.length}` })); event.preventDefault(); } }; // Create renderElement function base on the blocks store const renderElement = React.useCallback((props)=>baseRenderElement({ props, blocks, editor, dragDirection, setDragDirection, isMobile }), [ blocks, editor, dragDirection, isMobile, setDragDirection ]); const checkSnippet = (event)=>{ // Get current text block if (!editor.selection) { return; } const [textNode, textNodePath] = Editor.node(editor, editor.selection.anchor.path); // Narrow the type to a text node if (Editor.isEditor(textNode) || textNode.type !== 'text') { return; } // Don't check for snippets if we're not at the start of a block if (textNodePath.at(-1) !== 0) { return; } // Check if the text node starts with a known snippet const blockMatchingSnippet = Object.values(blocks).find((block)=>{ return block?.snippets?.includes(textNode.text); }); if (blockMatchingSnippet?.handleConvert) { // Prevent the space from being created and delete the snippet event.preventDefault(); Transforms.delete(editor, { distance: textNode.text.length, unit: 'character', reverse: true }); // Convert the selected block const maybeRenderModal = blockMatchingSnippet.handleConvert(editor); handleConversionResult(maybeRenderModal); } }; const handleEnter = (event)=>{ if (!editor.selection) { return; } const selectedNode = editor.children[editor.selection.anchor.path[0]]; const selectedBlock = Object.values(blocks).find((block)=>block?.matchNode(selectedNode)); if (!selectedBlock) { return; } // Allow forced line breaks when shift is pressed if (event.shiftKey && selectedNode.type !== 'image') { Transforms.insertText(editor, '\n'); return; } // Check if there's an enter handler for the selected block if (selectedBlock.handleEnterKey) { selectedBlock.handleEnterKey(editor); } else { blocks.paragraph?.handleEnterKey(editor); } }; const handleBackspaceEvent = (event)=>{ if (!editor.selection) { return; } const selectedNode = editor.children[editor.selection.anchor.path[0]]; const selectedBlock = Object.values(blocks).find((block)=>block?.matchNode(selectedNode)); if (!selectedBlock) { return; } if (selectedBlock.handleBackspaceKey) { selectedBlock.handleBackspaceKey(editor, event); } }; const handleTab = (event)=>{ if (!editor.selection) { return; } const selectedNode = editor.children[editor.selection.anchor.path[0]]; const selectedBlock = Object.values(blocks).find((block)=>block?.matchNode(selectedNode)); if (!selectedBlock) { return; } event.preventDefault(); if (event.shiftKey && selectedBlock.handleShiftTab) { // Handle Shift+Tab (unindent) selectedBlock.handleShiftTab(editor); } else if (!event.shiftKey && selectedBlock.handleTab) { // Handle Tab (indent) selectedBlock.handleTab(editor); } }; const handleKeyboardShortcuts = (event)=>{ const isCtrlOrCmd = event.metaKey || event.ctrlKey; if (isCtrlOrCmd) { // Check if there's a modifier to toggle Object.values(modifiers).forEach((value)=>{ if (value.isValidEventKey(event)) { event.preventDefault(); value.handleToggle(editor); return; } }); if (event.shiftKey && [ 'ArrowUp', 'ArrowDown' ].includes(event.key)) { handleMoveBlocks(editor, event); } } }; const handleKeyDown = (event)=>{ // Find the right block-specific handlers for enter and backspace key presses switch(event.key){ case 'Enter': if (!event.nativeEvent.isComposing) { event.preventDefault(); return handleEnter(event); } break; case 'Backspace': return handleBackspaceEvent(event); case 'Tab': return handleTab(event); case 'Escape': return ReactEditor.blur(editor); } handleKeyboardShortcuts(event); // Check if a snippet was triggered if (event.key === ' ') { checkSnippet(event); } }; /** * scrollSelectionIntoView : Slate's default method to scroll a DOM selection into the view, * thats shifting layout for us when there is a overflowY:scroll on the viewport. * We are overriding it to check if the selection is not fully within the visible area of the editor, * we use scrollBy one line to the bottom */ const handleScrollSelectionIntoView = React.useCallback(()=>{ if (!editor.selection || !blocksRef.current) { return; } const domRange = ReactEditor.toDOMRange(editor, editor.selection); const domRect = domRange.getBoundingClientRect(); const editorRect = blocksRef.current.getBoundingClientRect(); // Check if the selection is not fully within the visible area of the editor if (domRect.top < editorRect.top || domRect.bottom > editorRect.bottom) { // Scroll by one line to the bottom blocksRef.current.scrollBy({ top: 28, behavior: 'smooth' }); } }, [ editor ]); return /*#__PURE__*/ jsxs(Box, { ref: blocksRef, grow: 1, width: "100%", overflow: "auto", fontSize: 2, background: "neutral0", color: "neutral800", lineHeight: 6, paddingLeft: { initial: 4, medium: 0 }, paddingRight: 7, paddingTop: 6, paddingBottom: 3, children: [ /*#__PURE__*/ jsx(StyledEditable, { "aria-labelledby": ariaLabelId, readOnly: disabled, placeholder: placeholder, $isExpandedMode: isExpandedMode, decorate: decorateCode, renderElement: renderElement, renderLeaf: renderLeaf, onKeyDown: handleKeyDown, scrollSelectionIntoView: handleScrollSelectionIntoView, onBlur: flushPendingFormSync, // As we have our own handler to drag and drop the elements returing true will skip slate's own event handler onDrop: dragNoop, onDragStart: dragNoop }), modalElement ] }); }; export { BlocksContent }; //# sourceMappingURL=BlocksContent.mjs.map