diff --git a/apps/admin/src/pages/contents/ContentEditor.css b/apps/admin/src/pages/contents/ContentEditor.css index d805df8..94f616c 100644 --- a/apps/admin/src/pages/contents/ContentEditor.css +++ b/apps/admin/src/pages/contents/ContentEditor.css @@ -206,11 +206,11 @@ cursor: pointer; } -.editor-toolbar__button[title='Geri al']::before { +.editor-toolbar__button[data-toolbar-icon='undo']::before { background-image: url('./assets/icons/undo.svg'); } -.editor-toolbar__button[title='İleri al']::before { +.editor-toolbar__button[data-toolbar-icon='redo']::before { background-image: url('./assets/icons/redo.svg'); } @@ -1791,6 +1791,87 @@ body.editor-image-dialog-open .editor-image-context-toolbar { border: none; } +.table-text-alignment { + --table-align-surface: #ffffff; + --table-align-surface-active: #dbeafe; + --table-align-ink: #64748b; + --table-align-ink-active: #1d4ed8; + --table-align-border: #e2e8f0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 12px; +} + +.table-text-alignment__label { + color: #374151; + font-size: 12px; + font-weight: 600; +} + +.table-text-alignment__controls { + display: inline-grid; + grid-template-columns: repeat(3, 32px); + gap: 4px; + padding: 4px; + border: 1px solid var(--table-align-border); + border-radius: 8px; + background: #f8fafc; +} + +.table-text-alignment__button { + display: inline-flex; + width: 32px; + height: 28px; + align-items: center; + justify-content: center; + border: 0; + border-radius: 6px; + color: var(--table-align-ink); + background: transparent; +} + +.table-text-alignment__button:hover, +.table-text-alignment__button:focus-visible { + color: var(--table-align-ink-active); + background: var(--table-align-surface); + outline: none; + box-shadow: 0 0 0 2px #bfdbfe; +} + +.table-text-alignment__button.is-active { + color: var(--table-align-ink-active); + background: var(--table-align-surface-active); +} + +.table-text-alignment__icon { + display: flex; + width: 14px; + flex-direction: column; + gap: 2px; +} + +.table-text-alignment__icon i { + display: block; + width: 14px; + height: 1.5px; + border-radius: 999px; + background: currentColor; +} + +.table-text-alignment__icon i:nth-child(2) { + width: 9px; +} + +.table-text-alignment__icon--center i:nth-child(2) { + align-self: center; +} + +.table-text-alignment__icon--right i:nth-child(2) { + align-self: flex-end; +} + /* Color Picker Styles */ .table-action-item-with-submenu { position: relative; diff --git a/apps/admin/src/pages/contents/ContentEditor.jsx b/apps/admin/src/pages/contents/ContentEditor.jsx index de5dd85..02ce1cc 100644 --- a/apps/admin/src/pages/contents/ContentEditor.jsx +++ b/apps/admin/src/pages/contents/ContentEditor.jsx @@ -20,7 +20,6 @@ import { LexicalComposer } from '@lexical/react/LexicalComposer' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' import { ContentEditable } from '@lexical/react/LexicalContentEditable' import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary' -import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' import { $createParagraphNode, @@ -28,6 +27,7 @@ import { $getRoot, $getSelection, $getNodeByKey, + $isElementNode, $isRangeSelection, $isNodeSelection, $isTextNode, @@ -36,7 +36,6 @@ import { CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, - FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, REDO_COMMAND, SELECTION_CHANGE_COMMAND, @@ -72,6 +71,7 @@ import TableHoverActionsPlugin from './plugins/TableHoverActionsPlugin.jsx' import TableCellResizerPlugin from './plugins/TableCellResizerPlugin.jsx' import TableSelectionPlugin from './plugins/TableSelectionPlugin.jsx' import TableCellFocusPlugin from './plugins/TableCellFocusPlugin.jsx' +import EditorHistoryPlugin from './plugins/EditorHistoryPlugin.jsx' import ImagePlugin, { INSERT_IMAGE_COMMAND } from './plugins/ImagePlugin.jsx' import ImageHandlersPlugin from './plugins/ImageHandlersPlugin.jsx' import VideoPlugin, { INSERT_VIDEO_COMMAND } from './plugins/VideoPlugin.jsx' @@ -90,7 +90,9 @@ import { $createTableWithDimensions, $createTableNode, $createTableRowNode, - $createTableCellNode + $createTableCellNode, + $isTableCellNode, + $setTableCellTextAlignment, } from './nodes/TableNode.jsx' import { PhotoIcon, TrashIcon } from '@heroicons/react/24/outline' import MediaPickerModal from './components/MediaPickerModal.jsx' @@ -1874,7 +1876,7 @@ export default function ContentEditor() { placeholder={} ErrorBoundary={LexicalErrorBoundary} /> - + @@ -3196,6 +3198,23 @@ const BLOCK_OPTIONS = [ { value: 'code', label: 'Kod Bloğu' }, ] +function getNearestEditableBlock(node) { + if (!node) return null + + let current = $isElementNode(node) ? node : node.getParent() + while (current) { + if ($isTableCellNode(current)) { + return current + } + if ($isElementNode(current) && !current.isInline()) { + return current + } + current = current.getParent() + } + + return null +} + function Toolbar({ openMediaPicker = null, isTableSelectorOpen = false, @@ -3428,6 +3447,35 @@ function Toolbar({ [editor, blockType] ) + const applyBlockAlignment = useCallback((alignment) => { + editor.update(() => { + const selection = $getSelection() + if (!$isRangeSelection(selection)) return + + const targets = new Map() + selection.getNodes().forEach((node) => { + const target = getNearestEditableBlock(node) + if (target) { + targets.set(target.getKey(), target) + } + }) + + // A collapsed caret in an empty block may not yield a node list. + if (targets.size === 0) { + const target = getNearestEditableBlock(selection.anchor.getNode()) + if (target) targets.set(target.getKey(), target) + } + + targets.forEach((target) => { + if ($isTableCellNode(target)) { + $setTableCellTextAlignment(target, alignment) + } else { + target.setFormat(alignment) + } + }) + }) + }, [editor]) + const updateToolbar = useCallback(() => { editor.getEditorState().read(() => { const selection = $getSelection() @@ -3444,17 +3492,9 @@ function Toolbar({ }) const anchorNode = selection.anchor.getNode() - let element = null - - if (anchorNode.getType() === 'root') { - element = anchorNode.getLastChild() - if (!element) { - setBlockType('paragraph') - setBlockFormat('left') - } - } else { - element = anchorNode.getTopLevelElementOrThrow() - } + let element = anchorNode.getType() === 'root' + ? anchorNode.getLastChild() + : getNearestEditableBlock(anchorNode) if (!element || element.getType() === 'root') { element = element?.getLastChild() || null @@ -3672,12 +3712,16 @@ function Toolbar({ return (
editor.dispatchCommand(UNDO_COMMAND, undefined)} disabled={!canUndo} /> editor.dispatchCommand(REDO_COMMAND, undefined)} disabled={!canRedo} /> @@ -3813,19 +3857,19 @@ function Toolbar({ editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')} + onClick={() => applyBlockAlignment('left')} active={blockFormat === 'left'} > editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')} + onClick={() => applyBlockAlignment('center')} active={blockFormat === 'center'} > editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')} + onClick={() => applyBlockAlignment('right')} active={blockFormat === 'right'} > @@ -3833,7 +3877,7 @@ function Toolbar({ ) } -function ToolbarButton({ children, onClick, active = false, disabled = false, title }) { +function ToolbarButton({ children, onClick, active = false, disabled = false, title, ariaKeyShortcuts, icon }) { return ( + ))} +
+ + +
+