Html Toolbar yetenekli hale getirdik

This commit is contained in:
Sedat ÖZTÜRK 2026-09-24 11:42:44 +03:00
parent c102114a3a
commit 9ceacf5d64
17 changed files with 185 additions and 249 deletions

View file

@ -921,7 +921,7 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir.
| `dxColorBox` | Renk | String (16) | `editAlphaChannel`, `keyStep` |
| `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` |
| `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` |
| `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode` |
| `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.tabs`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode`, `tableResizing.enabled`, `tableContextMenu.enabled`. Boş `editorOptions` da tam araç çubuğu, dosyadan/URL'den resim, resim boyutlandırma ve tablo araçlarıyla gelir (`ui/src/views/shared/htmlEditorOptions.ts`); yazılan değer varsayılanın üstüne tek seviye birleştirilir, yalnızca farkı yaz. |
| `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* |
| `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` |
@ -2086,7 +2086,7 @@ Dizi elemanları yaprak sayılmaz; `toolbar.items` tek bir `json` alanıdır.
| Görsel | `uploadUrl`\*, `fileFieldName`, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\* | `dxImageUpload`, `dxImageViewer` |
| Seçim | `text` (`dxCheckBox`), `switchedOnText`/`switchedOffText` (`dxSwitch`), `layout` (`dxRadioGroup`), `editAlphaChannel`/`keyStep` (`dxColorBox`) | ilgili editör |
| Kaydırıcı | `min`, `max`, `step`, `tooltip.enabled`, `tooltip.showMode`, `showRange` | `dxSlider`, `dxRangeSlider` |
| HTML | `valueType`, `toolbar.multiline`, `toolbar.items`, `mediaResizing.enabled`, `imageUpload.fileUploadMode`, `imageUpload.uploadUrl` | `dxHtmlEditor` |
| HTML | `valueType`, `toolbar.multiline`, `toolbar.items`, `mediaResizing.enabled`, `imageUpload.tabs`, `imageUpload.fileUploadMode`, `imageUpload.uploadUrl`, `tableResizing.enabled`, `tableContextMenu.enabled` | `dxHtmlEditor` |
| Liste sütunu | `format` (metin hâli), `encodeHtml`, `buttons` | hepsi — bu üçü **grid sütununu** da etkiler |
`\*` işaretli anahtarlar backend'de tipli DTO'ya (`GridBoxOptionsDto`, `TagBoxOptionsDto`,

View file

@ -567,7 +567,7 @@ public class PlatformDbContext :
b.Property(x => x.Type).IsRequired().HasMaxLength(64);
b.Property(x => x.Subject).IsRequired().HasMaxLength(256);
b.Property(x => x.Content).IsRequired().HasMaxLength(2048);
b.Property(x => x.Content).IsRequired();
b.Property(x => x.EntityName).IsRequired().HasMaxLength(128);
b.Property(x => x.EntityId).IsRequired().HasMaxLength(64);
});
@ -1186,7 +1186,7 @@ public class PlatformDbContext :
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
b.Property(x => x.Excerpt).IsRequired().HasMaxLength(512);
b.Property(x => x.Content).IsRequired().HasMaxLength(4096);
b.Property(x => x.Content).IsRequired();
b.Property(x => x.Category).IsRequired().HasMaxLength(64);
b.Property(x => x.PublishDate).IsRequired();
b.Property(x => x.Attachments).HasMaxLength(2048);
@ -1302,7 +1302,7 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SocialPost)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Content).IsRequired().HasMaxLength(4096);
b.Property(x => x.Content).IsRequired();
b.Property(x => x.LikeCount).HasDefaultValue(0);
b.Property(x => x.IsOwnPost).HasDefaultValue(false);
b.Property(x => x.IsLiked).HasDefaultValue(false);

View file

@ -855,7 +855,6 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime")
@ -4305,8 +4304,7 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
@ -5624,7 +5622,6 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime")

View file

@ -519,7 +519,7 @@ namespace Sozsoft.Platform.Migrations
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Excerpt = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 4096, nullable: false),
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
ImageUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
Category = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
@ -662,7 +662,7 @@ namespace Sozsoft.Platform.Migrations
EntityId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Content = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
FilesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
@ -802,7 +802,7 @@ namespace Sozsoft.Platform.Migrations
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 4096, nullable: false),
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
LikeCount = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
IsLiked = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
IsOwnPost = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),

View file

@ -852,7 +852,6 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime")
@ -4302,8 +4301,7 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
@ -5621,7 +5619,6 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime")

View file

@ -1,27 +0,0 @@
export const sizeValues = ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt']
export const fontValues = [
'Arial',
'Courier New',
'Georgia',
'Impact',
'Lucida Console',
'Tahoma',
'Times New Roman',
'Verdana',
]
export const headerValues = [false, 1, 2, 3, 4, 5]
export const fontSizeOptions = {
inputAttr: {
'aria-label': 'Font size',
},
}
export const fontFamilyOptions = {
inputAttr: {
'aria-label': 'Font family',
},
}
export const headerOptions = {
inputAttr: {
'aria-label': 'Font family',
},
}

View file

@ -939,6 +939,27 @@ export const optionSpecs: OptionSpec[] = [
group: 'html',
editors: ['dxHtmlEditor'],
},
{
path: 'imageUpload.tabs',
label: 'imageUpload.tabs',
type: 'json',
group: 'html',
editors: ['dxHtmlEditor'],
},
{
path: 'tableResizing.enabled',
label: 'tableResizing.enabled',
type: 'boolean',
group: 'html',
editors: ['dxHtmlEditor'],
},
{
path: 'tableContextMenu.enabled',
label: 'tableContextMenu.enabled',
type: 'boolean',
group: 'html',
editors: ['dxHtmlEditor'],
},
// ── Liste sütununu da etkileyenler ───────────────────────────────
{

View file

@ -2,6 +2,7 @@
* Hazır editorOptions kalıpları. Her kalıp mevcut JSON ile birleştirilir,
* diğer ayarları silmez.
*/
import { buildHtmlEditorDefaults } from '@/views/shared/htmlEditorOptions'
import type { JsonObject } from './jsonUtils'
export type EditorOptionsPreset = {
@ -13,84 +14,8 @@ export type EditorOptionsPreset = {
value: JsonObject
}
const htmlToolbarItems = [
'undo',
'redo',
'separator',
'size',
'font',
'separator',
'bold',
'italic',
'strike',
'underline',
'separator',
'alignLeft',
'alignCenter',
'alignRight',
'alignJustify',
'separator',
'orderedList',
'bulletList',
'separator',
'header',
'separator',
'color',
'background',
'separator',
'link',
'image',
'separator',
'clear',
'codeBlock',
'blockquote',
'separator',
'insertTable',
'deleteTable',
'insertRowAbove',
'insertRowBelow',
'deleteRow',
'insertColumnLeft',
'insertColumnRight',
'deleteColumn',
'cellProperties',
'tableProperties',
]
const buildHtmlEditorOptions = (): JsonObject => ({
mediaResizing: { enabled: true },
imageUpload: { tabs: ['file', 'url'], fileUploadMode: 'base64' },
toolbar: {
multiline: true,
items: htmlToolbarItems.map((name) => {
if (name === 'size') {
return {
name,
acceptedValues: ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt'],
options: { inputAttr: { 'aria-label': 'Font size' } },
}
}
if (name === 'font') {
return {
name,
acceptedValues: [
'Arial',
'Courier New',
'Georgia',
'Impact',
'Lucida Console',
'Tahoma',
'Times New Roman',
'Verdana',
],
options: { inputAttr: { 'aria-label': 'Font family' } },
}
}
if (name === 'header') return { name, acceptedValues: [false, 1, 2, 3, 4, 5] }
return { name }
}),
},
})
const buildHtmlEditorOptions = (): JsonObject =>
JSON.parse(JSON.stringify(buildHtmlEditorDefaults())) as JsonObject
export const editorOptionsPresets: EditorOptionsPreset[] = [
{

View file

@ -6,8 +6,8 @@ import { FaFileAlt, FaFileUpload, FaPlus, FaTrash } from 'react-icons/fa'
import { Field, FieldProps, Form, Formik } from 'formik'
import * as Yup from 'yup'
import { noteService } from '@/services/note.service'
import { HtmlEditor, ImageUpload, MediaResizing } from 'devextreme-react/html-editor'
import { HtmlEditorToolbar } from '@/views/shared/HtmlEditorToolbarItems'
import { HtmlEditor } from 'devextreme-react/html-editor'
import { htmlEditorDefaults } from '@/views/shared/htmlEditorOptions'
import { useLocalization } from '@/utils/hooks/useLocalization'
const validationSchema = Yup.object({
@ -147,15 +147,12 @@ function NoteModalContent({
<Field name="content">
{({ field }: FieldProps) => (
<HtmlEditor
{...htmlEditorDefaults}
value={field.value}
onValueChanged={(e) => setFieldValue('content', e.value)}
height={isMaximized ? '50vh' : 220}
placeholder={translate('::App.ListFormNoteModal.NoteModalContent')}
>
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<HtmlEditorToolbar />
</HtmlEditor>
/>
)}
</Field>
</FormItem>

View file

@ -7,7 +7,11 @@ import { captionize } from 'devextreme/core/utils/inflector'
import type { GroupItem } from 'devextreme/ui/form'
import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
import { getEditingFormGroups } from '../../list/Utils'
import { isFormItemVisible, resolveEditorType } from '../../list/shared/editingForm'
import {
isFormItemVisible,
resolveEditorType,
withEditorTypeDefaults,
} from '../../list/shared/editingForm'
import type { GridColumnData } from '../../list/GridColumnData'
import type { RowMode, SimpleItemWithColData } from '../types'
@ -71,7 +75,10 @@ const toFormItem = (
editorType2: colFormat.editorType2,
editorType: resolveEditorType(colFormat.editorType2),
colSpan: colFormat.colSpan,
editorOptions: { ...lookupEditorOptions, ...parsedEditorOptions },
editorOptions: withEditorTypeDefaults(colFormat.editorType2, {
...lookupEditorOptions,
...parsedEditorOptions,
}),
colData: colFormat,
tagBoxOptions: colFormat.tagBoxOptions,
gridBoxOptions: colFormat.gridBoxOptions,

View file

@ -1,19 +1,12 @@
import { FaTimes } from 'react-icons/fa'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor'
import { HtmlEditor } from 'devextreme-react/html-editor'
import { htmlEditorDefaults } from '@/views/shared/htmlEditorOptions'
import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Button, FormContainer, FormItem } from '@/components/ui'
import { stripHtml } from './utils'
import {
fontFamilyOptions,
fontSizeOptions,
fontValues,
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/reports/data'
interface PostFormValues {
content: string
@ -112,6 +105,7 @@ export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostM
<Field name="content">
{({ field }: FieldProps) => (
<HtmlEditor
{...htmlEditorDefaults}
value={field.value}
height={300}
placeholder={
@ -120,59 +114,7 @@ export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostM
: translate('::App.ForumPostManagement.MessageNew')
}
onValueChanged={(e) => setFieldValue('content', e.value)}
>
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<Toolbar multiline={true}>
<Item name="undo" />
<Item name="redo" />
<Item name="separator" />
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
<Item
name="font"
acceptedValues={fontValues}
options={fontFamilyOptions}
/>
<Item name="separator" />
<Item name="bold" />
<Item name="italic" />
<Item name="strike" />
<Item name="underline" />
<Item name="separator" />
<Item name="alignLeft" />
<Item name="alignCenter" />
<Item name="alignRight" />
<Item name="alignJustify" />
<Item name="separator" />
<Item name="orderedList" />
<Item name="bulletList" />
<Item name="separator" />
<Item
name="header"
acceptedValues={headerValues}
options={headerOptions}
/>
<Item name="separator" />
<Item name="color" />
<Item name="background" />
<Item name="separator" />
<Item name="link" />
<Item name="image" />
<Item name="separator" />
<Item name="clear" />
<Item name="codeBlock" />
<Item name="blockquote" />
<Item name="separator" />
<Item name="insertTable" />
<Item name="deleteTable" />
<Item name="insertRowAbove" />
<Item name="insertRowBelow" />
<Item name="deleteRow" />
<Item name="insertColumnLeft" />
<Item name="insertColumnRight" />
<Item name="deleteColumn" />
</Toolbar>
</HtmlEditor>
)}
</Field>
</FormItem>

View file

@ -57,7 +57,7 @@ import {
getMobilePopupPosition,
shouldUseMobileEditPopup,
} from './shared/editPopup'
import { getValueByField, resolveEditorType } from './shared/editingForm'
import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
import { createEditingFormHandlers } from './shared/editingFormScripts'
import { flattenColumns } from './shared/columns'
import { getInitialExtraFilters, mergeFilterTriplets } from './shared/hooks'
@ -811,6 +811,7 @@ const CardView = (props: CardViewProps) => {
} catch (error) {
console.error('EditorOptions parse error:', item.fieldName, error)
}
editorOptions = withEditorTypeDefaults(item.editorType2, editorOptions)
if (column.sourceDbType === DbTypeEnum.Date) {
editorOptions = {

View file

@ -39,7 +39,7 @@ import {
getMobilePopupPosition,
shouldUseMobileEditPopup,
} from './shared/editPopup'
import { getValueByField, resolveEditorType } from './shared/editingForm'
import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
import { flattenGridColumns } from './shared/columns'
import { useListFormCustomSources, useListFormGridDto, useWidgetGroupHeight } from './shared/hooks'
@ -567,6 +567,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
} catch (err) {
console.log(err)
}
editorOptions = withEditorTypeDefaults(i.editorType2, editorOptions)
const listFormField = i
const listFormColumn = listFormColumns.find(

View file

@ -2,8 +2,8 @@ import type { GridDto, TodoOptionDto } from '@/proxy/form/models'
import type { IdentityUserDto } from '@/proxy/admin/models'
import type { DataGridRef } from 'devextreme-react/data-grid'
import type CustomStore from 'devextreme/data/custom_store'
import { HtmlEditor, ImageUpload, MediaResizing } from 'devextreme-react/html-editor'
import { HtmlEditorToolbar } from '@/views/shared/HtmlEditorToolbarItems'
import { HtmlEditor } from 'devextreme-react/html-editor'
import { htmlEditorDefaults } from '@/views/shared/htmlEditorOptions'
import Toolbar, { Item as ToolbarItem } from 'devextreme-react/toolbar'
import TagBox from 'devextreme-react/tag-box'
import { AVATAR_URL } from '@/constants/app.constant'
@ -1089,6 +1089,7 @@ const TodoBoardContent = ({
<div className="flex min-h-[260px] flex-1 flex-col pt-4">
{activeTab === 'description' && options.descriptionExpr && (
<HtmlEditor
{...htmlEditorDefaults}
height="100%"
placeholder={translate('::App.ListFormTodoBoard.DescriptionPlaceholder')}
readOnly={!canUpdate}
@ -1099,11 +1100,7 @@ const TodoBoardContent = ({
[options.descriptionExpr!]: event.value,
}))
}
>
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<HtmlEditorToolbar />
</HtmlEditor>
/>
)}
{activeTab === 'timesheet' && options.timesheetExpr && (
<div className="flex min-h-0 flex-1 flex-col gap-2">

View file

@ -15,6 +15,7 @@ import {
} from '@/proxy/form/models'
import { scanBarcode } from '@/components/shared/BarcodeScanner/scanBarcode'
import { getNextSequenceValue } from '@/services/form.service'
import { mergeHtmlEditorOptions } from '@/views/shared/htmlEditorOptions'
import type { EditorType2, RowMode, SimpleItemWithColData } from '../../form/types'
import {
autoNumber,
@ -423,6 +424,15 @@ export const resolveEditorType = (editorType2?: EditorType2) => {
return editorType2
}
/** Editör tipinin platform varsayılanlarını yapılandırmadan gelen editorOptions'ın altına serer. */
export const withEditorTypeDefaults = <T extends Record<string, any>>(
editorType2: EditorType2 | undefined,
editorOptions: T,
): T =>
editorType2 === 'dxHtmlEditor'
? mergeHtmlEditorOptions(editorOptions)
: editorOptions
type FormItemMapperParams = {
mode: RowMode
translate: (key: string) => string
@ -484,11 +494,11 @@ export const createFormItemMapper = ({
? { readOnly: true }
: {}
const editorOptions: EditorOptionsWithButtons = {
const editorOptions: EditorOptionsWithButtons = withEditorTypeDefaults(colFormat.editorType2, {
...defaultEditorOptions,
...parsedEditorOptions,
...forcedEditorOptions,
}
})
if (editorOptions.buttons) {
editorOptions.buttons = editorOptions.buttons.map((btn: any) => {

View file

@ -1,46 +0,0 @@
/**
* DevExtreme HtmlEditor için standart araç çubuğu.
* NoteModal ve TodoBoard içinde birebir aynı ~40 satırlık tanım tekrar ediyordu.
*/
import { Item, Toolbar } from 'devextreme-react/html-editor'
import {
fontFamilyOptions,
fontSizeOptions,
fontValues,
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/reports/data'
export const HtmlEditorToolbar = () => (
<Toolbar multiline>
<Item name="undo" />
<Item name="redo" />
<Item name="separator" />
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
<Item name="font" acceptedValues={fontValues} options={fontFamilyOptions} />
<Item name="separator" />
<Item name="bold" />
<Item name="italic" />
<Item name="underline" />
<Item name="strike" />
<Item name="separator" />
<Item name="orderedList" />
<Item name="bulletList" />
<Item name="separator" />
<Item name="header" acceptedValues={headerValues} options={headerOptions} />
<Item name="separator" />
<Item name="color" />
<Item name="background" />
<Item name="separator" />
<Item name="alignLeft" />
<Item name="alignCenter" />
<Item name="alignRight" />
<Item name="alignJustify" />
<Item name="separator" />
<Item name="link" />
<Item name="image" />
<Item name="separator" />
<Item name="clear" />
</Toolbar>
)

View file

@ -0,0 +1,114 @@
/**
* Uygulamadaki bütün HtmlEditor'ların ortak varsayılanı. Sabit ekranlar (not, todo, forum)
* `<HtmlEditor {...htmlEditorDefaults}>` ile, dinamik formlar `mergeHtmlEditorOptions` ile
* aynı yetenek setini alır; ListForm'daki "HTML toolbar" kalıbı da buradan beslenir.
* Değerler JSON'a yazılabilir kalmalıdır — kalıp editorOptions kolonuna olduğu gibi kaydedilir.
*/
import type { Properties as HtmlEditorProperties } from 'devextreme/ui/html_editor'
const sizeValues = ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt']
const fontValues = [
'Arial',
'Courier New',
'Georgia',
'Impact',
'Lucida Console',
'Segoe UI',
'Tahoma',
'Times New Roman',
'Verdana',
]
const headerValues = [false, 1, 2, 3, 4, 5, 6]
type ToolbarItem = NonNullable<NonNullable<HtmlEditorProperties['toolbar']>['items']>[number]
export const htmlToolbarItemNames = [
'undo',
'redo',
'separator',
'header',
'size',
'font',
'separator',
'bold',
'italic',
'underline',
'strike',
'superscript',
'subscript',
'separator',
'color',
'background',
'separator',
'alignLeft',
'alignCenter',
'alignRight',
'alignJustify',
'separator',
'orderedList',
'bulletList',
'decreaseIndent',
'increaseIndent',
'separator',
'blockquote',
'codeBlock',
'link',
'image',
'separator',
'insertTable',
'insertHeaderRow',
'insertRowAbove',
'insertRowBelow',
'deleteRow',
'insertColumnLeft',
'insertColumnRight',
'deleteColumn',
'cellProperties',
'tableProperties',
'deleteTable',
'separator',
'clear',
] as const
const toToolbarItem = (name: (typeof htmlToolbarItemNames)[number]): ToolbarItem => {
if (name === 'size') {
return { name, acceptedValues: sizeValues, options: { inputAttr: { 'aria-label': 'Font size' } } }
}
if (name === 'font') {
return { name, acceptedValues: fontValues, options: { inputAttr: { 'aria-label': 'Font family' } } }
}
if (name === 'header') {
return { name, acceptedValues: headerValues, options: { inputAttr: { 'aria-label': 'Header' } } }
}
return { name }
}
export const buildHtmlEditorDefaults = () =>
({
toolbar: { multiline: true, items: htmlToolbarItemNames.map(toToolbarItem) },
// Varsayılan sekme yalnızca 'url'dir; bilgisayardan resim seçmek için 'file' açıkça verilmeli.
imageUpload: { tabs: ['file', 'url'], fileUploadMode: 'base64' },
mediaResizing: { enabled: true },
tableResizing: { enabled: true },
tableContextMenu: { enabled: true },
}) satisfies HtmlEditorProperties
export const htmlEditorDefaults = buildHtmlEditorDefaults()
const nestedKeys = ['toolbar', 'imageUpload', 'mediaResizing', 'tableResizing', 'tableContextMenu'] as const
/**
* Yapılandırmadan gelen editorOptions'ı varsayılanın üstüne yazar. İç içe bloklar tek seviye
* birleştirilir; aksi hâlde yalnızca `imageUpload.fileUploadMode` yazan eski bir kayıt
* dosya sekmesini ve toolbar'ı sessizce kaybederdi.
*/
export const mergeHtmlEditorOptions = <T extends Record<string, any>>(options: T): T => {
const merged: Record<string, any> = { ...htmlEditorDefaults, ...options }
for (const key of nestedKeys) {
const own = options[key]
if (own && typeof own === 'object' && !Array.isArray(own)) {
merged[key] = { ...htmlEditorDefaults[key], ...own }
}
}
return merged as T
}