Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 83 additions & 2 deletions apps/admin/src/pages/contents/ContentEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down Expand Up @@ -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;
Expand Down
88 changes: 67 additions & 21 deletions apps/admin/src/pages/contents/ContentEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ 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,
$createTextNode,
$getRoot,
$getSelection,
$getNodeByKey,
$isElementNode,
$isRangeSelection,
$isNodeSelection,
$isTextNode,
Expand All @@ -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,
Expand Down Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -1874,7 +1876,7 @@ export default function ContentEditor() {
placeholder={<Placeholder />}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<EditorHistoryPlugin />
<ListPlugin />
<ListMaxIndentLevelPlugin maxDepth={4} />
<LinkPlugin />
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -3672,12 +3712,16 @@ function Toolbar({
return (
<div className="editor-toolbar mb-4">
<ToolbarButton
title="Geri al"
icon="undo"
title="Geri al (Ctrl/⌘ + Z)"
ariaKeyShortcuts="Control+Z Meta+Z"
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
disabled={!canUndo}
/>
<ToolbarButton
title="İleri al"
icon="redo"
title="İleri al (Ctrl + Y / Ctrl/⌘ + Shift + Z)"
ariaKeyShortcuts="Control+Y Control+Shift+Z Meta+Shift+Z"
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
disabled={!canRedo}
/>
Expand Down Expand Up @@ -3813,34 +3857,36 @@ function Toolbar({
<Divider />
<ToolbarButton
title="Sola hizala"
onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')}
onClick={() => applyBlockAlignment('left')}
active={blockFormat === 'left'}
>
</ToolbarButton>
<ToolbarButton
title="Ortala"
onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')}
onClick={() => applyBlockAlignment('center')}
active={blockFormat === 'center'}
>
</ToolbarButton>
<ToolbarButton
title="Sağa hizala"
onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')}
onClick={() => applyBlockAlignment('right')}
active={blockFormat === 'right'}
>
</ToolbarButton>
</div>
)
}

function ToolbarButton({ children, onClick, active = false, disabled = false, title }) {
function ToolbarButton({ children, onClick, active = false, disabled = false, title, ariaKeyShortcuts, icon }) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
title={title}
aria-label={title}
aria-keyshortcuts={ariaKeyShortcuts}
data-toolbar-icon={icon}
aria-pressed={active}
className={clsx('editor-toolbar__button', active && 'is-active', disabled && 'is-disabled')}
>
Expand Down
28 changes: 28 additions & 0 deletions apps/admin/src/pages/contents/nodes/ImageNode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ export class ImageNode extends DecoratorNode {

static importDOM() {
return {
figure: (domNode) => {
if (!(domNode instanceof HTMLElement) || !domNode.querySelector('img')) {
return null
}
return {
conversion: convertImageFigureElement,
priority: 3,
}
},
img: (domNode) => {
if (domNode instanceof HTMLImageElement) {
return {
Expand Down Expand Up @@ -280,6 +289,25 @@ export function $isImageNode(node) {

export { DEFAULT_IMAGE_DIMENSION }

function convertImageFigureElement(domNode) {
const image = domNode.querySelector('img')
if (!(image instanceof HTMLImageElement)) {
return null
}

const conversion = convertImageElement(image)
if (!conversion) {
return null
}

return {
...conversion,
// The caption is already stored on ImageNode. Consuming the figure's
// descendants prevents figcaption from becoming a second text block.
forChild: () => null,
}
}

function convertImageElement(domNode) {
if (!(domNode instanceof HTMLImageElement)) {
return null
Expand Down
30 changes: 30 additions & 0 deletions apps/admin/src/pages/contents/nodes/ImageNode.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { $getRoot, createEditor } from 'lexical'
import { $generateNodesFromDOM } from '@lexical/html'
import { describe, expect, it } from 'vitest'
import { $createImageNode, ImageNode } from './ImageNode.jsx'
import {
Expand Down Expand Up @@ -119,6 +120,35 @@ describe('ImageNode editable attributes', () => {
expect(attributes).toEqual(['center', '', '_blank'])
})

it('imports a figure caption only as image metadata', () => {
const editor = createEditor({ nodes: [ImageNode], onError: (error) => { throw error } })
const document = new DOMParser().parseFromString(`
<figure class="editor-image-container" data-alignment="center">
<div><img src="/team.jpg" alt="Ekip"><figcaption>Yıllık ekip buluşması</figcaption></div>
</figure>
`, 'text/html')
let result

editor.update(() => {
const nodes = $generateNodesFromDOM(editor, document)
$getRoot().append(...nodes)
const children = $getRoot().getChildren()
result = {
nodeCount: children.length,
nodeType: children[0]?.getType(),
caption: children[0]?.getCaption(),
trailingText: children.slice(1).map((node) => node.getTextContent()).join(''),
}
}, { discrete: true })

expect(result).toEqual({
nodeCount: 1,
nodeType: 'image',
caption: 'Yıllık ekip buluşması',
trailingText: '',
})
})

})

describe('image setting boundaries', () => {
Expand Down
Loading
Loading