chore: merge
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.0",
|
||||
"@faceless-ui/css-grid": "^1.2.0",
|
||||
"@faceless-ui/modal": "^2.0.1",
|
||||
"@faceless-ui/modal": "^2.0.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"graphql": "^16.8.1",
|
||||
"next": "^13.5.6",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"@faceless-ui/modal": "^2.0.1",
|
||||
"@faceless-ui/modal": "^2.0.2",
|
||||
"@payloadcms/plugin-form-builder": "^1.0.12",
|
||||
"@payloadcms/plugin-seo": "^1.0.8",
|
||||
"dotenv": "^8.2.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../scss/styles.scss';
|
||||
@import '../../../../ui/src/scss/styles.scss';
|
||||
|
||||
.leave-without-saving {
|
||||
@include blur-bg;
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client'
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Modal, useModal } from '@payloadcms/ui'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
|
||||
import { Button } from '../../elements/Button'
|
||||
import { useFormModified } from '../../forms/Form/context'
|
||||
import { useAuth } from '../../providers/Auth'
|
||||
import { useTranslation } from '../../providers/Translation'
|
||||
import { Button } from '../../../../ui/src/elements/Button'
|
||||
import { useFormModified } from '../../../../ui/src/forms/Form/context'
|
||||
import { useAuth } from '../../../../ui/src/providers/Auth'
|
||||
import { useTranslation } from '../../../../ui/src/providers/Translation'
|
||||
import './index.scss'
|
||||
import { usePreventLeave } from './usePreventLeave'
|
||||
|
||||
const modalSlug = 'leave-without-saving'
|
||||
|
||||
@@ -17,15 +18,15 @@ const Component: React.FC<{
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}> = ({ isActive, onCancel, onConfirm }) => {
|
||||
const { closeModal, openModal, modalState } = useModal()
|
||||
const { closeModal, modalState, openModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Manually check for modal state as 'esc' key will not trigger the nav inactivity
|
||||
useEffect(() => {
|
||||
if (!modalState?.[modalSlug]?.isOpen && isActive) {
|
||||
onCancel()
|
||||
}
|
||||
}, [modalState])
|
||||
// useEffect(() => {
|
||||
// if (!modalState?.[modalSlug]?.isOpen && isActive) {
|
||||
// onCancel()
|
||||
// }
|
||||
// }, [modalState, isActive, onCancel])
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) openModal(modalSlug)
|
||||
@@ -53,11 +54,26 @@ const Component: React.FC<{
|
||||
export const LeaveWithoutSaving: React.FC = () => {
|
||||
const modified = useFormModified()
|
||||
const { user } = useAuth()
|
||||
const [show, setShow] = React.useState(false)
|
||||
const [hasAccepted, setHasAccepted] = React.useState(false)
|
||||
|
||||
return null
|
||||
// <NavigationPrompt renderIfNotActive when={Boolean(modified && user)}>
|
||||
// {({ isActive, onCancel, onConfirm }) => (
|
||||
// <Component isActive={isActive} onCancel={onCancel} onConfirm={onConfirm} />
|
||||
// )}
|
||||
// </NavigationPrompt>
|
||||
const prevent = Boolean(modified && user)
|
||||
|
||||
const onPrevent = useCallback(() => {
|
||||
setShow(true)
|
||||
}, [])
|
||||
|
||||
usePreventLeave({ hasAccepted, onPrevent, prevent })
|
||||
|
||||
return (
|
||||
<Component
|
||||
isActive={show}
|
||||
onCancel={() => {
|
||||
setShow(false)
|
||||
}}
|
||||
onConfirm={() => {
|
||||
setHasAccepted(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Credit: @Taiki92777
|
||||
// - Source: https://github.com/vercel/next.js/discussions/32231#discussioncomment-7284386
|
||||
// Credit: `react-use` maintainers
|
||||
// - Source: https://github.com/streamich/react-use/blob/ade8d3905f544305515d010737b4ae604cc51024/src/useBeforeUnload.ts#L2
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
function on<T extends Document | EventTarget | HTMLElement | Window>(
|
||||
obj: T | null,
|
||||
...args: [string, Function | null, ...any] | Parameters<T['addEventListener']>
|
||||
): void {
|
||||
if (obj && obj.addEventListener) {
|
||||
obj.addEventListener(...(args as Parameters<HTMLElement['addEventListener']>))
|
||||
}
|
||||
}
|
||||
|
||||
function off<T extends Document | EventTarget | HTMLElement | Window>(
|
||||
obj: T | null,
|
||||
...args: [string, Function | null, ...any] | Parameters<T['removeEventListener']>
|
||||
): void {
|
||||
if (obj && obj.removeEventListener) {
|
||||
obj.removeEventListener(...(args as Parameters<HTMLElement['removeEventListener']>))
|
||||
}
|
||||
}
|
||||
|
||||
export const useBeforeUnload = (enabled: (() => boolean) | boolean = true, message?: string) => {
|
||||
const handler = useCallback(
|
||||
(event: BeforeUnloadEvent) => {
|
||||
const finalEnabled = typeof enabled === 'function' ? enabled() : true
|
||||
|
||||
if (!finalEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
if (message) {
|
||||
event.returnValue = message
|
||||
}
|
||||
|
||||
return message
|
||||
},
|
||||
[enabled, message],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
on(window, 'beforeunload', handler)
|
||||
|
||||
return () => off(window, 'beforeunload', handler)
|
||||
}, [enabled, handler])
|
||||
}
|
||||
|
||||
export const usePreventLeave = ({
|
||||
hasAccepted = false,
|
||||
message = 'Are you sure want to leave this page?',
|
||||
onPrevent,
|
||||
prevent = true,
|
||||
}: {
|
||||
hasAccepted: boolean
|
||||
// if no `onPrevent` is provided, the message will be displayed in a confirm dialog
|
||||
message?: string
|
||||
// to use a custom confirmation dialog, provide a function that returns a boolean
|
||||
onPrevent?: () => void
|
||||
prevent: boolean
|
||||
}) => {
|
||||
// check when page is about to be reloaded
|
||||
useBeforeUnload(prevent, message)
|
||||
|
||||
const router = useRouter()
|
||||
const cancelledURL = useRef<string>('')
|
||||
|
||||
// check when page is about to be changed
|
||||
useEffect(() => {
|
||||
function isAnchorOfCurrentUrl(currentUrl: string, newUrl: string) {
|
||||
const currentUrlObj = new URL(currentUrl)
|
||||
const newUrlObj = new URL(newUrl)
|
||||
// Compare hostname, pathname, and search parameters
|
||||
if (
|
||||
currentUrlObj.hostname === newUrlObj.hostname &&
|
||||
currentUrlObj.pathname === newUrlObj.pathname &&
|
||||
currentUrlObj.search === newUrlObj.search
|
||||
) {
|
||||
// Check if the new URL is just an anchor of the current URL page
|
||||
const currentHash = currentUrlObj.hash
|
||||
const newHash = newUrlObj.hash
|
||||
return (
|
||||
currentHash !== newHash &&
|
||||
currentUrlObj.href.replace(currentHash, '') === newUrlObj.href.replace(newHash, '')
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function findClosestAnchor(element: HTMLElement | null): HTMLAnchorElement | null {
|
||||
while (element && element.tagName.toLowerCase() !== 'a') {
|
||||
element = element.parentElement
|
||||
}
|
||||
return element as HTMLAnchorElement
|
||||
}
|
||||
function handleClick(event: MouseEvent) {
|
||||
try {
|
||||
const target = event.target as HTMLElement
|
||||
const anchor = findClosestAnchor(target)
|
||||
if (anchor) {
|
||||
const currentUrl = window.location.href
|
||||
const newUrl = anchor.href
|
||||
const isAnchor = isAnchorOfCurrentUrl(currentUrl, newUrl)
|
||||
const isDownloadLink = anchor.download !== ''
|
||||
|
||||
const isPageLeaving = !(newUrl === currentUrl || isAnchor || isDownloadLink)
|
||||
|
||||
if (isPageLeaving && prevent && (!onPrevent ? !window.confirm(message) : true)) {
|
||||
// Keep a reference of the href
|
||||
cancelledURL.current = newUrl
|
||||
|
||||
// Cancel the route change
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (typeof onPrevent === 'function') {
|
||||
onPrevent()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the global click event listener
|
||||
document.addEventListener('click', handleClick, true)
|
||||
|
||||
// Clean up the global click event listener when the component is unmounted
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick, true)
|
||||
}
|
||||
}, [onPrevent, prevent, message])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAccepted && cancelledURL.current) {
|
||||
router.push(cancelledURL.current)
|
||||
}
|
||||
}, [hasAccepted, router])
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
FieldPathProvider,
|
||||
Form,
|
||||
FormLoadingOverlayToggle,
|
||||
LeaveWithoutSaving,
|
||||
OperationProvider,
|
||||
getFormState,
|
||||
useComponentMap,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
import React, { Fragment, useCallback } from 'react'
|
||||
|
||||
import { Upload } from '../../../../../ui/src/elements/Upload'
|
||||
import { LeaveWithoutSaving } from '../../../elements/LeaveWithoutSaving'
|
||||
// import { getTranslation } from '@payloadcms/translations'
|
||||
import Auth from './Auth'
|
||||
import { SetDocumentTitle } from './SetDocumentTitle'
|
||||
@@ -45,7 +45,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
hasSavePermission,
|
||||
initialData: data,
|
||||
initialState,
|
||||
onSave: onSaveFromProps,
|
||||
onSave: onSaveFromContext,
|
||||
} = useDocumentInfo()
|
||||
|
||||
const config = useConfig()
|
||||
@@ -57,7 +57,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
serverURL,
|
||||
} = config
|
||||
|
||||
const { getFieldMap } = useComponentMap()
|
||||
const { componentMap, getFieldMap } = useComponentMap()
|
||||
|
||||
const collectionConfig =
|
||||
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
|
||||
@@ -95,8 +95,8 @@ export const DefaultEditView: React.FC = () => {
|
||||
// await refreshCookieAsync()
|
||||
// }
|
||||
|
||||
if (typeof onSaveFromProps === 'function') {
|
||||
onSaveFromProps({
|
||||
if (typeof onSaveFromContext === 'function') {
|
||||
onSaveFromContext({
|
||||
...json,
|
||||
operation: id ? 'update' : 'create',
|
||||
})
|
||||
@@ -104,7 +104,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
},
|
||||
[
|
||||
id,
|
||||
onSaveFromProps,
|
||||
onSaveFromContext,
|
||||
// refreshCookieAsync,
|
||||
// reportUpdate
|
||||
],
|
||||
@@ -142,6 +142,8 @@ export const DefaultEditView: React.FC = () => {
|
||||
[serverURL, apiRoute, id, operation, schemaPath, collectionSlug, globalSlug],
|
||||
)
|
||||
|
||||
const RegisterGetThumbnailFunction = componentMap?.[`${collectionSlug}.adminThumbnail`]
|
||||
|
||||
return (
|
||||
<main className={classes}>
|
||||
<FieldPathProvider path="" schemaPath={schemaPath}>
|
||||
@@ -220,11 +222,14 @@ export const DefaultEditView: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
{upload && (
|
||||
<Upload
|
||||
collectionSlug={collectionConfig.slug}
|
||||
initialState={initialState}
|
||||
uploadConfig={upload}
|
||||
/>
|
||||
<React.Fragment>
|
||||
{RegisterGetThumbnailFunction && <RegisterGetThumbnailFunction />}
|
||||
<Upload
|
||||
collectionSlug={collectionConfig.slug}
|
||||
initialState={initialState}
|
||||
uploadConfig={upload}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
'use client'
|
||||
import type { EditViewProps } from 'payload/config'
|
||||
|
||||
import { LoadingOverlay, useComponentMap, useDocumentInfo } from '@payloadcms/ui'
|
||||
import React, { Fragment } from 'react'
|
||||
import { LoadingOverlay, useComponentMap, useConfig, useDocumentInfo } from '@payloadcms/ui'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export const EditViewClient: React.FC<EditViewProps> = () => {
|
||||
const { id, collectionSlug, getDocPermissions, getVersions, globalSlug } = useDocumentInfo()
|
||||
const { id, collectionSlug, getDocPermissions, getVersions, globalSlug, setDocumentInfo } =
|
||||
useDocumentInfo()
|
||||
const {
|
||||
routes: { api: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const { componentMap } = useComponentMap()
|
||||
|
||||
@@ -22,7 +27,7 @@ export const EditViewClient: React.FC<EditViewProps> = () => {
|
||||
getDocPermissions()
|
||||
|
||||
if (!isEditing) {
|
||||
// setRedirect(`${admin}/collections/${collection.slug}/${json?.doc?.id}`)
|
||||
redirect(`${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}`)
|
||||
} else {
|
||||
// buildState(json.doc, {
|
||||
// fieldSchema: collection.fields,
|
||||
@@ -33,9 +38,16 @@ export const EditViewClient: React.FC<EditViewProps> = () => {
|
||||
// }))
|
||||
}
|
||||
},
|
||||
[getVersions, isEditing, getDocPermissions, collectionSlug],
|
||||
[getVersions, isEditing, getDocPermissions, collectionSlug, adminRoute],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentInfo((current) => ({
|
||||
...current,
|
||||
onSave,
|
||||
}))
|
||||
}, [setDocumentInfo, onSave])
|
||||
|
||||
// Allow the `DocumentInfoProvider` to hydrate
|
||||
if (!Edit || (!collectionSlug && !globalSlug)) {
|
||||
return <LoadingOverlay />
|
||||
|
||||
@@ -11,12 +11,13 @@ const baseClass = 'file'
|
||||
export interface FileCellProps extends CellComponentProps<any> {}
|
||||
|
||||
export const FileCell: React.FC<FileCellProps> = ({ cellData, customCellContext, rowData }) => {
|
||||
const { uploadConfig } = customCellContext
|
||||
const { collectionSlug, uploadConfig } = customCellContext
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Thumbnail
|
||||
className={`${baseClass}__thumbnail`}
|
||||
collectionSlug={collectionSlug}
|
||||
doc={{
|
||||
...rowData,
|
||||
filename: cellData,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
DocumentFields,
|
||||
FieldPathProvider,
|
||||
Form,
|
||||
LeaveWithoutSaving,
|
||||
LoadingOverlay,
|
||||
OperationProvider,
|
||||
getFormState,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
} from '@payloadcms/ui'
|
||||
import React, { Fragment, useCallback } from 'react'
|
||||
|
||||
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving'
|
||||
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle'
|
||||
import { SetStepNav } from '../Edit/Default/SetStepNav'
|
||||
import { LivePreviewProvider } from './Context'
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/translations": "workspace:^",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"bson-objectid": "2.0.4",
|
||||
"conf": "10.2.0",
|
||||
"console-table-printer": "2.11.2",
|
||||
|
||||
@@ -72,6 +72,10 @@ const sanitizeCollections = (
|
||||
if ('hidden' in sanitized.admin) {
|
||||
delete sanitized.admin.hidden
|
||||
}
|
||||
|
||||
if ('preview' in sanitized.admin) {
|
||||
delete sanitized.admin.preview
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized
|
||||
@@ -95,6 +99,10 @@ const sanitizeGlobals = (globals: SanitizedConfig['globals']): ClientConfig['glo
|
||||
if ('hidden' in sanitized.admin) {
|
||||
delete sanitized.admin.hidden
|
||||
}
|
||||
|
||||
if ('preview' in sanitized.admin) {
|
||||
delete sanitized.admin.preview
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized
|
||||
|
||||
@@ -25,56 +25,57 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
||||
|
||||
const mimeType: Field = {
|
||||
name: 'mimeType',
|
||||
type: 'text',
|
||||
admin: {
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
},
|
||||
label: 'MIME Type',
|
||||
type: 'text',
|
||||
}
|
||||
|
||||
const url: Field = {
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
},
|
||||
label: 'URL',
|
||||
type: 'text',
|
||||
}
|
||||
|
||||
const width: Field = {
|
||||
name: 'width',
|
||||
type: 'number',
|
||||
admin: {
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
},
|
||||
label: labels['upload:width'],
|
||||
type: 'number',
|
||||
}
|
||||
|
||||
const height: Field = {
|
||||
name: 'height',
|
||||
type: 'number',
|
||||
admin: {
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
},
|
||||
label: labels['upload:height'],
|
||||
type: 'number',
|
||||
}
|
||||
|
||||
const filesize: Field = {
|
||||
name: 'filesize',
|
||||
type: 'number',
|
||||
admin: {
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
},
|
||||
label: labels['upload:fileSize'],
|
||||
type: 'number',
|
||||
}
|
||||
|
||||
const filename: Field = {
|
||||
name: 'filename',
|
||||
type: 'text',
|
||||
admin: {
|
||||
disableBulkEdit: true,
|
||||
hidden: true,
|
||||
@@ -82,7 +83,6 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
||||
},
|
||||
index: true,
|
||||
label: labels['upload:fileName'],
|
||||
type: 'text',
|
||||
unique: true,
|
||||
}
|
||||
|
||||
@@ -93,10 +93,7 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
||||
afterRead: [
|
||||
({ data }) => {
|
||||
if (data?.filename) {
|
||||
if (uploadOptions.staticURL.startsWith('/')) {
|
||||
return `${config.serverURL}${uploadOptions.staticURL}/${data.filename}`
|
||||
}
|
||||
return `${uploadOptions.staticURL}/${data.filename}`
|
||||
return `${config.serverURL}${config.routes.api}/${collection.slug}/file/${data.filename}`
|
||||
}
|
||||
|
||||
return undefined
|
||||
@@ -119,11 +116,13 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
||||
uploadFields = uploadFields.concat([
|
||||
{
|
||||
name: 'sizes',
|
||||
type: 'group',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
fields: uploadOptions.imageSizes.map((size) => ({
|
||||
name: size.name,
|
||||
type: 'group',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
@@ -136,10 +135,7 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
||||
const sizeFilename = data?.sizes?.[size.name]?.filename
|
||||
|
||||
if (sizeFilename) {
|
||||
if (uploadOptions.staticURL.startsWith('/')) {
|
||||
return `${config.serverURL}${uploadOptions.staticURL}/${sizeFilename}`
|
||||
}
|
||||
return `${uploadOptions.staticURL}/${sizeFilename}`
|
||||
return `${config.serverURL}${config.routes.api}/${collection.slug}/file/${sizeFilename}`
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -157,10 +153,8 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
||||
},
|
||||
],
|
||||
label: size.name,
|
||||
type: 'group',
|
||||
})),
|
||||
label: labels['upload:Sizes'],
|
||||
type: 'group',
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export type ImageSize = Omit<ResizeOptions, 'withoutEnlargement'> & {
|
||||
export type GetAdminThumbnail = (args: { doc: Record<string, unknown> }) => false | null | string
|
||||
|
||||
export type IncomingUploadType = {
|
||||
adminThumbnail?: GetAdminThumbnail | string
|
||||
adminThumbnail?: React.ComponentType | string
|
||||
crop?: boolean
|
||||
disableLocalStorage?: boolean
|
||||
filesRequiredOnCreate?: boolean
|
||||
@@ -88,7 +88,12 @@ export type IncomingUploadType = {
|
||||
}
|
||||
|
||||
export type Upload = {
|
||||
adminThumbnail?: GetAdminThumbnail | string
|
||||
/**
|
||||
* Represents an admin thumbnail, which can be either a React component or a string.
|
||||
* - If a string, it should be one of the image size names.
|
||||
* - If a React component, register a function that generates the thumbnail URL using the `useAdminThumbnail` hook.
|
||||
**/
|
||||
adminThumbnail?: React.ComponentType | string
|
||||
crop?: boolean
|
||||
disableLocalStorage?: boolean
|
||||
filesRequiredOnCreate?: boolean
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"scripts": {
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"build": "pnpm build:swc && pnpm build:types",
|
||||
"build": "echo \"Build temporarily disabled.\" && exit 0",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"test": "echo \"No tests available.\""
|
||||
@@ -21,7 +21,7 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/ui": "workspace:^",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-html": "^1.0.3"
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/ui": "workspace:^",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"ts-deepmerge": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm build:swc && pnpm build:types",
|
||||
"build": "echo \"Build temporarily disabled.\" && exit 0",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
@@ -31,7 +31,7 @@
|
||||
"payload": "^1.1.8 || ^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/ui": "workspace:^",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"lodash.get": "^4.4.2",
|
||||
"stripe": "^10.2.0",
|
||||
"uuid": "^9.0.0"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faceless-ui/modal": "2.0.1",
|
||||
"@faceless-ui/modal": "2.0.2",
|
||||
"@lexical/headless": "0.13.1",
|
||||
"@lexical/link": "0.13.1",
|
||||
"@lexical/list": "0.13.1",
|
||||
@@ -29,7 +29,6 @@
|
||||
"@lexical/selection": "0.13.1",
|
||||
"@lexical/utils": "0.13.1",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"bson-objectid": "2.0.4",
|
||||
"classnames": "^2.3.2",
|
||||
"deep-equal": "2.2.3",
|
||||
@@ -43,6 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"@types/json-schema": "7.0.15",
|
||||
"@types/node": "20.6.2",
|
||||
"@types/react": "18.2.15",
|
||||
@@ -50,8 +50,8 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@payloadcms/translations": "workspace:^",
|
||||
"@payloadcms/ui": "workspace:^",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"payload": "^2.4.0"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Wraps the input formData in a blockFieldWrapperName, so that it can be read by the RenderFields component
|
||||
* which requires it to be wrapped in a group field
|
||||
*/
|
||||
export function transformInputFormData(data: any, blockFieldWrapperName: string) {
|
||||
const dataCopy = JSON.parse(JSON.stringify(data))
|
||||
|
||||
const fieldDataWithoutBlockFields = { ...dataCopy }
|
||||
delete fieldDataWithoutBlockFields['id']
|
||||
delete fieldDataWithoutBlockFields['blockName']
|
||||
delete fieldDataWithoutBlockFields['blockType']
|
||||
|
||||
// Wrap all fields inside blockFieldWrapperName.
|
||||
// This is necessary, because blockFieldWrapperName is set as the 'base' path for all fields in the block (in the RenderFields component).
|
||||
// Thus, in order for the data to be read, it has to be wrapped in this blockFieldWrapperName, as it's expected to be there.
|
||||
|
||||
// Why are we doing this? Because that way, all rendered fields of the blocks have different paths and names, and thus don't conflict with each other.
|
||||
// They have different paths and names, because they are wrapped in the blockFieldWrapperName, which has a name that is unique for each block.
|
||||
return {
|
||||
id: dataCopy.id,
|
||||
[blockFieldWrapperName]: fieldDataWithoutBlockFields,
|
||||
blockName: dataCopy.blockName,
|
||||
blockType: dataCopy.blockType,
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
export function transformInputFormSchema(formSchema: any, blockFieldWrapperName: string): Field[] {
|
||||
const formSchemaCopy = [...formSchema]
|
||||
|
||||
// First, check if it needs wrapping
|
||||
const hasBlockFieldWrapper = formSchemaCopy.some(
|
||||
(field) => 'name' in field && field.name === blockFieldWrapperName,
|
||||
)
|
||||
if (hasBlockFieldWrapper) {
|
||||
return formSchemaCopy
|
||||
}
|
||||
|
||||
// Add a group in the field schema, which represents all values saved in the blockFieldWrapperName
|
||||
return [
|
||||
...formSchemaCopy.filter(
|
||||
(field) => 'name' in field && ['blockName', 'blockType', 'id'].includes(field.name),
|
||||
),
|
||||
{
|
||||
name: blockFieldWrapperName,
|
||||
type: 'group',
|
||||
admin: {
|
||||
hideGutter: true,
|
||||
},
|
||||
fields: formSchemaCopy.filter(
|
||||
(field) => !('name' in field) || !['blockName', 'blockType', 'id'].includes(field.name),
|
||||
),
|
||||
label: '',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
ErrorPill,
|
||||
Pill,
|
||||
SectionTitle,
|
||||
createNestedFieldPath,
|
||||
useDocumentInfo,
|
||||
useFormSubmitted,
|
||||
useTranslation,
|
||||
@@ -21,7 +20,6 @@ import { $getNodeByKey } from 'lexical'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types'
|
||||
import type { FieldProps } from '../../../../types'
|
||||
import type { BlockFields, BlockNode } from '../nodes/BlocksNode'
|
||||
|
||||
import { FormSavePlugin } from './FormSavePlugin'
|
||||
@@ -89,8 +87,6 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const path = '' as const
|
||||
|
||||
const onFormChange = useCallback(
|
||||
({
|
||||
fullFieldsWithValues,
|
||||
@@ -101,9 +97,9 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
}) => {
|
||||
newFormData = {
|
||||
...newFormData,
|
||||
id: formData.id, // TODO: Why does form updatee not include theeeeem
|
||||
blockName: formData.blockName, // TODO: Why does form updatee not include theeeeem
|
||||
blockType: formData.blockType, // TODO: Why does form updatee not include theeeeem
|
||||
id: formData.id,
|
||||
blockName: newFormData.blockName2, // TODO: Find a better solution for this. We have to wrap it in blockName2 when using it here, as blockName does not accept or provide any updated values for some reason.
|
||||
blockType: formData.blockType,
|
||||
}
|
||||
|
||||
// Recursively remove all undefined values from even being present in formData, as they will
|
||||
@@ -123,8 +119,6 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
removeUndefinedAndNullRecursively(newFormData)
|
||||
removeUndefinedAndNullRecursively(formData)
|
||||
|
||||
console.log('before saving node data...', newFormData, 'old', formData)
|
||||
|
||||
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
|
||||
// which would trigger the "Leave without saving" dialog unnecessarily
|
||||
if (!isDeepEqual(formData, newFormData)) {
|
||||
@@ -136,7 +130,6 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
editor.update(() => {
|
||||
const node: BlockNode = $getNodeByKey(nodeKey)
|
||||
if (node) {
|
||||
console.log('saving node data...', newFormData)
|
||||
node.setFields(newFormData as BlockFields)
|
||||
}
|
||||
})
|
||||
@@ -197,14 +190,14 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
? getTranslation(labels.singular, i18n)
|
||||
: '[Singular Label]'}
|
||||
</Pill>
|
||||
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
|
||||
<SectionTitle path="blockName2" readOnly={field?.readOnly} />
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
|
||||
</div>
|
||||
{editor.isEditable() && (
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__removeButton`}
|
||||
disabled={field?.admin?.readOnly}
|
||||
disabled={field?.readOnly}
|
||||
icon="x"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
@@ -25,7 +25,6 @@ export const FormSavePlugin: React.FC<Props> = (props) => {
|
||||
const newFormData = reduceFieldsToValues(fields, true)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('FormSavePlugin', newFormData)
|
||||
if (onChange) {
|
||||
onChange({ fullFieldsWithValues: fields, newFormData })
|
||||
}
|
||||
@@ -4,15 +4,11 @@ import {
|
||||
Form,
|
||||
type FormProps,
|
||||
type FormState,
|
||||
buildInitialState,
|
||||
buildStateFromSchema,
|
||||
getFormState,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useFieldPath,
|
||||
useFormSubmitted,
|
||||
useLocale,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
@@ -21,6 +17,8 @@ const baseClass = 'lexical-block'
|
||||
|
||||
import type { Data } from 'payload/types'
|
||||
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types'
|
||||
import type { ClientComponentProps } from '../../types'
|
||||
import type { BlocksFeatureClientProps } from '../feature.client'
|
||||
@@ -54,8 +52,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
field: { richTextComponentMap },
|
||||
} = useEditorConfigContext()
|
||||
|
||||
console.log('1. Loading node data', formData)
|
||||
|
||||
const componentMapRenderedFieldsPath = `feature.blocks.fields.${formData?.blockType}`
|
||||
const schemaFieldsPath = `${schemaPath}.feature.blocks.${formData?.blockType}`
|
||||
|
||||
@@ -64,7 +60,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
?.clientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
|
||||
)?.reducedBlocks?.find((block) => block.slug === formData?.blockType)
|
||||
|
||||
const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath) // Field Schema
|
||||
const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath)
|
||||
// Field Schema
|
||||
useEffect(() => {
|
||||
const awaitInitialState = async () => {
|
||||
const state = await getFormState({
|
||||
@@ -79,7 +76,15 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
}) // Form State
|
||||
|
||||
if (state) {
|
||||
setInitialState(state)
|
||||
setInitialState({
|
||||
...state,
|
||||
blockName2: {
|
||||
initialValue: '',
|
||||
passesCondition: true,
|
||||
valid: true,
|
||||
value: formData.blockName,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +95,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
return await getFormState({
|
||||
const formState = await getFormState({
|
||||
apiRoute: config.routes.api,
|
||||
body: {
|
||||
id,
|
||||
@@ -100,18 +105,21 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
},
|
||||
serverURL: config.serverURL,
|
||||
})
|
||||
|
||||
return {
|
||||
...formState,
|
||||
blockName2: {
|
||||
initialValue: '',
|
||||
passesCondition: true,
|
||||
valid: true,
|
||||
value: formData.blockName,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
[config.routes.api, config.serverURL, schemaFieldsPath, id],
|
||||
[config.routes.api, config.serverURL, schemaFieldsPath, id, formData.blockName],
|
||||
)
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
//const formSchema = transformInputFormSchema(fieldMap, blockFieldWrapperName)
|
||||
|
||||
const initialStateRef = React.useRef<Data>(null) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
||||
|
||||
console.log('Bloocks initialState', initialState)
|
||||
|
||||
// Memoized Form JSX
|
||||
const formContent = useMemo(() => {
|
||||
return (
|
||||
@@ -123,6 +131,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
submitted={submitted}
|
||||
uuid={uuid()}
|
||||
>
|
||||
<BlockContent
|
||||
baseClass={baseClass}
|
||||
@@ -136,7 +145,16 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
</FieldPathProvider>
|
||||
)
|
||||
)
|
||||
}, [fieldMap, parentLexicalRichTextField, nodeKey, submitted, initialState, reducedBlock])
|
||||
}, [
|
||||
fieldMap,
|
||||
parentLexicalRichTextField,
|
||||
nodeKey,
|
||||
submitted,
|
||||
initialState,
|
||||
reducedBlock,
|
||||
blockFieldWrapperName,
|
||||
onChange,
|
||||
])
|
||||
|
||||
return <div className={baseClass}>{formContent}</div>
|
||||
}
|
||||
@@ -14,8 +14,6 @@ import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import ObjectID from 'bson-objectid'
|
||||
import React from 'react'
|
||||
|
||||
import { transformInputFormData } from '../utils/transformInputFormData'
|
||||
|
||||
export type BlockFields = {
|
||||
/** Block form data */
|
||||
[key: string]: any
|
||||
@@ -90,17 +88,7 @@ export class BlockNode extends DecoratorBlockNode {
|
||||
return false
|
||||
}
|
||||
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||
const blockFieldWrapperName = this.getFields().blockType + '-' + this.getFields().id
|
||||
const transformedFormData = transformInputFormData(this.getFields(), blockFieldWrapperName)
|
||||
|
||||
return (
|
||||
<BlockComponent
|
||||
blockFieldWrapperName={blockFieldWrapperName}
|
||||
formData={this.getFields()}
|
||||
nodeKey={this.getKey()}
|
||||
transformedFormData={transformedFormData}
|
||||
/>
|
||||
)
|
||||
return <BlockComponent formData={this.getFields()} nodeKey={this.getKey()} />
|
||||
}
|
||||
|
||||
exportDOM(): DOMExportOutput {
|
||||
@@ -129,18 +117,7 @@ export class BlockNode extends DecoratorBlockNode {
|
||||
}
|
||||
|
||||
setFields(fields: BlockFields): void {
|
||||
let fieldsCopy = JSON.parse(JSON.stringify(fields)) as BlockFields
|
||||
// Possibly transform fields
|
||||
const blockFieldWrapperName = fieldsCopy.blockType + '-' + fieldsCopy.id
|
||||
if (fieldsCopy[blockFieldWrapperName]) {
|
||||
fieldsCopy = {
|
||||
id: fieldsCopy.id,
|
||||
blockName: fieldsCopy.blockName,
|
||||
blockType: fieldsCopy.blockType,
|
||||
...fieldsCopy[blockFieldWrapperName],
|
||||
}
|
||||
delete fieldsCopy[blockFieldWrapperName]
|
||||
}
|
||||
const fieldsCopy = JSON.parse(JSON.stringify(fields)) as BlockFields
|
||||
|
||||
const writable = this.getWritable()
|
||||
writable.__fields = fieldsCopy
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { HTMLConverter } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
export type HTMLConverterFeatureProps = {
|
||||
converters?:
|
||||
| (({ defaultConverters }: { defaultConverters: HTMLConverter[] }) => HTMLConverter[])
|
||||
| HTMLConverter[]
|
||||
}
|
||||
|
||||
export const HTMLConverterFeature: FeatureProviderProviderServer<
|
||||
HTMLConverterFeatureProps,
|
||||
undefined
|
||||
> = (props) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
clientFeatureProps: null,
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'htmlConverter',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { Field, RichTextField, TextField } from 'payload/types'
|
||||
|
||||
import type { LexicalRichTextAdapter, SanitizedEditorConfig } from '../../../../../index'
|
||||
import type { LexicalRichTextAdapter, SanitizedServerEditorConfig } from '../../../../../index'
|
||||
import type { AdapterProps } from '../../../../../types'
|
||||
import type { HTMLConverter } from '../converter/types'
|
||||
import type { HTMLConverterFeatureProps } from '../index'
|
||||
import type { HTMLConverterFeatureProps } from '../feature.server'
|
||||
|
||||
import { convertLexicalToHTML } from '../converter'
|
||||
import { defaultHTMLConverters } from '../converter/defaultConverters'
|
||||
@@ -21,10 +21,11 @@ type Props = {
|
||||
export const consolidateHTMLConverters = ({
|
||||
editorConfig,
|
||||
}: {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
editorConfig: SanitizedServerEditorConfig
|
||||
}) => {
|
||||
const htmlConverterFeature = editorConfig.resolvedFeatureMap.get('htmlConverter')
|
||||
const htmlConverterFeatureProps: HTMLConverterFeatureProps = htmlConverterFeature?.props
|
||||
const htmlConverterFeatureProps: HTMLConverterFeatureProps =
|
||||
htmlConverterFeature?.serverFeatureProps
|
||||
|
||||
const defaultConvertersWithConvertersFromFeatures = defaultHTMLConverters
|
||||
|
||||
@@ -55,7 +56,7 @@ export const lexicalHTML: (
|
||||
) => TextField = (lexicalFieldName, props) => {
|
||||
const { name = 'lexicalHTML' } = props
|
||||
return {
|
||||
name: name,
|
||||
name,
|
||||
type: 'text',
|
||||
admin: {
|
||||
hidden: true,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { FeatureProvider } from '../../types'
|
||||
import type { HTMLConverter } from './converter/types'
|
||||
|
||||
export type HTMLConverterFeatureProps = {
|
||||
converters?:
|
||||
| (({ defaultConverters }: { defaultConverters: HTMLConverter[] }) => HTMLConverter[])
|
||||
| HTMLConverter[]
|
||||
}
|
||||
|
||||
/**
|
||||
* This feature only manages the converters. They are read and actually run / executed by the
|
||||
* Lexical field.
|
||||
*/
|
||||
export const HTMLConverterFeature = (props?: HTMLConverterFeatureProps): FeatureProvider => {
|
||||
if (!props) {
|
||||
props = {}
|
||||
}
|
||||
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
props,
|
||||
}
|
||||
},
|
||||
key: 'htmlConverter',
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
export const TestRecorderFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
plugins: [
|
||||
{
|
||||
Component: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('./plugin').then((module) => module.TestRecorderPlugin),
|
||||
position: 'bottom',
|
||||
},
|
||||
],
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
key: 'debug-testrecorder',
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { FeatureProvider } from '../../types'
|
||||
|
||||
export const TreeViewFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
plugins: [
|
||||
{
|
||||
Component: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('./plugin').then((module) => module.TreeViewPlugin),
|
||||
position: 'bottom',
|
||||
},
|
||||
],
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
key: 'debug-treeview',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { TestRecorderPlugin } from './plugin'
|
||||
|
||||
const TestRecorderFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => ({
|
||||
clientFeatureProps: props,
|
||||
plugins: [
|
||||
{
|
||||
Component: TestRecorderPlugin,
|
||||
position: 'bottom',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const TestRecorderFeatureClientComponent = createClientComponent(TestRecorderFeatureClient)
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { TestRecorderFeatureClientComponent } from './feature.client'
|
||||
|
||||
export const TestRecorderFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: TestRecorderFeatureClientComponent,
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'testrecorder',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -389,6 +389,7 @@ ${steps.map(formatStep).join(`\n`)}
|
||||
e.preventDefault()
|
||||
}}
|
||||
title={isRecording ? 'Disable test recorder' : 'Enable test recorder'}
|
||||
type="button"
|
||||
>
|
||||
{isRecording ? 'Disable test recorder' : 'Enable test recorder'}
|
||||
</button>
|
||||
@@ -404,6 +405,7 @@ ${steps.map(formatStep).join(`\n`)}
|
||||
e.preventDefault()
|
||||
}}
|
||||
title="Insert snapshot"
|
||||
type="button"
|
||||
>
|
||||
Insert Snapshot
|
||||
</button>
|
||||
@@ -415,6 +417,7 @@ ${steps.map(formatStep).join(`\n`)}
|
||||
e.preventDefault()
|
||||
}}
|
||||
title="Copy to clipboard"
|
||||
type="button"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
@@ -426,6 +429,7 @@ ${steps.map(formatStep).join(`\n`)}
|
||||
e.preventDefault()
|
||||
}}
|
||||
title="Download as a file"
|
||||
type="button"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { TreeViewPlugin } from './plugin'
|
||||
|
||||
const TreeViewFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => ({
|
||||
clientFeatureProps: props,
|
||||
plugins: [
|
||||
{
|
||||
Component: TreeViewPlugin,
|
||||
position: 'bottom',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const TreeViewFeatureClientComponent = createClientComponent(TreeViewFeatureClient)
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { TreeViewFeatureClientComponent } from './feature.client'
|
||||
|
||||
export const TreeViewFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: TreeViewFeatureClientComponent,
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'treeview',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import * as React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export function TreeViewPlugin(): JSX.Element {
|
||||
export function TreeViewPlugin(): React.ReactNode {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
return (
|
||||
<TreeView
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client'
|
||||
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { BoldIcon } from '../../../lexical/ui/icons/Bold'
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
import {
|
||||
BOLD_ITALIC_STAR,
|
||||
@@ -10,9 +13,9 @@ import {
|
||||
BOLD_UNDERSCORE,
|
||||
} from './markdownTransformers'
|
||||
|
||||
export const BoldTextFeature = (): FeatureProvider => {
|
||||
const BoldFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
dependenciesSoft: ['italic'],
|
||||
clientFeatureProps: props,
|
||||
feature: ({ featureProviderMap }) => {
|
||||
const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE]
|
||||
if (featureProviderMap.get('italic')) {
|
||||
@@ -20,13 +23,12 @@ export const BoldTextFeature = (): FeatureProvider => {
|
||||
}
|
||||
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../../lexical/ui/icons/Bold').then((module) => module.BoldIcon),
|
||||
ChildComponent: BoldIcon,
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('bold')
|
||||
@@ -42,10 +44,10 @@ export const BoldTextFeature = (): FeatureProvider => {
|
||||
]),
|
||||
],
|
||||
},
|
||||
markdownTransformers: markdownTransformers,
|
||||
props: null,
|
||||
markdownTransformers,
|
||||
}
|
||||
},
|
||||
key: 'bold',
|
||||
}
|
||||
}
|
||||
|
||||
export const BoldFeatureClientComponent = createClientComponent(BoldFeatureClient)
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { BoldFeatureClientComponent } from './feature.client'
|
||||
import {
|
||||
BOLD_ITALIC_STAR,
|
||||
BOLD_ITALIC_UNDERSCORE,
|
||||
BOLD_STAR,
|
||||
BOLD_UNDERSCORE,
|
||||
} from './markdownTransformers'
|
||||
|
||||
export const BoldFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||
return {
|
||||
dependenciesSoft: ['italic'],
|
||||
feature: ({ featureProviderMap }) => {
|
||||
const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE]
|
||||
if (featureProviderMap.get('italic')) {
|
||||
markdownTransformers.push(BOLD_ITALIC_UNDERSCORE, BOLD_ITALIC_STAR)
|
||||
}
|
||||
|
||||
return {
|
||||
ClientComponent: BoldFeatureClientComponent,
|
||||
markdownTransformers,
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'bold',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { CodeIcon } from '../../../lexical/ui/icons/Code'
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
import { INLINE_CODE } from './markdownTransformers'
|
||||
|
||||
export const InlineCodeTextFeature = (): FeatureProvider => {
|
||||
const InlineCodeFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../../lexical/ui/icons/Code').then((module) => module.CodeIcon),
|
||||
ChildComponent: CodeIcon,
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('code')
|
||||
@@ -31,10 +35,11 @@ export const InlineCodeTextFeature = (): FeatureProvider => {
|
||||
]),
|
||||
],
|
||||
},
|
||||
|
||||
markdownTransformers: [INLINE_CODE],
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
key: 'inlineCode',
|
||||
}
|
||||
}
|
||||
|
||||
export const InlineCodeFeatureClientComponent = createClientComponent(InlineCodeFeatureClient)
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { InlineCodeFeatureClientComponent } from './feature.client'
|
||||
import { INLINE_CODE } from './markdownTransformers'
|
||||
|
||||
export const InlineCodeFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: InlineCodeFeatureClientComponent,
|
||||
markdownTransformers: [INLINE_CODE],
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'inlinecode',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { ItalicIcon } from '../../../lexical/ui/icons/Italic'
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers'
|
||||
|
||||
export const ItalicTextFeature = (): FeatureProvider => {
|
||||
const ItalicFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../../lexical/ui/icons/Italic').then((module) => module.ItalicIcon),
|
||||
ChildComponent: ItalicIcon,
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('italic')
|
||||
@@ -32,9 +37,9 @@ export const ItalicTextFeature = (): FeatureProvider => {
|
||||
],
|
||||
},
|
||||
markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE],
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
key: 'italic',
|
||||
}
|
||||
}
|
||||
|
||||
export const ItalicFeatureClientComponent = createClientComponent(ItalicFeatureClient)
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { ItalicFeatureClientComponent } from './feature.client'
|
||||
import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers'
|
||||
|
||||
export const ItalicFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: ItalicFeatureClientComponent,
|
||||
markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE],
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'italic',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough'
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
import { STRIKETHROUGH } from './markdownTransformers'
|
||||
|
||||
export const StrikethroughTextFeature = (): FeatureProvider => {
|
||||
const StrikethroughFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../../lexical/ui/icons/Strikethrough').then(
|
||||
(module) => module.StrikethroughIcon,
|
||||
),
|
||||
ChildComponent: StrikethroughIcon,
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('strikethrough')
|
||||
@@ -34,9 +37,9 @@ export const StrikethroughTextFeature = (): FeatureProvider => {
|
||||
],
|
||||
},
|
||||
markdownTransformers: [STRIKETHROUGH],
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
key: 'strikethrough',
|
||||
}
|
||||
}
|
||||
|
||||
export const StrikethroughFeatureClientComponent = createClientComponent(StrikethroughFeatureClient)
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { StrikethroughFeatureClientComponent } from './feature.client'
|
||||
import { STRIKETHROUGH } from './markdownTransformers'
|
||||
|
||||
export const StrikethroughFeature: FeatureProviderProviderServer<undefined, undefined> = (
|
||||
props,
|
||||
) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: StrikethroughFeatureClientComponent,
|
||||
|
||||
markdownTransformers: [STRIKETHROUGH],
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'strikethrough',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { SubscriptIcon } from '../../../lexical/ui/icons/Subscript'
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
|
||||
export const SubscriptTextFeature = (): FeatureProvider => {
|
||||
const SubscriptFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../../lexical/ui/icons/Subscript').then(
|
||||
(module) => module.SubscriptIcon,
|
||||
),
|
||||
ChildComponent: SubscriptIcon,
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('subscript')
|
||||
@@ -32,9 +34,9 @@ export const SubscriptTextFeature = (): FeatureProvider => {
|
||||
]),
|
||||
],
|
||||
},
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
key: 'subscript',
|
||||
}
|
||||
}
|
||||
|
||||
export const SubscriptFeatureClientComponent = createClientComponent(SubscriptFeatureClient)
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { SubscriptFeatureClientComponent } from './feature.client'
|
||||
|
||||
export const SubscriptFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: SubscriptFeatureClientComponent,
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'subscript',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { SuperscriptIcon } from '../../../lexical/ui/icons/Superscript'
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
|
||||
export const SuperscriptTextFeature = (): FeatureProvider => {
|
||||
const SuperscriptFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../../lexical/ui/icons/Superscript').then(
|
||||
(module) => module.SuperscriptIcon,
|
||||
),
|
||||
ChildComponent: SuperscriptIcon,
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('superscript')
|
||||
@@ -32,9 +34,9 @@ export const SuperscriptTextFeature = (): FeatureProvider => {
|
||||
]),
|
||||
],
|
||||
},
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
key: 'superscript',
|
||||
}
|
||||
}
|
||||
|
||||
export const SuperscriptFeatureClientComponent = createClientComponent(SuperscriptFeatureClient)
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { SuperscriptFeatureClientComponent } from './feature.client'
|
||||
|
||||
export const SuperscriptFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: SuperscriptFeatureClientComponent,
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'superscript',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../../types'
|
||||
import type { FeatureProviderProviderClient } from '../../types'
|
||||
|
||||
import { UnderlineIcon } from '../../../lexical/ui/icons/Underline'
|
||||
import { createClientComponent } from '../../createClientComponent'
|
||||
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
|
||||
|
||||
export const UnderlineTextFeature = (): FeatureProvider => {
|
||||
const UnderlineFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
SectionWithEntries([
|
||||
{
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../../lexical/ui/icons/Underline').then(
|
||||
(module) => module.UnderlineIcon,
|
||||
),
|
||||
ChildComponent: UnderlineIcon,
|
||||
isActive: ({ selection }) => {
|
||||
if ($isRangeSelection(selection)) {
|
||||
return selection.hasFormat('underline')
|
||||
@@ -32,9 +34,9 @@ export const UnderlineTextFeature = (): FeatureProvider => {
|
||||
]),
|
||||
],
|
||||
},
|
||||
props: null,
|
||||
}
|
||||
},
|
||||
key: 'underline',
|
||||
}
|
||||
}
|
||||
|
||||
export const UnderlineFeatureClientComponent = createClientComponent(UnderlineFeatureClient)
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FeatureProviderProviderServer } from '../../types'
|
||||
|
||||
import { UnderlineFeatureClientComponent } from './feature.client'
|
||||
|
||||
export const UnderlineFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: UnderlineFeatureClientComponent,
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'underline',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,24 @@
|
||||
import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text'
|
||||
'use client'
|
||||
|
||||
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text'
|
||||
import type { HeadingTagType } from '@lexical/rich-text'
|
||||
|
||||
import { HeadingNode } from '@lexical/rich-text'
|
||||
import { $createHeadingNode } from '@lexical/rich-text'
|
||||
import { $setBlocksType } from '@lexical/selection'
|
||||
import { $getSelection } from 'lexical'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types'
|
||||
import type { FeatureProvider } from '../types'
|
||||
import type { FeatureProviderProviderClient } from '../types'
|
||||
import type { HeadingFeatureProps } from './feature.server'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { H1Icon } from '../../lexical/ui/icons/H1'
|
||||
import { H2Icon } from '../../lexical/ui/icons/H2'
|
||||
import { H3Icon } from '../../lexical/ui/icons/H3'
|
||||
import { H4Icon } from '../../lexical/ui/icons/H4'
|
||||
import { H5Icon } from '../../lexical/ui/icons/H5'
|
||||
import { H6Icon } from '../../lexical/ui/icons/H6'
|
||||
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||
import { createClientComponent } from '../createClientComponent'
|
||||
import { MarkdownTransformer } from './markdownTransformer'
|
||||
|
||||
const setHeading = (headingSize: HeadingTagType) => {
|
||||
@@ -17,31 +26,23 @@ const setHeading = (headingSize: HeadingTagType) => {
|
||||
$setBlocksType(selection, () => $createHeadingNode(headingSize))
|
||||
}
|
||||
|
||||
type Props = {
|
||||
enabledHeadingSizes?: HeadingTagType[]
|
||||
}
|
||||
|
||||
const iconImports = {
|
||||
// @ts-expect-error-next-line
|
||||
h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon),
|
||||
// @ts-expect-error-next-line
|
||||
h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon),
|
||||
// @ts-expect-error-next-line
|
||||
h3: () => import('../../lexical/ui/icons/H3').then((module) => module.H3Icon),
|
||||
// @ts-expect-error-next-line
|
||||
h4: () => import('../../lexical/ui/icons/H4').then((module) => module.H4Icon),
|
||||
// @ts-expect-error-next-line
|
||||
h5: () => import('../../lexical/ui/icons/H5').then((module) => module.H5Icon),
|
||||
// @ts-expect-error-next-line
|
||||
h6: () => import('../../lexical/ui/icons/H6').then((module) => module.H6Icon),
|
||||
h1: H1Icon,
|
||||
h2: H2Icon,
|
||||
h3: H3Icon,
|
||||
h4: H4Icon,
|
||||
h5: H5Icon,
|
||||
h6: H6Icon,
|
||||
}
|
||||
|
||||
export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
const HeadingFeatureClient: FeatureProviderProviderClient<HeadingFeatureProps> = (props) => {
|
||||
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
|
||||
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
...enabledHeadingSizes.map((headingSize, i) =>
|
||||
@@ -63,30 +64,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
],
|
||||
},
|
||||
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
|
||||
nodes: [
|
||||
{
|
||||
type: HeadingNode.getType(),
|
||||
converters: {
|
||||
html: {
|
||||
converter: async ({ converters, node, parent }) => {
|
||||
const childrenText = await convertLexicalNodesToHTML({
|
||||
converters,
|
||||
lexicalNodes: node.children,
|
||||
parent: {
|
||||
...node,
|
||||
parent,
|
||||
},
|
||||
})
|
||||
|
||||
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
|
||||
},
|
||||
nodeTypes: [HeadingNode.getType()],
|
||||
} as HTMLConverter<SerializedHeadingNode>,
|
||||
},
|
||||
node: HeadingNode,
|
||||
},
|
||||
],
|
||||
props,
|
||||
nodes: [HeadingNode],
|
||||
slashMenu: {
|
||||
options: [
|
||||
...enabledHeadingSizes.map((headingSize) => {
|
||||
@@ -109,6 +87,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
},
|
||||
}
|
||||
},
|
||||
key: 'heading',
|
||||
}
|
||||
}
|
||||
|
||||
export const HeadingFeatureClientComponent = createClientComponent(HeadingFeatureClient)
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { HeadingTagType } from '@lexical/rich-text'
|
||||
|
||||
import { HeadingNode, type SerializedHeadingNode } from '@lexical/rich-text'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types'
|
||||
import type { FeatureProviderProviderServer } from '../types'
|
||||
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||
import { HeadingFeatureClientComponent } from './feature.client'
|
||||
import { MarkdownTransformer } from './markdownTransformer'
|
||||
|
||||
export type HeadingFeatureProps = {
|
||||
enabledHeadingSizes?: HeadingTagType[]
|
||||
}
|
||||
|
||||
export const HeadingFeature: FeatureProviderProviderServer<
|
||||
HeadingFeatureProps,
|
||||
HeadingFeatureProps
|
||||
> = (props) => {
|
||||
if (!props) {
|
||||
props = {}
|
||||
}
|
||||
|
||||
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
|
||||
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: HeadingFeatureClientComponent,
|
||||
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
|
||||
nodes: [
|
||||
{
|
||||
type: HeadingNode.getType(),
|
||||
converters: {
|
||||
html: {
|
||||
converter: async ({ converters, node, parent }) => {
|
||||
const childrenText = await convertLexicalNodesToHTML({
|
||||
converters,
|
||||
lexicalNodes: node.children,
|
||||
parent: {
|
||||
...node,
|
||||
parent,
|
||||
},
|
||||
})
|
||||
|
||||
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
|
||||
},
|
||||
nodeTypes: [HeadingNode.getType()],
|
||||
} as HTMLConverter<SerializedHeadingNode>,
|
||||
},
|
||||
node: HeadingNode,
|
||||
},
|
||||
],
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'heading',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,6 @@ export const CheckListFeature = (): FeatureProvider => {
|
||||
},
|
||||
}
|
||||
},
|
||||
key: 'checkList',
|
||||
key: 'checklist',
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,6 @@ export const OrderedListFeature = (): FeatureProvider => {
|
||||
},
|
||||
}
|
||||
},
|
||||
key: 'orderedList',
|
||||
key: 'orderedlist',
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,6 @@ export const UnorderedListFeature = (): FeatureProvider => {
|
||||
},
|
||||
}
|
||||
},
|
||||
key: 'unorderedList',
|
||||
key: 'unorderedlist',
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user