node_modules ignore

This commit is contained in:
oier committed 2025-05-08 23:43:47 +02:00
1 parent e19d52f172
commit 4574544c9f
65041 files changed
+10593536

No files matched your search

@@ -0,0 +1,248 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var strapiAdmin = require('@strapi/admin/strapi-admin');
var reactIntl = require('react-intl');
var reactRouterDom = require('react-router-dom');
var Fields = require('../components/ConfigurationForm/Fields.js');
var Form = require('../components/ConfigurationForm/Form.js');
var useContentTypeSchema = require('../hooks/useContentTypeSchema.js');
var useDocumentLayout = require('../hooks/useDocumentLayout.js');
var hooks = require('../modules/hooks.js');
var components = require('../services/components.js');
var init = require('../services/init.js');
var objects = require('../utils/objects.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
/* -------------------------------------------------------------------------------------------------
* ComponentConfigurationPage
* -----------------------------------------------------------------------------------------------*/ const ComponentConfigurationPage = ()=>{
/**
* useDocumentLayout only works for documents, not components,
* it feels weird to make that hook work for both when this is SUCH
* a unique use case and we only do it here, so in short, we essentially
* just extracted the logic to make an edit view layout and reproduced it here.
*/ const { slug: model } = reactRouterDom.useParams();
const { toggleNotification } = strapiAdmin.useNotification();
const { formatMessage } = reactIntl.useIntl();
const { _unstableFormatAPIError: formatAPIError } = strapiAdmin.useAPIErrorHandler();
const { components: components$1, fieldSizes, schema, error: errorSchema, isLoading: isLoadingSchema, isFetching: isFetchingSchema } = init.useGetInitialDataQuery(undefined, {
selectFromResult: (res)=>{
const schema = res.data?.components.find((ct)=>ct.uid === model);
const componentsByKey = res.data?.components.reduce((acc, component)=>{
acc[component.uid] = component;
return acc;
}, {});
const components = useContentTypeSchema.extractContentTypeComponents(schema?.attributes, componentsByKey);
const fieldSizes = Object.entries(res.data?.fieldSizes ?? {}).reduce((acc, [attributeName, { default: size }])=>{
acc[attributeName] = size;
return acc;
}, {});
return {
isFetching: res.isFetching,
isLoading: res.isLoading,
error: res.error,
components,
schema,
fieldSizes
};
}
});
React__namespace.useEffect(()=>{
if (errorSchema) {
toggleNotification({
type: 'danger',
message: formatAPIError(errorSchema)
});
}
}, [
errorSchema,
formatAPIError,
toggleNotification
]);
const { data, isLoading: isLoadingConfig, isFetching: isFetchingConfig, error } = components.useGetComponentConfigurationQuery(model ?? '');
React__namespace.useEffect(()=>{
if (error) {
toggleNotification({
type: 'danger',
message: formatAPIError(error)
});
}
}, [
error,
formatAPIError,
toggleNotification
]);
/**
* you **must** check if we're loading or fetching in case the component gets new props
* but nothing was unmounted, it then becomes a fetch, not a load.
*/ const isLoading = isLoadingConfig || isLoadingSchema || isFetchingConfig || isFetchingSchema;
const editLayout = React__namespace.useMemo(()=>data && !isLoading ? formatEditLayout(data, {
schema,
components: components$1
}) : {
layout: [],
components: {},
metadatas: {},
options: {},
settings: useDocumentLayout.DEFAULT_SETTINGS
}, [
data,
isLoading,
schema,
components$1
]);
const [updateConfiguration] = components.useUpdateComponentConfigurationMutation();
const handleSubmit = async (formData)=>{
try {
/**
* We reconstruct the metadatas object by taking the existing list metadatas
* and re-merging that by attribute name with the current list metadatas, whilst overwriting
* the data from the form we've built.
*/ const meta = Object.entries(data?.component.metadatas ?? {}).reduce((acc, [name, { edit, list }])=>{
const { __temp_key__, size: _size, name: _name, ...editedMetadata } = formData.layout.flatMap((row)=>row.children).find((field)=>field.name === name) ?? {};
acc[name] = {
edit: {
...edit,
...editedMetadata
},
list
};
return acc;
}, {});
const res = await updateConfiguration({
layouts: {
edit: formData.layout.map((row)=>row.children.reduce((acc, { name, size })=>{
if (name !== Fields.TEMP_FIELD_NAME) {
return [
...acc,
{
name,
size
}
];
}
return acc;
}, [])),
list: data?.component.layouts.list
},
settings: objects.setIn(formData.settings, 'displayName', undefined),
metadatas: meta,
uid: model
});
if ('data' in res) {
toggleNotification({
type: 'success',
message: formatMessage({
id: 'notification.success.saved',
defaultMessage: 'Saved'
})
});
} else {
toggleNotification({
type: 'danger',
message: formatAPIError(res.error)
});
}
} catch {
toggleNotification({
type: 'danger',
message: formatMessage({
id: 'notification.error',
defaultMessage: 'An error occurred'
})
});
}
};
if (isLoading) {
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Loading, {});
}
if (error || errorSchema || !schema) {
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Error, {});
}
return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
/*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Title, {
children: `Configure ${editLayout.settings.displayName} Edit View`
}),
/*#__PURE__*/ jsxRuntime.jsx(Form.ConfigurationForm, {
onSubmit: handleSubmit,
attributes: schema.attributes,
fieldSizes: fieldSizes,
layout: editLayout
})
]
});
};
/* -------------------------------------------------------------------------------------------------
* Header
* -----------------------------------------------------------------------------------------------*/ const formatEditLayout = (data, { schema, components })=>{
const editAttributes = useDocumentLayout.convertEditLayoutToFieldLayouts(data.component.layouts.edit, schema?.attributes, data.component.metadatas, {
configurations: data.components,
schemas: components
});
const componentEditAttributes = Object.entries(data.components).reduce((acc, [uid, configuration])=>{
acc[uid] = {
layout: useDocumentLayout.convertEditLayoutToFieldLayouts(configuration.layouts.edit, components[uid].attributes, configuration.metadatas),
settings: {
...configuration.settings,
icon: components[uid].info.icon,
displayName: components[uid].info.displayName
}
};
return acc;
}, {});
const editMetadatas = Object.entries(data.component.metadatas).reduce((acc, [attribute, metadata])=>{
return {
...acc,
[attribute]: metadata.edit
};
}, {});
return {
layout: [
editAttributes
],
components: componentEditAttributes,
metadatas: editMetadatas,
options: {
...schema?.options,
...schema?.pluginOptions
},
settings: {
...data.component.settings,
displayName: schema?.info.displayName
}
};
};
/* -------------------------------------------------------------------------------------------------
* Header
* -----------------------------------------------------------------------------------------------*/ const ProtectedComponentConfigurationPage = ()=>{
const permissions = hooks.useTypedSelector((state)=>state.admin_app.permissions.contentManager?.componentsConfigurations);
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Protect, {
permissions: permissions,
children: /*#__PURE__*/ jsxRuntime.jsx(ComponentConfigurationPage, {})
});
};
exports.ComponentConfigurationPage = ComponentConfigurationPage;
exports.ProtectedComponentConfigurationPage = ProtectedComponentConfigurationPage;
//# sourceMappingURL=ComponentConfigurationPage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ComponentConfigurationPage.js","sources":["../../../admin/src/pages/ComponentConfigurationPage.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Page, useNotification, useAPIErrorHandler } from '@strapi/admin/strapi-admin';\nimport { useIntl } from 'react-intl';\nimport { useParams } from 'react-router-dom';\n\nimport { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields';\nimport { ConfigurationForm, ConfigurationFormProps } from '../components/ConfigurationForm/Form';\nimport { ComponentsDictionary, extractContentTypeComponents } from '../hooks/useContentTypeSchema';\nimport {\n DEFAULT_SETTINGS,\n EditLayout,\n convertEditLayoutToFieldLayouts,\n} from '../hooks/useDocumentLayout';\nimport { useTypedSelector } from '../modules/hooks';\nimport {\n useGetComponentConfigurationQuery,\n useUpdateComponentConfigurationMutation,\n} from '../services/components';\nimport { useGetInitialDataQuery } from '../services/init';\nimport { setIn } from '../utils/objects';\n\nimport type { Component, FindComponentConfiguration } from '../../../shared/contracts/components';\nimport type { Metadatas } from '../../../shared/contracts/content-types';\n\n/* -------------------------------------------------------------------------------------------------\n * ComponentConfigurationPage\n * -----------------------------------------------------------------------------------------------*/\n\nconst ComponentConfigurationPage = () => {\n /**\n * useDocumentLayout only works for documents, not components,\n * it feels weird to make that hook work for both when this is SUCH\n * a unique use case and we only do it here, so in short, we essentially\n * just extracted the logic to make an edit view layout and reproduced it here.\n */\n const { slug: model } = useParams<{ slug: string }>();\n const { toggleNotification } = useNotification();\n const { formatMessage } = useIntl();\n const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();\n\n const {\n components,\n fieldSizes,\n schema,\n error: errorSchema,\n isLoading: isLoadingSchema,\n isFetching: isFetchingSchema,\n } = useGetInitialDataQuery(undefined, {\n selectFromResult: (res) => {\n const schema = res.data?.components.find((ct) => ct.uid === model);\n\n const componentsByKey = res.data?.components.reduce<ComponentsDictionary>(\n (acc, component) => {\n acc[component.uid] = component;\n\n return acc;\n },\n {}\n );\n\n const components = extractContentTypeComponents(schema?.attributes, componentsByKey);\n\n const fieldSizes = Object.entries(res.data?.fieldSizes ?? {}).reduce<\n ConfigurationFormProps['fieldSizes']\n >((acc, [attributeName, { default: size }]) => {\n acc[attributeName] = size;\n\n return acc;\n }, {});\n\n return {\n isFetching: res.isFetching,\n isLoading: res.isLoading,\n error: res.error,\n components,\n schema,\n fieldSizes,\n };\n },\n });\n\n React.useEffect(() => {\n if (errorSchema) {\n toggleNotification({\n type: 'danger',\n message: formatAPIError(errorSchema),\n });\n }\n }, [errorSchema, formatAPIError, toggleNotification]);\n\n const {\n data,\n isLoading: isLoadingConfig,\n isFetching: isFetchingConfig,\n error,\n } = useGetComponentConfigurationQuery(model ?? '');\n\n React.useEffect(() => {\n if (error) {\n toggleNotification({\n type: 'danger',\n message: formatAPIError(error),\n });\n }\n }, [error, formatAPIError, toggleNotification]);\n\n /**\n * you **must** check if we're loading or fetching in case the component gets new props\n * but nothing was unmounted, it then becomes a fetch, not a load.\n */\n const isLoading = isLoadingConfig || isLoadingSchema || isFetchingConfig || isFetchingSchema;\n\n const editLayout = React.useMemo(\n () =>\n data && !isLoading\n ? formatEditLayout(data, { schema, components })\n : ({\n layout: [],\n components: {},\n metadatas: {},\n options: {},\n settings: DEFAULT_SETTINGS,\n } as EditLayout),\n [data, isLoading, schema, components]\n );\n\n const [updateConfiguration] = useUpdateComponentConfigurationMutation();\n const handleSubmit: ConfigurationFormProps['onSubmit'] = async (formData) => {\n try {\n /**\n * We reconstruct the metadatas object by taking the existing list metadatas\n * and re-merging that by attribute name with the current list metadatas, whilst overwriting\n * the data from the form we've built.\n */\n const meta = Object.entries(data?.component.metadatas ?? {}).reduce<Metadatas>(\n (acc, [name, { edit, list }]) => {\n const {\n __temp_key__,\n size: _size,\n name: _nameLine truncated
@@ -0,0 +1,226 @@
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import * as React from 'react';
import { useNotification, useAPIErrorHandler, Page } from '@strapi/admin/strapi-admin';
import { useIntl } from 'react-intl';
import { useParams } from 'react-router-dom';
import { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields.mjs';
import { ConfigurationForm } from '../components/ConfigurationForm/Form.mjs';
import { extractContentTypeComponents } from '../hooks/useContentTypeSchema.mjs';
import { DEFAULT_SETTINGS, convertEditLayoutToFieldLayouts } from '../hooks/useDocumentLayout.mjs';
import { useTypedSelector } from '../modules/hooks.mjs';
import { useGetComponentConfigurationQuery, useUpdateComponentConfigurationMutation } from '../services/components.mjs';
import { useGetInitialDataQuery } from '../services/init.mjs';
import { setIn } from '../utils/objects.mjs';
/* -------------------------------------------------------------------------------------------------
* ComponentConfigurationPage
* -----------------------------------------------------------------------------------------------*/ const ComponentConfigurationPage = ()=>{
/**
* useDocumentLayout only works for documents, not components,
* it feels weird to make that hook work for both when this is SUCH
* a unique use case and we only do it here, so in short, we essentially
* just extracted the logic to make an edit view layout and reproduced it here.
*/ const { slug: model } = useParams();
const { toggleNotification } = useNotification();
const { formatMessage } = useIntl();
const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();
const { components, fieldSizes, schema, error: errorSchema, isLoading: isLoadingSchema, isFetching: isFetchingSchema } = useGetInitialDataQuery(undefined, {
selectFromResult: (res)=>{
const schema = res.data?.components.find((ct)=>ct.uid === model);
const componentsByKey = res.data?.components.reduce((acc, component)=>{
acc[component.uid] = component;
return acc;
}, {});
const components = extractContentTypeComponents(schema?.attributes, componentsByKey);
const fieldSizes = Object.entries(res.data?.fieldSizes ?? {}).reduce((acc, [attributeName, { default: size }])=>{
acc[attributeName] = size;
return acc;
}, {});
return {
isFetching: res.isFetching,
isLoading: res.isLoading,
error: res.error,
components,
schema,
fieldSizes
};
}
});
React.useEffect(()=>{
if (errorSchema) {
toggleNotification({
type: 'danger',
message: formatAPIError(errorSchema)
});
}
}, [
errorSchema,
formatAPIError,
toggleNotification
]);
const { data, isLoading: isLoadingConfig, isFetching: isFetchingConfig, error } = useGetComponentConfigurationQuery(model ?? '');
React.useEffect(()=>{
if (error) {
toggleNotification({
type: 'danger',
message: formatAPIError(error)
});
}
}, [
error,
formatAPIError,
toggleNotification
]);
/**
* you **must** check if we're loading or fetching in case the component gets new props
* but nothing was unmounted, it then becomes a fetch, not a load.
*/ const isLoading = isLoadingConfig || isLoadingSchema || isFetchingConfig || isFetchingSchema;
const editLayout = React.useMemo(()=>data && !isLoading ? formatEditLayout(data, {
schema,
components
}) : {
layout: [],
components: {},
metadatas: {},
options: {},
settings: DEFAULT_SETTINGS
}, [
data,
isLoading,
schema,
components
]);
const [updateConfiguration] = useUpdateComponentConfigurationMutation();
const handleSubmit = async (formData)=>{
try {
/**
* We reconstruct the metadatas object by taking the existing list metadatas
* and re-merging that by attribute name with the current list metadatas, whilst overwriting
* the data from the form we've built.
*/ const meta = Object.entries(data?.component.metadatas ?? {}).reduce((acc, [name, { edit, list }])=>{
const { __temp_key__, size: _size, name: _name, ...editedMetadata } = formData.layout.flatMap((row)=>row.children).find((field)=>field.name === name) ?? {};
acc[name] = {
edit: {
...edit,
...editedMetadata
},
list
};
return acc;
}, {});
const res = await updateConfiguration({
layouts: {
edit: formData.layout.map((row)=>row.children.reduce((acc, { name, size })=>{
if (name !== TEMP_FIELD_NAME) {
return [
...acc,
{
name,
size
}
];
}
return acc;
}, [])),
list: data?.component.layouts.list
},
settings: setIn(formData.settings, 'displayName', undefined),
metadatas: meta,
uid: model
});
if ('data' in res) {
toggleNotification({
type: 'success',
message: formatMessage({
id: 'notification.success.saved',
defaultMessage: 'Saved'
})
});
} else {
toggleNotification({
type: 'danger',
message: formatAPIError(res.error)
});
}
} catch {
toggleNotification({
type: 'danger',
message: formatMessage({
id: 'notification.error',
defaultMessage: 'An error occurred'
})
});
}
};
if (isLoading) {
return /*#__PURE__*/ jsx(Page.Loading, {});
}
if (error || errorSchema || !schema) {
return /*#__PURE__*/ jsx(Page.Error, {});
}
return /*#__PURE__*/ jsxs(Fragment, {
children: [
/*#__PURE__*/ jsx(Page.Title, {
children: `Configure ${editLayout.settings.displayName} Edit View`
}),
/*#__PURE__*/ jsx(ConfigurationForm, {
onSubmit: handleSubmit,
attributes: schema.attributes,
fieldSizes: fieldSizes,
layout: editLayout
})
]
});
};
/* -------------------------------------------------------------------------------------------------
* Header
* -----------------------------------------------------------------------------------------------*/ const formatEditLayout = (data, { schema, components })=>{
const editAttributes = convertEditLayoutToFieldLayouts(data.component.layouts.edit, schema?.attributes, data.component.metadatas, {
configurations: data.components,
schemas: components
});
const componentEditAttributes = Object.entries(data.components).reduce((acc, [uid, configuration])=>{
acc[uid] = {
layout: convertEditLayoutToFieldLayouts(configuration.layouts.edit, components[uid].attributes, configuration.metadatas),
settings: {
...configuration.settings,
icon: components[uid].info.icon,
displayName: components[uid].info.displayName
}
};
return acc;
}, {});
const editMetadatas = Object.entries(data.component.metadatas).reduce((acc, [attribute, metadata])=>{
return {
...acc,
[attribute]: metadata.edit
};
}, {});
return {
layout: [
editAttributes
],
components: componentEditAttributes,
metadatas: editMetadatas,
options: {
...schema?.options,
...schema?.pluginOptions
},
settings: {
...data.component.settings,
displayName: schema?.info.displayName
}
};
};
/* -------------------------------------------------------------------------------------------------
* Header
* -----------------------------------------------------------------------------------------------*/ const ProtectedComponentConfigurationPage = ()=>{
const permissions = useTypedSelector((state)=>state.admin_app.permissions.contentManager?.componentsConfigurations);
return /*#__PURE__*/ jsx(Page.Protect, {
permissions: permissions,
children: /*#__PURE__*/ jsx(ComponentConfigurationPage, {})
});
};
export { ComponentConfigurationPage, ProtectedComponentConfigurationPage };
//# sourceMappingURL=ComponentConfigurationPage.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"ComponentConfigurationPage.mjs","sources":["../../../admin/src/pages/ComponentConfigurationPage.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Page, useNotification, useAPIErrorHandler } from '@strapi/admin/strapi-admin';\nimport { useIntl } from 'react-intl';\nimport { useParams } from 'react-router-dom';\n\nimport { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields';\nimport { ConfigurationForm, ConfigurationFormProps } from '../components/ConfigurationForm/Form';\nimport { ComponentsDictionary, extractContentTypeComponents } from '../hooks/useContentTypeSchema';\nimport {\n DEFAULT_SETTINGS,\n EditLayout,\n convertEditLayoutToFieldLayouts,\n} from '../hooks/useDocumentLayout';\nimport { useTypedSelector } from '../modules/hooks';\nimport {\n useGetComponentConfigurationQuery,\n useUpdateComponentConfigurationMutation,\n} from '../services/components';\nimport { useGetInitialDataQuery } from '../services/init';\nimport { setIn } from '../utils/objects';\n\nimport type { Component, FindComponentConfiguration } from '../../../shared/contracts/components';\nimport type { Metadatas } from '../../../shared/contracts/content-types';\n\n/* -------------------------------------------------------------------------------------------------\n * ComponentConfigurationPage\n * -----------------------------------------------------------------------------------------------*/\n\nconst ComponentConfigurationPage = () => {\n /**\n * useDocumentLayout only works for documents, not components,\n * it feels weird to make that hook work for both when this is SUCH\n * a unique use case and we only do it here, so in short, we essentially\n * just extracted the logic to make an edit view layout and reproduced it here.\n */\n const { slug: model } = useParams<{ slug: string }>();\n const { toggleNotification } = useNotification();\n const { formatMessage } = useIntl();\n const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();\n\n const {\n components,\n fieldSizes,\n schema,\n error: errorSchema,\n isLoading: isLoadingSchema,\n isFetching: isFetchingSchema,\n } = useGetInitialDataQuery(undefined, {\n selectFromResult: (res) => {\n const schema = res.data?.components.find((ct) => ct.uid === model);\n\n const componentsByKey = res.data?.components.reduce<ComponentsDictionary>(\n (acc, component) => {\n acc[component.uid] = component;\n\n return acc;\n },\n {}\n );\n\n const components = extractContentTypeComponents(schema?.attributes, componentsByKey);\n\n const fieldSizes = Object.entries(res.data?.fieldSizes ?? {}).reduce<\n ConfigurationFormProps['fieldSizes']\n >((acc, [attributeName, { default: size }]) => {\n acc[attributeName] = size;\n\n return acc;\n }, {});\n\n return {\n isFetching: res.isFetching,\n isLoading: res.isLoading,\n error: res.error,\n components,\n schema,\n fieldSizes,\n };\n },\n });\n\n React.useEffect(() => {\n if (errorSchema) {\n toggleNotification({\n type: 'danger',\n message: formatAPIError(errorSchema),\n });\n }\n }, [errorSchema, formatAPIError, toggleNotification]);\n\n const {\n data,\n isLoading: isLoadingConfig,\n isFetching: isFetchingConfig,\n error,\n } = useGetComponentConfigurationQuery(model ?? '');\n\n React.useEffect(() => {\n if (error) {\n toggleNotification({\n type: 'danger',\n message: formatAPIError(error),\n });\n }\n }, [error, formatAPIError, toggleNotification]);\n\n /**\n * you **must** check if we're loading or fetching in case the component gets new props\n * but nothing was unmounted, it then becomes a fetch, not a load.\n */\n const isLoading = isLoadingConfig || isLoadingSchema || isFetchingConfig || isFetchingSchema;\n\n const editLayout = React.useMemo(\n () =>\n data && !isLoading\n ? formatEditLayout(data, { schema, components })\n : ({\n layout: [],\n components: {},\n metadatas: {},\n options: {},\n settings: DEFAULT_SETTINGS,\n } as EditLayout),\n [data, isLoading, schema, components]\n );\n\n const [updateConfiguration] = useUpdateComponentConfigurationMutation();\n const handleSubmit: ConfigurationFormProps['onSubmit'] = async (formData) => {\n try {\n /**\n * We reconstruct the metadatas object by taking the existing list metadatas\n * and re-merging that by attribute name with the current list metadatas, whilst overwriting\n * the data from the form we've built.\n */\n const meta = Object.entries(data?.component.metadatas ?? {}).reduce<Metadatas>(\n (acc, [name, { edit, list }]) => {\n const {\n __temp_key__,\n size: _size,\n name: _namLine truncated
@@ -0,0 +1,164 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var strapiAdmin = require('@strapi/admin/strapi-admin');
var reactIntl = require('react-intl');
var Fields = require('../components/ConfigurationForm/Fields.js');
var Form = require('../components/ConfigurationForm/Form.js');
var useDocument = require('../hooks/useDocument.js');
var useDocumentLayout = require('../hooks/useDocumentLayout.js');
var hooks = require('../modules/hooks.js');
var contentTypes = require('../services/contentTypes.js');
var init = require('../services/init.js');
var objects = require('../utils/objects.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const EditConfigurationPage = ()=>{
const { trackUsage } = strapiAdmin.useTracking();
const { formatMessage } = reactIntl.useIntl();
const { toggleNotification } = strapiAdmin.useNotification();
const { _unstableFormatAPIError: formatAPIError } = strapiAdmin.useAPIErrorHandler();
const { isLoading: isLoadingSchema, schema, model } = useDocument.useDoc();
const { isLoading: isLoadingLayout, error, list, edit } = useDocumentLayout.useDocLayout();
const { fieldSizes, error: errorFieldSizes, isLoading: isLoadingFieldSizes, isFetching: isFetchingFieldSizes } = init.useGetInitialDataQuery(undefined, {
selectFromResult: (res)=>{
const fieldSizes = Object.entries(res.data?.fieldSizes ?? {}).reduce((acc, [attributeName, { default: size }])=>{
acc[attributeName] = size;
return acc;
}, {});
return {
isFetching: res.isFetching,
isLoading: res.isLoading,
error: res.error,
fieldSizes
};
}
});
React__namespace.useEffect(()=>{
if (errorFieldSizes) {
toggleNotification({
type: 'danger',
message: formatAPIError(errorFieldSizes)
});
}
}, [
errorFieldSizes,
formatAPIError,
toggleNotification
]);
const isLoading = isLoadingSchema || isLoadingLayout || isLoadingFieldSizes || isFetchingFieldSizes;
const [updateConfiguration] = contentTypes.useUpdateContentTypeConfigurationMutation();
const handleSubmit = async (data)=>{
try {
trackUsage('willSaveContentTypeLayout');
/**
* We reconstruct the metadatas object by taking the existing list metadatas
* and re-merging that by attribute name with the current list metadatas, whilst overwriting
* the data from the form we've built.
*/ const meta = Object.entries(list.metadatas).reduce((acc, [name, { mainField: _mainField, ...listMeta }])=>{
const existingEditMeta = edit.metadatas[name];
const { __temp_key__, size: _size, name: _name, ...editedMetadata } = data.layout.flatMap((row)=>row.children).find((field)=>field.name === name) ?? {};
acc[name] = {
edit: {
...existingEditMeta,
...editedMetadata
},
list: listMeta
};
return acc;
}, {});
const res = await updateConfiguration({
layouts: {
edit: data.layout.map((row)=>row.children.reduce((acc, { name, size })=>{
if (name !== Fields.TEMP_FIELD_NAME) {
return [
...acc,
{
name,
size
}
];
}
return acc;
}, [])),
list: list.layout.map((field)=>field.name)
},
settings: objects.setIn(data.settings, 'displayName', undefined),
metadatas: meta,
uid: model
});
if ('data' in res) {
trackUsage('didEditEditSettings');
toggleNotification({
type: 'success',
message: formatMessage({
id: 'notification.success.saved',
defaultMessage: 'Saved'
})
});
} else {
toggleNotification({
type: 'danger',
message: formatAPIError(res.error)
});
}
} catch {
toggleNotification({
type: 'danger',
message: formatMessage({
id: 'notification.error',
defaultMessage: 'An error occurred'
})
});
}
};
if (isLoading) {
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Loading, {});
}
if (errorFieldSizes || error || !schema) {
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Error, {});
}
return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
/*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Title, {
children: `Configure ${edit.settings.displayName} Edit View`
}),
/*#__PURE__*/ jsxRuntime.jsx(Form.ConfigurationForm, {
onSubmit: handleSubmit,
attributes: schema.attributes,
fieldSizes: fieldSizes,
layout: edit
})
]
});
};
const ProtectedEditConfigurationPage = ()=>{
const permissions = hooks.useTypedSelector((state)=>state.admin_app.permissions.contentManager?.collectionTypesConfigurations);
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Protect, {
permissions: permissions,
children: /*#__PURE__*/ jsxRuntime.jsx(EditConfigurationPage, {})
});
};
exports.EditConfigurationPage = EditConfigurationPage;
exports.ProtectedEditConfigurationPage = ProtectedEditConfigurationPage;
//# sourceMappingURL=EditConfigurationPage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"EditConfigurationPage.js","sources":["../../../admin/src/pages/EditConfigurationPage.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Page, useNotification, useTracking, useAPIErrorHandler } from '@strapi/admin/strapi-admin';\nimport { useIntl } from 'react-intl';\n\nimport { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields';\nimport { ConfigurationForm, ConfigurationFormProps } from '../components/ConfigurationForm/Form';\nimport { useDoc } from '../hooks/useDocument';\nimport { useDocLayout } from '../hooks/useDocumentLayout';\nimport { useTypedSelector } from '../modules/hooks';\nimport { useUpdateContentTypeConfigurationMutation } from '../services/contentTypes';\nimport { useGetInitialDataQuery } from '../services/init';\nimport { setIn } from '../utils/objects';\n\nimport type { Metadatas } from '../../../shared/contracts/content-types';\n\nconst EditConfigurationPage = () => {\n const { trackUsage } = useTracking();\n const { formatMessage } = useIntl();\n const { toggleNotification } = useNotification();\n const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();\n const { isLoading: isLoadingSchema, schema, model } = useDoc();\n const { isLoading: isLoadingLayout, error, list, edit } = useDocLayout();\n\n const {\n fieldSizes,\n error: errorFieldSizes,\n isLoading: isLoadingFieldSizes,\n isFetching: isFetchingFieldSizes,\n } = useGetInitialDataQuery(undefined, {\n selectFromResult: (res) => {\n const fieldSizes = Object.entries(res.data?.fieldSizes ?? {}).reduce<\n ConfigurationFormProps['fieldSizes']\n >((acc, [attributeName, { default: size }]) => {\n acc[attributeName] = size;\n\n return acc;\n }, {});\n\n return {\n isFetching: res.isFetching,\n isLoading: res.isLoading,\n error: res.error,\n fieldSizes,\n };\n },\n });\n\n React.useEffect(() => {\n if (errorFieldSizes) {\n toggleNotification({\n type: 'danger',\n message: formatAPIError(errorFieldSizes),\n });\n }\n }, [errorFieldSizes, formatAPIError, toggleNotification]);\n\n const isLoading =\n isLoadingSchema || isLoadingLayout || isLoadingFieldSizes || isFetchingFieldSizes;\n\n const [updateConfiguration] = useUpdateContentTypeConfigurationMutation();\n const handleSubmit: ConfigurationFormProps['onSubmit'] = async (data) => {\n try {\n trackUsage('willSaveContentTypeLayout');\n\n /**\n * We reconstruct the metadatas object by taking the existing list metadatas\n * and re-merging that by attribute name with the current list metadatas, whilst overwriting\n * the data from the form we've built.\n */\n const meta = Object.entries(list.metadatas).reduce<Metadatas>(\n (acc, [name, { mainField: _mainField, ...listMeta }]) => {\n const existingEditMeta = edit.metadatas[name];\n\n const {\n __temp_key__,\n size: _size,\n name: _name,\n ...editedMetadata\n } = data.layout.flatMap((row) => row.children).find((field) => field.name === name) ?? {};\n\n acc[name] = {\n edit: {\n ...existingEditMeta,\n ...editedMetadata,\n },\n list: listMeta,\n };\n\n return acc;\n },\n {}\n );\n\n const res = await updateConfiguration({\n layouts: {\n edit: data.layout.map((row) =>\n row.children.reduce<Array<{ name: string; size: number }>>((acc, { name, size }) => {\n if (name !== TEMP_FIELD_NAME) {\n return [...acc, { name, size }];\n }\n\n return acc;\n }, [])\n ),\n list: list.layout.map((field) => field.name),\n },\n settings: setIn(data.settings, 'displayName', undefined),\n metadatas: meta,\n uid: model,\n });\n\n if ('data' in res) {\n trackUsage('didEditEditSettings');\n toggleNotification({\n type: 'success',\n message: formatMessage({ id: 'notification.success.saved', defaultMessage: 'Saved' }),\n });\n } else {\n toggleNotification({\n type: 'danger',\n message: formatAPIError(res.error),\n });\n }\n } catch {\n toggleNotification({\n type: 'danger',\n message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }),\n });\n }\n };\n\n if (isLoading) {\n return <Page.Loading />;\n }\n\n if (errorFieldSizes || error || !schema) {\n return <Page.Error />;\n }\n\n return (\n <>\n <Page.Title>{`Configure ${edit.settings.displayName} Edit View`}</Page.Title>\n <ConfigurationForm\n onSubmit={handleSubmit}\n attributes={schema.attributes}\n fieldSizes={fieldSizes}\n layout={edit}\n />\n </>Line truncated
@@ -0,0 +1,142 @@
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import * as React from 'react';
import { useTracking, useNotification, useAPIErrorHandler, Page } from '@strapi/admin/strapi-admin';
import { useIntl } from 'react-intl';
import { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields.mjs';
import { ConfigurationForm } from '../components/ConfigurationForm/Form.mjs';
import { useDoc } from '../hooks/useDocument.mjs';
import { useDocLayout } from '../hooks/useDocumentLayout.mjs';
import { useTypedSelector } from '../modules/hooks.mjs';
import { useUpdateContentTypeConfigurationMutation } from '../services/contentTypes.mjs';
import { useGetInitialDataQuery } from '../services/init.mjs';
import { setIn } from '../utils/objects.mjs';
const EditConfigurationPage = ()=>{
const { trackUsage } = useTracking();
const { formatMessage } = useIntl();
const { toggleNotification } = useNotification();
const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();
const { isLoading: isLoadingSchema, schema, model } = useDoc();
const { isLoading: isLoadingLayout, error, list, edit } = useDocLayout();
const { fieldSizes, error: errorFieldSizes, isLoading: isLoadingFieldSizes, isFetching: isFetchingFieldSizes } = useGetInitialDataQuery(undefined, {
selectFromResult: (res)=>{
const fieldSizes = Object.entries(res.data?.fieldSizes ?? {}).reduce((acc, [attributeName, { default: size }])=>{
acc[attributeName] = size;
return acc;
}, {});
return {
isFetching: res.isFetching,
isLoading: res.isLoading,
error: res.error,
fieldSizes
};
}
});
React.useEffect(()=>{
if (errorFieldSizes) {
toggleNotification({
type: 'danger',
message: formatAPIError(errorFieldSizes)
});
}
}, [
errorFieldSizes,
formatAPIError,
toggleNotification
]);
const isLoading = isLoadingSchema || isLoadingLayout || isLoadingFieldSizes || isFetchingFieldSizes;
const [updateConfiguration] = useUpdateContentTypeConfigurationMutation();
const handleSubmit = async (data)=>{
try {
trackUsage('willSaveContentTypeLayout');
/**
* We reconstruct the metadatas object by taking the existing list metadatas
* and re-merging that by attribute name with the current list metadatas, whilst overwriting
* the data from the form we've built.
*/ const meta = Object.entries(list.metadatas).reduce((acc, [name, { mainField: _mainField, ...listMeta }])=>{
const existingEditMeta = edit.metadatas[name];
const { __temp_key__, size: _size, name: _name, ...editedMetadata } = data.layout.flatMap((row)=>row.children).find((field)=>field.name === name) ?? {};
acc[name] = {
edit: {
...existingEditMeta,
...editedMetadata
},
list: listMeta
};
return acc;
}, {});
const res = await updateConfiguration({
layouts: {
edit: data.layout.map((row)=>row.children.reduce((acc, { name, size })=>{
if (name !== TEMP_FIELD_NAME) {
return [
...acc,
{
name,
size
}
];
}
return acc;
}, [])),
list: list.layout.map((field)=>field.name)
},
settings: setIn(data.settings, 'displayName', undefined),
metadatas: meta,
uid: model
});
if ('data' in res) {
trackUsage('didEditEditSettings');
toggleNotification({
type: 'success',
message: formatMessage({
id: 'notification.success.saved',
defaultMessage: 'Saved'
})
});
} else {
toggleNotification({
type: 'danger',
message: formatAPIError(res.error)
});
}
} catch {
toggleNotification({
type: 'danger',
message: formatMessage({
id: 'notification.error',
defaultMessage: 'An error occurred'
})
});
}
};
if (isLoading) {
return /*#__PURE__*/ jsx(Page.Loading, {});
}
if (errorFieldSizes || error || !schema) {
return /*#__PURE__*/ jsx(Page.Error, {});
}
return /*#__PURE__*/ jsxs(Fragment, {
children: [
/*#__PURE__*/ jsx(Page.Title, {
children: `Configure ${edit.settings.displayName} Edit View`
}),
/*#__PURE__*/ jsx(ConfigurationForm, {
onSubmit: handleSubmit,
attributes: schema.attributes,
fieldSizes: fieldSizes,
layout: edit
})
]
});
};
const ProtectedEditConfigurationPage = ()=>{
const permissions = useTypedSelector((state)=>state.admin_app.permissions.contentManager?.collectionTypesConfigurations);
return /*#__PURE__*/ jsx(Page.Protect, {
permissions: permissions,
children: /*#__PURE__*/ jsx(EditConfigurationPage, {})
});
};
export { EditConfigurationPage, ProtectedEditConfigurationPage };
//# sourceMappingURL=EditConfigurationPage.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"EditConfigurationPage.mjs","sources":["../../../admin/src/pages/EditConfigurationPage.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Page, useNotification, useTracking, useAPIErrorHandler } from '@strapi/admin/strapi-admin';\nimport { useIntl } from 'react-intl';\n\nimport { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields';\nimport { ConfigurationForm, ConfigurationFormProps } from '../components/ConfigurationForm/Form';\nimport { useDoc } from '../hooks/useDocument';\nimport { useDocLayout } from '../hooks/useDocumentLayout';\nimport { useTypedSelector } from '../modules/hooks';\nimport { useUpdateContentTypeConfigurationMutation } from '../services/contentTypes';\nimport { useGetInitialDataQuery } from '../services/init';\nimport { setIn } from '../utils/objects';\n\nimport type { Metadatas } from '../../../shared/contracts/content-types';\n\nconst EditConfigurationPage = () => {\n const { trackUsage } = useTracking();\n const { formatMessage } = useIntl();\n const { toggleNotification } = useNotification();\n const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();\n const { isLoading: isLoadingSchema, schema, model } = useDoc();\n const { isLoading: isLoadingLayout, error, list, edit } = useDocLayout();\n\n const {\n fieldSizes,\n error: errorFieldSizes,\n isLoading: isLoadingFieldSizes,\n isFetching: isFetchingFieldSizes,\n } = useGetInitialDataQuery(undefined, {\n selectFromResult: (res) => {\n const fieldSizes = Object.entries(res.data?.fieldSizes ?? {}).reduce<\n ConfigurationFormProps['fieldSizes']\n >((acc, [attributeName, { default: size }]) => {\n acc[attributeName] = size;\n\n return acc;\n }, {});\n\n return {\n isFetching: res.isFetching,\n isLoading: res.isLoading,\n error: res.error,\n fieldSizes,\n };\n },\n });\n\n React.useEffect(() => {\n if (errorFieldSizes) {\n toggleNotification({\n type: 'danger',\n message: formatAPIError(errorFieldSizes),\n });\n }\n }, [errorFieldSizes, formatAPIError, toggleNotification]);\n\n const isLoading =\n isLoadingSchema || isLoadingLayout || isLoadingFieldSizes || isFetchingFieldSizes;\n\n const [updateConfiguration] = useUpdateContentTypeConfigurationMutation();\n const handleSubmit: ConfigurationFormProps['onSubmit'] = async (data) => {\n try {\n trackUsage('willSaveContentTypeLayout');\n\n /**\n * We reconstruct the metadatas object by taking the existing list metadatas\n * and re-merging that by attribute name with the current list metadatas, whilst overwriting\n * the data from the form we've built.\n */\n const meta = Object.entries(list.metadatas).reduce<Metadatas>(\n (acc, [name, { mainField: _mainField, ...listMeta }]) => {\n const existingEditMeta = edit.metadatas[name];\n\n const {\n __temp_key__,\n size: _size,\n name: _name,\n ...editedMetadata\n } = data.layout.flatMap((row) => row.children).find((field) => field.name === name) ?? {};\n\n acc[name] = {\n edit: {\n ...existingEditMeta,\n ...editedMetadata,\n },\n list: listMeta,\n };\n\n return acc;\n },\n {}\n );\n\n const res = await updateConfiguration({\n layouts: {\n edit: data.layout.map((row) =>\n row.children.reduce<Array<{ name: string; size: number }>>((acc, { name, size }) => {\n if (name !== TEMP_FIELD_NAME) {\n return [...acc, { name, size }];\n }\n\n return acc;\n }, [])\n ),\n list: list.layout.map((field) => field.name),\n },\n settings: setIn(data.settings, 'displayName', undefined),\n metadatas: meta,\n uid: model,\n });\n\n if ('data' in res) {\n trackUsage('didEditEditSettings');\n toggleNotification({\n type: 'success',\n message: formatMessage({ id: 'notification.success.saved', defaultMessage: 'Saved' }),\n });\n } else {\n toggleNotification({\n type: 'danger',\n message: formatAPIError(res.error),\n });\n }\n } catch {\n toggleNotification({\n type: 'danger',\n message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }),\n });\n }\n };\n\n if (isLoading) {\n return <Page.Loading />;\n }\n\n if (errorFieldSizes || error || !schema) {\n return <Page.Error />;\n }\n\n return (\n <>\n <Page.Title>{`Configure ${edit.settings.displayName} Edit View`}</Page.Title>\n <ConfigurationForm\n onSubmit={handleSubmit}\n attributes={schema.attributes}\n fieldSizes={fieldSizes}\n layout={edit}\n />\n </Line truncated
@@ -0,0 +1,235 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
require('react');
var strapiAdmin = require('@strapi/admin/strapi-admin');
var designSystem = require('@strapi/design-system');
var reactIntl = require('react-intl');
var reactRouterDom = require('react-router-dom');
var styledComponents = require('styled-components');
var collections = require('../../constants/collections.js');
var plugin = require('../../constants/plugin.js');
var DocumentRBAC = require('../../features/DocumentRBAC.js');
var useDocument = require('../../hooks/useDocument.js');
var useDocumentLayout = require('../../hooks/useDocumentLayout.js');
var useLazyComponents = require('../../hooks/useLazyComponents.js');
var useOnce = require('../../hooks/useOnce.js');
var translations = require('../../utils/translations.js');
var validation = require('../../utils/validation.js');
var FormLayout = require('./components/FormLayout.js');
var Header = require('./components/Header.js');
var Panels = require('./components/Panels.js');
/* -------------------------------------------------------------------------------------------------
* EditViewPage
* -----------------------------------------------------------------------------------------------*/ const EditViewPage = ()=>{
const location = reactRouterDom.useLocation();
const [{ query: { status } }, setQuery] = strapiAdmin.useQueryParams({
status: 'draft'
});
const { formatMessage } = reactIntl.useIntl();
const { toggleNotification } = strapiAdmin.useNotification();
const doc = useDocument.useDoc();
const { document, meta, isLoading: isLoadingDocument, schema, components, collectionType, id, model, hasError, getTitle, getInitialFormValues } = doc;
const hasDraftAndPublished = schema?.options?.draftAndPublish ?? false;
useOnce.useOnce(()=>{
/**
* We only ever want to fire the notification once otherwise
* whenever the app re-renders it'll pop up regardless of
* what we do because the state comes from react-router-dom
*/ if (location?.state && 'error' in location.state) {
toggleNotification({
type: 'danger',
message: location.state.error,
timeout: 5000
});
}
});
const isLoadingActionsRBAC = DocumentRBAC.useDocumentRBAC('EditViewPage', (state)=>state.isLoading);
const isSingleType = collectionType === collections.SINGLE_TYPES;
/**
* single-types don't current have an id, but because they're a singleton
* we can simply use the update operation to continuously update the same
* document with varying params.
*/ const isCreatingDocument = !id && !isSingleType;
const { isLoading: isLoadingLayout, edit: { layout, settings: { mainField } } } = useDocumentLayout.useDocumentLayout(model);
const { isLazyLoading } = useLazyComponents.useLazyComponents([]);
const isLoading = isLoadingActionsRBAC || isLoadingDocument || isLoadingLayout || isLazyLoading;
const initialValues = getInitialFormValues(isCreatingDocument);
if (isLoading && !document?.documentId) {
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Loading, {});
}
if (!initialValues || hasError) {
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Error, {});
}
const handleTabChange = (status)=>{
if (status === 'published' || status === 'draft') {
setQuery({
status
}, 'push', true);
}
};
const validateSync = (values, options)=>{
const yupSchema = validation.createYupSchema(schema?.attributes, components, {
status,
...options
});
return yupSchema.validateSync(values, {
abortEarly: false
});
};
return /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Main, {
paddingLeft: 10,
paddingRight: 10,
children: [
/*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Title, {
children: getTitle(mainField)
}),
/*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Form, {
disabled: hasDraftAndPublished && status === 'published',
initialValues: initialValues,
method: isCreatingDocument ? 'POST' : 'PUT',
validate: (values, options)=>{
const yupSchema = validation.createYupSchema(schema?.attributes, components, {
status,
...options
});
return yupSchema.validate(values, {
abortEarly: false
});
},
initialErrors: location?.state?.forceValidation ? validateSync(initialValues, {}) : {},
children: ({ resetForm })=>/*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
/*#__PURE__*/ jsxRuntime.jsx(Header.Header, {
isCreating: isCreatingDocument,
status: hasDraftAndPublished ? getDocumentStatus(document, meta) : undefined,
title: getTitle(mainField)
}),
/*#__PURE__*/ jsxRuntime.jsxs(designSystem.Tabs.Root, {
variant: "simple",
value: status,
onValueChange: handleTabChange,
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Tabs.List, {
"aria-label": formatMessage({
id: translations.getTranslation('containers.edit.tabs.label'),
defaultMessage: 'Document status'
}),
children: hasDraftAndPublished ? /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
/*#__PURE__*/ jsxRuntime.jsx(StatusTab, {
value: "draft",
children: formatMessage({
id: translations.getTranslation('containers.edit.tabs.draft'),
defaultMessage: 'draft'
})
}),
/*#__PURE__*/ jsxRuntime.jsx(StatusTab, {
disabled: !meta || meta.availableStatus.length === 0,
value: "published",
children: formatMessage({
id: translations.getTranslation('containers.edit.tabs.published'),
defaultMessage: 'published'
})
})
]
}) : null
}),
/*#__PURE__*/ jsxRuntime.jsxs(designSystem.Grid.Root, {
paddingTop: 8,
gap: 4,
children: [
/*#__PURE__*/ jsxRuntime.jsxs(designSystem.Grid.Item, {
col: 9,
s: 12,
direction: "column",
alignItems: "stretch",
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Tabs.Content, {
value: "draft",
children: /*#__PURE__*/ jsxRuntime.jsx(FormLayout.FormLayout, {
layout: layout,
document: doc
})
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Tabs.Content, {
value: "published",
children: /*#__PURE__*/ jsxRuntime.jsx(FormLayout.FormLayout, {
layout: layout,
document: doc
})
})
]
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Grid.Item, {
col: 3,
s: 12,
direction: "column",
alignItems: "stretch",
children: /*#__PURE__*/ jsxRuntime.jsx(Panels.Panels, {})
})
]
})
]
}),
/*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Blocker, {
// We reset the form to the published version to avoid errors like – https://strapi-inc.atlassian.net/browse/CONTENT-2284
onProceed: resetForm
})
]
})
})
]
});
};
const StatusTab = styledComponents.styled(designSystem.Tabs.Trigger)`
text-transform: uppercase;
`;
/**
* @internal
* @description Returns the status of the document where its latest state takes priority,
* this typically will be "published" unless a user has edited their draft in which we should
* display "modified".
*/ const getDocumentStatus = (document, meta)=>{
const docStatus = document?.status;
const statuses = meta?.availableStatus ?? [];
/**
* Creating an entry
*/ if (!docStatus) {
return 'draft';
}
/**
* We're viewing a draft, but the document could have a published version
*/ if (docStatus === 'draft' && statuses.find((doc)=>doc.publishedAt !== null)) {
return 'published';
}
return docStatus;
};
/* -------------------------------------------------------------------------------------------------
* ProtectedEditViewPage
* -----------------------------------------------------------------------------------------------*/ const ProtectedEditViewPage = ()=>{
const { slug = '' } = reactRouterDom.useParams();
const { permissions = [], isLoading, error } = strapiAdmin.useRBAC(plugin.PERMISSIONS.map((action)=>({
action,
subject: slug
})));
if (isLoading) {
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Loading, {});
}
if (error || !slug) {
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Error, {});
}
return /*#__PURE__*/ jsxRuntime.jsx(strapiAdmin.Page.Protect, {
permissions: permissions,
children: ({ permissions })=>/*#__PURE__*/ jsxRuntime.jsx(DocumentRBAC.DocumentRBAC, {
permissions: permissions,
children: /*#__PURE__*/ jsxRuntime.jsx(EditViewPage, {})
})
});
};
exports.EditViewPage = EditViewPage;
exports.ProtectedEditViewPage = ProtectedEditViewPage;
exports.getDocumentStatus = getDocumentStatus;
//# sourceMappingURL=EditViewPage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"EditViewPage.js","sources":["../../../../admin/src/pages/EditView/EditViewPage.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport {\n Page,\n Blocker,\n Form,\n useRBAC,\n useNotification,\n useQueryParams,\n} from '@strapi/admin/strapi-admin';\nimport { Grid, Main, Tabs } from '@strapi/design-system';\nimport { useIntl } from 'react-intl';\nimport { useLocation, useParams } from 'react-router-dom';\nimport { styled } from 'styled-components';\n\nimport { SINGLE_TYPES } from '../../constants/collections';\nimport { PERMISSIONS } from '../../constants/plugin';\nimport { DocumentRBAC, useDocumentRBAC } from '../../features/DocumentRBAC';\nimport { useDoc, type UseDocument } from '../../hooks/useDocument';\nimport { useDocumentLayout } from '../../hooks/useDocumentLayout';\nimport { useLazyComponents } from '../../hooks/useLazyComponents';\nimport { useOnce } from '../../hooks/useOnce';\nimport { getTranslation } from '../../utils/translations';\nimport { createYupSchema } from '../../utils/validation';\n\nimport { FormLayout } from './components/FormLayout';\nimport { Header } from './components/Header';\nimport { Panels } from './components/Panels';\n\n/* -------------------------------------------------------------------------------------------------\n * EditViewPage\n * -----------------------------------------------------------------------------------------------*/\n\nconst EditViewPage = () => {\n const location = useLocation();\n const [\n {\n query: { status },\n },\n setQuery,\n ] = useQueryParams<{ status: 'draft' | 'published' }>({\n status: 'draft',\n });\n const { formatMessage } = useIntl();\n const { toggleNotification } = useNotification();\n\n const doc = useDoc();\n const {\n document,\n meta,\n isLoading: isLoadingDocument,\n schema,\n components,\n collectionType,\n id,\n model,\n hasError,\n getTitle,\n getInitialFormValues,\n } = doc;\n\n const hasDraftAndPublished = schema?.options?.draftAndPublish ?? false;\n\n useOnce(() => {\n /**\n * We only ever want to fire the notification once otherwise\n * whenever the app re-renders it'll pop up regardless of\n * what we do because the state comes from react-router-dom\n */\n if (location?.state && 'error' in location.state) {\n toggleNotification({\n type: 'danger',\n message: location.state.error,\n timeout: 5000,\n });\n }\n });\n\n const isLoadingActionsRBAC = useDocumentRBAC('EditViewPage', (state) => state.isLoading);\n\n const isSingleType = collectionType === SINGLE_TYPES;\n\n /**\n * single-types don't current have an id, but because they're a singleton\n * we can simply use the update operation to continuously update the same\n * document with varying params.\n */\n const isCreatingDocument = !id && !isSingleType;\n\n const {\n isLoading: isLoadingLayout,\n edit: {\n layout,\n settings: { mainField },\n },\n } = useDocumentLayout(model);\n\n const { isLazyLoading } = useLazyComponents([]);\n\n const isLoading = isLoadingActionsRBAC || isLoadingDocument || isLoadingLayout || isLazyLoading;\n\n const initialValues = getInitialFormValues(isCreatingDocument);\n\n if (isLoading && !document?.documentId) {\n return <Page.Loading />;\n }\n\n if (!initialValues || hasError) {\n return <Page.Error />;\n }\n\n const handleTabChange = (status: string) => {\n if (status === 'published' || status === 'draft') {\n setQuery({ status }, 'push', true);\n }\n };\n\n const validateSync = (values: Record<string, unknown>, options: Record<string, string>) => {\n const yupSchema = createYupSchema(schema?.attributes, components, {\n status,\n ...options,\n });\n\n return yupSchema.validateSync(values, { abortEarly: false });\n };\n\n return (\n <Main paddingLeft={10} paddingRight={10}>\n <Page.Title>{getTitle(mainField)}</Page.Title>\n <Form\n disabled={hasDraftAndPublished && status === 'published'}\n initialValues={initialValues}\n method={isCreatingDocument ? 'POST' : 'PUT'}\n validate={(values: Record<string, unknown>, options: Record<string, string>) => {\n const yupSchema = createYupSchema(schema?.attributes, components, {\n status,\n ...options,\n });\n\n return yupSchema.validate(values, { abortEarly: false });\n }}\n initialErrors={location?.state?.forceValidation ? validateSync(initialValues, {}) : {}}\n >\n {({ resetForm }) => (\n <>\n <Header\n isCreating={isCreatingDocument}\n status={hasDraftAndPublished ? getDocumentStatus(document, meta) : undefined}\n title={getTitle(mainField)}\n />\n <Tabs.Root variant=\"simple\" value={status} onValueChange={handleTabChange}>\n <Tabs.List\n ariaLine truncated
@@ -0,0 +1,231 @@
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import 'react';
import { useQueryParams, useNotification, Page, Form, Blocker, useRBAC } from '@strapi/admin/strapi-admin';
import { Tabs, Main, Grid } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { useLocation, useParams } from 'react-router-dom';
import { styled } from 'styled-components';
import { SINGLE_TYPES } from '../../constants/collections.mjs';
import { PERMISSIONS } from '../../constants/plugin.mjs';
import { useDocumentRBAC, DocumentRBAC } from '../../features/DocumentRBAC.mjs';
import { useDoc } from '../../hooks/useDocument.mjs';
import { useDocumentLayout } from '../../hooks/useDocumentLayout.mjs';
import { useLazyComponents } from '../../hooks/useLazyComponents.mjs';
import { useOnce } from '../../hooks/useOnce.mjs';
import { getTranslation } from '../../utils/translations.mjs';
import { createYupSchema } from '../../utils/validation.mjs';
import { FormLayout } from './components/FormLayout.mjs';
import { Header } from './components/Header.mjs';
import { Panels } from './components/Panels.mjs';
/* -------------------------------------------------------------------------------------------------
* EditViewPage
* -----------------------------------------------------------------------------------------------*/ const EditViewPage = ()=>{
const location = useLocation();
const [{ query: { status } }, setQuery] = useQueryParams({
status: 'draft'
});
const { formatMessage } = useIntl();
const { toggleNotification } = useNotification();
const doc = useDoc();
const { document, meta, isLoading: isLoadingDocument, schema, components, collectionType, id, model, hasError, getTitle, getInitialFormValues } = doc;
const hasDraftAndPublished = schema?.options?.draftAndPublish ?? false;
useOnce(()=>{
/**
* We only ever want to fire the notification once otherwise
* whenever the app re-renders it'll pop up regardless of
* what we do because the state comes from react-router-dom
*/ if (location?.state && 'error' in location.state) {
toggleNotification({
type: 'danger',
message: location.state.error,
timeout: 5000
});
}
});
const isLoadingActionsRBAC = useDocumentRBAC('EditViewPage', (state)=>state.isLoading);
const isSingleType = collectionType === SINGLE_TYPES;
/**
* single-types don't current have an id, but because they're a singleton
* we can simply use the update operation to continuously update the same
* document with varying params.
*/ const isCreatingDocument = !id && !isSingleType;
const { isLoading: isLoadingLayout, edit: { layout, settings: { mainField } } } = useDocumentLayout(model);
const { isLazyLoading } = useLazyComponents([]);
const isLoading = isLoadingActionsRBAC || isLoadingDocument || isLoadingLayout || isLazyLoading;
const initialValues = getInitialFormValues(isCreatingDocument);
if (isLoading && !document?.documentId) {
return /*#__PURE__*/ jsx(Page.Loading, {});
}
if (!initialValues || hasError) {
return /*#__PURE__*/ jsx(Page.Error, {});
}
const handleTabChange = (status)=>{
if (status === 'published' || status === 'draft') {
setQuery({
status
}, 'push', true);
}
};
const validateSync = (values, options)=>{
const yupSchema = createYupSchema(schema?.attributes, components, {
status,
...options
});
return yupSchema.validateSync(values, {
abortEarly: false
});
};
return /*#__PURE__*/ jsxs(Main, {
paddingLeft: 10,
paddingRight: 10,
children: [
/*#__PURE__*/ jsx(Page.Title, {
children: getTitle(mainField)
}),
/*#__PURE__*/ jsx(Form, {
disabled: hasDraftAndPublished && status === 'published',
initialValues: initialValues,
method: isCreatingDocument ? 'POST' : 'PUT',
validate: (values, options)=>{
const yupSchema = createYupSchema(schema?.attributes, components, {
status,
...options
});
return yupSchema.validate(values, {
abortEarly: false
});
},
initialErrors: location?.state?.forceValidation ? validateSync(initialValues, {}) : {},
children: ({ resetForm })=>/*#__PURE__*/ jsxs(Fragment, {
children: [
/*#__PURE__*/ jsx(Header, {
isCreating: isCreatingDocument,
status: hasDraftAndPublished ? getDocumentStatus(document, meta) : undefined,
title: getTitle(mainField)
}),
/*#__PURE__*/ jsxs(Tabs.Root, {
variant: "simple",
value: status,
onValueChange: handleTabChange,
children: [
/*#__PURE__*/ jsx(Tabs.List, {
"aria-label": formatMessage({
id: getTranslation('containers.edit.tabs.label'),
defaultMessage: 'Document status'
}),
children: hasDraftAndPublished ? /*#__PURE__*/ jsxs(Fragment, {
children: [
/*#__PURE__*/ jsx(StatusTab, {
value: "draft",
children: formatMessage({
id: getTranslation('containers.edit.tabs.draft'),
defaultMessage: 'draft'
})
}),
/*#__PURE__*/ jsx(StatusTab, {
disabled: !meta || meta.availableStatus.length === 0,
value: "published",
children: formatMessage({
id: getTranslation('containers.edit.tabs.published'),
defaultMessage: 'published'
})
})
]
}) : null
}),
/*#__PURE__*/ jsxs(Grid.Root, {
paddingTop: 8,
gap: 4,
children: [
/*#__PURE__*/ jsxs(Grid.Item, {
col: 9,
s: 12,
direction: "column",
alignItems: "stretch",
children: [
/*#__PURE__*/ jsx(Tabs.Content, {
value: "draft",
children: /*#__PURE__*/ jsx(FormLayout, {
layout: layout,
document: doc
})
}),
/*#__PURE__*/ jsx(Tabs.Content, {
value: "published",
children: /*#__PURE__*/ jsx(FormLayout, {
layout: layout,
document: doc
})
})
]
}),
/*#__PURE__*/ jsx(Grid.Item, {
col: 3,
s: 12,
direction: "column",
alignItems: "stretch",
children: /*#__PURE__*/ jsx(Panels, {})
})
]
})
]
}),
/*#__PURE__*/ jsx(Blocker, {
// We reset the form to the published version to avoid errors like – https://strapi-inc.atlassian.net/browse/CONTENT-2284
onProceed: resetForm
})
]
})
})
]
});
};
const StatusTab = styled(Tabs.Trigger)`
text-transform: uppercase;
`;
/**
* @internal
* @description Returns the status of the document where its latest state takes priority,
* this typically will be "published" unless a user has edited their draft in which we should
* display "modified".
*/ const getDocumentStatus = (document, meta)=>{
const docStatus = document?.status;
const statuses = meta?.availableStatus ?? [];
/**
* Creating an entry
*/ if (!docStatus) {
return 'draft';
}
/**
* We're viewing a draft, but the document could have a published version
*/ if (docStatus === 'draft' && statuses.find((doc)=>doc.publishedAt !== null)) {
return 'published';
}
return docStatus;
};
/* -------------------------------------------------------------------------------------------------
* ProtectedEditViewPage
* -----------------------------------------------------------------------------------------------*/ const ProtectedEditViewPage = ()=>{
const { slug = '' } = useParams();
const { permissions = [], isLoading, error } = useRBAC(PERMISSIONS.map((action)=>({
action,
subject: slug
})));
if (isLoading) {
return /*#__PURE__*/ jsx(Page.Loading, {});
}
if (error || !slug) {
return /*#__PURE__*/ jsx(Page.Error, {});
}
return /*#__PURE__*/ jsx(Page.Protect, {
permissions: permissions,
children: ({ permissions })=>/*#__PURE__*/ jsx(DocumentRBAC, {
permissions: permissions,
children: /*#__PURE__*/ jsx(EditViewPage, {})
})
});
};
export { EditViewPage, ProtectedEditViewPage, getDocumentStatus };
//# sourceMappingURL=EditViewPage.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"EditViewPage.mjs","sources":["../../../../admin/src/pages/EditView/EditViewPage.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport {\n Page,\n Blocker,\n Form,\n useRBAC,\n useNotification,\n useQueryParams,\n} from '@strapi/admin/strapi-admin';\nimport { Grid, Main, Tabs } from '@strapi/design-system';\nimport { useIntl } from 'react-intl';\nimport { useLocation, useParams } from 'react-router-dom';\nimport { styled } from 'styled-components';\n\nimport { SINGLE_TYPES } from '../../constants/collections';\nimport { PERMISSIONS } from '../../constants/plugin';\nimport { DocumentRBAC, useDocumentRBAC } from '../../features/DocumentRBAC';\nimport { useDoc, type UseDocument } from '../../hooks/useDocument';\nimport { useDocumentLayout } from '../../hooks/useDocumentLayout';\nimport { useLazyComponents } from '../../hooks/useLazyComponents';\nimport { useOnce } from '../../hooks/useOnce';\nimport { getTranslation } from '../../utils/translations';\nimport { createYupSchema } from '../../utils/validation';\n\nimport { FormLayout } from './components/FormLayout';\nimport { Header } from './components/Header';\nimport { Panels } from './components/Panels';\n\n/* -------------------------------------------------------------------------------------------------\n * EditViewPage\n * -----------------------------------------------------------------------------------------------*/\n\nconst EditViewPage = () => {\n const location = useLocation();\n const [\n {\n query: { status },\n },\n setQuery,\n ] = useQueryParams<{ status: 'draft' | 'published' }>({\n status: 'draft',\n });\n const { formatMessage } = useIntl();\n const { toggleNotification } = useNotification();\n\n const doc = useDoc();\n const {\n document,\n meta,\n isLoading: isLoadingDocument,\n schema,\n components,\n collectionType,\n id,\n model,\n hasError,\n getTitle,\n getInitialFormValues,\n } = doc;\n\n const hasDraftAndPublished = schema?.options?.draftAndPublish ?? false;\n\n useOnce(() => {\n /**\n * We only ever want to fire the notification once otherwise\n * whenever the app re-renders it'll pop up regardless of\n * what we do because the state comes from react-router-dom\n */\n if (location?.state && 'error' in location.state) {\n toggleNotification({\n type: 'danger',\n message: location.state.error,\n timeout: 5000,\n });\n }\n });\n\n const isLoadingActionsRBAC = useDocumentRBAC('EditViewPage', (state) => state.isLoading);\n\n const isSingleType = collectionType === SINGLE_TYPES;\n\n /**\n * single-types don't current have an id, but because they're a singleton\n * we can simply use the update operation to continuously update the same\n * document with varying params.\n */\n const isCreatingDocument = !id && !isSingleType;\n\n const {\n isLoading: isLoadingLayout,\n edit: {\n layout,\n settings: { mainField },\n },\n } = useDocumentLayout(model);\n\n const { isLazyLoading } = useLazyComponents([]);\n\n const isLoading = isLoadingActionsRBAC || isLoadingDocument || isLoadingLayout || isLazyLoading;\n\n const initialValues = getInitialFormValues(isCreatingDocument);\n\n if (isLoading && !document?.documentId) {\n return <Page.Loading />;\n }\n\n if (!initialValues || hasError) {\n return <Page.Error />;\n }\n\n const handleTabChange = (status: string) => {\n if (status === 'published' || status === 'draft') {\n setQuery({ status }, 'push', true);\n }\n };\n\n const validateSync = (values: Record<string, unknown>, options: Record<string, string>) => {\n const yupSchema = createYupSchema(schema?.attributes, components, {\n status,\n ...options,\n });\n\n return yupSchema.validateSync(values, { abortEarly: false });\n };\n\n return (\n <Main paddingLeft={10} paddingRight={10}>\n <Page.Title>{getTitle(mainField)}</Page.Title>\n <Form\n disabled={hasDraftAndPublished && status === 'published'}\n initialValues={initialValues}\n method={isCreatingDocument ? 'POST' : 'PUT'}\n validate={(values: Record<string, unknown>, options: Record<string, string>) => {\n const yupSchema = createYupSchema(schema?.attributes, components, {\n status,\n ...options,\n });\n\n return yupSchema.validate(values, { abortEarly: false });\n }}\n initialErrors={location?.state?.forceValidation ? validateSync(initialValues, {}) : {}}\n >\n {({ resetForm }) => (\n <>\n <Header\n isCreating={isCreatingDocument}\n status={hasDraftAndPublished ? getDocumentStatus(document, meta) : undefined}\n title={getTitle(mainField)}\n />\n <Tabs.Root variant=\"simple\" value={status} onValueChange={handleTabChange}>\n <Tabs.List\n ariLine truncated
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
{"version":3,"file":"DocumentActions.js","sources":["../../../../../admin/src/pages/EditView/components/DocumentActions.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport {\n useForm,\n useNotification,\n NotificationConfig,\n useAPIErrorHandler,\n useQueryParams,\n} from '@strapi/admin/strapi-admin';\nimport {\n Button,\n Dialog,\n Flex,\n Modal,\n Radio,\n Typography,\n VisuallyHidden,\n Menu,\n ButtonProps,\n} from '@strapi/design-system';\nimport { Cross, More, WarningCircle } from '@strapi/icons';\nimport mapValues from 'lodash/fp/mapValues';\nimport get from 'lodash/get';\nimport merge from 'lodash/merge';\nimport set from 'lodash/set';\nimport { useIntl } from 'react-intl';\nimport { useMatch, useNavigate, useParams } from 'react-router-dom';\nimport { DefaultTheme, styled } from 'styled-components';\n\nimport { Create, Publish } from '../../../../../shared/contracts/collection-types';\nimport { PUBLISHED_AT_ATTRIBUTE_NAME } from '../../../constants/attributes';\nimport { SINGLE_TYPES } from '../../../constants/collections';\nimport { useDocumentRBAC } from '../../../features/DocumentRBAC';\nimport { useDoc, useDocument } from '../../../hooks/useDocument';\nimport { useDocumentActions } from '../../../hooks/useDocumentActions';\nimport { useDocumentContext } from '../../../hooks/useDocumentContext';\nimport { usePreviewContext } from '../../../preview/pages/Preview';\nimport { CLONE_PATH, LIST_PATH } from '../../../router';\nimport {\n useGetDraftRelationCountQuery,\n useUpdateDocumentMutation,\n} from '../../../services/documents';\nimport { isBaseQueryError, buildValidParams } from '../../../utils/api';\nimport { getTranslation } from '../../../utils/translations';\nimport { AnyData } from '../utils/data';\n\nimport { useRelationModal } from './FormInputs/Relations/RelationModal';\n\nimport type { RelationsFormValue } from './FormInputs/Relations/Relations';\nimport type { DocumentActionComponent } from '../../../content-manager';\n/* -------------------------------------------------------------------------------------------------\n * Types\n * -----------------------------------------------------------------------------------------------*/\ntype DocumentActionPosition = 'panel' | 'header' | 'table-row' | 'preview' | 'relation-modal';\n\ninterface DocumentActionDescription {\n label: string;\n onClick?: (event: React.SyntheticEvent) => Promise<boolean | void> | boolean | void;\n icon?: React.ReactNode;\n /**\n * @default false\n */\n disabled?: boolean;\n /**\n * @default 'panel'\n * @description Where the action should be rendered.\n */\n position?: DocumentActionPosition | DocumentActionPosition[];\n dialog?: DialogOptions | NotificationOptions | ModalOptions;\n /**\n * @default 'secondary'\n */\n variant?: ButtonProps['variant'];\n loading?: ButtonProps['loading'];\n}\n\ninterface DialogOptions {\n type: 'dialog';\n title: string;\n content?: React.ReactNode;\n variant?: ButtonProps['variant'];\n onConfirm?: () => void | Promise<void>;\n onCancel?: () => void | Promise<void>;\n}\n\ninterface NotificationOptions {\n type: 'notification';\n title: string;\n link?: {\n label: string;\n url: string;\n target?: string;\n };\n content?: string;\n onClose?: () => void;\n status?: NotificationConfig['type'];\n timeout?: number;\n}\n\ninterface ModalOptions {\n type: 'modal';\n title: string;\n content: React.ComponentType<{ onClose: () => void }> | React.ReactNode;\n footer?: React.ComponentType<{ onClose: () => void }> | React.ReactNode;\n onClose?: () => void;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * DocumentActions\n * -----------------------------------------------------------------------------------------------*/\n\ninterface Action extends DocumentActionDescription {\n id: string;\n}\n\ninterface DocumentActionsProps {\n actions: Action[];\n}\n\nconst connectRelationToParent = (\n parentDataToUpdate: AnyData | undefined,\n fieldToConnect: string,\n data: Create.Response['data'] | Publish.Response['data'],\n fieldToConnectUID?: string\n) => {\n /*\n * Check if the fieldToConnect is already present in the parentDataToUpdate.\n * This happens in particular when in the parentDocument you have created\n * a new component without saving.\n */\n const isFieldPresent = !!get(parentDataToUpdate, fieldToConnect);\n const fieldToConnectPath = isFieldPresent\n ? fieldToConnect\n : // Compute the path to the parent object\n fieldToConnect.split('.').slice(0, -1).join('.');\n const fieldToConnectValue = isFieldPresent\n ? {\n connect: [\n {\n id: data.documentId,\n documentId: data.documentId,\n locale: data.locale,\n },\n ],\n }\n : {\n [fieldToConnect.split('.').pop()!]: {\n connect: [\n {\n id: data.documentId,\nLine truncated
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
{"version":3,"file":"DocumentActions.mjs","sources":["../../../../../admin/src/pages/EditView/components/DocumentActions.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport {\n useForm,\n useNotification,\n NotificationConfig,\n useAPIErrorHandler,\n useQueryParams,\n} from '@strapi/admin/strapi-admin';\nimport {\n Button,\n Dialog,\n Flex,\n Modal,\n Radio,\n Typography,\n VisuallyHidden,\n Menu,\n ButtonProps,\n} from '@strapi/design-system';\nimport { Cross, More, WarningCircle } from '@strapi/icons';\nimport mapValues from 'lodash/fp/mapValues';\nimport get from 'lodash/get';\nimport merge from 'lodash/merge';\nimport set from 'lodash/set';\nimport { useIntl } from 'react-intl';\nimport { useMatch, useNavigate, useParams } from 'react-router-dom';\nimport { DefaultTheme, styled } from 'styled-components';\n\nimport { Create, Publish } from '../../../../../shared/contracts/collection-types';\nimport { PUBLISHED_AT_ATTRIBUTE_NAME } from '../../../constants/attributes';\nimport { SINGLE_TYPES } from '../../../constants/collections';\nimport { useDocumentRBAC } from '../../../features/DocumentRBAC';\nimport { useDoc, useDocument } from '../../../hooks/useDocument';\nimport { useDocumentActions } from '../../../hooks/useDocumentActions';\nimport { useDocumentContext } from '../../../hooks/useDocumentContext';\nimport { usePreviewContext } from '../../../preview/pages/Preview';\nimport { CLONE_PATH, LIST_PATH } from '../../../router';\nimport {\n useGetDraftRelationCountQuery,\n useUpdateDocumentMutation,\n} from '../../../services/documents';\nimport { isBaseQueryError, buildValidParams } from '../../../utils/api';\nimport { getTranslation } from '../../../utils/translations';\nimport { AnyData } from '../utils/data';\n\nimport { useRelationModal } from './FormInputs/Relations/RelationModal';\n\nimport type { RelationsFormValue } from './FormInputs/Relations/Relations';\nimport type { DocumentActionComponent } from '../../../content-manager';\n/* -------------------------------------------------------------------------------------------------\n * Types\n * -----------------------------------------------------------------------------------------------*/\ntype DocumentActionPosition = 'panel' | 'header' | 'table-row' | 'preview' | 'relation-modal';\n\ninterface DocumentActionDescription {\n label: string;\n onClick?: (event: React.SyntheticEvent) => Promise<boolean | void> | boolean | void;\n icon?: React.ReactNode;\n /**\n * @default false\n */\n disabled?: boolean;\n /**\n * @default 'panel'\n * @description Where the action should be rendered.\n */\n position?: DocumentActionPosition | DocumentActionPosition[];\n dialog?: DialogOptions | NotificationOptions | ModalOptions;\n /**\n * @default 'secondary'\n */\n variant?: ButtonProps['variant'];\n loading?: ButtonProps['loading'];\n}\n\ninterface DialogOptions {\n type: 'dialog';\n title: string;\n content?: React.ReactNode;\n variant?: ButtonProps['variant'];\n onConfirm?: () => void | Promise<void>;\n onCancel?: () => void | Promise<void>;\n}\n\ninterface NotificationOptions {\n type: 'notification';\n title: string;\n link?: {\n label: string;\n url: string;\n target?: string;\n };\n content?: string;\n onClose?: () => void;\n status?: NotificationConfig['type'];\n timeout?: number;\n}\n\ninterface ModalOptions {\n type: 'modal';\n title: string;\n content: React.ComponentType<{ onClose: () => void }> | React.ReactNode;\n footer?: React.ComponentType<{ onClose: () => void }> | React.ReactNode;\n onClose?: () => void;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * DocumentActions\n * -----------------------------------------------------------------------------------------------*/\n\ninterface Action extends DocumentActionDescription {\n id: string;\n}\n\ninterface DocumentActionsProps {\n actions: Action[];\n}\n\nconst connectRelationToParent = (\n parentDataToUpdate: AnyData | undefined,\n fieldToConnect: string,\n data: Create.Response['data'] | Publish.Response['data'],\n fieldToConnectUID?: string\n) => {\n /*\n * Check if the fieldToConnect is already present in the parentDataToUpdate.\n * This happens in particular when in the parentDocument you have created\n * a new component without saving.\n */\n const isFieldPresent = !!get(parentDataToUpdate, fieldToConnect);\n const fieldToConnectPath = isFieldPresent\n ? fieldToConnect\n : // Compute the path to the parent object\n fieldToConnect.split('.').slice(0, -1).join('.');\n const fieldToConnectValue = isFieldPresent\n ? {\n connect: [\n {\n id: data.documentId,\n documentId: data.documentId,\n locale: data.locale,\n },\n ],\n }\n : {\n [fieldToConnect.split('.').pop()!]: {\n connect: [\n {\n id: data.documentId,\Line truncated
@@ -0,0 +1,34 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var designSystem = require('@strapi/design-system');
var reactIntl = require('react-intl');
var strings = require('../../../utils/strings.js');
/**
* @public
* @description Displays the status of a document (draft, published, etc.)
* and automatically calculates the appropriate variant for the status.
*/ const DocumentStatus = ({ status = 'draft', size = 'S', ...restProps })=>{
const statusVariant = status === 'draft' ? 'secondary' : status === 'published' ? 'success' : 'alternative';
const { formatMessage } = reactIntl.useIntl();
return /*#__PURE__*/ jsxRuntime.jsx(designSystem.Status, {
...restProps,
size: size,
variant: statusVariant,
role: "status",
"aria-label": status,
children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
tag: "span",
variant: "omega",
fontWeight: "bold",
children: formatMessage({
id: `content-manager.containers.List.${status}`,
defaultMessage: strings.capitalise(status)
})
})
});
};
exports.DocumentStatus = DocumentStatus;
//# sourceMappingURL=DocumentStatus.js.map
@@ -0,0 +1 @@
{"version":3,"file":"DocumentStatus.js","sources":["../../../../../admin/src/pages/EditView/components/DocumentStatus.tsx"],"sourcesContent":["import { Status, StatusProps, Typography } from '@strapi/design-system';\nimport { useIntl } from 'react-intl';\n\nimport { capitalise } from '../../../utils/strings';\n\ninterface DocumentStatusProps extends Omit<StatusProps, 'children' | 'variant'> {\n /**\n * The status of the document (draft, published, etc.)\n * @default 'draft'\n */\n status?: string;\n}\n\n/**\n * @public\n * @description Displays the status of a document (draft, published, etc.)\n * and automatically calculates the appropriate variant for the status.\n */\nconst DocumentStatus = ({ status = 'draft', size = 'S', ...restProps }: DocumentStatusProps) => {\n const statusVariant =\n status === 'draft' ? 'secondary' : status === 'published' ? 'success' : 'alternative';\n\n const { formatMessage } = useIntl();\n\n return (\n <Status {...restProps} size={size} variant={statusVariant} role=\"status\" aria-label={status}>\n <Typography tag=\"span\" variant=\"omega\" fontWeight=\"bold\">\n {formatMessage({\n id: `content-manager.containers.List.${status}`,\n defaultMessage: capitalise(status),\n })}\n </Typography>\n </Status>\n );\n};\n\nexport { DocumentStatus };\nexport type { DocumentStatusProps };\n"],"names":["DocumentStatus","status","size","restProps","statusVariant","formatMessage","useIntl","_jsx","Status","variant","role","aria-label","Typography","tag","fontWeight","id","defaultMessage","capitalise"],"mappings":";;;;;;;AAaA;;;;IAKA,MAAMA,cAAiB,GAAA,CAAC,EAAEC,MAAAA,GAAS,OAAO,EAAEC,IAAO,GAAA,GAAG,EAAE,GAAGC,SAAgC,EAAA,GAAA;AACzF,IAAA,MAAMC,gBACJH,MAAW,KAAA,OAAA,GAAU,WAAcA,GAAAA,MAAAA,KAAW,cAAc,SAAY,GAAA,aAAA;IAE1E,MAAM,EAAEI,aAAa,EAAE,GAAGC,iBAAAA,EAAAA;AAE1B,IAAA,qBACEC,cAACC,CAAAA,mBAAAA,EAAAA;AAAQ,QAAA,GAAGL,SAAS;QAAED,IAAMA,EAAAA,IAAAA;QAAMO,OAASL,EAAAA,aAAAA;QAAeM,IAAK,EAAA,QAAA;QAASC,YAAYV,EAAAA,MAAAA;AACnF,QAAA,QAAA,gBAAAM,cAACK,CAAAA,uBAAAA,EAAAA;YAAWC,GAAI,EAAA,MAAA;YAAOJ,OAAQ,EAAA,OAAA;YAAQK,UAAW,EAAA,MAAA;sBAC/CT,aAAc,CAAA;AACbU,gBAAAA,EAAAA,EAAI,CAAC,gCAAgC,EAAEd,MAAAA,CAAO,CAAC;AAC/Ce,gBAAAA,cAAAA,EAAgBC,kBAAWhB,CAAAA,MAAAA;AAC7B,aAAA;;;AAIR;;;;"}
@@ -0,0 +1,32 @@
import { jsx } from 'react/jsx-runtime';
import { Status, Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { capitalise } from '../../../utils/strings.mjs';
/**
* @public
* @description Displays the status of a document (draft, published, etc.)
* and automatically calculates the appropriate variant for the status.
*/ const DocumentStatus = ({ status = 'draft', size = 'S', ...restProps })=>{
const statusVariant = status === 'draft' ? 'secondary' : status === 'published' ? 'success' : 'alternative';
const { formatMessage } = useIntl();
return /*#__PURE__*/ jsx(Status, {
...restProps,
size: size,
variant: statusVariant,
role: "status",
"aria-label": status,
children: /*#__PURE__*/ jsx(Typography, {
tag: "span",
variant: "omega",
fontWeight: "bold",
children: formatMessage({
id: `content-manager.containers.List.${status}`,
defaultMessage: capitalise(status)
})
})
});
};
export { DocumentStatus };
//# sourceMappingURL=DocumentStatus.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"DocumentStatus.mjs","sources":["../../../../../admin/src/pages/EditView/components/DocumentStatus.tsx"],"sourcesContent":["import { Status, StatusProps, Typography } from '@strapi/design-system';\nimport { useIntl } from 'react-intl';\n\nimport { capitalise } from '../../../utils/strings';\n\ninterface DocumentStatusProps extends Omit<StatusProps, 'children' | 'variant'> {\n /**\n * The status of the document (draft, published, etc.)\n * @default 'draft'\n */\n status?: string;\n}\n\n/**\n * @public\n * @description Displays the status of a document (draft, published, etc.)\n * and automatically calculates the appropriate variant for the status.\n */\nconst DocumentStatus = ({ status = 'draft', size = 'S', ...restProps }: DocumentStatusProps) => {\n const statusVariant =\n status === 'draft' ? 'secondary' : status === 'published' ? 'success' : 'alternative';\n\n const { formatMessage } = useIntl();\n\n return (\n <Status {...restProps} size={size} variant={statusVariant} role=\"status\" aria-label={status}>\n <Typography tag=\"span\" variant=\"omega\" fontWeight=\"bold\">\n {formatMessage({\n id: `content-manager.containers.List.${status}`,\n defaultMessage: capitalise(status),\n })}\n </Typography>\n </Status>\n );\n};\n\nexport { DocumentStatus };\nexport type { DocumentStatusProps };\n"],"names":["DocumentStatus","status","size","restProps","statusVariant","formatMessage","useIntl","_jsx","Status","variant","role","aria-label","Typography","tag","fontWeight","id","defaultMessage","capitalise"],"mappings":";;;;;AAaA;;;;IAKA,MAAMA,cAAiB,GAAA,CAAC,EAAEC,MAAAA,GAAS,OAAO,EAAEC,IAAO,GAAA,GAAG,EAAE,GAAGC,SAAgC,EAAA,GAAA;AACzF,IAAA,MAAMC,gBACJH,MAAW,KAAA,OAAA,GAAU,WAAcA,GAAAA,MAAAA,KAAW,cAAc,SAAY,GAAA,aAAA;IAE1E,MAAM,EAAEI,aAAa,EAAE,GAAGC,OAAAA,EAAAA;AAE1B,IAAA,qBACEC,GAACC,CAAAA,MAAAA,EAAAA;AAAQ,QAAA,GAAGL,SAAS;QAAED,IAAMA,EAAAA,IAAAA;QAAMO,OAASL,EAAAA,aAAAA;QAAeM,IAAK,EAAA,QAAA;QAASC,YAAYV,EAAAA,MAAAA;AACnF,QAAA,QAAA,gBAAAM,GAACK,CAAAA,UAAAA,EAAAA;YAAWC,GAAI,EAAA,MAAA;YAAOJ,OAAQ,EAAA,OAAA;YAAQK,UAAW,EAAA,MAAA;sBAC/CT,aAAc,CAAA;AACbU,gBAAAA,EAAAA,EAAI,CAAC,gCAAgC,EAAEd,MAAAA,CAAO,CAAC;AAC/Ce,gBAAAA,cAAAA,EAAgBC,UAAWhB,CAAAA,MAAAA;AAC7B,aAAA;;;AAIR;;;;"}
@@ -0,0 +1,120 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var strapiAdmin = require('@strapi/admin/strapi-admin');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var reactIntl = require('react-intl');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const ObservedToolbarComponent = ({ index, lastVisibleIndex, setLastVisibleIndex, rootRef, children })=>{
const isVisible = index <= lastVisibleIndex;
const containerRef = strapiAdmin.useElementOnScreen((isVisible)=>{
/**
* It's the MoreMenu's job to make an item not visible when there's not room for it.
* But we need to react here to the element becoming visible again.
*/ if (isVisible) {
setLastVisibleIndex((prev)=>Math.max(prev, index));
}
}, {
threshold: 1,
root: rootRef.current
});
return /*#__PURE__*/ jsxRuntime.jsx("div", {
ref: containerRef,
style: {
/**
* Use visibility so that the element occupies the space if requires even when there's not
* enough room for it to be visible. The empty reserved space will be clipped by the
* overflow:hidden rule on the parent, so it doesn't affect the UI.
* This way we can keep observing its visiblity and react to browser resize events.
*/ visibility: isVisible ? 'visible' : 'hidden'
},
children: children
});
};
const EditorToolbarObserver = ({ observedComponents, menuTriggerVariant = 'ghost' })=>{
const { formatMessage } = reactIntl.useIntl();
const toolbarRef = React__namespace.useRef(null);
const [lastVisibleIndex, setLastVisibleIndex] = React__namespace.useState(observedComponents.length - 1);
const hasHiddenItems = lastVisibleIndex < observedComponents.length - 1;
const menuIndex = lastVisibleIndex + 1;
const [open, setOpen] = React__namespace.useState(false);
const isMenuOpenWithContent = open && hasHiddenItems;
const menuTriggerRef = strapiAdmin.useElementOnScreen((isVisible)=>{
// We only react to the menu becoming invisible. When that happens, we hide the last item.
if (!isVisible) {
/**
* If there's no room for any item, the index can be -1.
* This is intentional, in that case only the more menu will be visible.
**/ setLastVisibleIndex((prev)=>prev - 1);
// Maintain the menu state if it has content
setOpen(isMenuOpenWithContent);
}
}, {
threshold: 1,
root: toolbarRef.current
});
return observedComponents.map((component, index)=>{
return /*#__PURE__*/ jsxRuntime.jsx(ObservedToolbarComponent, {
index: index,
lastVisibleIndex: lastVisibleIndex,
setLastVisibleIndex: setLastVisibleIndex,
rootRef: toolbarRef,
children: component.toolbar
}, component.key);
}).toSpliced(menuIndex, 0, /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Menu.Root, {
defaultOpen: false,
open: isMenuOpenWithContent,
onOpenChange: setOpen,
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Menu.Trigger, {
paddingLeft: 0,
paddingRight: 0,
ref: menuTriggerRef,
variant: menuTriggerVariant,
style: {
visibility: hasHiddenItems ? 'visible' : 'hidden'
},
label: formatMessage({
id: 'global.more',
defaultMessage: 'More'
}),
tag: designSystem.IconButton,
icon: /*#__PURE__*/ jsxRuntime.jsx(Icons.More, {})
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Menu.Content, {
onCloseAutoFocus: (e)=>e.preventDefault(),
maxHeight: "100%",
minWidth: "256px",
popoverPlacement: "bottom-end",
zIndex: 2,
children: observedComponents.slice(menuIndex).map((component)=>/*#__PURE__*/ jsxRuntime.jsx(React__namespace.Fragment, {
children: component.menu
}, component.key))
})
]
}, "more-menu"));
};
exports.EditorToolbarObserver = EditorToolbarObserver;
//# sourceMappingURL=EditorToolbarObserver.js.map
@@ -0,0 +1 @@
{"version":3,"file":"EditorToolbarObserver.js","sources":["../../../../../admin/src/pages/EditView/components/EditorToolbarObserver.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { useElementOnScreen } from '@strapi/admin/strapi-admin';\nimport { IconButton, Menu } from '@strapi/design-system';\nimport { More } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\n\n/* -------------------------------------------------------------------------------------------------\n * ObservedToolbarComponent\n * -----------------------------------------------------------------------------------------------*/\n\ninterface ObservedToolbarComponentProps {\n index: number;\n lastVisibleIndex: number;\n setLastVisibleIndex: React.Dispatch<React.SetStateAction<number>>;\n rootRef: React.RefObject<HTMLElement>;\n children: React.ReactNode;\n}\n\nconst ObservedToolbarComponent = ({\n index,\n lastVisibleIndex,\n setLastVisibleIndex,\n rootRef,\n children,\n}: ObservedToolbarComponentProps) => {\n const isVisible = index <= lastVisibleIndex;\n\n const containerRef = useElementOnScreen<HTMLDivElement>(\n (isVisible) => {\n /**\n * It's the MoreMenu's job to make an item not visible when there's not room for it.\n * But we need to react here to the element becoming visible again.\n */\n if (isVisible) {\n setLastVisibleIndex((prev) => Math.max(prev, index));\n }\n },\n { threshold: 1, root: rootRef.current }\n );\n\n return (\n <div\n ref={containerRef}\n style={{\n /**\n * Use visibility so that the element occupies the space if requires even when there's not\n * enough room for it to be visible. The empty reserved space will be clipped by the\n * overflow:hidden rule on the parent, so it doesn't affect the UI.\n * This way we can keep observing its visiblity and react to browser resize events.\n */\n visibility: isVisible ? 'visible' : 'hidden',\n }}\n >\n {children}\n </div>\n );\n};\n\n/* -------------------------------------------------------------------------------------------------\n * EditorToolbarObserver\n * -----------------------------------------------------------------------------------------------*/\n\nexport interface ObservedComponent {\n toolbar: React.ReactNode;\n menu: React.ReactNode;\n key: string;\n}\n\nexport const EditorToolbarObserver = ({\n observedComponents,\n menuTriggerVariant = 'ghost',\n}: {\n observedComponents: ObservedComponent[];\n menuTriggerVariant?: Menu.TriggerProps['variant'];\n}) => {\n const { formatMessage } = useIntl();\n const toolbarRef = React.useRef<HTMLElement>(null);\n\n const [lastVisibleIndex, setLastVisibleIndex] = React.useState<number>(\n observedComponents.length - 1\n );\n const hasHiddenItems = lastVisibleIndex < observedComponents.length - 1;\n const menuIndex = lastVisibleIndex + 1;\n\n const [open, setOpen] = React.useState(false);\n const isMenuOpenWithContent = open && hasHiddenItems;\n const menuTriggerRef = useElementOnScreen<HTMLButtonElement>(\n (isVisible) => {\n // We only react to the menu becoming invisible. When that happens, we hide the last item.\n if (!isVisible) {\n /**\n * If there's no room for any item, the index can be -1.\n * This is intentional, in that case only the more menu will be visible.\n **/\n setLastVisibleIndex((prev) => prev - 1);\n // Maintain the menu state if it has content\n setOpen(isMenuOpenWithContent);\n }\n },\n { threshold: 1, root: toolbarRef.current }\n );\n\n return observedComponents\n .map((component, index) => {\n return (\n <ObservedToolbarComponent\n key={component.key}\n index={index}\n lastVisibleIndex={lastVisibleIndex}\n setLastVisibleIndex={setLastVisibleIndex}\n rootRef={toolbarRef}\n >\n {component.toolbar}\n </ObservedToolbarComponent>\n );\n })\n .toSpliced(\n menuIndex,\n 0,\n <Menu.Root\n key=\"more-menu\"\n defaultOpen={false}\n open={isMenuOpenWithContent}\n onOpenChange={setOpen}\n >\n <Menu.Trigger\n paddingLeft={0}\n paddingRight={0}\n ref={menuTriggerRef}\n variant={menuTriggerVariant}\n style={{ visibility: hasHiddenItems ? 'visible' : 'hidden' }}\n label={formatMessage({ id: 'global.more', defaultMessage: 'More' })}\n tag={IconButton}\n icon={<More />}\n />\n <Menu.Content\n onCloseAutoFocus={(e) => e.preventDefault()}\n maxHeight=\"100%\"\n minWidth=\"256px\"\n popoverPlacement=\"bottom-end\"\n zIndex={2}\n >\n {observedComponents.slice(menuIndex).map((component) => (\n <React.Fragment key={component.key}>{component.menu}</React.FragLine truncated
@@ -0,0 +1,99 @@
import { jsx, jsxs } from 'react/jsx-runtime';
import * as React from 'react';
import { useElementOnScreen } from '@strapi/admin/strapi-admin';
import { Menu, IconButton } from '@strapi/design-system';
import { More } from '@strapi/icons';
import { useIntl } from 'react-intl';
const ObservedToolbarComponent = ({ index, lastVisibleIndex, setLastVisibleIndex, rootRef, children })=>{
const isVisible = index <= lastVisibleIndex;
const containerRef = useElementOnScreen((isVisible)=>{
/**
* It's the MoreMenu's job to make an item not visible when there's not room for it.
* But we need to react here to the element becoming visible again.
*/ if (isVisible) {
setLastVisibleIndex((prev)=>Math.max(prev, index));
}
}, {
threshold: 1,
root: rootRef.current
});
return /*#__PURE__*/ jsx("div", {
ref: containerRef,
style: {
/**
* Use visibility so that the element occupies the space if requires even when there's not
* enough room for it to be visible. The empty reserved space will be clipped by the
* overflow:hidden rule on the parent, so it doesn't affect the UI.
* This way we can keep observing its visiblity and react to browser resize events.
*/ visibility: isVisible ? 'visible' : 'hidden'
},
children: children
});
};
const EditorToolbarObserver = ({ observedComponents, menuTriggerVariant = 'ghost' })=>{
const { formatMessage } = useIntl();
const toolbarRef = React.useRef(null);
const [lastVisibleIndex, setLastVisibleIndex] = React.useState(observedComponents.length - 1);
const hasHiddenItems = lastVisibleIndex < observedComponents.length - 1;
const menuIndex = lastVisibleIndex + 1;
const [open, setOpen] = React.useState(false);
const isMenuOpenWithContent = open && hasHiddenItems;
const menuTriggerRef = useElementOnScreen((isVisible)=>{
// We only react to the menu becoming invisible. When that happens, we hide the last item.
if (!isVisible) {
/**
* If there's no room for any item, the index can be -1.
* This is intentional, in that case only the more menu will be visible.
**/ setLastVisibleIndex((prev)=>prev - 1);
// Maintain the menu state if it has content
setOpen(isMenuOpenWithContent);
}
}, {
threshold: 1,
root: toolbarRef.current
});
return observedComponents.map((component, index)=>{
return /*#__PURE__*/ jsx(ObservedToolbarComponent, {
index: index,
lastVisibleIndex: lastVisibleIndex,
setLastVisibleIndex: setLastVisibleIndex,
rootRef: toolbarRef,
children: component.toolbar
}, component.key);
}).toSpliced(menuIndex, 0, /*#__PURE__*/ jsxs(Menu.Root, {
defaultOpen: false,
open: isMenuOpenWithContent,
onOpenChange: setOpen,
children: [
/*#__PURE__*/ jsx(Menu.Trigger, {
paddingLeft: 0,
paddingRight: 0,
ref: menuTriggerRef,
variant: menuTriggerVariant,
style: {
visibility: hasHiddenItems ? 'visible' : 'hidden'
},
label: formatMessage({
id: 'global.more',
defaultMessage: 'More'
}),
tag: IconButton,
icon: /*#__PURE__*/ jsx(More, {})
}),
/*#__PURE__*/ jsx(Menu.Content, {
onCloseAutoFocus: (e)=>e.preventDefault(),
maxHeight: "100%",
minWidth: "256px",
popoverPlacement: "bottom-end",
zIndex: 2,
children: observedComponents.slice(menuIndex).map((component)=>/*#__PURE__*/ jsx(React.Fragment, {
children: component.menu
}, component.key))
})
]
}, "more-menu"));
};
export { EditorToolbarObserver };
//# sourceMappingURL=EditorToolbarObserver.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"EditorToolbarObserver.mjs","sources":["../../../../../admin/src/pages/EditView/components/EditorToolbarObserver.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { useElementOnScreen } from '@strapi/admin/strapi-admin';\nimport { IconButton, Menu } from '@strapi/design-system';\nimport { More } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\n\n/* -------------------------------------------------------------------------------------------------\n * ObservedToolbarComponent\n * -----------------------------------------------------------------------------------------------*/\n\ninterface ObservedToolbarComponentProps {\n index: number;\n lastVisibleIndex: number;\n setLastVisibleIndex: React.Dispatch<React.SetStateAction<number>>;\n rootRef: React.RefObject<HTMLElement>;\n children: React.ReactNode;\n}\n\nconst ObservedToolbarComponent = ({\n index,\n lastVisibleIndex,\n setLastVisibleIndex,\n rootRef,\n children,\n}: ObservedToolbarComponentProps) => {\n const isVisible = index <= lastVisibleIndex;\n\n const containerRef = useElementOnScreen<HTMLDivElement>(\n (isVisible) => {\n /**\n * It's the MoreMenu's job to make an item not visible when there's not room for it.\n * But we need to react here to the element becoming visible again.\n */\n if (isVisible) {\n setLastVisibleIndex((prev) => Math.max(prev, index));\n }\n },\n { threshold: 1, root: rootRef.current }\n );\n\n return (\n <div\n ref={containerRef}\n style={{\n /**\n * Use visibility so that the element occupies the space if requires even when there's not\n * enough room for it to be visible. The empty reserved space will be clipped by the\n * overflow:hidden rule on the parent, so it doesn't affect the UI.\n * This way we can keep observing its visiblity and react to browser resize events.\n */\n visibility: isVisible ? 'visible' : 'hidden',\n }}\n >\n {children}\n </div>\n );\n};\n\n/* -------------------------------------------------------------------------------------------------\n * EditorToolbarObserver\n * -----------------------------------------------------------------------------------------------*/\n\nexport interface ObservedComponent {\n toolbar: React.ReactNode;\n menu: React.ReactNode;\n key: string;\n}\n\nexport const EditorToolbarObserver = ({\n observedComponents,\n menuTriggerVariant = 'ghost',\n}: {\n observedComponents: ObservedComponent[];\n menuTriggerVariant?: Menu.TriggerProps['variant'];\n}) => {\n const { formatMessage } = useIntl();\n const toolbarRef = React.useRef<HTMLElement>(null);\n\n const [lastVisibleIndex, setLastVisibleIndex] = React.useState<number>(\n observedComponents.length - 1\n );\n const hasHiddenItems = lastVisibleIndex < observedComponents.length - 1;\n const menuIndex = lastVisibleIndex + 1;\n\n const [open, setOpen] = React.useState(false);\n const isMenuOpenWithContent = open && hasHiddenItems;\n const menuTriggerRef = useElementOnScreen<HTMLButtonElement>(\n (isVisible) => {\n // We only react to the menu becoming invisible. When that happens, we hide the last item.\n if (!isVisible) {\n /**\n * If there's no room for any item, the index can be -1.\n * This is intentional, in that case only the more menu will be visible.\n **/\n setLastVisibleIndex((prev) => prev - 1);\n // Maintain the menu state if it has content\n setOpen(isMenuOpenWithContent);\n }\n },\n { threshold: 1, root: toolbarRef.current }\n );\n\n return observedComponents\n .map((component, index) => {\n return (\n <ObservedToolbarComponent\n key={component.key}\n index={index}\n lastVisibleIndex={lastVisibleIndex}\n setLastVisibleIndex={setLastVisibleIndex}\n rootRef={toolbarRef}\n >\n {component.toolbar}\n </ObservedToolbarComponent>\n );\n })\n .toSpliced(\n menuIndex,\n 0,\n <Menu.Root\n key=\"more-menu\"\n defaultOpen={false}\n open={isMenuOpenWithContent}\n onOpenChange={setOpen}\n >\n <Menu.Trigger\n paddingLeft={0}\n paddingRight={0}\n ref={menuTriggerRef}\n variant={menuTriggerVariant}\n style={{ visibility: hasHiddenItems ? 'visible' : 'hidden' }}\n label={formatMessage({ id: 'global.more', defaultMessage: 'More' })}\n tag={IconButton}\n icon={<More />}\n />\n <Menu.Content\n onCloseAutoFocus={(e) => e.preventDefault()}\n maxHeight=\"100%\"\n minWidth=\"256px\"\n popoverPlacement=\"bottom-end\"\n zIndex={2}\n >\n {observedComponents.slice(menuIndex).map((component) => (\n <React.Fragment key={component.key}>{component.menu}</React.FraLine truncated
@@ -0,0 +1,224 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var Prism = require('prismjs');
var reactIntl = require('react-intl');
var slate = require('slate');
var slateReact = require('slate-react');
var styledComponents = require('styled-components');
var BlocksEditor = require('../BlocksEditor.js');
var constants = require('../utils/constants.js');
var conversions = require('../utils/conversions.js');
var enterKey = require('../utils/enterKey.js');
require('prismjs/themes/prism-solarizedlight.css');
require('prismjs/components/prism-asmatmel');
require('prismjs/components/prism-bash');
require('prismjs/components/prism-basic');
require('prismjs/components/prism-c');
require('prismjs/components/prism-clojure');
require('prismjs/components/prism-cobol');
require('prismjs/components/prism-cpp');
require('prismjs/components/prism-csharp');
require('prismjs/components/prism-dart');
require('prismjs/components/prism-docker');
require('prismjs/components/prism-elixir');
require('prismjs/components/prism-erlang');
require('prismjs/components/prism-fortran');
require('prismjs/components/prism-fsharp');
require('prismjs/components/prism-go');
require('prismjs/components/prism-graphql');
require('prismjs/components/prism-groovy');
require('prismjs/components/prism-haskell');
require('prismjs/components/prism-haxe');
require('prismjs/components/prism-ini');
require('prismjs/components/prism-java');
require('prismjs/components/prism-javascript');
require('prismjs/components/prism-jsx');
require('prismjs/components/prism-json');
require('prismjs/components/prism-julia');
require('prismjs/components/prism-kotlin');
require('prismjs/components/prism-latex');
require('prismjs/components/prism-lua');
require('prismjs/components/prism-markdown');
require('prismjs/components/prism-matlab');
require('prismjs/components/prism-makefile');
require('prismjs/components/prism-objectivec');
require('prismjs/components/prism-perl');
require('prismjs/components/prism-php');
require('prismjs/components/prism-powershell');
require('prismjs/components/prism-python');
require('prismjs/components/prism-r');
require('prismjs/components/prism-ruby');
require('prismjs/components/prism-rust');
require('prismjs/components/prism-sas');
require('prismjs/components/prism-scala');
require('prismjs/components/prism-scheme');
require('prismjs/components/prism-sql');
require('prismjs/components/prism-stata');
require('prismjs/components/prism-swift');
require('prismjs/components/prism-typescript');
require('prismjs/components/prism-tsx');
require('prismjs/components/prism-vbnet');
require('prismjs/components/prism-yaml');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
var Prism__namespace = /*#__PURE__*/_interopNamespaceDefault(Prism);
const decorateCode = ([node, path])=>{
const ranges = [];
// make sure it is an Slate Element
if (!slate.Element.isElement(node) || node.type !== 'code') return ranges;
// transform the Element into a string
const text = slate.Node.string(node);
const language = constants.codeLanguages.find((lang)=>lang.value === node.language);
const decorateKey = language?.decorate ?? language?.value;
const selectedLanguage = Prism__namespace.languages[decorateKey || 'plaintext'];
// create "tokens" with "prismjs" and put them in "ranges"
const tokens = Prism__namespace.tokenize(text, selectedLanguage);
let start = 0;
for (const token of tokens){
const length = token.length;
const end = start + length;
if (typeof token !== 'string') {
ranges.push({
anchor: {
path,
offset: start
},
focus: {
path,
offset: end
},
className: `token ${token.type}`
});
}
start = end;
}
// these will be found in "renderLeaf" in "leaf" and their "className" will be applied
return ranges;
};
const CodeBlock = styledComponents.styled.pre`
border-radius: ${({ theme })=>theme.borderRadius};
background-color: ${({ theme })=>theme.colors.neutral100};
max-width: 100%;
overflow: auto;
padding: ${({ theme })=>`${theme.spaces[3]} ${theme.spaces[4]}`};
flex-shrink: 1;
& > code {
font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas,
monospace;
color: ${({ theme })=>theme.colors.neutral800};
overflow: auto;
max-width: 100%;
}
`;
const CodeEditor = (props)=>{
const { editor } = BlocksEditor.useBlocksEditorContext('ImageDialog');
const editorIsFocused = slateReact.useFocused();
const imageIsSelected = slateReact.useSelected();
const { formatMessage } = reactIntl.useIntl();
const [isSelectOpen, setIsSelectOpen] = React__namespace.useState(false);
const shouldDisplayLanguageSelect = editorIsFocused && imageIsSelected || isSelectOpen;
return /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Box, {
position: "relative",
width: "100%",
children: [
/*#__PURE__*/ jsxRuntime.jsx(CodeBlock, {
...props.attributes,
children: /*#__PURE__*/ jsxRuntime.jsx("code", {
children: props.children
})
}),
shouldDisplayLanguageSelect && /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
position: "absolute",
background: "neutral0",
borderColor: "neutral150",
borderStyle: "solid",
borderWidth: "0.5px",
shadow: "tableShadow",
top: "100%",
marginTop: 1,
right: 0,
padding: 1,
hasRadius: true,
children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.SingleSelect, {
onChange: (open)=>{
slate.Transforms.setNodes(editor, {
language: open.toString()
}, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'code'
});
},
value: props.element.type === 'code' && props.element.language || 'plaintext',
onOpenChange: (open)=>{
setIsSelectOpen(open);
// Focus the editor again when closing the select so the user can continue typing
if (!open) {
slateReact.ReactEditor.focus(editor);
}
},
onCloseAutoFocus: (e)=>e.preventDefault(),
"aria-label": formatMessage({
id: 'components.Blocks.blocks.code.languageLabel',
defaultMessage: 'Select a language'
}),
children: constants.codeLanguages.map(({ value, label })=>/*#__PURE__*/ jsxRuntime.jsx(designSystem.SingleSelectOption, {
value: value,
children: label
}, value))
})
})
]
});
};
const codeBlocks = {
code: {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(CodeEditor, {
...props
}),
icon: Icons.CodeBlock,
label: {
id: 'components.Blocks.blocks.code',
defaultMessage: 'Code block'
},
matchNode: (node)=>node.type === 'code',
isInBlocksSelector: true,
handleConvert (editor) {
conversions.baseHandleConvert(editor, {
type: 'code',
language: 'plaintext'
});
},
handleEnterKey (editor) {
enterKey.pressEnterTwiceToExit(editor);
},
snippets: [
'```'
]
}
};
exports.codeBlocks = codeBlocks;
exports.decorateCode = decorateCode;
//# sourceMappingURL=Code.js.map
@@ -0,0 +1 @@
{"version":3,"file":"Code.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Code.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Box, SingleSelect, SingleSelectOption } from '@strapi/design-system';\nimport { CodeBlock as CodeBlockIcon } from '@strapi/icons';\nimport * as Prism from 'prismjs';\nimport { useIntl } from 'react-intl';\nimport { BaseRange, Element, Editor, Node, NodeEntry, Transforms } from 'slate';\nimport { useSelected, type RenderElementProps, useFocused, ReactEditor } from 'slate-react';\nimport { styled } from 'styled-components';\n\nimport { useBlocksEditorContext, type BlocksStore } from '../BlocksEditor';\nimport { codeLanguages } from '../utils/constants';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { pressEnterTwiceToExit } from '../utils/enterKey';\nimport { type Block } from '../utils/types';\n\nimport 'prismjs/themes/prism-solarizedlight.css';\nimport 'prismjs/components/prism-asmatmel';\nimport 'prismjs/components/prism-bash';\nimport 'prismjs/components/prism-basic';\nimport 'prismjs/components/prism-c';\nimport 'prismjs/components/prism-clojure';\nimport 'prismjs/components/prism-cobol';\nimport 'prismjs/components/prism-cpp';\nimport 'prismjs/components/prism-csharp';\nimport 'prismjs/components/prism-dart';\nimport 'prismjs/components/prism-docker';\nimport 'prismjs/components/prism-elixir';\nimport 'prismjs/components/prism-erlang';\nimport 'prismjs/components/prism-fortran';\nimport 'prismjs/components/prism-fsharp';\nimport 'prismjs/components/prism-go';\nimport 'prismjs/components/prism-graphql';\nimport 'prismjs/components/prism-groovy';\nimport 'prismjs/components/prism-haskell';\nimport 'prismjs/components/prism-haxe';\nimport 'prismjs/components/prism-ini';\nimport 'prismjs/components/prism-java';\nimport 'prismjs/components/prism-javascript';\nimport 'prismjs/components/prism-jsx';\nimport 'prismjs/components/prism-json';\nimport 'prismjs/components/prism-julia';\nimport 'prismjs/components/prism-kotlin';\nimport 'prismjs/components/prism-latex';\nimport 'prismjs/components/prism-lua';\nimport 'prismjs/components/prism-markdown';\nimport 'prismjs/components/prism-matlab';\nimport 'prismjs/components/prism-makefile';\nimport 'prismjs/components/prism-objectivec';\nimport 'prismjs/components/prism-perl';\nimport 'prismjs/components/prism-php';\nimport 'prismjs/components/prism-powershell';\nimport 'prismjs/components/prism-python';\nimport 'prismjs/components/prism-r';\nimport 'prismjs/components/prism-ruby';\nimport 'prismjs/components/prism-rust';\nimport 'prismjs/components/prism-sas';\nimport 'prismjs/components/prism-scala';\nimport 'prismjs/components/prism-scheme';\nimport 'prismjs/components/prism-sql';\nimport 'prismjs/components/prism-stata';\nimport 'prismjs/components/prism-swift';\nimport 'prismjs/components/prism-typescript';\nimport 'prismjs/components/prism-tsx';\nimport 'prismjs/components/prism-vbnet';\nimport 'prismjs/components/prism-yaml';\n\ntype BaseRangeCustom = BaseRange & { className: string };\n\nexport const decorateCode = ([node, path]: NodeEntry) => {\n const ranges: BaseRangeCustom[] = [];\n\n // make sure it is an Slate Element\n if (!Element.isElement(node) || node.type !== 'code') return ranges;\n // transform the Element into a string\n const text = Node.string(node);\n const language = codeLanguages.find((lang) => lang.value === node.language);\n const decorateKey = language?.decorate ?? language?.value;\n\n const selectedLanguage = Prism.languages[decorateKey || 'plaintext'];\n\n // create \"tokens\" with \"prismjs\" and put them in \"ranges\"\n const tokens = Prism.tokenize(text, selectedLanguage);\n let start = 0;\n for (const token of tokens) {\n const length = token.length;\n const end = start + length;\n if (typeof token !== 'string') {\n ranges.push({\n anchor: { path, offset: start },\n focus: { path, offset: end },\n className: `token ${token.type}`,\n });\n }\n start = end;\n }\n\n // these will be found in \"renderLeaf\" in \"leaf\" and their \"className\" will be applied\n return ranges;\n};\n\nconst CodeBlock = styled.pre`\n border-radius: ${({ theme }) => theme.borderRadius};\n background-color: ${({ theme }) => theme.colors.neutral100};\n max-width: 100%;\n overflow: auto;\n padding: ${({ theme }) => `${theme.spaces[3]} ${theme.spaces[4]}`};\n flex-shrink: 1;\n\n & > code {\n font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas,\n monospace;\n color: ${({ theme }) => theme.colors.neutral800};\n overflow: auto;\n max-width: 100%;\n }\n`;\n\nconst CodeEditor = (props: RenderElementProps) => {\n const { editor } = useBlocksEditorContext('ImageDialog');\n const editorIsFocused = useFocused();\n const imageIsSelected = useSelected();\n const { formatMessage } = useIntl();\n const [isSelectOpen, setIsLine truncated
@@ -0,0 +1,201 @@
import { jsx, jsxs } from 'react/jsx-runtime';
import * as React from 'react';
import { Box, SingleSelect, SingleSelectOption } from '@strapi/design-system';
import { CodeBlock as CodeBlock$1 } from '@strapi/icons';
import * as Prism from 'prismjs';
import { useIntl } from 'react-intl';
import { Element, Node, Transforms, Editor } from 'slate';
import { useFocused, useSelected, ReactEditor } from 'slate-react';
import { styled } from 'styled-components';
import { useBlocksEditorContext } from '../BlocksEditor.mjs';
import { codeLanguages } from '../utils/constants.mjs';
import { baseHandleConvert } from '../utils/conversions.mjs';
import { pressEnterTwiceToExit } from '../utils/enterKey.mjs';
import 'prismjs/themes/prism-solarizedlight.css';
import 'prismjs/components/prism-asmatmel';
import 'prismjs/components/prism-bash';
import 'prismjs/components/prism-basic';
import 'prismjs/components/prism-c';
import 'prismjs/components/prism-clojure';
import 'prismjs/components/prism-cobol';
import 'prismjs/components/prism-cpp';
import 'prismjs/components/prism-csharp';
import 'prismjs/components/prism-dart';
import 'prismjs/components/prism-docker';
import 'prismjs/components/prism-elixir';
import 'prismjs/components/prism-erlang';
import 'prismjs/components/prism-fortran';
import 'prismjs/components/prism-fsharp';
import 'prismjs/components/prism-go';
import 'prismjs/components/prism-graphql';
import 'prismjs/components/prism-groovy';
import 'prismjs/components/prism-haskell';
import 'prismjs/components/prism-haxe';
import 'prismjs/components/prism-ini';
import 'prismjs/components/prism-java';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-jsx';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-julia';
import 'prismjs/components/prism-kotlin';
import 'prismjs/components/prism-latex';
import 'prismjs/components/prism-lua';
import 'prismjs/components/prism-markdown';
import 'prismjs/components/prism-matlab';
import 'prismjs/components/prism-makefile';
import 'prismjs/components/prism-objectivec';
import 'prismjs/components/prism-perl';
import 'prismjs/components/prism-php';
import 'prismjs/components/prism-powershell';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-r';
import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-rust';
import 'prismjs/components/prism-sas';
import 'prismjs/components/prism-scala';
import 'prismjs/components/prism-scheme';
import 'prismjs/components/prism-sql';
import 'prismjs/components/prism-stata';
import 'prismjs/components/prism-swift';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-tsx';
import 'prismjs/components/prism-vbnet';
import 'prismjs/components/prism-yaml';
const decorateCode = ([node, path])=>{
const ranges = [];
// make sure it is an Slate Element
if (!Element.isElement(node) || node.type !== 'code') return ranges;
// transform the Element into a string
const text = Node.string(node);
const language = codeLanguages.find((lang)=>lang.value === node.language);
const decorateKey = language?.decorate ?? language?.value;
const selectedLanguage = Prism.languages[decorateKey || 'plaintext'];
// create "tokens" with "prismjs" and put them in "ranges"
const tokens = Prism.tokenize(text, selectedLanguage);
let start = 0;
for (const token of tokens){
const length = token.length;
const end = start + length;
if (typeof token !== 'string') {
ranges.push({
anchor: {
path,
offset: start
},
focus: {
path,
offset: end
},
className: `token ${token.type}`
});
}
start = end;
}
// these will be found in "renderLeaf" in "leaf" and their "className" will be applied
return ranges;
};
const CodeBlock = styled.pre`
border-radius: ${({ theme })=>theme.borderRadius};
background-color: ${({ theme })=>theme.colors.neutral100};
max-width: 100%;
overflow: auto;
padding: ${({ theme })=>`${theme.spaces[3]} ${theme.spaces[4]}`};
flex-shrink: 1;
& > code {
font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas,
monospace;
color: ${({ theme })=>theme.colors.neutral800};
overflow: auto;
max-width: 100%;
}
`;
const CodeEditor = (props)=>{
const { editor } = useBlocksEditorContext('ImageDialog');
const editorIsFocused = useFocused();
const imageIsSelected = useSelected();
const { formatMessage } = useIntl();
const [isSelectOpen, setIsSelectOpen] = React.useState(false);
const shouldDisplayLanguageSelect = editorIsFocused && imageIsSelected || isSelectOpen;
return /*#__PURE__*/ jsxs(Box, {
position: "relative",
width: "100%",
children: [
/*#__PURE__*/ jsx(CodeBlock, {
...props.attributes,
children: /*#__PURE__*/ jsx("code", {
children: props.children
})
}),
shouldDisplayLanguageSelect && /*#__PURE__*/ jsx(Box, {
position: "absolute",
background: "neutral0",
borderColor: "neutral150",
borderStyle: "solid",
borderWidth: "0.5px",
shadow: "tableShadow",
top: "100%",
marginTop: 1,
right: 0,
padding: 1,
hasRadius: true,
children: /*#__PURE__*/ jsx(SingleSelect, {
onChange: (open)=>{
Transforms.setNodes(editor, {
language: open.toString()
}, {
match: (node)=>!Editor.isEditor(node) && node.type === 'code'
});
},
value: props.element.type === 'code' && props.element.language || 'plaintext',
onOpenChange: (open)=>{
setIsSelectOpen(open);
// Focus the editor again when closing the select so the user can continue typing
if (!open) {
ReactEditor.focus(editor);
}
},
onCloseAutoFocus: (e)=>e.preventDefault(),
"aria-label": formatMessage({
id: 'components.Blocks.blocks.code.languageLabel',
defaultMessage: 'Select a language'
}),
children: codeLanguages.map(({ value, label })=>/*#__PURE__*/ jsx(SingleSelectOption, {
value: value,
children: label
}, value))
})
})
]
});
};
const codeBlocks = {
code: {
renderElement: (props)=>/*#__PURE__*/ jsx(CodeEditor, {
...props
}),
icon: CodeBlock$1,
label: {
id: 'components.Blocks.blocks.code',
defaultMessage: 'Code block'
},
matchNode: (node)=>node.type === 'code',
isInBlocksSelector: true,
handleConvert (editor) {
baseHandleConvert(editor, {
type: 'code',
language: 'plaintext'
});
},
handleEnterKey (editor) {
pressEnterTwiceToExit(editor);
},
snippets: [
'```'
]
}
};
export { codeBlocks, decorateCode };
//# sourceMappingURL=Code.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"Code.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Code.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Box, SingleSelect, SingleSelectOption } from '@strapi/design-system';\nimport { CodeBlock as CodeBlockIcon } from '@strapi/icons';\nimport * as Prism from 'prismjs';\nimport { useIntl } from 'react-intl';\nimport { BaseRange, Element, Editor, Node, NodeEntry, Transforms } from 'slate';\nimport { useSelected, type RenderElementProps, useFocused, ReactEditor } from 'slate-react';\nimport { styled } from 'styled-components';\n\nimport { useBlocksEditorContext, type BlocksStore } from '../BlocksEditor';\nimport { codeLanguages } from '../utils/constants';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { pressEnterTwiceToExit } from '../utils/enterKey';\nimport { type Block } from '../utils/types';\n\nimport 'prismjs/themes/prism-solarizedlight.css';\nimport 'prismjs/components/prism-asmatmel';\nimport 'prismjs/components/prism-bash';\nimport 'prismjs/components/prism-basic';\nimport 'prismjs/components/prism-c';\nimport 'prismjs/components/prism-clojure';\nimport 'prismjs/components/prism-cobol';\nimport 'prismjs/components/prism-cpp';\nimport 'prismjs/components/prism-csharp';\nimport 'prismjs/components/prism-dart';\nimport 'prismjs/components/prism-docker';\nimport 'prismjs/components/prism-elixir';\nimport 'prismjs/components/prism-erlang';\nimport 'prismjs/components/prism-fortran';\nimport 'prismjs/components/prism-fsharp';\nimport 'prismjs/components/prism-go';\nimport 'prismjs/components/prism-graphql';\nimport 'prismjs/components/prism-groovy';\nimport 'prismjs/components/prism-haskell';\nimport 'prismjs/components/prism-haxe';\nimport 'prismjs/components/prism-ini';\nimport 'prismjs/components/prism-java';\nimport 'prismjs/components/prism-javascript';\nimport 'prismjs/components/prism-jsx';\nimport 'prismjs/components/prism-json';\nimport 'prismjs/components/prism-julia';\nimport 'prismjs/components/prism-kotlin';\nimport 'prismjs/components/prism-latex';\nimport 'prismjs/components/prism-lua';\nimport 'prismjs/components/prism-markdown';\nimport 'prismjs/components/prism-matlab';\nimport 'prismjs/components/prism-makefile';\nimport 'prismjs/components/prism-objectivec';\nimport 'prismjs/components/prism-perl';\nimport 'prismjs/components/prism-php';\nimport 'prismjs/components/prism-powershell';\nimport 'prismjs/components/prism-python';\nimport 'prismjs/components/prism-r';\nimport 'prismjs/components/prism-ruby';\nimport 'prismjs/components/prism-rust';\nimport 'prismjs/components/prism-sas';\nimport 'prismjs/components/prism-scala';\nimport 'prismjs/components/prism-scheme';\nimport 'prismjs/components/prism-sql';\nimport 'prismjs/components/prism-stata';\nimport 'prismjs/components/prism-swift';\nimport 'prismjs/components/prism-typescript';\nimport 'prismjs/components/prism-tsx';\nimport 'prismjs/components/prism-vbnet';\nimport 'prismjs/components/prism-yaml';\n\ntype BaseRangeCustom = BaseRange & { className: string };\n\nexport const decorateCode = ([node, path]: NodeEntry) => {\n const ranges: BaseRangeCustom[] = [];\n\n // make sure it is an Slate Element\n if (!Element.isElement(node) || node.type !== 'code') return ranges;\n // transform the Element into a string\n const text = Node.string(node);\n const language = codeLanguages.find((lang) => lang.value === node.language);\n const decorateKey = language?.decorate ?? language?.value;\n\n const selectedLanguage = Prism.languages[decorateKey || 'plaintext'];\n\n // create \"tokens\" with \"prismjs\" and put them in \"ranges\"\n const tokens = Prism.tokenize(text, selectedLanguage);\n let start = 0;\n for (const token of tokens) {\n const length = token.length;\n const end = start + length;\n if (typeof token !== 'string') {\n ranges.push({\n anchor: { path, offset: start },\n focus: { path, offset: end },\n className: `token ${token.type}`,\n });\n }\n start = end;\n }\n\n // these will be found in \"renderLeaf\" in \"leaf\" and their \"className\" will be applied\n return ranges;\n};\n\nconst CodeBlock = styled.pre`\n border-radius: ${({ theme }) => theme.borderRadius};\n background-color: ${({ theme }) => theme.colors.neutral100};\n max-width: 100%;\n overflow: auto;\n padding: ${({ theme }) => `${theme.spaces[3]} ${theme.spaces[4]}`};\n flex-shrink: 1;\n\n & > code {\n font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas,\n monospace;\n color: ${({ theme }) => theme.colors.neutral800};\n overflow: auto;\n max-width: 100%;\n }\n`;\n\nconst CodeEditor = (props: RenderElementProps) => {\n const { editor } = useBlocksEditorContext('ImageDialog');\n const editorIsFocused = useFocused();\n const imageIsSelected = useSelected();\n const { formatMessage } = useIntl();\n const [isSelectOpen, setILine truncated
@@ -0,0 +1,165 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
require('react');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var styledComponents = require('styled-components');
var conversions = require('../utils/conversions.js');
const H1 = styledComponents.styled(designSystem.Typography).attrs({
tag: 'h1'
})`
font-size: 4.2rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H2 = styledComponents.styled(designSystem.Typography).attrs({
tag: 'h2'
})`
font-size: 3.5rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H3 = styledComponents.styled(designSystem.Typography).attrs({
tag: 'h3'
})`
font-size: 2.9rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H4 = styledComponents.styled(designSystem.Typography).attrs({
tag: 'h4'
})`
font-size: 2.4rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H5 = styledComponents.styled(designSystem.Typography).attrs({
tag: 'h5'
})`
font-size: 2rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H6 = styledComponents.styled(designSystem.Typography).attrs({
tag: 'h6'
})`
font-size: 1.6rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
/**
* Common handler for converting a node to a heading
*/ const handleConvertToHeading = (editor, level)=>{
conversions.baseHandleConvert(editor, {
type: 'heading',
level
});
};
const headingBlocks = {
'heading-one': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(H1, {
...props.attributes,
children: props.children
}),
icon: Icons.HeadingOne,
label: {
id: 'components.Blocks.blocks.heading1',
defaultMessage: 'Heading 1'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 1),
matchNode: (node)=>node.type === 'heading' && node.level === 1,
isInBlocksSelector: true,
snippets: [
'#'
],
dragHandleTopMargin: '14px'
},
'heading-two': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(H2, {
...props.attributes,
children: props.children
}),
icon: Icons.HeadingTwo,
label: {
id: 'components.Blocks.blocks.heading2',
defaultMessage: 'Heading 2'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 2),
matchNode: (node)=>node.type === 'heading' && node.level === 2,
isInBlocksSelector: true,
snippets: [
'##'
],
dragHandleTopMargin: '10px'
},
'heading-three': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(H3, {
...props.attributes,
children: props.children
}),
icon: Icons.HeadingThree,
label: {
id: 'components.Blocks.blocks.heading3',
defaultMessage: 'Heading 3'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 3),
matchNode: (node)=>node.type === 'heading' && node.level === 3,
isInBlocksSelector: true,
snippets: [
'###'
],
dragHandleTopMargin: '7px'
},
'heading-four': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(H4, {
...props.attributes,
children: props.children
}),
icon: Icons.HeadingFour,
label: {
id: 'components.Blocks.blocks.heading4',
defaultMessage: 'Heading 4'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 4),
matchNode: (node)=>node.type === 'heading' && node.level === 4,
isInBlocksSelector: true,
snippets: [
'####'
],
dragHandleTopMargin: '4px'
},
'heading-five': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(H5, {
...props.attributes,
children: props.children
}),
icon: Icons.HeadingFive,
label: {
id: 'components.Blocks.blocks.heading5',
defaultMessage: 'Heading 5'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 5),
matchNode: (node)=>node.type === 'heading' && node.level === 5,
isInBlocksSelector: true,
snippets: [
'#####'
]
},
'heading-six': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(H6, {
...props.attributes,
children: props.children
}),
icon: Icons.HeadingSix,
label: {
id: 'components.Blocks.blocks.heading6',
defaultMessage: 'Heading 6'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 6),
matchNode: (node)=>node.type === 'heading' && node.level === 6,
isInBlocksSelector: true,
snippets: [
'######'
],
dragHandleTopMargin: '-2px'
}
};
exports.headingBlocks = headingBlocks;
//# sourceMappingURL=Heading.js.map
@@ -0,0 +1 @@
{"version":3,"file":"Heading.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Heading.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Typography, TypographyComponent } from '@strapi/design-system';\nimport {\n HeadingFive,\n HeadingFour,\n HeadingOne,\n HeadingSix,\n HeadingThree,\n HeadingTwo,\n} from '@strapi/icons';\nimport { Editor } from 'slate';\nimport { styled } from 'styled-components';\n\nimport { type BlocksStore } from '../BlocksEditor';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { type Block } from '../utils/types';\n\nconst H1 = styled<TypographyComponent<'h1'>>(Typography).attrs({ tag: 'h1' })`\n font-size: 4.2rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H2 = styled<TypographyComponent<'h2'>>(Typography).attrs({ tag: 'h2' })`\n font-size: 3.5rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H3 = styled<TypographyComponent<'h3'>>(Typography).attrs({ tag: 'h3' })`\n font-size: 2.9rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H4 = styled<TypographyComponent<'h4'>>(Typography).attrs({ tag: 'h4' })`\n font-size: 2.4rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H5 = styled<TypographyComponent<'h5'>>(Typography).attrs({ tag: 'h5' })`\n font-size: 2rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H6 = styled<TypographyComponent<'h6'>>(Typography).attrs({ tag: 'h6' })`\n font-size: 1.6rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\n/**\n * Common handler for converting a node to a heading\n */\nconst handleConvertToHeading = (editor: Editor, level: Block<'heading'>['level']) => {\n baseHandleConvert<Block<'heading'>>(editor, { type: 'heading', level });\n};\n\nconst headingBlocks: Pick<\n BlocksStore,\n 'heading-one' | 'heading-two' | 'heading-three' | 'heading-four' | 'heading-five' | 'heading-six'\n> = {\n 'heading-one': {\n renderElement: (props) => <H1 {...props.attributes}>{props.children}</H1>,\n icon: HeadingOne,\n label: {\n id: 'components.Blocks.blocks.heading1',\n defaultMessage: 'Heading 1',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 1),\n matchNode: (node) => node.type === 'heading' && node.level === 1,\n isInBlocksSelector: true,\n snippets: ['#'],\n dragHandleTopMargin: '14px',\n },\n 'heading-two': {\n renderElement: (props) => <H2 {...props.attributes}>{props.children}</H2>,\n icon: HeadingTwo,\n label: {\n id: 'components.Blocks.blocks.heading2',\n defaultMessage: 'Heading 2',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 2),\n matchNode: (node) => node.type === 'heading' && node.level === 2,\n isInBlocksSelector: true,\n snippets: ['##'],\n dragHandleTopMargin: '10px',\n },\n 'heading-three': {\n renderElement: (props) => <H3 {...props.attributes}>{props.children}</H3>,\n icon: HeadingThree,\n label: {\n id: 'components.Blocks.blocks.heading3',\n defaultMessage: 'Heading 3',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 3),\n matchNode: (node) => node.type === 'heading' && node.level === 3,\n isInBlocksSelector: true,\n snippets: ['###'],\n dragHandleTopMargin: '7px',\n },\n 'heading-four': {\n renderElement: (props) => <H4 {...props.attributes}>{props.children}</H4>,\n icon: HeadingFour,\n label: {\n id: 'components.Blocks.blocks.heading4',\n defaultMessage: 'Heading 4',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 4),\n matchNode: (node) => node.type === 'heading' && node.level === 4,\n isInBlocksSelector: true,\n snippets: ['####'],\n dragHandleTopMargin: '4px',\n },\n 'heading-five': {\n renderElement: (props) => <H5 {...props.attributes}>{props.children}</H5>,\n icon: HeadingFive,\n label: {\n id: 'components.Blocks.blocks.heading5',\n defaultMessage: 'Heading 5',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 5),\n matchNode: (node) => node.type === 'heading' && node.level === 5,\n isInBlocksSelector: true,\n snippets: ['#####'],\n },\n 'heading-six': {\n renderElement: (props) => <H6 {...props.attributes}>{props.children}</H6>,\n icon: HeadingSix,\n label: {\n id: 'components.Blocks.blocks.heading6',\n defaultMessage: 'Heading 6',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 6),\n matchNode: (node) => node.type === 'heading' && node.level === 6,\n isInBlocksSelector: true,\n snippets: ['######'],\n dragHandleTopMargin: '-2px',\n },\n};\n\nexport { headingBlocks };\n"],"names":["H1","styled","Typography","attrs","tag","theme","lineHeights","H2","H3","H4","H5","H6","handleConvertToHeading","editor","level","baseHandleConvert","type","headingBlocks","renderElement","proLine truncated
@@ -0,0 +1,163 @@
import { jsx } from 'react/jsx-runtime';
import 'react';
import { Typography } from '@strapi/design-system';
import { HeadingOne, HeadingTwo, HeadingThree, HeadingFour, HeadingFive, HeadingSix } from '@strapi/icons';
import { styled } from 'styled-components';
import { baseHandleConvert } from '../utils/conversions.mjs';
const H1 = styled(Typography).attrs({
tag: 'h1'
})`
font-size: 4.2rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H2 = styled(Typography).attrs({
tag: 'h2'
})`
font-size: 3.5rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H3 = styled(Typography).attrs({
tag: 'h3'
})`
font-size: 2.9rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H4 = styled(Typography).attrs({
tag: 'h4'
})`
font-size: 2.4rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H5 = styled(Typography).attrs({
tag: 'h5'
})`
font-size: 2rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
const H6 = styled(Typography).attrs({
tag: 'h6'
})`
font-size: 1.6rem;
line-height: ${({ theme })=>theme.lineHeights[1]};
`;
/**
* Common handler for converting a node to a heading
*/ const handleConvertToHeading = (editor, level)=>{
baseHandleConvert(editor, {
type: 'heading',
level
});
};
const headingBlocks = {
'heading-one': {
renderElement: (props)=>/*#__PURE__*/ jsx(H1, {
...props.attributes,
children: props.children
}),
icon: HeadingOne,
label: {
id: 'components.Blocks.blocks.heading1',
defaultMessage: 'Heading 1'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 1),
matchNode: (node)=>node.type === 'heading' && node.level === 1,
isInBlocksSelector: true,
snippets: [
'#'
],
dragHandleTopMargin: '14px'
},
'heading-two': {
renderElement: (props)=>/*#__PURE__*/ jsx(H2, {
...props.attributes,
children: props.children
}),
icon: HeadingTwo,
label: {
id: 'components.Blocks.blocks.heading2',
defaultMessage: 'Heading 2'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 2),
matchNode: (node)=>node.type === 'heading' && node.level === 2,
isInBlocksSelector: true,
snippets: [
'##'
],
dragHandleTopMargin: '10px'
},
'heading-three': {
renderElement: (props)=>/*#__PURE__*/ jsx(H3, {
...props.attributes,
children: props.children
}),
icon: HeadingThree,
label: {
id: 'components.Blocks.blocks.heading3',
defaultMessage: 'Heading 3'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 3),
matchNode: (node)=>node.type === 'heading' && node.level === 3,
isInBlocksSelector: true,
snippets: [
'###'
],
dragHandleTopMargin: '7px'
},
'heading-four': {
renderElement: (props)=>/*#__PURE__*/ jsx(H4, {
...props.attributes,
children: props.children
}),
icon: HeadingFour,
label: {
id: 'components.Blocks.blocks.heading4',
defaultMessage: 'Heading 4'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 4),
matchNode: (node)=>node.type === 'heading' && node.level === 4,
isInBlocksSelector: true,
snippets: [
'####'
],
dragHandleTopMargin: '4px'
},
'heading-five': {
renderElement: (props)=>/*#__PURE__*/ jsx(H5, {
...props.attributes,
children: props.children
}),
icon: HeadingFive,
label: {
id: 'components.Blocks.blocks.heading5',
defaultMessage: 'Heading 5'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 5),
matchNode: (node)=>node.type === 'heading' && node.level === 5,
isInBlocksSelector: true,
snippets: [
'#####'
]
},
'heading-six': {
renderElement: (props)=>/*#__PURE__*/ jsx(H6, {
...props.attributes,
children: props.children
}),
icon: HeadingSix,
label: {
id: 'components.Blocks.blocks.heading6',
defaultMessage: 'Heading 6'
},
handleConvert: (editor)=>handleConvertToHeading(editor, 6),
matchNode: (node)=>node.type === 'heading' && node.level === 6,
isInBlocksSelector: true,
snippets: [
'######'
],
dragHandleTopMargin: '-2px'
}
};
export { headingBlocks };
//# sourceMappingURL=Heading.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"Heading.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Heading.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Typography, TypographyComponent } from '@strapi/design-system';\nimport {\n HeadingFive,\n HeadingFour,\n HeadingOne,\n HeadingSix,\n HeadingThree,\n HeadingTwo,\n} from '@strapi/icons';\nimport { Editor } from 'slate';\nimport { styled } from 'styled-components';\n\nimport { type BlocksStore } from '../BlocksEditor';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { type Block } from '../utils/types';\n\nconst H1 = styled<TypographyComponent<'h1'>>(Typography).attrs({ tag: 'h1' })`\n font-size: 4.2rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H2 = styled<TypographyComponent<'h2'>>(Typography).attrs({ tag: 'h2' })`\n font-size: 3.5rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H3 = styled<TypographyComponent<'h3'>>(Typography).attrs({ tag: 'h3' })`\n font-size: 2.9rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H4 = styled<TypographyComponent<'h4'>>(Typography).attrs({ tag: 'h4' })`\n font-size: 2.4rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H5 = styled<TypographyComponent<'h5'>>(Typography).attrs({ tag: 'h5' })`\n font-size: 2rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\nconst H6 = styled<TypographyComponent<'h6'>>(Typography).attrs({ tag: 'h6' })`\n font-size: 1.6rem;\n line-height: ${({ theme }) => theme.lineHeights[1]};\n`;\n\n/**\n * Common handler for converting a node to a heading\n */\nconst handleConvertToHeading = (editor: Editor, level: Block<'heading'>['level']) => {\n baseHandleConvert<Block<'heading'>>(editor, { type: 'heading', level });\n};\n\nconst headingBlocks: Pick<\n BlocksStore,\n 'heading-one' | 'heading-two' | 'heading-three' | 'heading-four' | 'heading-five' | 'heading-six'\n> = {\n 'heading-one': {\n renderElement: (props) => <H1 {...props.attributes}>{props.children}</H1>,\n icon: HeadingOne,\n label: {\n id: 'components.Blocks.blocks.heading1',\n defaultMessage: 'Heading 1',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 1),\n matchNode: (node) => node.type === 'heading' && node.level === 1,\n isInBlocksSelector: true,\n snippets: ['#'],\n dragHandleTopMargin: '14px',\n },\n 'heading-two': {\n renderElement: (props) => <H2 {...props.attributes}>{props.children}</H2>,\n icon: HeadingTwo,\n label: {\n id: 'components.Blocks.blocks.heading2',\n defaultMessage: 'Heading 2',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 2),\n matchNode: (node) => node.type === 'heading' && node.level === 2,\n isInBlocksSelector: true,\n snippets: ['##'],\n dragHandleTopMargin: '10px',\n },\n 'heading-three': {\n renderElement: (props) => <H3 {...props.attributes}>{props.children}</H3>,\n icon: HeadingThree,\n label: {\n id: 'components.Blocks.blocks.heading3',\n defaultMessage: 'Heading 3',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 3),\n matchNode: (node) => node.type === 'heading' && node.level === 3,\n isInBlocksSelector: true,\n snippets: ['###'],\n dragHandleTopMargin: '7px',\n },\n 'heading-four': {\n renderElement: (props) => <H4 {...props.attributes}>{props.children}</H4>,\n icon: HeadingFour,\n label: {\n id: 'components.Blocks.blocks.heading4',\n defaultMessage: 'Heading 4',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 4),\n matchNode: (node) => node.type === 'heading' && node.level === 4,\n isInBlocksSelector: true,\n snippets: ['####'],\n dragHandleTopMargin: '4px',\n },\n 'heading-five': {\n renderElement: (props) => <H5 {...props.attributes}>{props.children}</H5>,\n icon: HeadingFive,\n label: {\n id: 'components.Blocks.blocks.heading5',\n defaultMessage: 'Heading 5',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 5),\n matchNode: (node) => node.type === 'heading' && node.level === 5,\n isInBlocksSelector: true,\n snippets: ['#####'],\n },\n 'heading-six': {\n renderElement: (props) => <H6 {...props.attributes}>{props.children}</H6>,\n icon: HeadingSix,\n label: {\n id: 'components.Blocks.blocks.heading6',\n defaultMessage: 'Heading 6',\n },\n handleConvert: (editor) => handleConvertToHeading(editor, 6),\n matchNode: (node) => node.type === 'heading' && node.level === 6,\n isInBlocksSelector: true,\n snippets: ['######'],\n dragHandleTopMargin: '-2px',\n },\n};\n\nexport { headingBlocks };\n"],"names":["H1","styled","Typography","attrs","tag","theme","lineHeights","H2","H3","H4","H5","H6","handleConvertToHeading","editor","level","baseHandleConvert","type","headingBlocks","renderElement","prLine truncated
@@ -0,0 +1,230 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var strapiAdmin = require('@strapi/admin/strapi-admin');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var slate = require('slate');
var slateReact = require('slate-react');
var styledComponents = require('styled-components');
var urls = require('../../../../../../utils/urls.js');
var BlocksEditor = require('../BlocksEditor.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const ImageWrapper = styledComponents.styled(designSystem.Flex)`
transition-property: box-shadow;
transition-duration: 0.2s;
${(props)=>props.$isFocused && styledComponents.css`
box-shadow: ${props.theme.colors.primary600} 0px 0px 0px 3px;
`}
& > img {
height: auto;
// The max-height is decided with the design team, the 56px is the height of the toolbar
max-height: calc(512px - 56px);
max-width: 100%;
object-fit: contain;
}
`;
const IMAGE_SCHEMA_FIELDS = [
'name',
'alternativeText',
'url',
'caption',
'width',
'height',
'formats',
'hash',
'ext',
'mime',
'size',
'previewUrl',
'provider',
'provider_metadata',
'createdAt',
'updatedAt'
];
const pick = (object, keys)=>{
const entries = keys.map((key)=>[
key,
object[key]
]);
return Object.fromEntries(entries);
};
// Type guard to force TypeScript to narrow the type of the element in Blocks component
const isImage = (element)=>{
return element.type === 'image';
};
// Added a background color to the image wrapper to make it easier to recognize the image block
const Image = ({ attributes, children, element })=>{
const editorIsFocused = slateReact.useFocused();
const imageIsSelected = slateReact.useSelected();
if (!isImage(element)) {
return null;
}
const { url, alternativeText, width, height } = element.image;
return /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Box, {
...attributes,
children: [
children,
/*#__PURE__*/ jsxRuntime.jsx(ImageWrapper, {
background: "neutral100",
contentEditable: false,
justifyContent: "center",
$isFocused: editorIsFocused && imageIsSelected,
hasRadius: true,
children: /*#__PURE__*/ jsxRuntime.jsx("img", {
src: url,
alt: alternativeText,
width: width,
height: height
})
})
]
});
};
const ImageDialog = ()=>{
const [isOpen, setIsOpen] = React__namespace.useState(true);
const { editor } = BlocksEditor.useBlocksEditorContext('ImageDialog');
const components = strapiAdmin.useStrapiApp('ImageDialog', (state)=>state.components);
if (!components || !isOpen) return null;
const MediaLibraryDialog = components['media-library'];
const insertImages = (images)=>{
// If the selection is inside a list, split the list so that the modified block is outside of it
slate.Transforms.unwrapNodes(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list',
split: true
});
// Save the path of the node that is being replaced by an image to insert the images there later
// It's the closest full block node above the selection
const nodeEntryBeingReplaced = slate.Editor.above(editor, {
match (node) {
if (slate.Editor.isEditor(node)) return false;
const isInlineNode = [
'text',
'link'
].includes(node.type);
return !isInlineNode;
}
});
if (!nodeEntryBeingReplaced) return;
const [, pathToInsert] = nodeEntryBeingReplaced;
// Remove the previous node that is being replaced by an image
slate.Transforms.removeNodes(editor);
// Convert images to nodes and insert them
const nodesToInsert = images.map((image)=>{
const imageNode = {
type: 'image',
image,
children: [
{
type: 'text',
text: ''
}
]
};
return imageNode;
});
slate.Transforms.insertNodes(editor, nodesToInsert, {
at: pathToInsert
});
// Set the selection on the image since it was cleared by calling removeNodes
slate.Transforms.select(editor, pathToInsert);
};
const handleSelectAssets = (images)=>{
const formattedImages = images.map((image)=>{
// Create an object with imageSchema defined and exclude unnecessary props coming from media library config
const expectedImage = pick(image, IMAGE_SCHEMA_FIELDS);
const nodeImage = {
...expectedImage,
alternativeText: expectedImage.alternativeText || expectedImage.name,
url: urls.prefixFileUrlWithBackendUrl(image.url)
};
return nodeImage;
});
insertImages(formattedImages);
setIsOpen(false);
};
return /*#__PURE__*/ jsxRuntime.jsx(MediaLibraryDialog, {
allowedTypes: [
'images'
],
onClose: ()=>setIsOpen(false),
onSelectAssets: handleSelectAssets
});
};
const imageBlocks = {
image: {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(Image, {
...props
}),
icon: Icons.Image,
label: {
id: 'components.Blocks.blocks.image',
defaultMessage: 'Image'
},
matchNode: (node)=>node.type === 'image',
isInBlocksSelector: true,
handleBackspaceKey (editor) {
// Prevent issue where the image remains when it's the only block in the document
if (editor.children.length === 1) {
slate.Transforms.setNodes(editor, {
type: 'paragraph',
// @ts-expect-error we're only setting image as null so that Slate deletes it
image: null,
children: [
{
type: 'text',
text: ''
}
]
});
} else {
slate.Transforms.removeNodes(editor);
}
},
handleEnterKey (editor) {
slate.Transforms.insertNodes(editor, {
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
});
},
handleConvert: ()=>{
/**
* All the logic is managed inside the ImageDialog component,
* because the blocks are only created when the user selects images in the modal and submits
* and if he closes the modal, then no changes are made to the editor
*/ return ()=>/*#__PURE__*/ jsxRuntime.jsx(ImageDialog, {});
},
snippets: [
'!['
]
}
};
exports.imageBlocks = imageBlocks;
//# sourceMappingURL=Image.js.map
@@ -0,0 +1 @@
{"version":3,"file":"Image.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { useStrapiApp } from '@strapi/admin/strapi-admin';\nimport { Box, Flex, FlexComponent } from '@strapi/design-system';\nimport { Image as Picture } from '@strapi/icons';\nimport { type Element, Transforms, Editor } from 'slate';\nimport { useFocused, type RenderElementProps, useSelected } from 'slate-react';\nimport { styled, css } from 'styled-components';\n\nimport { prefixFileUrlWithBackendUrl } from '../../../../../../utils/urls';\nimport { useBlocksEditorContext, type BlocksStore } from '../BlocksEditor';\nimport { type Block } from '../utils/types';\n\nimport type { Schema } from '@strapi/types';\n\nconst ImageWrapper = styled<FlexComponent>(Flex)<{ $isFocused?: boolean }>`\n transition-property: box-shadow;\n transition-duration: 0.2s;\n ${(props) =>\n props.$isFocused &&\n css`\n box-shadow: ${props.theme.colors.primary600} 0px 0px 0px 3px;\n `}\n\n & > img {\n height: auto;\n // The max-height is decided with the design team, the 56px is the height of the toolbar\n max-height: calc(512px - 56px);\n max-width: 100%;\n object-fit: contain;\n }\n`;\n\nconst IMAGE_SCHEMA_FIELDS = [\n 'name',\n 'alternativeText',\n 'url',\n 'caption',\n 'width',\n 'height',\n 'formats',\n 'hash',\n 'ext',\n 'mime',\n 'size',\n 'previewUrl',\n 'provider',\n 'provider_metadata',\n 'createdAt',\n 'updatedAt',\n];\n\nconst pick = <T extends object, K extends keyof T>(object: T, keys: K[]): Pick<T, K> => {\n const entries = keys.map((key) => [key, object[key]]);\n return Object.fromEntries(entries);\n};\n\n// Type guard to force TypeScript to narrow the type of the element in Blocks component\nconst isImage = (element: Element): element is Block<'image'> => {\n return element.type === 'image';\n};\n\n// Added a background color to the image wrapper to make it easier to recognize the image block\nconst Image = ({ attributes, children, element }: RenderElementProps) => {\n const editorIsFocused = useFocused();\n const imageIsSelected = useSelected();\n\n if (!isImage(element)) {\n return null;\n }\n const { url, alternativeText, width, height } = element.image;\n\n return (\n <Box {...attributes}>\n {children}\n <ImageWrapper\n background=\"neutral100\"\n contentEditable={false}\n justifyContent=\"center\"\n $isFocused={editorIsFocused && imageIsSelected}\n hasRadius\n >\n <img src={url} alt={alternativeText} width={width} height={height} />\n </ImageWrapper>\n </Box>\n );\n};\n\nconst ImageDialog = () => {\n const [isOpen, setIsOpen] = React.useState(true);\n const { editor } = useBlocksEditorContext('ImageDialog');\n const components = useStrapiApp('ImageDialog', (state) => state.components);\n\n if (!components || !isOpen) return null;\n\n const MediaLibraryDialog = components['media-library'] as React.ComponentType<{\n allowedTypes: Schema.Attribute.MediaKind[];\n onClose: () => void;\n onSelectAssets: (_images: Schema.Attribute.MediaValue<true>) => void;\n }>;\n\n const insertImages = (images: Block<'image'>['image'][]) => {\n // If the selection is inside a list, split the list so that the modified block is outside of it\n Transforms.unwrapNodes(editor, {\n match: (node) => !Editor.isEditor(node) && node.type === 'list',\n split: true,\n });\n\n // Save the path of the node that is being replaced by an image to insert the images there later\n // It's the closest full block node above the selection\n const nodeEntryBeingReplaced = Editor.above(editor, {\n match(node) {\n if (Editor.isEditor(node)) return false;\n\n const isInlineNode = ['text', 'link'].includes(node.type);\n\n return !isInlineNode;\n },\n });\n\n if (!nodeEntryBeingReplaced) return;\n const [, pathToInsert] = nodeEntryBeingReplaced;\n\n // Remove the previous node that is being replaced by an image\n Transforms.removeNodes(editor);\n\n // Convert images to nodes and insert them\n const nodesToInsert = images.map((image) => {\n const imageNode: Block<'image'> = {\n type: 'image',\n image,\n children: [{ type: 'text', text: '' }],\n };\n return imageNode;\n });\n Transforms.insertNodes(editor, nodesToInsert, { at: pathToInsert });\n\n // Set the selection on the image since it was cleared by calling removeNodes\n Transforms.select(editor, pathToInsert);\n };\n\n const handleSelectAssets = (images: Schema.Attribute.MediaValue<true>) => {\n const formattedImages = images.map((image) => {\n // Create an object with imageSchema defined and exclude unnecessary props coming from media library config\n const expectedImage = pick(image, IMAGE_SCHEMA_FIELDS);\n\n const nodeLine truncated
@@ -0,0 +1,209 @@
import { jsx, jsxs } from 'react/jsx-runtime';
import * as React from 'react';
import { useStrapiApp } from '@strapi/admin/strapi-admin';
import { Flex, Box } from '@strapi/design-system';
import { Image as Image$1 } from '@strapi/icons';
import { Transforms, Editor } from 'slate';
import { useFocused, useSelected } from 'slate-react';
import { styled, css } from 'styled-components';
import { prefixFileUrlWithBackendUrl } from '../../../../../../utils/urls.mjs';
import { useBlocksEditorContext } from '../BlocksEditor.mjs';
const ImageWrapper = styled(Flex)`
transition-property: box-shadow;
transition-duration: 0.2s;
${(props)=>props.$isFocused && css`
box-shadow: ${props.theme.colors.primary600} 0px 0px 0px 3px;
`}
& > img {
height: auto;
// The max-height is decided with the design team, the 56px is the height of the toolbar
max-height: calc(512px - 56px);
max-width: 100%;
object-fit: contain;
}
`;
const IMAGE_SCHEMA_FIELDS = [
'name',
'alternativeText',
'url',
'caption',
'width',
'height',
'formats',
'hash',
'ext',
'mime',
'size',
'previewUrl',
'provider',
'provider_metadata',
'createdAt',
'updatedAt'
];
const pick = (object, keys)=>{
const entries = keys.map((key)=>[
key,
object[key]
]);
return Object.fromEntries(entries);
};
// Type guard to force TypeScript to narrow the type of the element in Blocks component
const isImage = (element)=>{
return element.type === 'image';
};
// Added a background color to the image wrapper to make it easier to recognize the image block
const Image = ({ attributes, children, element })=>{
const editorIsFocused = useFocused();
const imageIsSelected = useSelected();
if (!isImage(element)) {
return null;
}
const { url, alternativeText, width, height } = element.image;
return /*#__PURE__*/ jsxs(Box, {
...attributes,
children: [
children,
/*#__PURE__*/ jsx(ImageWrapper, {
background: "neutral100",
contentEditable: false,
justifyContent: "center",
$isFocused: editorIsFocused && imageIsSelected,
hasRadius: true,
children: /*#__PURE__*/ jsx("img", {
src: url,
alt: alternativeText,
width: width,
height: height
})
})
]
});
};
const ImageDialog = ()=>{
const [isOpen, setIsOpen] = React.useState(true);
const { editor } = useBlocksEditorContext('ImageDialog');
const components = useStrapiApp('ImageDialog', (state)=>state.components);
if (!components || !isOpen) return null;
const MediaLibraryDialog = components['media-library'];
const insertImages = (images)=>{
// If the selection is inside a list, split the list so that the modified block is outside of it
Transforms.unwrapNodes(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list',
split: true
});
// Save the path of the node that is being replaced by an image to insert the images there later
// It's the closest full block node above the selection
const nodeEntryBeingReplaced = Editor.above(editor, {
match (node) {
if (Editor.isEditor(node)) return false;
const isInlineNode = [
'text',
'link'
].includes(node.type);
return !isInlineNode;
}
});
if (!nodeEntryBeingReplaced) return;
const [, pathToInsert] = nodeEntryBeingReplaced;
// Remove the previous node that is being replaced by an image
Transforms.removeNodes(editor);
// Convert images to nodes and insert them
const nodesToInsert = images.map((image)=>{
const imageNode = {
type: 'image',
image,
children: [
{
type: 'text',
text: ''
}
]
};
return imageNode;
});
Transforms.insertNodes(editor, nodesToInsert, {
at: pathToInsert
});
// Set the selection on the image since it was cleared by calling removeNodes
Transforms.select(editor, pathToInsert);
};
const handleSelectAssets = (images)=>{
const formattedImages = images.map((image)=>{
// Create an object with imageSchema defined and exclude unnecessary props coming from media library config
const expectedImage = pick(image, IMAGE_SCHEMA_FIELDS);
const nodeImage = {
...expectedImage,
alternativeText: expectedImage.alternativeText || expectedImage.name,
url: prefixFileUrlWithBackendUrl(image.url)
};
return nodeImage;
});
insertImages(formattedImages);
setIsOpen(false);
};
return /*#__PURE__*/ jsx(MediaLibraryDialog, {
allowedTypes: [
'images'
],
onClose: ()=>setIsOpen(false),
onSelectAssets: handleSelectAssets
});
};
const imageBlocks = {
image: {
renderElement: (props)=>/*#__PURE__*/ jsx(Image, {
...props
}),
icon: Image$1,
label: {
id: 'components.Blocks.blocks.image',
defaultMessage: 'Image'
},
matchNode: (node)=>node.type === 'image',
isInBlocksSelector: true,
handleBackspaceKey (editor) {
// Prevent issue where the image remains when it's the only block in the document
if (editor.children.length === 1) {
Transforms.setNodes(editor, {
type: 'paragraph',
// @ts-expect-error we're only setting image as null so that Slate deletes it
image: null,
children: [
{
type: 'text',
text: ''
}
]
});
} else {
Transforms.removeNodes(editor);
}
},
handleEnterKey (editor) {
Transforms.insertNodes(editor, {
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
});
},
handleConvert: ()=>{
/**
* All the logic is managed inside the ImageDialog component,
* because the blocks are only created when the user selects images in the modal and submits
* and if he closes the modal, then no changes are made to the editor
*/ return ()=>/*#__PURE__*/ jsx(ImageDialog, {});
},
snippets: [
'!['
]
}
};
export { imageBlocks };
//# sourceMappingURL=Image.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"Image.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { useStrapiApp } from '@strapi/admin/strapi-admin';\nimport { Box, Flex, FlexComponent } from '@strapi/design-system';\nimport { Image as Picture } from '@strapi/icons';\nimport { type Element, Transforms, Editor } from 'slate';\nimport { useFocused, type RenderElementProps, useSelected } from 'slate-react';\nimport { styled, css } from 'styled-components';\n\nimport { prefixFileUrlWithBackendUrl } from '../../../../../../utils/urls';\nimport { useBlocksEditorContext, type BlocksStore } from '../BlocksEditor';\nimport { type Block } from '../utils/types';\n\nimport type { Schema } from '@strapi/types';\n\nconst ImageWrapper = styled<FlexComponent>(Flex)<{ $isFocused?: boolean }>`\n transition-property: box-shadow;\n transition-duration: 0.2s;\n ${(props) =>\n props.$isFocused &&\n css`\n box-shadow: ${props.theme.colors.primary600} 0px 0px 0px 3px;\n `}\n\n & > img {\n height: auto;\n // The max-height is decided with the design team, the 56px is the height of the toolbar\n max-height: calc(512px - 56px);\n max-width: 100%;\n object-fit: contain;\n }\n`;\n\nconst IMAGE_SCHEMA_FIELDS = [\n 'name',\n 'alternativeText',\n 'url',\n 'caption',\n 'width',\n 'height',\n 'formats',\n 'hash',\n 'ext',\n 'mime',\n 'size',\n 'previewUrl',\n 'provider',\n 'provider_metadata',\n 'createdAt',\n 'updatedAt',\n];\n\nconst pick = <T extends object, K extends keyof T>(object: T, keys: K[]): Pick<T, K> => {\n const entries = keys.map((key) => [key, object[key]]);\n return Object.fromEntries(entries);\n};\n\n// Type guard to force TypeScript to narrow the type of the element in Blocks component\nconst isImage = (element: Element): element is Block<'image'> => {\n return element.type === 'image';\n};\n\n// Added a background color to the image wrapper to make it easier to recognize the image block\nconst Image = ({ attributes, children, element }: RenderElementProps) => {\n const editorIsFocused = useFocused();\n const imageIsSelected = useSelected();\n\n if (!isImage(element)) {\n return null;\n }\n const { url, alternativeText, width, height } = element.image;\n\n return (\n <Box {...attributes}>\n {children}\n <ImageWrapper\n background=\"neutral100\"\n contentEditable={false}\n justifyContent=\"center\"\n $isFocused={editorIsFocused && imageIsSelected}\n hasRadius\n >\n <img src={url} alt={alternativeText} width={width} height={height} />\n </ImageWrapper>\n </Box>\n );\n};\n\nconst ImageDialog = () => {\n const [isOpen, setIsOpen] = React.useState(true);\n const { editor } = useBlocksEditorContext('ImageDialog');\n const components = useStrapiApp('ImageDialog', (state) => state.components);\n\n if (!components || !isOpen) return null;\n\n const MediaLibraryDialog = components['media-library'] as React.ComponentType<{\n allowedTypes: Schema.Attribute.MediaKind[];\n onClose: () => void;\n onSelectAssets: (_images: Schema.Attribute.MediaValue<true>) => void;\n }>;\n\n const insertImages = (images: Block<'image'>['image'][]) => {\n // If the selection is inside a list, split the list so that the modified block is outside of it\n Transforms.unwrapNodes(editor, {\n match: (node) => !Editor.isEditor(node) && node.type === 'list',\n split: true,\n });\n\n // Save the path of the node that is being replaced by an image to insert the images there later\n // It's the closest full block node above the selection\n const nodeEntryBeingReplaced = Editor.above(editor, {\n match(node) {\n if (Editor.isEditor(node)) return false;\n\n const isInlineNode = ['text', 'link'].includes(node.type);\n\n return !isInlineNode;\n },\n });\n\n if (!nodeEntryBeingReplaced) return;\n const [, pathToInsert] = nodeEntryBeingReplaced;\n\n // Remove the previous node that is being replaced by an image\n Transforms.removeNodes(editor);\n\n // Convert images to nodes and insert them\n const nodesToInsert = images.map((image) => {\n const imageNode: Block<'image'> = {\n type: 'image',\n image,\n children: [{ type: 'text', text: '' }],\n };\n return imageNode;\n });\n Transforms.insertNodes(editor, nodesToInsert, { at: pathToInsert });\n\n // Set the selection on the image since it was cleared by calling removeNodes\n Transforms.select(editor, pathToInsert);\n };\n\n const handleSelectAssets = (images: Schema.Attribute.MediaValue<true>) => {\n const formattedImages = images.map((image) => {\n // Create an object with imageSchema defined and exclude unnecessary props coming from media library config\n const expectedImage = pick(image, IMAGE_SCHEMA_FIELDS);\n\n const nodLine truncated
@@ -0,0 +1,232 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var designSystem = require('@strapi/design-system');
var reactIntl = require('react-intl');
var slate = require('slate');
var slateReact = require('slate-react');
var styledComponents = require('styled-components');
var BlocksEditor = require('../BlocksEditor.js');
var links = require('../utils/links.js');
var types = require('../utils/types.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const StyledLink = styledComponents.styled(designSystem.Box)`
text-decoration: none;
`;
const RemoveButton = styledComponents.styled(designSystem.Button)`
visibility: ${(props)=>props.$visible ? 'visible' : 'hidden'};
`;
const LinkContent = /*#__PURE__*/ React__namespace.forwardRef(({ link, children, attributes }, forwardedRef)=>{
const { formatMessage } = reactIntl.useIntl();
const { editor } = BlocksEditor.useBlocksEditorContext('Link');
const path = slateReact.ReactEditor.findPath(editor, link);
const [popoverOpen, setPopoverOpen] = React__namespace.useState(editor.lastInsertedLinkPath ? slate.Path.equals(path, editor.lastInsertedLinkPath) : false);
const elementText = link.children.map((child)=>child.text).join('');
const [linkText, setLinkText] = React__namespace.useState(elementText);
const [linkUrl, setLinkUrl] = React__namespace.useState(link.url);
const linkInputRef = React__namespace.useRef(null);
const isLastInsertedLink = editor.lastInsertedLinkPath ? !slate.Path.equals(path, editor.lastInsertedLinkPath) : true;
const [isSaveDisabled, setIsSaveDisabled] = React__namespace.useState(false);
const onLinkChange = (e)=>{
setIsSaveDisabled(false);
setLinkUrl(e.target.value);
try {
// eslint-disable-next-line no-new
new URL(e.target.value?.startsWith('/') ? `https://strapi.io${e.target.value}` : e.target.value);
} catch (error) {
setIsSaveDisabled(true);
}
};
const handleSave = (e)=>{
e.stopPropagation();
// If the selection is collapsed, we select the parent node because we want all the link to be replaced)
if (editor.selection && slate.Range.isCollapsed(editor.selection)) {
const [, parentPath] = slate.Editor.parent(editor, editor.selection.focus?.path);
slate.Transforms.select(editor, parentPath);
}
links.editLink(editor, {
url: linkUrl,
text: linkText
});
setPopoverOpen(false);
editor.lastInsertedLinkPath = null;
slateReact.ReactEditor.focus(editor);
};
const handleClose = ()=>{
if (link.url === '') {
links.removeLink(editor);
}
setPopoverOpen(false);
slateReact.ReactEditor.focus(editor);
};
React__namespace.useEffect(()=>{
// Focus on the link input element when the popover opens
if (popoverOpen) linkInputRef.current?.focus();
}, [
popoverOpen
]);
const inputNotDirty = !linkText || !linkUrl || link.url && link.url === linkUrl && elementText && elementText === linkText;
return /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Popover.Root, {
open: popoverOpen,
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Popover.Trigger, {
children: /*#__PURE__*/ jsxRuntime.jsx(StyledLink, {
...attributes,
ref: forwardedRef,
tag: "a",
href: link.url,
onClick: ()=>setPopoverOpen(true),
color: "primary600",
children: children
})
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Popover.Content, {
onPointerDownOutside: handleClose,
children: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
padding: 4,
direction: "column",
gap: 4,
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Root, {
width: "368px",
children: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
direction: "column",
gap: 1,
alignItems: "stretch",
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Label, {
children: formatMessage({
id: 'components.Blocks.popover.text',
defaultMessage: 'Text'
})
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Input, {
name: "text",
placeholder: formatMessage({
id: 'components.Blocks.popover.text.placeholder',
defaultMessage: 'Enter link text'
}),
value: linkText,
onChange: (e)=>{
setLinkText(e.target.value);
}
})
]
})
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Root, {
width: "368px",
children: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
direction: "column",
gap: 1,
alignItems: "stretch",
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Label, {
children: formatMessage({
id: 'components.Blocks.popover.link',
defaultMessage: 'Link'
})
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Input, {
ref: linkInputRef,
name: "url",
placeholder: formatMessage({
id: 'components.Blocks.popover.link.placeholder',
defaultMessage: 'Paste link'
}),
value: linkUrl,
onChange: onLinkChange
})
]
})
}),
/*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
justifyContent: "space-between",
width: "100%",
children: [
/*#__PURE__*/ jsxRuntime.jsx(RemoveButton, {
variant: "danger-light",
onClick: ()=>links.removeLink(editor),
$visible: isLastInsertedLink,
children: formatMessage({
id: 'components.Blocks.popover.remove',
defaultMessage: 'Remove'
})
}),
/*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
gap: 2,
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
variant: "tertiary",
onClick: handleClose,
children: formatMessage({
id: 'global.cancel',
defaultMessage: 'Cancel'
})
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
disabled: Boolean(inputNotDirty) || isSaveDisabled,
onClick: handleSave,
children: formatMessage({
id: 'global.save',
defaultMessage: 'Save'
})
})
]
})
]
})
]
})
})
]
});
});
const Link = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
if (!types.isLinkNode(props.element)) {
return null;
}
// LinkContent uses React hooks that rely on props.element being a link. If the type guard above
// doesn't pass, those hooks would be called conditionnally, which is not allowed.
// Hence the need for a separate component.
return /*#__PURE__*/ jsxRuntime.jsx(LinkContent, {
...props,
link: props.element,
ref: forwardedRef
});
});
const linkBlocks = {
link: {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(Link, {
element: props.element,
attributes: props.attributes,
children: props.children
}),
// No handleConvert here, links are created via the link button in the toolbar
matchNode: (node)=>node.type === 'link',
isInBlocksSelector: false
}
};
exports.linkBlocks = linkBlocks;
//# sourceMappingURL=Link.js.map
@@ -0,0 +1 @@
{"version":3,"file":"Link.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Box, Button, Field, Flex, Popover, useComposedRefs } from '@strapi/design-system';\nimport { useIntl } from 'react-intl';\nimport { Editor, Path, Range, Transforms } from 'slate';\nimport { type RenderElementProps, ReactEditor } from 'slate-react';\nimport { styled } from 'styled-components';\n\nimport { type BlocksStore, useBlocksEditorContext } from '../BlocksEditor';\nimport { editLink, removeLink } from '../utils/links';\nimport { isLinkNode, type Block } from '../utils/types';\n\nconst StyledLink = styled(Box)`\n text-decoration: none;\n`;\n\nconst RemoveButton = styled(Button)<{ $visible: boolean }>`\n visibility: ${(props) => (props.$visible ? 'visible' : 'hidden')};\n`;\n\ninterface LinkContentProps extends RenderElementProps {\n link: Block<'link'>;\n}\n\nconst LinkContent = React.forwardRef<HTMLAnchorElement, LinkContentProps>(\n ({ link, children, attributes }, forwardedRef) => {\n const { formatMessage } = useIntl();\n const { editor } = useBlocksEditorContext('Link');\n const path = ReactEditor.findPath(editor, link);\n const [popoverOpen, setPopoverOpen] = React.useState(\n editor.lastInsertedLinkPath ? Path.equals(path, editor.lastInsertedLinkPath) : false\n );\n const elementText = link.children.map((child) => child.text).join('');\n const [linkText, setLinkText] = React.useState(elementText);\n const [linkUrl, setLinkUrl] = React.useState(link.url);\n const linkInputRef = React.useRef<HTMLInputElement>(null);\n const isLastInsertedLink = editor.lastInsertedLinkPath\n ? !Path.equals(path, editor.lastInsertedLinkPath)\n : true;\n const [isSaveDisabled, setIsSaveDisabled] = React.useState(false);\n\n const onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setIsSaveDisabled(false);\n setLinkUrl(e.target.value);\n\n try {\n // eslint-disable-next-line no-new\n new URL(\n e.target.value?.startsWith('/') ? `https://strapi.io${e.target.value}` : e.target.value\n );\n } catch (error) {\n setIsSaveDisabled(true);\n }\n };\n\n const handleSave: React.FormEventHandler = (e) => {\n e.stopPropagation();\n\n // If the selection is collapsed, we select the parent node because we want all the link to be replaced)\n if (editor.selection && Range.isCollapsed(editor.selection)) {\n const [, parentPath] = Editor.parent(editor, editor.selection.focus?.path);\n Transforms.select(editor, parentPath);\n }\n\n editLink(editor, { url: linkUrl, text: linkText });\n setPopoverOpen(false);\n editor.lastInsertedLinkPath = null;\n ReactEditor.focus(editor);\n };\n\n const handleClose = () => {\n if (link.url === '') {\n removeLink(editor);\n }\n\n setPopoverOpen(false);\n ReactEditor.focus(editor);\n };\n\n React.useEffect(() => {\n // Focus on the link input element when the popover opens\n if (popoverOpen) linkInputRef.current?.focus();\n }, [popoverOpen]);\n\n const inputNotDirty =\n !linkText ||\n !linkUrl ||\n (link.url && link.url === linkUrl && elementText && elementText === linkText);\n\n return (\n <Popover.Root open={popoverOpen}>\n <Popover.Trigger>\n <StyledLink\n {...attributes}\n ref={forwardedRef}\n tag=\"a\"\n href={link.url}\n onClick={() => setPopoverOpen(true)}\n color=\"primary600\"\n >\n {children}\n </StyledLink>\n </Popover.Trigger>\n <Popover.Content onPointerDownOutside={handleClose}>\n <Flex padding={4} direction=\"column\" gap={4}>\n <Field.Root width=\"368px\">\n <Flex direction=\"column\" gap={1} alignItems=\"stretch\">\n <Field.Label>\n {formatMessage({\n id: 'components.Blocks.popover.text',\n defaultMessage: 'Text',\n })}\n </Field.Label>\n <Field.Input\n name=\"text\"\n placeholder={formatMessage({\n id: 'components.Blocks.popover.text.placeholder',\n defaultMessage: 'Enter link text',\n })}\n value={linkText}\n onChange={(e) => {\n setLinkText(e.target.value);\n }}\n />\n </Flex>\n </Field.Root>\n <Field.Root width=\"368px\">\n <Flex direction=\"column\" gap={1} alignItems=\"stretch\">\n <Field.Label>\n {formatMessage({\n id: 'components.Blocks.popover.link',\n defauLine truncated
@@ -0,0 +1,211 @@
import { jsx, jsxs } from 'react/jsx-runtime';
import * as React from 'react';
import { Box, Button, Popover, Flex, Field } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { Path, Range, Editor, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import { styled } from 'styled-components';
import { useBlocksEditorContext } from '../BlocksEditor.mjs';
import { removeLink, editLink } from '../utils/links.mjs';
import { isLinkNode } from '../utils/types.mjs';
const StyledLink = styled(Box)`
text-decoration: none;
`;
const RemoveButton = styled(Button)`
visibility: ${(props)=>props.$visible ? 'visible' : 'hidden'};
`;
const LinkContent = /*#__PURE__*/ React.forwardRef(({ link, children, attributes }, forwardedRef)=>{
const { formatMessage } = useIntl();
const { editor } = useBlocksEditorContext('Link');
const path = ReactEditor.findPath(editor, link);
const [popoverOpen, setPopoverOpen] = React.useState(editor.lastInsertedLinkPath ? Path.equals(path, editor.lastInsertedLinkPath) : false);
const elementText = link.children.map((child)=>child.text).join('');
const [linkText, setLinkText] = React.useState(elementText);
const [linkUrl, setLinkUrl] = React.useState(link.url);
const linkInputRef = React.useRef(null);
const isLastInsertedLink = editor.lastInsertedLinkPath ? !Path.equals(path, editor.lastInsertedLinkPath) : true;
const [isSaveDisabled, setIsSaveDisabled] = React.useState(false);
const onLinkChange = (e)=>{
setIsSaveDisabled(false);
setLinkUrl(e.target.value);
try {
// eslint-disable-next-line no-new
new URL(e.target.value?.startsWith('/') ? `https://strapi.io${e.target.value}` : e.target.value);
} catch (error) {
setIsSaveDisabled(true);
}
};
const handleSave = (e)=>{
e.stopPropagation();
// If the selection is collapsed, we select the parent node because we want all the link to be replaced)
if (editor.selection && Range.isCollapsed(editor.selection)) {
const [, parentPath] = Editor.parent(editor, editor.selection.focus?.path);
Transforms.select(editor, parentPath);
}
editLink(editor, {
url: linkUrl,
text: linkText
});
setPopoverOpen(false);
editor.lastInsertedLinkPath = null;
ReactEditor.focus(editor);
};
const handleClose = ()=>{
if (link.url === '') {
removeLink(editor);
}
setPopoverOpen(false);
ReactEditor.focus(editor);
};
React.useEffect(()=>{
// Focus on the link input element when the popover opens
if (popoverOpen) linkInputRef.current?.focus();
}, [
popoverOpen
]);
const inputNotDirty = !linkText || !linkUrl || link.url && link.url === linkUrl && elementText && elementText === linkText;
return /*#__PURE__*/ jsxs(Popover.Root, {
open: popoverOpen,
children: [
/*#__PURE__*/ jsx(Popover.Trigger, {
children: /*#__PURE__*/ jsx(StyledLink, {
...attributes,
ref: forwardedRef,
tag: "a",
href: link.url,
onClick: ()=>setPopoverOpen(true),
color: "primary600",
children: children
})
}),
/*#__PURE__*/ jsx(Popover.Content, {
onPointerDownOutside: handleClose,
children: /*#__PURE__*/ jsxs(Flex, {
padding: 4,
direction: "column",
gap: 4,
children: [
/*#__PURE__*/ jsx(Field.Root, {
width: "368px",
children: /*#__PURE__*/ jsxs(Flex, {
direction: "column",
gap: 1,
alignItems: "stretch",
children: [
/*#__PURE__*/ jsx(Field.Label, {
children: formatMessage({
id: 'components.Blocks.popover.text',
defaultMessage: 'Text'
})
}),
/*#__PURE__*/ jsx(Field.Input, {
name: "text",
placeholder: formatMessage({
id: 'components.Blocks.popover.text.placeholder',
defaultMessage: 'Enter link text'
}),
value: linkText,
onChange: (e)=>{
setLinkText(e.target.value);
}
})
]
})
}),
/*#__PURE__*/ jsx(Field.Root, {
width: "368px",
children: /*#__PURE__*/ jsxs(Flex, {
direction: "column",
gap: 1,
alignItems: "stretch",
children: [
/*#__PURE__*/ jsx(Field.Label, {
children: formatMessage({
id: 'components.Blocks.popover.link',
defaultMessage: 'Link'
})
}),
/*#__PURE__*/ jsx(Field.Input, {
ref: linkInputRef,
name: "url",
placeholder: formatMessage({
id: 'components.Blocks.popover.link.placeholder',
defaultMessage: 'Paste link'
}),
value: linkUrl,
onChange: onLinkChange
})
]
})
}),
/*#__PURE__*/ jsxs(Flex, {
justifyContent: "space-between",
width: "100%",
children: [
/*#__PURE__*/ jsx(RemoveButton, {
variant: "danger-light",
onClick: ()=>removeLink(editor),
$visible: isLastInsertedLink,
children: formatMessage({
id: 'components.Blocks.popover.remove',
defaultMessage: 'Remove'
})
}),
/*#__PURE__*/ jsxs(Flex, {
gap: 2,
children: [
/*#__PURE__*/ jsx(Button, {
variant: "tertiary",
onClick: handleClose,
children: formatMessage({
id: 'global.cancel',
defaultMessage: 'Cancel'
})
}),
/*#__PURE__*/ jsx(Button, {
disabled: Boolean(inputNotDirty) || isSaveDisabled,
onClick: handleSave,
children: formatMessage({
id: 'global.save',
defaultMessage: 'Save'
})
})
]
})
]
})
]
})
})
]
});
});
const Link = /*#__PURE__*/ React.forwardRef((props, forwardedRef)=>{
if (!isLinkNode(props.element)) {
return null;
}
// LinkContent uses React hooks that rely on props.element being a link. If the type guard above
// doesn't pass, those hooks would be called conditionnally, which is not allowed.
// Hence the need for a separate component.
return /*#__PURE__*/ jsx(LinkContent, {
...props,
link: props.element,
ref: forwardedRef
});
});
const linkBlocks = {
link: {
renderElement: (props)=>/*#__PURE__*/ jsx(Link, {
element: props.element,
attributes: props.attributes,
children: props.children
}),
// No handleConvert here, links are created via the link button in the toolbar
matchNode: (node)=>node.type === 'link',
isInBlocksSelector: false
}
};
export { linkBlocks };
//# sourceMappingURL=Link.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"Link.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Box, Button, Field, Flex, Popover, useComposedRefs } from '@strapi/design-system';\nimport { useIntl } from 'react-intl';\nimport { Editor, Path, Range, Transforms } from 'slate';\nimport { type RenderElementProps, ReactEditor } from 'slate-react';\nimport { styled } from 'styled-components';\n\nimport { type BlocksStore, useBlocksEditorContext } from '../BlocksEditor';\nimport { editLink, removeLink } from '../utils/links';\nimport { isLinkNode, type Block } from '../utils/types';\n\nconst StyledLink = styled(Box)`\n text-decoration: none;\n`;\n\nconst RemoveButton = styled(Button)<{ $visible: boolean }>`\n visibility: ${(props) => (props.$visible ? 'visible' : 'hidden')};\n`;\n\ninterface LinkContentProps extends RenderElementProps {\n link: Block<'link'>;\n}\n\nconst LinkContent = React.forwardRef<HTMLAnchorElement, LinkContentProps>(\n ({ link, children, attributes }, forwardedRef) => {\n const { formatMessage } = useIntl();\n const { editor } = useBlocksEditorContext('Link');\n const path = ReactEditor.findPath(editor, link);\n const [popoverOpen, setPopoverOpen] = React.useState(\n editor.lastInsertedLinkPath ? Path.equals(path, editor.lastInsertedLinkPath) : false\n );\n const elementText = link.children.map((child) => child.text).join('');\n const [linkText, setLinkText] = React.useState(elementText);\n const [linkUrl, setLinkUrl] = React.useState(link.url);\n const linkInputRef = React.useRef<HTMLInputElement>(null);\n const isLastInsertedLink = editor.lastInsertedLinkPath\n ? !Path.equals(path, editor.lastInsertedLinkPath)\n : true;\n const [isSaveDisabled, setIsSaveDisabled] = React.useState(false);\n\n const onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setIsSaveDisabled(false);\n setLinkUrl(e.target.value);\n\n try {\n // eslint-disable-next-line no-new\n new URL(\n e.target.value?.startsWith('/') ? `https://strapi.io${e.target.value}` : e.target.value\n );\n } catch (error) {\n setIsSaveDisabled(true);\n }\n };\n\n const handleSave: React.FormEventHandler = (e) => {\n e.stopPropagation();\n\n // If the selection is collapsed, we select the parent node because we want all the link to be replaced)\n if (editor.selection && Range.isCollapsed(editor.selection)) {\n const [, parentPath] = Editor.parent(editor, editor.selection.focus?.path);\n Transforms.select(editor, parentPath);\n }\n\n editLink(editor, { url: linkUrl, text: linkText });\n setPopoverOpen(false);\n editor.lastInsertedLinkPath = null;\n ReactEditor.focus(editor);\n };\n\n const handleClose = () => {\n if (link.url === '') {\n removeLink(editor);\n }\n\n setPopoverOpen(false);\n ReactEditor.focus(editor);\n };\n\n React.useEffect(() => {\n // Focus on the link input element when the popover opens\n if (popoverOpen) linkInputRef.current?.focus();\n }, [popoverOpen]);\n\n const inputNotDirty =\n !linkText ||\n !linkUrl ||\n (link.url && link.url === linkUrl && elementText && elementText === linkText);\n\n return (\n <Popover.Root open={popoverOpen}>\n <Popover.Trigger>\n <StyledLink\n {...attributes}\n ref={forwardedRef}\n tag=\"a\"\n href={link.url}\n onClick={() => setPopoverOpen(true)}\n color=\"primary600\"\n >\n {children}\n </StyledLink>\n </Popover.Trigger>\n <Popover.Content onPointerDownOutside={handleClose}>\n <Flex padding={4} direction=\"column\" gap={4}>\n <Field.Root width=\"368px\">\n <Flex direction=\"column\" gap={1} alignItems=\"stretch\">\n <Field.Label>\n {formatMessage({\n id: 'components.Blocks.popover.text',\n defaultMessage: 'Text',\n })}\n </Field.Label>\n <Field.Input\n name=\"text\"\n placeholder={formatMessage({\n id: 'components.Blocks.popover.text.placeholder',\n defaultMessage: 'Enter link text',\n })}\n value={linkText}\n onChange={(e) => {\n setLinkText(e.target.value);\n }}\n />\n </Flex>\n </Field.Root>\n <Field.Root width=\"368px\">\n <Flex direction=\"column\" gap={1} alignItems=\"stretch\">\n <Field.Label>\n {formatMessage({\n id: 'components.Blocks.popover.link',\n defaLine truncated
@@ -0,0 +1,360 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
require('react');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var slate = require('slate');
var slateReact = require('slate-react');
var styledComponents = require('styled-components');
var conversions = require('../utils/conversions.js');
var types = require('../utils/types.js');
const listStyle = styledComponents.css`
display: flex;
flex-direction: column;
gap: ${({ theme })=>theme.spaces[2]};
margin-inline-start: ${({ theme })=>theme.spaces[0]};
margin-inline-end: ${({ theme })=>theme.spaces[0]};
padding-inline-start: ${({ theme })=>theme.spaces[2]};
ol,
ul {
margin-block-start: ${({ theme })=>theme.spaces[0]};
margin-block-end: ${({ theme })=>theme.spaces[0]};
}
li {
margin-inline-start: ${({ theme })=>theme.spaces[3]};
}
`;
const Orderedlist = styledComponents.styled.ol`
list-style-type: ${(props)=>props.$listStyleType};
${listStyle}
`;
const Unorderedlist = styledComponents.styled.ul`
list-style-type: ${(props)=>props.$listStyleType};
${listStyle}
`;
const orderedStyles = [
'decimal',
'lower-alpha',
'upper-roman'
];
const unorderedStyles = [
'disc',
'circle',
'square'
];
const List = ({ attributes, children, element })=>{
if (!types.isListNode(element)) {
return null;
}
// Decide the subsequent style by referencing the given styles according to the format,
// allowing for infinite nested lists
const listStyles = element.format === 'ordered' ? orderedStyles : unorderedStyles;
const nextIndex = (element.indentLevel || 0) % listStyles.length;
const listStyleType = listStyles[nextIndex];
if (element.format === 'ordered') {
return /*#__PURE__*/ jsxRuntime.jsx(Orderedlist, {
$listStyleType: listStyleType,
...attributes,
children: children
});
}
return /*#__PURE__*/ jsxRuntime.jsx(Unorderedlist, {
$listStyleType: listStyleType,
...attributes,
children: children
});
};
const replaceListWithEmptyBlock = (editor, currentListPath)=>{
// Delete the empty list
slate.Transforms.removeNodes(editor, {
at: currentListPath
});
if (currentListPath[0] === 0) {
// If the list was the only (or first) block element then insert empty paragraph as editor needs default value
slate.Transforms.insertNodes(editor, {
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
}, {
at: currentListPath
});
slate.Transforms.select(editor, currentListPath);
}
};
const isText = (node)=>{
return slate.Node.isNode(node) && !slate.Editor.isEditor(node) && node.type === 'text';
};
/**
* Common handler for the backspace event on ordered and unordered lists
*/ const handleBackspaceKeyOnList = (editor, event)=>{
if (!editor.selection) return;
const [currentListItem, currentListItemPath] = slate.Editor.parent(editor, editor.selection.anchor);
const [currentList, currentListPath] = slate.Editor.parent(editor, currentListItemPath);
const isListEmpty = currentList.children.length === 1 && isText(currentListItem.children[0]) && currentListItem.children[0].text === '';
const isListItemEmpty = currentListItem.children.length === 1 && isText(currentListItem.children[0]) && currentListItem.children[0].text === '';
const isFocusAtTheBeginningOfAChild = editor.selection.focus.offset === 0 && editor.selection.focus.path.at(-2) === 0;
if (isListEmpty) {
const parentListEntry = slate.Editor.above(editor, {
at: currentListPath,
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list'
});
if (!parentListEntry) {
event.preventDefault();
replaceListWithEmptyBlock(editor, currentListPath);
}
} else if (isFocusAtTheBeginningOfAChild) {
// If the focus is at the beginning of a child node we need to replace it with a paragraph
slate.Transforms.liftNodes(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list-item'
});
slate.Transforms.setNodes(editor, {
type: 'paragraph'
});
} else if (isListItemEmpty) {
const previousEntry = slate.Editor.previous(editor, {
at: currentListItemPath
});
const nextEntry = slate.Editor.next(editor, {
at: currentListItemPath
});
if (previousEntry && nextEntry) {
// If previous and next nodes are lists or list-items, delete empty list item
event.preventDefault();
slate.Transforms.removeNodes(editor, {
at: currentListItemPath
});
// If previous and next nodes are lists with same format and indent Levels, then merge the nodes
const [previousList] = previousEntry;
const [nextList] = nextEntry;
if (!slate.Editor.isEditor(previousList) && !isText(previousList) && types.isListNode(previousList) && !slate.Editor.isEditor(nextList) && !isText(nextList) && types.isListNode(nextList)) {
if (previousList.type === 'list' && nextList.type === 'list' && previousList.format === nextList.format && previousList.indentLevel === nextList.indentLevel) {
slate.Transforms.mergeNodes(editor, {
at: currentListItemPath
});
}
}
}
}
};
/**
* Common handler for the enter key on ordered and unordered lists
*/ const handleEnterKeyOnList = (editor)=>{
const currentListItemEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list-item'
});
if (!currentListItemEntry || !editor.selection) {
return;
}
const [currentListItem, currentListItemPath] = currentListItemEntry;
const [currentList, currentListPath] = slate.Editor.parent(editor, currentListItemPath);
const isListEmpty = currentList.children.length === 1 && isText(currentListItem.children[0]) && currentListItem.children[0].text === '';
const isListItemEmpty = currentListItem.children.length === 1 && isText(currentListItem.children[0]) && currentListItem.children[0].text === '';
const isFocusAtTheBeginningOfAChild = editor.selection.focus.offset === 0 && editor.selection.focus.path.at(-1) === 0;
if (isListEmpty) {
replaceListWithEmptyBlock(editor, currentListPath);
} else if (isFocusAtTheBeginningOfAChild && !isListItemEmpty) {
// If the focus is at the beginning of a child node, shift below the list item and create a new list-item
const currentNode = slate.Editor.above(editor, {
at: editor.selection.anchor
});
slate.Transforms.insertNodes(editor, {
type: 'list-item',
children: [
{
type: 'text',
text: ''
}
]
});
if (currentNode) {
const path = currentNode[1];
const updatedPath = [
...path.slice(0, -1),
path[path.length - 1] + 1
];
slate.Transforms.select(editor, {
anchor: {
path: updatedPath.concat(0),
offset: 0
},
focus: {
path: updatedPath.concat(0),
offset: 0
}
});
}
} else if (isListItemEmpty) {
// Check if there is a list above the current list and shift list-item under it
if (!slate.Editor.isEditor(currentList) && types.isListNode(currentList) && currentList?.indentLevel && currentList.indentLevel > 0) {
const previousIndentLevel = currentList.indentLevel - 1;
const parentListNodeEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list' && (node.indentLevel || 0) === previousIndentLevel
});
if (parentListNodeEntry) {
// Get the parent list path and add 1 to it to exit from the current list
const modifiedPath = currentListItemPath.slice(0, -1);
if (modifiedPath.length > 0) {
modifiedPath[modifiedPath.length - 1] += 1;
}
// Shift list-item under parent list
slate.Transforms.moveNodes(editor, {
at: currentListItemPath,
to: modifiedPath
});
return;
}
}
// Otherwise delete the empty list item and create a new paragraph below the parent list
slate.Transforms.removeNodes(editor, {
at: currentListItemPath
});
const createdParagraphPath = slate.Path.next(currentListPath);
slate.Transforms.insertNodes(editor, {
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
}, {
at: createdParagraphPath
});
// Move the selection to the newly created paragraph
slate.Transforms.select(editor, createdParagraphPath);
} else {
// Check if the cursor is at the end of the list item
const isNodeEnd = slate.Editor.isEnd(editor, editor.selection.anchor, currentListItemPath);
if (isNodeEnd) {
// If there was nothing after the cursor, create a fresh new list item,
// in order to avoid carrying over the modifiers from the previous list item
slate.Transforms.insertNodes(editor, {
type: 'list-item',
children: [
{
type: 'text',
text: ''
}
]
});
} else {
// If there is something after the cursor, split the current list item,
// so that we keep the content and the modifiers
slate.Transforms.splitNodes(editor);
}
}
};
/**
* Common handler for converting a node to a list
*/ const handleConvertToList = (editor, format)=>{
const convertedPath = conversions.baseHandleConvert(editor, {
type: 'list-item'
});
if (!convertedPath) return;
slate.Transforms.wrapNodes(editor, {
type: 'list',
format,
children: []
}, {
at: convertedPath
});
};
/**
* Common handler for the tab key on ordered and unordered lists
*/ const handleTabOnList = (editor)=>{
const currentListItemEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list-item'
});
if (!currentListItemEntry || !editor.selection) {
return;
}
const [currentListItem, currentListItemPath] = currentListItemEntry;
const [currentList] = slate.Editor.parent(editor, currentListItemPath);
// Skip tabbing if list-item is the first item in the list
if (currentListItem === currentList.children[0]) return;
const currentListItemIndex = currentList.children.findIndex((item)=>item === currentListItem);
const previousNode = currentList.children[currentListItemIndex - 1];
// If previous node is a list block then move the list-item under it
if (previousNode.type === 'list') {
const nodePath = slateReact.ReactEditor.findPath(editor, previousNode);
const insertAtPath = previousNode.children.length;
slate.Transforms.moveNodes(editor, {
at: currentListItemPath,
to: nodePath.concat(insertAtPath)
});
return;
}
if (!slate.Editor.isEditor(currentList) && types.isListNode(currentList)) {
// Wrap list-item with list block on tab
slate.Transforms.wrapNodes(editor, {
type: 'list',
format: currentList.format,
indentLevel: (currentList.indentLevel || 0) + 1,
children: []
});
}
};
const listBlocks = {
'list-ordered': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(List, {
...props
}),
label: {
id: 'components.Blocks.blocks.orderedList',
defaultMessage: 'Numbered list'
},
icon: Icons.NumberList,
matchNode: (node)=>node.type === 'list' && node.format === 'ordered',
isInBlocksSelector: true,
handleConvert: (editor)=>handleConvertToList(editor, 'ordered'),
handleEnterKey: handleEnterKeyOnList,
handleBackspaceKey: handleBackspaceKeyOnList,
handleTab: handleTabOnList,
snippets: [
'1.'
]
},
'list-unordered': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(List, {
...props
}),
label: {
id: 'components.Blocks.blocks.unorderedList',
defaultMessage: 'Bulleted list'
},
icon: Icons.BulletList,
matchNode: (node)=>node.type === 'list' && node.format === 'unordered',
isInBlocksSelector: true,
handleConvert: (editor)=>handleConvertToList(editor, 'unordered'),
handleEnterKey: handleEnterKeyOnList,
handleBackspaceKey: handleBackspaceKeyOnList,
handleTab: handleTabOnList,
snippets: [
'-',
'*',
'+'
]
},
'list-item': {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
tag: "li",
...props.attributes,
children: props.children
}),
// No handleConvert, list items are created when converting to the parent list
matchNode: (node)=>node.type === 'list-item',
isInBlocksSelector: false,
dragHandleTopMargin: '-2px'
}
};
exports.listBlocks = listBlocks;
//# sourceMappingURL=List.js.map
@@ -0,0 +1 @@
{"version":3,"file":"List.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Typography } from '@strapi/design-system';\nimport { BulletList, NumberList } from '@strapi/icons';\nimport { type Text, Editor, Node, Transforms, Path } from 'slate';\nimport { type RenderElementProps, ReactEditor } from 'slate-react';\nimport { styled, type CSSProperties, css } from 'styled-components';\n\nimport { type BlocksStore } from '../BlocksEditor';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { isListNode, type Block } from '../utils/types';\n\nconst listStyle = css`\n display: flex;\n flex-direction: column;\n gap: ${({ theme }) => theme.spaces[2]};\n margin-inline-start: ${({ theme }) => theme.spaces[0]};\n margin-inline-end: ${({ theme }) => theme.spaces[0]};\n padding-inline-start: ${({ theme }) => theme.spaces[2]};\n\n ol,\n ul {\n margin-block-start: ${({ theme }) => theme.spaces[0]};\n margin-block-end: ${({ theme }) => theme.spaces[0]};\n }\n\n li {\n margin-inline-start: ${({ theme }) => theme.spaces[3]};\n }\n`;\n\nconst Orderedlist = styled.ol<{ $listStyleType: CSSProperties['listStyleType'] }>`\n list-style-type: ${(props) => props.$listStyleType};\n ${listStyle}\n`;\n\nconst Unorderedlist = styled.ul<{ $listStyleType: CSSProperties['listStyleType'] }>`\n list-style-type: ${(props) => props.$listStyleType};\n ${listStyle}\n`;\n\nconst orderedStyles = ['decimal', 'lower-alpha', 'upper-roman'];\nconst unorderedStyles = ['disc', 'circle', 'square'];\n\nconst List = ({ attributes, children, element }: RenderElementProps) => {\n if (!isListNode(element)) {\n return null;\n }\n\n // Decide the subsequent style by referencing the given styles according to the format,\n // allowing for infinite nested lists\n const listStyles = element.format === 'ordered' ? orderedStyles : unorderedStyles;\n const nextIndex = (element.indentLevel || 0) % listStyles!.length;\n const listStyleType = listStyles![nextIndex];\n\n if (element.format === 'ordered') {\n return (\n <Orderedlist $listStyleType={listStyleType} {...attributes}>\n {children}\n </Orderedlist>\n );\n }\n\n return (\n <Unorderedlist $listStyleType={listStyleType} {...attributes}>\n {children}\n </Unorderedlist>\n );\n};\n\nconst replaceListWithEmptyBlock = (editor: Editor, currentListPath: Path) => {\n // Delete the empty list\n Transforms.removeNodes(editor, { at: currentListPath });\n\n if (currentListPath[0] === 0) {\n // If the list was the only (or first) block element then insert empty paragraph as editor needs default value\n Transforms.insertNodes(\n editor,\n {\n type: 'paragraph',\n children: [{ type: 'text', text: '' }],\n },\n { at: currentListPath }\n );\n Transforms.select(editor, currentListPath);\n }\n};\n\nconst isText = (node: unknown): node is Text => {\n return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';\n};\n\n/**\n * Common handler for the backspace event on ordered and unordered lists\n */\nconst handleBackspaceKeyOnList = (editor: Editor, event: React.KeyboardEvent<HTMLElement>) => {\n if (!editor.selection) return;\n\n const [currentListItem, currentListItemPath] = Editor.parent(editor, editor.selection.anchor);\n const [currentList, currentListPath] = Editor.parent(editor, currentListItemPath);\n const isListEmpty =\n currentList.children.length === 1 &&\n isText(currentListItem.children[0]) &&\n currentListItem.children[0].text === '';\n const isListItemEmpty =\n currentListItem.children.length === 1 &&\n isText(currentListItem.children[0]) &&\n currentListItem.children[0].text === '';\n const isFocusAtTheBeginningOfAChild =\n editor.selection.focus.offset === 0 && editor.selection.focus.path.at(-2) === 0;\n\n if (isListEmpty) {\n const parentListEntry = Editor.above(editor, {\n at: currentListPath,\n match: (node) => !Editor.isEditor(node) && node.type === 'list',\n });\n if (!parentListEntry) {\n event.preventDefault();\n replaceListWithEmptyBlock(editor, currentListPath);\n }\n } else if (isFocusAtTheBeginningOfAChild) {\n // If the focus is at the beginning of a child node we need to replace it with a paragraph\n Transforms.liftNodes(editor, {\n match: (node) => !Editor.isEditor(node) && node.type === 'list-item',\n });\n Transforms.setNodes(editor, { type: 'paragraph' });\n } else if (isListItemEmpty) {\n const previousEntry = Editor.previous(editor, {\n at: currentListItemPath,\n });\n const nextEntry = Editor.next(editor, {\n at: currentListItemPath,\n });\n\n if (previousEntry && nextEntry) {\n // If previous and next nodes are lists or list-items, delete empty list item\n event.preventDefault();\n Transforms.removeNodeLine truncated
@@ -0,0 +1,358 @@
import { jsx } from 'react/jsx-runtime';
import 'react';
import { Typography } from '@strapi/design-system';
import { NumberList, BulletList } from '@strapi/icons';
import { Editor, Transforms, Path, Node } from 'slate';
import { ReactEditor } from 'slate-react';
import { css, styled } from 'styled-components';
import { baseHandleConvert } from '../utils/conversions.mjs';
import { isListNode } from '../utils/types.mjs';
const listStyle = css`
display: flex;
flex-direction: column;
gap: ${({ theme })=>theme.spaces[2]};
margin-inline-start: ${({ theme })=>theme.spaces[0]};
margin-inline-end: ${({ theme })=>theme.spaces[0]};
padding-inline-start: ${({ theme })=>theme.spaces[2]};
ol,
ul {
margin-block-start: ${({ theme })=>theme.spaces[0]};
margin-block-end: ${({ theme })=>theme.spaces[0]};
}
li {
margin-inline-start: ${({ theme })=>theme.spaces[3]};
}
`;
const Orderedlist = styled.ol`
list-style-type: ${(props)=>props.$listStyleType};
${listStyle}
`;
const Unorderedlist = styled.ul`
list-style-type: ${(props)=>props.$listStyleType};
${listStyle}
`;
const orderedStyles = [
'decimal',
'lower-alpha',
'upper-roman'
];
const unorderedStyles = [
'disc',
'circle',
'square'
];
const List = ({ attributes, children, element })=>{
if (!isListNode(element)) {
return null;
}
// Decide the subsequent style by referencing the given styles according to the format,
// allowing for infinite nested lists
const listStyles = element.format === 'ordered' ? orderedStyles : unorderedStyles;
const nextIndex = (element.indentLevel || 0) % listStyles.length;
const listStyleType = listStyles[nextIndex];
if (element.format === 'ordered') {
return /*#__PURE__*/ jsx(Orderedlist, {
$listStyleType: listStyleType,
...attributes,
children: children
});
}
return /*#__PURE__*/ jsx(Unorderedlist, {
$listStyleType: listStyleType,
...attributes,
children: children
});
};
const replaceListWithEmptyBlock = (editor, currentListPath)=>{
// Delete the empty list
Transforms.removeNodes(editor, {
at: currentListPath
});
if (currentListPath[0] === 0) {
// If the list was the only (or first) block element then insert empty paragraph as editor needs default value
Transforms.insertNodes(editor, {
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
}, {
at: currentListPath
});
Transforms.select(editor, currentListPath);
}
};
const isText = (node)=>{
return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';
};
/**
* Common handler for the backspace event on ordered and unordered lists
*/ const handleBackspaceKeyOnList = (editor, event)=>{
if (!editor.selection) return;
const [currentListItem, currentListItemPath] = Editor.parent(editor, editor.selection.anchor);
const [currentList, currentListPath] = Editor.parent(editor, currentListItemPath);
const isListEmpty = currentList.children.length === 1 && isText(currentListItem.children[0]) && currentListItem.children[0].text === '';
const isListItemEmpty = currentListItem.children.length === 1 && isText(currentListItem.children[0]) && currentListItem.children[0].text === '';
const isFocusAtTheBeginningOfAChild = editor.selection.focus.offset === 0 && editor.selection.focus.path.at(-2) === 0;
if (isListEmpty) {
const parentListEntry = Editor.above(editor, {
at: currentListPath,
match: (node)=>!Editor.isEditor(node) && node.type === 'list'
});
if (!parentListEntry) {
event.preventDefault();
replaceListWithEmptyBlock(editor, currentListPath);
}
} else if (isFocusAtTheBeginningOfAChild) {
// If the focus is at the beginning of a child node we need to replace it with a paragraph
Transforms.liftNodes(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list-item'
});
Transforms.setNodes(editor, {
type: 'paragraph'
});
} else if (isListItemEmpty) {
const previousEntry = Editor.previous(editor, {
at: currentListItemPath
});
const nextEntry = Editor.next(editor, {
at: currentListItemPath
});
if (previousEntry && nextEntry) {
// If previous and next nodes are lists or list-items, delete empty list item
event.preventDefault();
Transforms.removeNodes(editor, {
at: currentListItemPath
});
// If previous and next nodes are lists with same format and indent Levels, then merge the nodes
const [previousList] = previousEntry;
const [nextList] = nextEntry;
if (!Editor.isEditor(previousList) && !isText(previousList) && isListNode(previousList) && !Editor.isEditor(nextList) && !isText(nextList) && isListNode(nextList)) {
if (previousList.type === 'list' && nextList.type === 'list' && previousList.format === nextList.format && previousList.indentLevel === nextList.indentLevel) {
Transforms.mergeNodes(editor, {
at: currentListItemPath
});
}
}
}
}
};
/**
* Common handler for the enter key on ordered and unordered lists
*/ const handleEnterKeyOnList = (editor)=>{
const currentListItemEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list-item'
});
if (!currentListItemEntry || !editor.selection) {
return;
}
const [currentListItem, currentListItemPath] = currentListItemEntry;
const [currentList, currentListPath] = Editor.parent(editor, currentListItemPath);
const isListEmpty = currentList.children.length === 1 && isText(currentListItem.children[0]) && currentListItem.children[0].text === '';
const isListItemEmpty = currentListItem.children.length === 1 && isText(currentListItem.children[0]) && currentListItem.children[0].text === '';
const isFocusAtTheBeginningOfAChild = editor.selection.focus.offset === 0 && editor.selection.focus.path.at(-1) === 0;
if (isListEmpty) {
replaceListWithEmptyBlock(editor, currentListPath);
} else if (isFocusAtTheBeginningOfAChild && !isListItemEmpty) {
// If the focus is at the beginning of a child node, shift below the list item and create a new list-item
const currentNode = Editor.above(editor, {
at: editor.selection.anchor
});
Transforms.insertNodes(editor, {
type: 'list-item',
children: [
{
type: 'text',
text: ''
}
]
});
if (currentNode) {
const path = currentNode[1];
const updatedPath = [
...path.slice(0, -1),
path[path.length - 1] + 1
];
Transforms.select(editor, {
anchor: {
path: updatedPath.concat(0),
offset: 0
},
focus: {
path: updatedPath.concat(0),
offset: 0
}
});
}
} else if (isListItemEmpty) {
// Check if there is a list above the current list and shift list-item under it
if (!Editor.isEditor(currentList) && isListNode(currentList) && currentList?.indentLevel && currentList.indentLevel > 0) {
const previousIndentLevel = currentList.indentLevel - 1;
const parentListNodeEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list' && (node.indentLevel || 0) === previousIndentLevel
});
if (parentListNodeEntry) {
// Get the parent list path and add 1 to it to exit from the current list
const modifiedPath = currentListItemPath.slice(0, -1);
if (modifiedPath.length > 0) {
modifiedPath[modifiedPath.length - 1] += 1;
}
// Shift list-item under parent list
Transforms.moveNodes(editor, {
at: currentListItemPath,
to: modifiedPath
});
return;
}
}
// Otherwise delete the empty list item and create a new paragraph below the parent list
Transforms.removeNodes(editor, {
at: currentListItemPath
});
const createdParagraphPath = Path.next(currentListPath);
Transforms.insertNodes(editor, {
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
}, {
at: createdParagraphPath
});
// Move the selection to the newly created paragraph
Transforms.select(editor, createdParagraphPath);
} else {
// Check if the cursor is at the end of the list item
const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, currentListItemPath);
if (isNodeEnd) {
// If there was nothing after the cursor, create a fresh new list item,
// in order to avoid carrying over the modifiers from the previous list item
Transforms.insertNodes(editor, {
type: 'list-item',
children: [
{
type: 'text',
text: ''
}
]
});
} else {
// If there is something after the cursor, split the current list item,
// so that we keep the content and the modifiers
Transforms.splitNodes(editor);
}
}
};
/**
* Common handler for converting a node to a list
*/ const handleConvertToList = (editor, format)=>{
const convertedPath = baseHandleConvert(editor, {
type: 'list-item'
});
if (!convertedPath) return;
Transforms.wrapNodes(editor, {
type: 'list',
format,
children: []
}, {
at: convertedPath
});
};
/**
* Common handler for the tab key on ordered and unordered lists
*/ const handleTabOnList = (editor)=>{
const currentListItemEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list-item'
});
if (!currentListItemEntry || !editor.selection) {
return;
}
const [currentListItem, currentListItemPath] = currentListItemEntry;
const [currentList] = Editor.parent(editor, currentListItemPath);
// Skip tabbing if list-item is the first item in the list
if (currentListItem === currentList.children[0]) return;
const currentListItemIndex = currentList.children.findIndex((item)=>item === currentListItem);
const previousNode = currentList.children[currentListItemIndex - 1];
// If previous node is a list block then move the list-item under it
if (previousNode.type === 'list') {
const nodePath = ReactEditor.findPath(editor, previousNode);
const insertAtPath = previousNode.children.length;
Transforms.moveNodes(editor, {
at: currentListItemPath,
to: nodePath.concat(insertAtPath)
});
return;
}
if (!Editor.isEditor(currentList) && isListNode(currentList)) {
// Wrap list-item with list block on tab
Transforms.wrapNodes(editor, {
type: 'list',
format: currentList.format,
indentLevel: (currentList.indentLevel || 0) + 1,
children: []
});
}
};
const listBlocks = {
'list-ordered': {
renderElement: (props)=>/*#__PURE__*/ jsx(List, {
...props
}),
label: {
id: 'components.Blocks.blocks.orderedList',
defaultMessage: 'Numbered list'
},
icon: NumberList,
matchNode: (node)=>node.type === 'list' && node.format === 'ordered',
isInBlocksSelector: true,
handleConvert: (editor)=>handleConvertToList(editor, 'ordered'),
handleEnterKey: handleEnterKeyOnList,
handleBackspaceKey: handleBackspaceKeyOnList,
handleTab: handleTabOnList,
snippets: [
'1.'
]
},
'list-unordered': {
renderElement: (props)=>/*#__PURE__*/ jsx(List, {
...props
}),
label: {
id: 'components.Blocks.blocks.unorderedList',
defaultMessage: 'Bulleted list'
},
icon: BulletList,
matchNode: (node)=>node.type === 'list' && node.format === 'unordered',
isInBlocksSelector: true,
handleConvert: (editor)=>handleConvertToList(editor, 'unordered'),
handleEnterKey: handleEnterKeyOnList,
handleBackspaceKey: handleBackspaceKeyOnList,
handleTab: handleTabOnList,
snippets: [
'-',
'*',
'+'
]
},
'list-item': {
renderElement: (props)=>/*#__PURE__*/ jsx(Typography, {
tag: "li",
...props.attributes,
children: props.children
}),
// No handleConvert, list items are created when converting to the parent list
matchNode: (node)=>node.type === 'list-item',
isInBlocksSelector: false,
dragHandleTopMargin: '-2px'
}
};
export { listBlocks };
//# sourceMappingURL=List.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"List.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Typography } from '@strapi/design-system';\nimport { BulletList, NumberList } from '@strapi/icons';\nimport { type Text, Editor, Node, Transforms, Path } from 'slate';\nimport { type RenderElementProps, ReactEditor } from 'slate-react';\nimport { styled, type CSSProperties, css } from 'styled-components';\n\nimport { type BlocksStore } from '../BlocksEditor';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { isListNode, type Block } from '../utils/types';\n\nconst listStyle = css`\n display: flex;\n flex-direction: column;\n gap: ${({ theme }) => theme.spaces[2]};\n margin-inline-start: ${({ theme }) => theme.spaces[0]};\n margin-inline-end: ${({ theme }) => theme.spaces[0]};\n padding-inline-start: ${({ theme }) => theme.spaces[2]};\n\n ol,\n ul {\n margin-block-start: ${({ theme }) => theme.spaces[0]};\n margin-block-end: ${({ theme }) => theme.spaces[0]};\n }\n\n li {\n margin-inline-start: ${({ theme }) => theme.spaces[3]};\n }\n`;\n\nconst Orderedlist = styled.ol<{ $listStyleType: CSSProperties['listStyleType'] }>`\n list-style-type: ${(props) => props.$listStyleType};\n ${listStyle}\n`;\n\nconst Unorderedlist = styled.ul<{ $listStyleType: CSSProperties['listStyleType'] }>`\n list-style-type: ${(props) => props.$listStyleType};\n ${listStyle}\n`;\n\nconst orderedStyles = ['decimal', 'lower-alpha', 'upper-roman'];\nconst unorderedStyles = ['disc', 'circle', 'square'];\n\nconst List = ({ attributes, children, element }: RenderElementProps) => {\n if (!isListNode(element)) {\n return null;\n }\n\n // Decide the subsequent style by referencing the given styles according to the format,\n // allowing for infinite nested lists\n const listStyles = element.format === 'ordered' ? orderedStyles : unorderedStyles;\n const nextIndex = (element.indentLevel || 0) % listStyles!.length;\n const listStyleType = listStyles![nextIndex];\n\n if (element.format === 'ordered') {\n return (\n <Orderedlist $listStyleType={listStyleType} {...attributes}>\n {children}\n </Orderedlist>\n );\n }\n\n return (\n <Unorderedlist $listStyleType={listStyleType} {...attributes}>\n {children}\n </Unorderedlist>\n );\n};\n\nconst replaceListWithEmptyBlock = (editor: Editor, currentListPath: Path) => {\n // Delete the empty list\n Transforms.removeNodes(editor, { at: currentListPath });\n\n if (currentListPath[0] === 0) {\n // If the list was the only (or first) block element then insert empty paragraph as editor needs default value\n Transforms.insertNodes(\n editor,\n {\n type: 'paragraph',\n children: [{ type: 'text', text: '' }],\n },\n { at: currentListPath }\n );\n Transforms.select(editor, currentListPath);\n }\n};\n\nconst isText = (node: unknown): node is Text => {\n return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';\n};\n\n/**\n * Common handler for the backspace event on ordered and unordered lists\n */\nconst handleBackspaceKeyOnList = (editor: Editor, event: React.KeyboardEvent<HTMLElement>) => {\n if (!editor.selection) return;\n\n const [currentListItem, currentListItemPath] = Editor.parent(editor, editor.selection.anchor);\n const [currentList, currentListPath] = Editor.parent(editor, currentListItemPath);\n const isListEmpty =\n currentList.children.length === 1 &&\n isText(currentListItem.children[0]) &&\n currentListItem.children[0].text === '';\n const isListItemEmpty =\n currentListItem.children.length === 1 &&\n isText(currentListItem.children[0]) &&\n currentListItem.children[0].text === '';\n const isFocusAtTheBeginningOfAChild =\n editor.selection.focus.offset === 0 && editor.selection.focus.path.at(-2) === 0;\n\n if (isListEmpty) {\n const parentListEntry = Editor.above(editor, {\n at: currentListPath,\n match: (node) => !Editor.isEditor(node) && node.type === 'list',\n });\n if (!parentListEntry) {\n event.preventDefault();\n replaceListWithEmptyBlock(editor, currentListPath);\n }\n } else if (isFocusAtTheBeginningOfAChild) {\n // If the focus is at the beginning of a child node we need to replace it with a paragraph\n Transforms.liftNodes(editor, {\n match: (node) => !Editor.isEditor(node) && node.type === 'list-item',\n });\n Transforms.setNodes(editor, { type: 'paragraph' });\n } else if (isListItemEmpty) {\n const previousEntry = Editor.previous(editor, {\n at: currentListItemPath,\n });\n const nextEntry = Editor.next(editor, {\n at: currentListItemPath,\n });\n\n if (previousEntry && nextEntry) {\n // If previous and next nodes are lists or list-items, delete empty list item\n event.preventDefault();\n Transforms.removeNodLine truncated
@@ -0,0 +1,95 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
require('react');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var slate = require('slate');
var conversions = require('../utils/conversions.js');
const paragraphBlocks = {
paragraph: {
renderElement: (props)=>/*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
tag: "p",
variant: "omega",
...props.attributes,
children: props.children
}),
icon: Icons.Paragraph,
label: {
id: 'components.Blocks.blocks.text',
defaultMessage: 'Text'
},
matchNode: (node)=>node.type === 'paragraph',
isInBlocksSelector: true,
dragHandleTopMargin: '-2px',
handleConvert (editor) {
conversions.baseHandleConvert(editor, {
type: 'paragraph'
});
},
handleEnterKey (editor) {
if (!editor.selection) {
return;
}
// We need to keep track of the initial position of the cursor
const anchorPathInitialPosition = editor.selection.anchor.path;
/**
* Split the nodes where the cursor is. This will create a new paragraph with the content
* after the cursor, while retaining all the children, modifiers etc.
*/ slate.Transforms.splitNodes(editor, {
// Makes sure we always create a new node,
// even if there's nothing to the right of the cursor in the node.
always: true
});
// Check if the created node is empty (if there was no text after the cursor in the node)
// This lets us know if we need to carry over the modifiers from the previous node
const parentBlockEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type !== 'text'
});
if (!parentBlockEntry) {
return;
}
const [, parentBlockPath] = parentBlockEntry;
const isNodeEnd = slate.Editor.isEnd(editor, editor.selection.anchor, parentBlockPath);
/**
* Delete and recreate the node that was created at the right of the cursor.
* This is to avoid node pollution
* (e.g. keeping the level attribute when converting a heading to a paragraph).
* Select the parent of the selection because we want the full block, not the leaf.
* And copy its children to make sure we keep the modifiers.
*/ const [fragmentedNode] = slate.Editor.parent(editor, editor.selection.anchor.path);
slate.Transforms.removeNodes(editor);
// Check if after the current position there is another node
const hasNextNode = editor.children.length - anchorPathInitialPosition[0] > 1;
// Insert the new node at the right position.
// The next line after the editor selection if present or otherwise at the end of the editor.
slate.Transforms.insertNodes(editor, {
type: 'paragraph',
// Don't carry over the modifiers from the previous node if there was no text after the cursor
children: isNodeEnd ? [
{
type: 'text',
text: ''
}
] : fragmentedNode.children
}, {
at: hasNextNode ? [
anchorPathInitialPosition[0] + 1
] : [
editor.children.length
]
});
/**
* The new selection will by default be at the end of the created node.
* Instead we manually move it to the start of the created node.
* Make sure to we go to the start of the node and not the start of the leaf.
*/ slate.Transforms.select(editor, editor.start([
anchorPathInitialPosition[0] + 1
]));
}
}
};
exports.paragraphBlocks = paragraphBlocks;
//# sourceMappingURL=Paragraph.js.map
@@ -0,0 +1 @@
{"version":3,"file":"Paragraph.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Paragraph.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Typography } from '@strapi/design-system';\nimport { Paragraph } from '@strapi/icons';\nimport { type Text, Editor, Transforms } from 'slate';\n\nimport { type BlocksStore } from '../BlocksEditor';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { type Block } from '../utils/types';\n\nconst paragraphBlocks: Pick<BlocksStore, 'paragraph'> = {\n paragraph: {\n renderElement: (props) => (\n <Typography tag=\"p\" variant=\"omega\" {...props.attributes}>\n {props.children}\n </Typography>\n ),\n icon: Paragraph,\n label: {\n id: 'components.Blocks.blocks.text',\n defaultMessage: 'Text',\n },\n matchNode: (node) => node.type === 'paragraph',\n isInBlocksSelector: true,\n dragHandleTopMargin: '-2px',\n handleConvert(editor) {\n baseHandleConvert<Block<'paragraph'>>(editor, { type: 'paragraph' });\n },\n handleEnterKey(editor) {\n if (!editor.selection) {\n return;\n }\n\n // We need to keep track of the initial position of the cursor\n const anchorPathInitialPosition = editor.selection.anchor.path;\n /**\n * Split the nodes where the cursor is. This will create a new paragraph with the content\n * after the cursor, while retaining all the children, modifiers etc.\n */\n Transforms.splitNodes(editor, {\n // Makes sure we always create a new node,\n // even if there's nothing to the right of the cursor in the node.\n always: true,\n });\n\n // Check if the created node is empty (if there was no text after the cursor in the node)\n // This lets us know if we need to carry over the modifiers from the previous node\n const parentBlockEntry = Editor.above(editor, {\n match: (node) => !Editor.isEditor(node) && node.type !== 'text',\n });\n if (!parentBlockEntry) {\n return;\n }\n const [, parentBlockPath] = parentBlockEntry;\n const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, parentBlockPath);\n\n /**\n * Delete and recreate the node that was created at the right of the cursor.\n * This is to avoid node pollution\n * (e.g. keeping the level attribute when converting a heading to a paragraph).\n * Select the parent of the selection because we want the full block, not the leaf.\n * And copy its children to make sure we keep the modifiers.\n */\n const [fragmentedNode] = Editor.parent(editor, editor.selection.anchor.path);\n Transforms.removeNodes(editor);\n\n // Check if after the current position there is another node\n const hasNextNode = editor.children.length - anchorPathInitialPosition[0] > 1;\n\n // Insert the new node at the right position.\n // The next line after the editor selection if present or otherwise at the end of the editor.\n Transforms.insertNodes(\n editor,\n {\n type: 'paragraph',\n // Don't carry over the modifiers from the previous node if there was no text after the cursor\n children: (isNodeEnd ? [{ type: 'text', text: '' }] : fragmentedNode.children) as Text[],\n },\n {\n at: hasNextNode ? [anchorPathInitialPosition[0] + 1] : [editor.children.length],\n }\n );\n\n /**\n * The new selection will by default be at the end of the created node.\n * Instead we manually move it to the start of the created node.\n * Make sure to we go to the start of the node and not the start of the leaf.\n */\n Transforms.select(editor, editor.start([anchorPathInitialPosition[0] + 1]));\n },\n },\n};\n\nexport { paragraphBlocks };\n"],"names":["paragraphBlocks","paragraph","renderElement","props","_jsx","Typography","tag","variant","attributes","children","icon","Paragraph","label","id","defaultMessage","matchNode","node","type","isInBlocksSelector","dragHandleTopMargin","handleConvert","editor","baseHandleConvert","handleEnterKey","selection","anchorPathInitialPosition","anchor","path","Transforms","splitNodes","always","parentBlockEntry","Editor","above","match","isEditor","parentBlockPath","isNodeEnd","isEnd","fragmentedNode","parent","removeNodes","hasNextNode","length","insertNodes","text","at","select","start"],"mappings":";;;;;;;;;AAUA,MAAMA,eAAkD,GAAA;IACtDC,SAAW,EAAA;QACTC,aAAe,EAAA,CAACC,sBACdC,cAACC,CAAAA,uBAAAA,EAAAA;gBAAWC,GAAI,EAAA,GAAA;gBAAIC,OAAQ,EAAA,OAAA;AAAS,gBAAA,GAAGJ,MAAMK,UAAU;AACrDL,gBAAAA,QAAAA,EAAAA,KAAAA,CAAMM;;QAGXC,IAAMC,EAAAA,eAAAA;QACNC,KAAO,EAAA;YACLC,EAAI,EAAA,+BAAA;YACJC,cAAgB,EAAA;AAClB,SAAA;AACAC,QAAAA,SAAAA,EAAW,CAACC,IAAAA,GAASA,IAAKC,CAAAA,IAAI,KAAK,WAAA;QACnCC,kBAAoB,EAAA,IAAA;QACpBC,mBAAqB,EAAA,MAAA;AACrBC,QAAAA,aAAAA,CAAAA,CAAcC,MAAM,EAAA;AAClBC,YAALine truncated
@@ -0,0 +1,93 @@
import { jsx } from 'react/jsx-runtime';
import 'react';
import { Typography } from '@strapi/design-system';
import { Paragraph } from '@strapi/icons';
import { Transforms, Editor } from 'slate';
import { baseHandleConvert } from '../utils/conversions.mjs';
const paragraphBlocks = {
paragraph: {
renderElement: (props)=>/*#__PURE__*/ jsx(Typography, {
tag: "p",
variant: "omega",
...props.attributes,
children: props.children
}),
icon: Paragraph,
label: {
id: 'components.Blocks.blocks.text',
defaultMessage: 'Text'
},
matchNode: (node)=>node.type === 'paragraph',
isInBlocksSelector: true,
dragHandleTopMargin: '-2px',
handleConvert (editor) {
baseHandleConvert(editor, {
type: 'paragraph'
});
},
handleEnterKey (editor) {
if (!editor.selection) {
return;
}
// We need to keep track of the initial position of the cursor
const anchorPathInitialPosition = editor.selection.anchor.path;
/**
* Split the nodes where the cursor is. This will create a new paragraph with the content
* after the cursor, while retaining all the children, modifiers etc.
*/ Transforms.splitNodes(editor, {
// Makes sure we always create a new node,
// even if there's nothing to the right of the cursor in the node.
always: true
});
// Check if the created node is empty (if there was no text after the cursor in the node)
// This lets us know if we need to carry over the modifiers from the previous node
const parentBlockEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type !== 'text'
});
if (!parentBlockEntry) {
return;
}
const [, parentBlockPath] = parentBlockEntry;
const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, parentBlockPath);
/**
* Delete and recreate the node that was created at the right of the cursor.
* This is to avoid node pollution
* (e.g. keeping the level attribute when converting a heading to a paragraph).
* Select the parent of the selection because we want the full block, not the leaf.
* And copy its children to make sure we keep the modifiers.
*/ const [fragmentedNode] = Editor.parent(editor, editor.selection.anchor.path);
Transforms.removeNodes(editor);
// Check if after the current position there is another node
const hasNextNode = editor.children.length - anchorPathInitialPosition[0] > 1;
// Insert the new node at the right position.
// The next line after the editor selection if present or otherwise at the end of the editor.
Transforms.insertNodes(editor, {
type: 'paragraph',
// Don't carry over the modifiers from the previous node if there was no text after the cursor
children: isNodeEnd ? [
{
type: 'text',
text: ''
}
] : fragmentedNode.children
}, {
at: hasNextNode ? [
anchorPathInitialPosition[0] + 1
] : [
editor.children.length
]
});
/**
* The new selection will by default be at the end of the created node.
* Instead we manually move it to the start of the created node.
* Make sure to we go to the start of the node and not the start of the leaf.
*/ Transforms.select(editor, editor.start([
anchorPathInitialPosition[0] + 1
]));
}
}
};
export { paragraphBlocks };
//# sourceMappingURL=Paragraph.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"Paragraph.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Paragraph.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Typography } from '@strapi/design-system';\nimport { Paragraph } from '@strapi/icons';\nimport { type Text, Editor, Transforms } from 'slate';\n\nimport { type BlocksStore } from '../BlocksEditor';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { type Block } from '../utils/types';\n\nconst paragraphBlocks: Pick<BlocksStore, 'paragraph'> = {\n paragraph: {\n renderElement: (props) => (\n <Typography tag=\"p\" variant=\"omega\" {...props.attributes}>\n {props.children}\n </Typography>\n ),\n icon: Paragraph,\n label: {\n id: 'components.Blocks.blocks.text',\n defaultMessage: 'Text',\n },\n matchNode: (node) => node.type === 'paragraph',\n isInBlocksSelector: true,\n dragHandleTopMargin: '-2px',\n handleConvert(editor) {\n baseHandleConvert<Block<'paragraph'>>(editor, { type: 'paragraph' });\n },\n handleEnterKey(editor) {\n if (!editor.selection) {\n return;\n }\n\n // We need to keep track of the initial position of the cursor\n const anchorPathInitialPosition = editor.selection.anchor.path;\n /**\n * Split the nodes where the cursor is. This will create a new paragraph with the content\n * after the cursor, while retaining all the children, modifiers etc.\n */\n Transforms.splitNodes(editor, {\n // Makes sure we always create a new node,\n // even if there's nothing to the right of the cursor in the node.\n always: true,\n });\n\n // Check if the created node is empty (if there was no text after the cursor in the node)\n // This lets us know if we need to carry over the modifiers from the previous node\n const parentBlockEntry = Editor.above(editor, {\n match: (node) => !Editor.isEditor(node) && node.type !== 'text',\n });\n if (!parentBlockEntry) {\n return;\n }\n const [, parentBlockPath] = parentBlockEntry;\n const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, parentBlockPath);\n\n /**\n * Delete and recreate the node that was created at the right of the cursor.\n * This is to avoid node pollution\n * (e.g. keeping the level attribute when converting a heading to a paragraph).\n * Select the parent of the selection because we want the full block, not the leaf.\n * And copy its children to make sure we keep the modifiers.\n */\n const [fragmentedNode] = Editor.parent(editor, editor.selection.anchor.path);\n Transforms.removeNodes(editor);\n\n // Check if after the current position there is another node\n const hasNextNode = editor.children.length - anchorPathInitialPosition[0] > 1;\n\n // Insert the new node at the right position.\n // The next line after the editor selection if present or otherwise at the end of the editor.\n Transforms.insertNodes(\n editor,\n {\n type: 'paragraph',\n // Don't carry over the modifiers from the previous node if there was no text after the cursor\n children: (isNodeEnd ? [{ type: 'text', text: '' }] : fragmentedNode.children) as Text[],\n },\n {\n at: hasNextNode ? [anchorPathInitialPosition[0] + 1] : [editor.children.length],\n }\n );\n\n /**\n * The new selection will by default be at the end of the created node.\n * Instead we manually move it to the start of the created node.\n * Make sure to we go to the start of the node and not the start of the leaf.\n */\n Transforms.select(editor, editor.start([anchorPathInitialPosition[0] + 1]));\n },\n },\n};\n\nexport { paragraphBlocks };\n"],"names":["paragraphBlocks","paragraph","renderElement","props","_jsx","Typography","tag","variant","attributes","children","icon","Paragraph","label","id","defaultMessage","matchNode","node","type","isInBlocksSelector","dragHandleTopMargin","handleConvert","editor","baseHandleConvert","handleEnterKey","selection","anchorPathInitialPosition","anchor","path","Transforms","splitNodes","always","parentBlockEntry","Editor","above","match","isEditor","parentBlockPath","isNodeEnd","isEnd","fragmentedNode","parent","removeNodes","hasNextNode","length","insertNodes","text","at","select","start"],"mappings":";;;;;;;AAUA,MAAMA,eAAkD,GAAA;IACtDC,SAAW,EAAA;QACTC,aAAe,EAAA,CAACC,sBACdC,GAACC,CAAAA,UAAAA,EAAAA;gBAAWC,GAAI,EAAA,GAAA;gBAAIC,OAAQ,EAAA,OAAA;AAAS,gBAAA,GAAGJ,MAAMK,UAAU;AACrDL,gBAAAA,QAAAA,EAAAA,KAAAA,CAAMM;;QAGXC,IAAMC,EAAAA,SAAAA;QACNC,KAAO,EAAA;YACLC,EAAI,EAAA,+BAAA;YACJC,cAAgB,EAAA;AAClB,SAAA;AACAC,QAAAA,SAAAA,EAAW,CAACC,IAAAA,GAASA,IAAKC,CAAAA,IAAI,KAAK,WAAA;QACnCC,kBAAoB,EAAA,IAAA;QACpBC,mBAAqB,EAAA,MAAA;AACrBC,QAAAA,aAAAA,CAAAA,CAAcC,MAAM,EAAA;AAClBC,YAAAALine truncated
@@ -0,0 +1,49 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var Icons = require('@strapi/icons');
var styledComponents = require('styled-components');
var conversions = require('../utils/conversions.js');
var enterKey = require('../utils/enterKey.js');
const Blockquote = styledComponents.styled.blockquote.attrs({
role: 'blockquote'
})`
font-weight: ${({ theme })=>theme.fontWeights.regular};
border-left: ${({ theme })=>`${theme.spaces[1]} solid ${theme.colors.neutral200}`};
padding: ${({ theme })=>theme.spaces[2]} ${({ theme })=>theme.spaces[4]};
color: ${({ theme })=>theme.colors.neutral600};
`;
const quoteBlocks = {
quote: {
renderElement: (props)=>// The div is needed to make sure the padding bottom from BlocksContent is applied properly
// when the quote is the last block in the editor
/*#__PURE__*/ jsxRuntime.jsx("div", {
children: /*#__PURE__*/ jsxRuntime.jsx(Blockquote, {
...props.attributes,
children: props.children
})
}),
icon: Icons.Quotes,
label: {
id: 'components.Blocks.blocks.quote',
defaultMessage: 'Quote'
},
matchNode: (node)=>node.type === 'quote',
isInBlocksSelector: true,
handleConvert (editor) {
conversions.baseHandleConvert(editor, {
type: 'quote'
});
},
handleEnterKey (editor) {
enterKey.pressEnterTwiceToExit(editor);
},
snippets: [
'>'
]
}
};
exports.quoteBlocks = quoteBlocks;
//# sourceMappingURL=Quote.js.map
@@ -0,0 +1 @@
{"version":3,"file":"Quote.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Quote.tsx"],"sourcesContent":["import { Quotes } from '@strapi/icons';\nimport { styled } from 'styled-components';\n\nimport { type BlocksStore } from '../BlocksEditor';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { pressEnterTwiceToExit } from '../utils/enterKey';\nimport { type Block } from '../utils/types';\n\nconst Blockquote = styled.blockquote.attrs({ role: 'blockquote' })`\n font-weight: ${({ theme }) => theme.fontWeights.regular};\n border-left: ${({ theme }) => `${theme.spaces[1]} solid ${theme.colors.neutral200}`};\n padding: ${({ theme }) => theme.spaces[2]} ${({ theme }) => theme.spaces[4]};\n color: ${({ theme }) => theme.colors.neutral600};\n`;\n\nconst quoteBlocks: Pick<BlocksStore, 'quote'> = {\n quote: {\n renderElement: (props) => (\n // The div is needed to make sure the padding bottom from BlocksContent is applied properly\n // when the quote is the last block in the editor\n <div>\n <Blockquote {...props.attributes}>{props.children}</Blockquote>\n </div>\n ),\n icon: Quotes,\n label: {\n id: 'components.Blocks.blocks.quote',\n defaultMessage: 'Quote',\n },\n matchNode: (node) => node.type === 'quote',\n isInBlocksSelector: true,\n handleConvert(editor) {\n baseHandleConvert<Block<'quote'>>(editor, { type: 'quote' });\n },\n handleEnterKey(editor) {\n pressEnterTwiceToExit(editor);\n },\n snippets: ['>'],\n },\n};\n\nexport { quoteBlocks };\n"],"names":["Blockquote","styled","blockquote","attrs","role","theme","fontWeights","regular","spaces","colors","neutral200","neutral600","quoteBlocks","quote","renderElement","props","_jsx","div","attributes","children","icon","Quotes","label","id","defaultMessage","matchNode","node","type","isInBlocksSelector","handleConvert","editor","baseHandleConvert","handleEnterKey","pressEnterTwiceToExit","snippets"],"mappings":";;;;;;;;AAQA,MAAMA,UAAaC,GAAAA,uBAAAA,CAAOC,UAAU,CAACC,KAAK,CAAC;IAAEC,IAAM,EAAA;AAAa,CAAA,CAAE;eACnD,EAAE,CAAC,EAAEC,KAAK,EAAE,GAAKA,KAAMC,CAAAA,WAAW,CAACC,OAAO,CAAC;eAC3C,EAAE,CAAC,EAAEF,KAAK,EAAE,GAAK,CAAC,EAAEA,MAAMG,MAAM,CAAC,EAAE,CAAC,OAAO,EAAEH,KAAMI,CAAAA,MAAM,CAACC,UAAU,CAAC,CAAC,CAAC;WAC3E,EAAE,CAAC,EAAEL,KAAK,EAAE,GAAKA,KAAMG,CAAAA,MAAM,CAAC,CAAE,CAAA,CAAC,CAAC,EAAE,CAAC,EAAEH,KAAK,EAAE,GAAKA,KAAMG,CAAAA,MAAM,CAAC,CAAA,CAAE,CAAC;SACrE,EAAE,CAAC,EAAEH,KAAK,EAAE,GAAKA,KAAMI,CAAAA,MAAM,CAACE,UAAU,CAAC;AAClD,CAAC;AAED,MAAMC,WAA0C,GAAA;IAC9CC,KAAO,EAAA;QACLC,aAAe,EAAA,CAACC;;0BAGdC,cAACC,CAAAA,KAAAA,EAAAA;AACC,gBAAA,QAAA,gBAAAD,cAAChB,CAAAA,UAAAA,EAAAA;AAAY,oBAAA,GAAGe,MAAMG,UAAU;AAAGH,oBAAAA,QAAAA,EAAAA,KAAAA,CAAMI;;;QAG7CC,IAAMC,EAAAA,YAAAA;QACNC,KAAO,EAAA;YACLC,EAAI,EAAA,gCAAA;YACJC,cAAgB,EAAA;AAClB,SAAA;AACAC,QAAAA,SAAAA,EAAW,CAACC,IAAAA,GAASA,IAAKC,CAAAA,IAAI,KAAK,OAAA;QACnCC,kBAAoB,EAAA,IAAA;AACpBC,QAAAA,aAAAA,CAAAA,CAAcC,MAAM,EAAA;AAClBC,YAAAA,6BAAAA,CAAkCD,MAAQ,EAAA;gBAAEH,IAAM,EAAA;AAAQ,aAAA,CAAA;AAC5D,SAAA;AACAK,QAAAA,cAAAA,CAAAA,CAAeF,MAAM,EAAA;YACnBG,8BAAsBH,CAAAA,MAAAA,CAAAA;AACxB,SAAA;QACAI,QAAU,EAAA;AAAC,YAAA;AAAI;AACjB;AACF;;;;"}
@@ -0,0 +1,47 @@
import { jsx } from 'react/jsx-runtime';
import { Quotes } from '@strapi/icons';
import { styled } from 'styled-components';
import { baseHandleConvert } from '../utils/conversions.mjs';
import { pressEnterTwiceToExit } from '../utils/enterKey.mjs';
const Blockquote = styled.blockquote.attrs({
role: 'blockquote'
})`
font-weight: ${({ theme })=>theme.fontWeights.regular};
border-left: ${({ theme })=>`${theme.spaces[1]} solid ${theme.colors.neutral200}`};
padding: ${({ theme })=>theme.spaces[2]} ${({ theme })=>theme.spaces[4]};
color: ${({ theme })=>theme.colors.neutral600};
`;
const quoteBlocks = {
quote: {
renderElement: (props)=>// The div is needed to make sure the padding bottom from BlocksContent is applied properly
// when the quote is the last block in the editor
/*#__PURE__*/ jsx("div", {
children: /*#__PURE__*/ jsx(Blockquote, {
...props.attributes,
children: props.children
})
}),
icon: Quotes,
label: {
id: 'components.Blocks.blocks.quote',
defaultMessage: 'Quote'
},
matchNode: (node)=>node.type === 'quote',
isInBlocksSelector: true,
handleConvert (editor) {
baseHandleConvert(editor, {
type: 'quote'
});
},
handleEnterKey (editor) {
pressEnterTwiceToExit(editor);
},
snippets: [
'>'
]
}
};
export { quoteBlocks };
//# sourceMappingURL=Quote.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"Quote.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Quote.tsx"],"sourcesContent":["import { Quotes } from '@strapi/icons';\nimport { styled } from 'styled-components';\n\nimport { type BlocksStore } from '../BlocksEditor';\nimport { baseHandleConvert } from '../utils/conversions';\nimport { pressEnterTwiceToExit } from '../utils/enterKey';\nimport { type Block } from '../utils/types';\n\nconst Blockquote = styled.blockquote.attrs({ role: 'blockquote' })`\n font-weight: ${({ theme }) => theme.fontWeights.regular};\n border-left: ${({ theme }) => `${theme.spaces[1]} solid ${theme.colors.neutral200}`};\n padding: ${({ theme }) => theme.spaces[2]} ${({ theme }) => theme.spaces[4]};\n color: ${({ theme }) => theme.colors.neutral600};\n`;\n\nconst quoteBlocks: Pick<BlocksStore, 'quote'> = {\n quote: {\n renderElement: (props) => (\n // The div is needed to make sure the padding bottom from BlocksContent is applied properly\n // when the quote is the last block in the editor\n <div>\n <Blockquote {...props.attributes}>{props.children}</Blockquote>\n </div>\n ),\n icon: Quotes,\n label: {\n id: 'components.Blocks.blocks.quote',\n defaultMessage: 'Quote',\n },\n matchNode: (node) => node.type === 'quote',\n isInBlocksSelector: true,\n handleConvert(editor) {\n baseHandleConvert<Block<'quote'>>(editor, { type: 'quote' });\n },\n handleEnterKey(editor) {\n pressEnterTwiceToExit(editor);\n },\n snippets: ['>'],\n },\n};\n\nexport { quoteBlocks };\n"],"names":["Blockquote","styled","blockquote","attrs","role","theme","fontWeights","regular","spaces","colors","neutral200","neutral600","quoteBlocks","quote","renderElement","props","_jsx","div","attributes","children","icon","Quotes","label","id","defaultMessage","matchNode","node","type","isInBlocksSelector","handleConvert","editor","baseHandleConvert","handleEnterKey","pressEnterTwiceToExit","snippets"],"mappings":";;;;;;AAQA,MAAMA,UAAaC,GAAAA,MAAAA,CAAOC,UAAU,CAACC,KAAK,CAAC;IAAEC,IAAM,EAAA;AAAa,CAAA,CAAE;eACnD,EAAE,CAAC,EAAEC,KAAK,EAAE,GAAKA,KAAMC,CAAAA,WAAW,CAACC,OAAO,CAAC;eAC3C,EAAE,CAAC,EAAEF,KAAK,EAAE,GAAK,CAAC,EAAEA,MAAMG,MAAM,CAAC,EAAE,CAAC,OAAO,EAAEH,KAAMI,CAAAA,MAAM,CAACC,UAAU,CAAC,CAAC,CAAC;WAC3E,EAAE,CAAC,EAAEL,KAAK,EAAE,GAAKA,KAAMG,CAAAA,MAAM,CAAC,CAAE,CAAA,CAAC,CAAC,EAAE,CAAC,EAAEH,KAAK,EAAE,GAAKA,KAAMG,CAAAA,MAAM,CAAC,CAAA,CAAE,CAAC;SACrE,EAAE,CAAC,EAAEH,KAAK,EAAE,GAAKA,KAAMI,CAAAA,MAAM,CAACE,UAAU,CAAC;AAClD,CAAC;AAED,MAAMC,WAA0C,GAAA;IAC9CC,KAAO,EAAA;QACLC,aAAe,EAAA,CAACC;;0BAGdC,GAACC,CAAAA,KAAAA,EAAAA;AACC,gBAAA,QAAA,gBAAAD,GAAChB,CAAAA,UAAAA,EAAAA;AAAY,oBAAA,GAAGe,MAAMG,UAAU;AAAGH,oBAAAA,QAAAA,EAAAA,KAAAA,CAAMI;;;QAG7CC,IAAMC,EAAAA,MAAAA;QACNC,KAAO,EAAA;YACLC,EAAI,EAAA,gCAAA;YACJC,cAAgB,EAAA;AAClB,SAAA;AACAC,QAAAA,SAAAA,EAAW,CAACC,IAAAA,GAASA,IAAKC,CAAAA,IAAI,KAAK,OAAA;QACnCC,kBAAoB,EAAA,IAAA;AACpBC,QAAAA,aAAAA,CAAAA,CAAcC,MAAM,EAAA;AAClBC,YAAAA,iBAAAA,CAAkCD,MAAQ,EAAA;gBAAEH,IAAM,EAAA;AAAQ,aAAA,CAAA;AAC5D,SAAA;AACAK,QAAAA,cAAAA,CAAAA,CAAeF,MAAM,EAAA;YACnBG,qBAAsBH,CAAAA,MAAAA,CAAAA;AACxB,SAAA;QACAI,QAAU,EAAA;AAAC,YAAA;AAAI;AACjB;AACF;;;;"}
@@ -0,0 +1,532 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var reactIntl = require('react-intl');
var slate = require('slate');
var slateReact = require('slate-react');
var styledComponents = require('styled-components');
var dragAndDrop = require('../../../../../constants/dragAndDrop.js');
var useDragAndDrop = require('../../../../../hooks/useDragAndDrop.js');
var translations = require('../../../../../utils/translations.js');
var Code = require('./Blocks/Code.js');
var BlocksEditor = require('./BlocksEditor.js');
var BlocksToolbar = require('./BlocksToolbar.js');
var types = require('./utils/types.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const StyledEditable = styledComponents.styled(slateReact.Editable)`
// The outline style is set on the wrapper with :focus-within
outline: none;
display: flex;
flex-direction: column;
gap: ${({ theme })=>theme.spaces[3]};
height: 100%;
// For fullscreen align input in the center with fixed width
width: ${(props)=>props.isExpandedMode ? '512px' : '100%'};
margin: auto;
> *:last-child {
padding-bottom: ${({ theme })=>theme.spaces[3]};
}
`;
const Wrapper = styledComponents.styled(designSystem.Box)`
position: ${({ isOverDropTarget })=>isOverDropTarget && 'relative'};
`;
const DropPlaceholder = styledComponents.styled(designSystem.Box)`
position: absolute;
right: 0;
// Show drop placeholder 8px above or below the drop target
${({ dragDirection, theme, placeholderMargin })=>styledComponents.css`
top: ${dragDirection === useDragAndDrop.DIRECTIONS.UPWARD && `-${theme.spaces[placeholderMargin]}`};
bottom: ${dragDirection === useDragAndDrop.DIRECTIONS.DOWNWARD && `-${theme.spaces[placeholderMargin]}`};
`}
`;
const DragItem = styledComponents.styled(designSystem.Flex)`
// Style each block rendered using renderElement()
& > [data-slate-node='element'] {
width: 100%;
opacity: inherit;
}
// Set the visibility of drag button
[role='button'] {
visibility: ${(props)=>props.$dragVisibility};
opacity: inherit;
}
&[aria-disabled='true'] {
user-drag: none;
}
`;
const DragIconButton = styledComponents.styled(designSystem.IconButton)`
user-select: none;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: ${({ theme })=>theme.borderRadius};
padding-left: ${({ theme })=>theme.spaces[0]};
padding-right: ${({ theme })=>theme.spaces[0]};
padding-top: ${({ theme })=>theme.spaces[1]};
padding-bottom: ${({ theme })=>theme.spaces[1]};
visibility: hidden;
cursor: grab;
opacity: inherit;
margin-top: ${(props)=>props.$dragHandleTopMargin ?? 0};
&:hover {
background: ${({ theme })=>theme.colors.neutral100};
}
&:active {
cursor: grabbing;
background: ${({ theme })=>theme.colors.neutral150};
}
&[aria-disabled='true'] {
visibility: hidden;
}
svg {
min-width: ${({ theme })=>theme.spaces[3]};
path {
fill: ${({ theme })=>theme.colors.neutral500};
}
}
`;
const DragAndDropElement = ({ children, index, setDragDirection, dragDirection, dragHandleTopMargin })=>{
const { editor, disabled, name: name1, setLiveText } = BlocksEditor.useBlocksEditorContext('drag-and-drop');
const { formatMessage } = reactIntl.useIntl();
const [dragVisibility, setDragVisibility] = React__namespace.useState('hidden');
const handleMoveBlock = React__namespace.useCallback((newIndex, currentIndex)=>{
slate.Transforms.moveNodes(editor, {
at: currentIndex,
to: newIndex
});
// Add 1 to the index for the live text message
const currentIndexPosition = [
currentIndex[0] + 1,
...currentIndex.slice(1)
];
const newIndexPosition = [
newIndex[0] + 1,
...newIndex.slice(1)
];
setLiveText(formatMessage({
id: translations.getTranslation('components.Blocks.dnd.reorder'),
defaultMessage: '{item}, moved. New position in the editor: {position}.'
}, {
item: `${name1}.${currentIndexPosition.join(',')}`,
position: `${newIndexPosition.join(',')} of ${editor.children.length}`
}));
}, [
editor,
formatMessage,
name1,
setLiveText
]);
const [{ handlerId, isDragging, isOverDropTarget, direction }, blockRef, dropRef, dragRef] = useDragAndDrop.useDragAndDrop(!disabled, {
type: `${dragAndDrop.ItemTypes.BLOCKS}_${name1}`,
index,
item: {
index,
displayedValue: children
},
onDropItem (currentIndex, newIndex) {
if (newIndex) handleMoveBlock(newIndex, currentIndex);
}
});
const composedBoxRefs = designSystem.useComposedRefs(blockRef, dropRef);
// Set Drag direction before loosing state while dragging
React__namespace.useEffect(()=>{
if (direction) {
setDragDirection(direction);
}
}, [
direction,
setDragDirection
]);
// On selection change hide drag handle
React__namespace.useEffect(()=>{
setDragVisibility('hidden');
}, [
editor.selection
]);
return /*#__PURE__*/ jsxRuntime.jsxs(Wrapper, {
ref: composedBoxRefs,
isOverDropTarget: isOverDropTarget,
children: [
isOverDropTarget && /*#__PURE__*/ jsxRuntime.jsx(DropPlaceholder, {
borderStyle: "solid",
borderColor: "secondary200",
borderWidth: "2px",
width: "calc(100% - 24px)",
marginLeft: "auto",
dragDirection: dragDirection,
// For list items placeholder reduce the margin around
placeholderMargin: children.props.as && children.props.as === 'li' ? 1 : 2
}),
isDragging ? /*#__PURE__*/ jsxRuntime.jsx(CloneDragItem, {
dragHandleTopMargin: dragHandleTopMargin,
children: children
}) : /*#__PURE__*/ jsxRuntime.jsxs(DragItem, {
ref: dragRef,
"data-handler-id": handlerId,
gap: 2,
paddingLeft: 2,
alignItems: "start",
onDragStart: (event)=>{
const target = event.target;
const currentTarget = event.currentTarget;
// Dragging action should only trigger drag event when button is dragged, however update styles on the whole dragItem.
if (target.getAttribute('role') !== 'button') {
event.preventDefault();
} else {
// Setting styles using dragging state is not working, so set it on current target element as nodes get dragged
currentTarget.style.opacity = '0.5';
}
},
onDragEnd: (event)=>{
const currentTarget = event.currentTarget;
currentTarget.style.opacity = '1';
},
onMouseMove: ()=>setDragVisibility('visible'),
onSelect: ()=>setDragVisibility('visible'),
onMouseLeave: ()=>setDragVisibility('hidden'),
"aria-disabled": disabled,
$dragVisibility: dragVisibility,
children: [
/*#__PURE__*/ jsxRuntime.jsx(DragIconButton, {
tag: "div",
contentEditable: false,
role: "button",
tabIndex: 0,
withTooltip: false,
label: formatMessage({
id: translations.getTranslation('components.DragHandle-label'),
defaultMessage: 'Drag'
}),
onClick: (e)=>e.stopPropagation(),
"aria-disabled": disabled,
disabled: disabled,
draggable: true,
// For some blocks top margin added to drag handle to align at the text level
$dragHandleTopMargin: dragHandleTopMargin,
children: /*#__PURE__*/ jsxRuntime.jsx(Icons.Drag, {
color: "primary500"
})
}),
children
]
})
]
});
};
// To prevent applying opacity to the original item being dragged, display a cloned element without opacity.
const CloneDragItem = ({ children, dragHandleTopMargin })=>{
const { formatMessage } = reactIntl.useIntl();
return /*#__PURE__*/ jsxRuntime.jsxs(DragItem, {
gap: 2,
paddingLeft: 2,
alignItems: "start",
$dragVisibility: "visible",
children: [
/*#__PURE__*/ jsxRuntime.jsx(DragIconButton, {
tag: "div",
role: "button",
withTooltip: false,
label: formatMessage({
id: translations.getTranslation('components.DragHandle-label'),
defaultMessage: 'Drag'
}),
$dragHandleTopMargin: dragHandleTopMargin,
children: /*#__PURE__*/ jsxRuntime.jsx(Icons.Drag, {
color: "neutral600"
})
}),
children
]
});
};
const baseRenderLeaf = (props, modifiers)=>{
// Recursively wrap the children for each active modifier
const wrappedChildren = types.getEntries(modifiers).reduce((currentChildren, modifierEntry)=>{
const [name1, modifier] = modifierEntry;
if (props.leaf[name1]) {
return modifier.renderLeaf(currentChildren);
}
return currentChildren;
}, props.children);
return /*#__PURE__*/ jsxRuntime.jsx("span", {
...props.attributes,
className: props.leaf.className,
children: wrappedChildren
});
};
const baseRenderElement = ({ props, blocks, editor, setDragDirection, dragDirection })=>{
const { element } = props;
const blockMatch = Object.values(blocks).find((block)=>block.matchNode(element));
const block = blockMatch || blocks.paragraph;
const nodePath = slateReact.ReactEditor.findPath(editor, element);
// Link is inline block so it cannot be dragged
// List items and nested list blocks i.e. lists with indent level higher than 0 are skipped from dragged items
if (types.isLinkNode(element) || types.isListNode(element) && element.indentLevel && element.indentLevel > 0 || element.type === 'list-item') {
return block.renderElement(props);
}
return /*#__PURE__*/ jsxRuntime.jsx(DragAndDropElement, {
index: nodePath,
setDragDirection: setDragDirection,
dragDirection: dragDirection,
dragHandleTopMargin: block.dragHandleTopMargin,
children: block.renderElement(props)
});
};
const BlocksContent = ({ placeholder, ariaLabelId })=>{
const { editor, disabled, blocks, modifiers, setLiveText, isExpandedMode } = BlocksEditor.useBlocksEditorContext('BlocksContent');
const blocksRef = React__namespace.useRef(null);
const { formatMessage } = reactIntl.useIntl();
const [dragDirection, setDragDirection] = React__namespace.useState(null);
const { modalElement, handleConversionResult } = BlocksToolbar.useConversionModal();
// Create renderLeaf function based on the modifiers store
const renderLeaf = React__namespace.useCallback((props)=>baseRenderLeaf(props, modifiers), [
modifiers
]);
const handleMoveBlocks = (editor, event)=>{
if (!editor.selection) return;
const start = slate.Range.start(editor.selection);
const currentIndex = [
start.path[0]
];
let newIndexPosition = 0;
if (event.key === 'ArrowUp') {
newIndexPosition = currentIndex[0] > 0 ? currentIndex[0] - 1 : currentIndex[0];
} else {
newIndexPosition = currentIndex[0] < editor.children.length - 1 ? currentIndex[0] + 1 : currentIndex[0];
}
const newIndex = [
newIndexPosition
];
if (newIndexPosition !== currentIndex[0]) {
slate.Transforms.moveNodes(editor, {
at: currentIndex,
to: newIndex
});
setLiveText(formatMessage({
id: translations.getTranslation('components.Blocks.dnd.reorder'),
defaultMessage: '{item}, moved. New position in the editor: {position}.'
}, {
item: `${name}.${currentIndex[0] + 1}`,
position: `${newIndex[0] + 1} of ${editor.children.length}`
}));
event.preventDefault();
}
};
// Create renderElement function base on the blocks store
const renderElement = React__namespace.useCallback((props)=>baseRenderElement({
props,
blocks,
editor,
dragDirection,
setDragDirection
}), [
blocks,
editor,
dragDirection,
setDragDirection
]);
const checkSnippet = (event)=>{
// Get current text block
if (!editor.selection) {
return;
}
const [textNode, textNodePath] = slate.Editor.node(editor, editor.selection.anchor.path);
// Narrow the type to a text node
if (slate.Editor.isEditor(textNode) || textNode.type !== 'text') {
return;
}
// Don't check for snippets if we're not at the start of a block
if (textNodePath.at(-1) !== 0) {
return;
}
// Check if the text node starts with a known snippet
const blockMatchingSnippet = Object.values(blocks).find((block)=>{
return block.snippets?.includes(textNode.text);
});
if (blockMatchingSnippet?.handleConvert) {
// Prevent the space from being created and delete the snippet
event.preventDefault();
slate.Transforms.delete(editor, {
distance: textNode.text.length,
unit: 'character',
reverse: true
});
// Convert the selected block
const maybeRenderModal = blockMatchingSnippet.handleConvert(editor);
handleConversionResult(maybeRenderModal);
}
};
const handleEnter = (event)=>{
if (!editor.selection) {
return;
}
const selectedNode = editor.children[editor.selection.anchor.path[0]];
const selectedBlock = Object.values(blocks).find((block)=>block.matchNode(selectedNode));
if (!selectedBlock) {
return;
}
// Allow forced line breaks when shift is pressed
if (event.shiftKey && selectedNode.type !== 'image') {
slate.Transforms.insertText(editor, '\n');
return;
}
// Check if there's an enter handler for the selected block
if (selectedBlock.handleEnterKey) {
selectedBlock.handleEnterKey(editor);
} else {
blocks.paragraph.handleEnterKey(editor);
}
};
const handleBackspaceEvent = (event)=>{
if (!editor.selection) {
return;
}
const selectedNode = editor.children[editor.selection.anchor.path[0]];
const selectedBlock = Object.values(blocks).find((block)=>block.matchNode(selectedNode));
if (!selectedBlock) {
return;
}
if (selectedBlock.handleBackspaceKey) {
selectedBlock.handleBackspaceKey(editor, event);
}
};
const handleTab = (event)=>{
if (!editor.selection) {
return;
}
const selectedNode = editor.children[editor.selection.anchor.path[0]];
const selectedBlock = Object.values(blocks).find((block)=>block.matchNode(selectedNode));
if (!selectedBlock) {
return;
}
if (selectedBlock.handleTab) {
event.preventDefault();
selectedBlock.handleTab(editor);
}
};
const handleKeyboardShortcuts = (event)=>{
const isCtrlOrCmd = event.metaKey || event.ctrlKey;
if (isCtrlOrCmd) {
// Check if there's a modifier to toggle
Object.values(modifiers).forEach((value)=>{
if (value.isValidEventKey(event)) {
value.handleToggle(editor);
return;
}
});
if (event.shiftKey && [
'ArrowUp',
'ArrowDown'
].includes(event.key)) {
handleMoveBlocks(editor, event);
}
}
};
const handleKeyDown = (event)=>{
// Find the right block-specific handlers for enter and backspace key presses
switch(event.key){
case 'Enter':
event.preventDefault();
return handleEnter(event);
case 'Backspace':
return handleBackspaceEvent(event);
case 'Tab':
return handleTab(event);
case 'Escape':
return slateReact.ReactEditor.blur(editor);
}
handleKeyboardShortcuts(event);
// Check if a snippet was triggered
if (event.key === ' ') {
checkSnippet(event);
}
};
/**
* scrollSelectionIntoView : Slate's default method to scroll a DOM selection into the view,
* thats shifting layout for us when there is a overflowY:scroll on the viewport.
* We are overriding it to check if the selection is not fully within the visible area of the editor,
* we use scrollBy one line to the bottom
*/ const handleScrollSelectionIntoView = ()=>{
if (!editor.selection) return;
const domRange = slateReact.ReactEditor.toDOMRange(editor, editor.selection);
const domRect = domRange.getBoundingClientRect();
const blocksInput = blocksRef.current;
if (!blocksInput) {
return;
}
const editorRect = blocksInput.getBoundingClientRect();
// Check if the selection is not fully within the visible area of the editor
if (domRect.top < editorRect.top || domRect.bottom > editorRect.bottom) {
// Scroll by one line to the bottom
blocksInput.scrollBy({
top: 28,
behavior: 'smooth'
});
}
};
return /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Box, {
ref: blocksRef,
grow: 1,
width: "100%",
overflow: "auto",
fontSize: 2,
background: "neutral0",
color: "neutral800",
lineHeight: 6,
paddingRight: 7,
paddingTop: 6,
paddingBottom: 3,
children: [
/*#__PURE__*/ jsxRuntime.jsx(StyledEditable, {
"aria-labelledby": ariaLabelId,
readOnly: disabled,
placeholder: placeholder,
isExpandedMode: isExpandedMode,
decorate: Code.decorateCode,
renderElement: renderElement,
renderLeaf: renderLeaf,
onKeyDown: handleKeyDown,
scrollSelectionIntoView: handleScrollSelectionIntoView,
// As we have our own handler to drag and drop the elements returing true will skip slate's own event handler
onDrop: ()=>{
return true;
},
onDragStart: ()=>{
return true;
}
}),
modalElement
]
});
};
exports.BlocksContent = BlocksContent;
//# sourceMappingURL=BlocksContent.js.map
@@ -0,0 +1 @@
{"version":3,"file":"BlocksContent.js","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport {\n Box,\n BoxComponent,\n Flex,\n FlexComponent,\n IconButton,\n IconButtonComponent,\n useComposedRefs,\n} from '@strapi/design-system';\nimport { Drag } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\nimport { Editor, Range, Transforms } from 'slate';\nimport { ReactEditor, type RenderElementProps, type RenderLeafProps, Editable } from 'slate-react';\nimport { styled, CSSProperties, css } from 'styled-components';\n\nimport { ItemTypes } from '../../../../../constants/dragAndDrop';\nimport { useDragAndDrop, DIRECTIONS } from '../../../../../hooks/useDragAndDrop';\nimport { getTranslation } from '../../../../../utils/translations';\n\nimport { decorateCode } from './Blocks/Code';\nimport { type BlocksStore, useBlocksEditorContext } from './BlocksEditor';\nimport { useConversionModal } from './BlocksToolbar';\nimport { type ModifiersStore } from './Modifiers';\nimport { getEntries, isLinkNode, isListNode } from './utils/types';\n\nconst StyledEditable = styled(Editable)<{ isExpandedMode: boolean }>`\n // The outline style is set on the wrapper with :focus-within\n outline: none;\n display: flex;\n flex-direction: column;\n gap: ${({ theme }) => theme.spaces[3]};\n height: 100%;\n // For fullscreen align input in the center with fixed width\n width: ${(props) => (props.isExpandedMode ? '512px' : '100%')};\n margin: auto;\n\n > *:last-child {\n padding-bottom: ${({ theme }) => theme.spaces[3]};\n }\n`;\n\nconst Wrapper = styled<BoxComponent>(Box)<{ isOverDropTarget: boolean }>`\n position: ${({ isOverDropTarget }) => isOverDropTarget && 'relative'};\n`;\n\ntype DragDirection = (typeof DIRECTIONS)[keyof typeof DIRECTIONS];\n\nconst DropPlaceholder = styled<BoxComponent>(Box)<{\n dragDirection: DragDirection | null;\n placeholderMargin: 1 | 2;\n}>`\n position: absolute;\n right: 0;\n\n // Show drop placeholder 8px above or below the drop target\n ${({ dragDirection, theme, placeholderMargin }) => css`\n top: ${dragDirection === DIRECTIONS.UPWARD && `-${theme.spaces[placeholderMargin]}`};\n bottom: ${dragDirection === DIRECTIONS.DOWNWARD && `-${theme.spaces[placeholderMargin]}`};\n `}\n`;\n\nconst DragItem = styled<FlexComponent>(Flex)<{ $dragVisibility: CSSProperties['visibility'] }>`\n // Style each block rendered using renderElement()\n & > [data-slate-node='element'] {\n width: 100%;\n opacity: inherit;\n }\n\n // Set the visibility of drag button\n [role='button'] {\n visibility: ${(props) => props.$dragVisibility};\n opacity: inherit;\n }\n &[aria-disabled='true'] {\n user-drag: none;\n }\n`;\n\nconst DragIconButton = styled<IconButtonComponent<'div'>>(IconButton)<{\n $dragHandleTopMargin?: CSSProperties['marginTop'];\n}>`\n user-select: none;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: ${({ theme }) => theme.borderRadius};\n padding-left: ${({ theme }) => theme.spaces[0]};\n padding-right: ${({ theme }) => theme.spaces[0]};\n padding-top: ${({ theme }) => theme.spaces[1]};\n padding-bottom: ${({ theme }) => theme.spaces[1]};\n visibility: hidden;\n cursor: grab;\n opacity: inherit;\n margin-top: ${(props) => props.$dragHandleTopMargin ?? 0};\n\n &:hover {\n background: ${({ theme }) => theme.colors.neutral100};\n }\n &:active {\n cursor: grabbing;\n background: ${({ theme }) => theme.colors.neutral150};\n }\n &[aria-disabled='true'] {\n visibility: hidden;\n }\n svg {\n min-width: ${({ theme }) => theme.spaces[3]};\n\n path {\n fill: ${({ theme }) => theme.colors.neutral500};\n }\n }\n`;\n\ntype Direction = {\n setDragDirection: (direction: DragDirection) => void;\n dragDirection: DragDirection | null;\n};\n\ntype DragAndDropElementProps = Direction & {\n children: RenderElementProps['children'];\n index: Array<number>;\n dragHandleTopMargin?: CSSProperties['marginTop'];\n};\n\nconst DragAndDropElement = ({\n children,\n index,\n setDragDirection,\n dragDirection,\n dragHandleTopMargin,\n}: DragAndDropElementProps) => {\n const { editor, disabled, name, setLiveText } = useBlocksEditorContext('drag-and-drop');\n const { formatMessage } = useIntl();\n const [dragVisibility, setDragVisibility] = React.useState<CSSProperties['visibility']>('hidden');\n\n const handleMoveBlock = React.useCallback(\n (newIndex: Array<number>, currentIndex: Array<number>) => {\n Transforms.moveNodes(editor, {\n at: currentIndex,\n to: newIndex,\n });\n\n // Add 1 to the index for the live text message\n const currentIndexPosition = [currentIndex[0] + 1, ...currentIndex.slice(1)];\n const newIndexPosition = [newIndex[0] + 1, ...newIndex.slice(1)];\n\n setLiveText(\n formatMessageLine truncated
@@ -0,0 +1,511 @@
import { jsxs, jsx } from 'react/jsx-runtime';
import * as React from 'react';
import { Box, Flex, IconButton, useComposedRefs } from '@strapi/design-system';
import { Drag } from '@strapi/icons';
import { useIntl } from 'react-intl';
import { Transforms, Editor, Range } from 'slate';
import { Editable, ReactEditor } from 'slate-react';
import { styled, css } from 'styled-components';
import { ItemTypes } from '../../../../../constants/dragAndDrop.mjs';
import { DIRECTIONS, useDragAndDrop } from '../../../../../hooks/useDragAndDrop.mjs';
import { getTranslation } from '../../../../../utils/translations.mjs';
import { decorateCode } from './Blocks/Code.mjs';
import { useBlocksEditorContext } from './BlocksEditor.mjs';
import { useConversionModal } from './BlocksToolbar.mjs';
import { getEntries, isLinkNode, isListNode } from './utils/types.mjs';
const StyledEditable = styled(Editable)`
// The outline style is set on the wrapper with :focus-within
outline: none;
display: flex;
flex-direction: column;
gap: ${({ theme })=>theme.spaces[3]};
height: 100%;
// For fullscreen align input in the center with fixed width
width: ${(props)=>props.isExpandedMode ? '512px' : '100%'};
margin: auto;
> *:last-child {
padding-bottom: ${({ theme })=>theme.spaces[3]};
}
`;
const Wrapper = styled(Box)`
position: ${({ isOverDropTarget })=>isOverDropTarget && 'relative'};
`;
const DropPlaceholder = styled(Box)`
position: absolute;
right: 0;
// Show drop placeholder 8px above or below the drop target
${({ dragDirection, theme, placeholderMargin })=>css`
top: ${dragDirection === DIRECTIONS.UPWARD && `-${theme.spaces[placeholderMargin]}`};
bottom: ${dragDirection === DIRECTIONS.DOWNWARD && `-${theme.spaces[placeholderMargin]}`};
`}
`;
const DragItem = styled(Flex)`
// Style each block rendered using renderElement()
& > [data-slate-node='element'] {
width: 100%;
opacity: inherit;
}
// Set the visibility of drag button
[role='button'] {
visibility: ${(props)=>props.$dragVisibility};
opacity: inherit;
}
&[aria-disabled='true'] {
user-drag: none;
}
`;
const DragIconButton = styled(IconButton)`
user-select: none;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: ${({ theme })=>theme.borderRadius};
padding-left: ${({ theme })=>theme.spaces[0]};
padding-right: ${({ theme })=>theme.spaces[0]};
padding-top: ${({ theme })=>theme.spaces[1]};
padding-bottom: ${({ theme })=>theme.spaces[1]};
visibility: hidden;
cursor: grab;
opacity: inherit;
margin-top: ${(props)=>props.$dragHandleTopMargin ?? 0};
&:hover {
background: ${({ theme })=>theme.colors.neutral100};
}
&:active {
cursor: grabbing;
background: ${({ theme })=>theme.colors.neutral150};
}
&[aria-disabled='true'] {
visibility: hidden;
}
svg {
min-width: ${({ theme })=>theme.spaces[3]};
path {
fill: ${({ theme })=>theme.colors.neutral500};
}
}
`;
const DragAndDropElement = ({ children, index, setDragDirection, dragDirection, dragHandleTopMargin })=>{
const { editor, disabled, name: name1, setLiveText } = useBlocksEditorContext('drag-and-drop');
const { formatMessage } = useIntl();
const [dragVisibility, setDragVisibility] = React.useState('hidden');
const handleMoveBlock = React.useCallback((newIndex, currentIndex)=>{
Transforms.moveNodes(editor, {
at: currentIndex,
to: newIndex
});
// Add 1 to the index for the live text message
const currentIndexPosition = [
currentIndex[0] + 1,
...currentIndex.slice(1)
];
const newIndexPosition = [
newIndex[0] + 1,
...newIndex.slice(1)
];
setLiveText(formatMessage({
id: getTranslation('components.Blocks.dnd.reorder'),
defaultMessage: '{item}, moved. New position in the editor: {position}.'
}, {
item: `${name1}.${currentIndexPosition.join(',')}`,
position: `${newIndexPosition.join(',')} of ${editor.children.length}`
}));
}, [
editor,
formatMessage,
name1,
setLiveText
]);
const [{ handlerId, isDragging, isOverDropTarget, direction }, blockRef, dropRef, dragRef] = useDragAndDrop(!disabled, {
type: `${ItemTypes.BLOCKS}_${name1}`,
index,
item: {
index,
displayedValue: children
},
onDropItem (currentIndex, newIndex) {
if (newIndex) handleMoveBlock(newIndex, currentIndex);
}
});
const composedBoxRefs = useComposedRefs(blockRef, dropRef);
// Set Drag direction before loosing state while dragging
React.useEffect(()=>{
if (direction) {
setDragDirection(direction);
}
}, [
direction,
setDragDirection
]);
// On selection change hide drag handle
React.useEffect(()=>{
setDragVisibility('hidden');
}, [
editor.selection
]);
return /*#__PURE__*/ jsxs(Wrapper, {
ref: composedBoxRefs,
isOverDropTarget: isOverDropTarget,
children: [
isOverDropTarget && /*#__PURE__*/ jsx(DropPlaceholder, {
borderStyle: "solid",
borderColor: "secondary200",
borderWidth: "2px",
width: "calc(100% - 24px)",
marginLeft: "auto",
dragDirection: dragDirection,
// For list items placeholder reduce the margin around
placeholderMargin: children.props.as && children.props.as === 'li' ? 1 : 2
}),
isDragging ? /*#__PURE__*/ jsx(CloneDragItem, {
dragHandleTopMargin: dragHandleTopMargin,
children: children
}) : /*#__PURE__*/ jsxs(DragItem, {
ref: dragRef,
"data-handler-id": handlerId,
gap: 2,
paddingLeft: 2,
alignItems: "start",
onDragStart: (event)=>{
const target = event.target;
const currentTarget = event.currentTarget;
// Dragging action should only trigger drag event when button is dragged, however update styles on the whole dragItem.
if (target.getAttribute('role') !== 'button') {
event.preventDefault();
} else {
// Setting styles using dragging state is not working, so set it on current target element as nodes get dragged
currentTarget.style.opacity = '0.5';
}
},
onDragEnd: (event)=>{
const currentTarget = event.currentTarget;
currentTarget.style.opacity = '1';
},
onMouseMove: ()=>setDragVisibility('visible'),
onSelect: ()=>setDragVisibility('visible'),
onMouseLeave: ()=>setDragVisibility('hidden'),
"aria-disabled": disabled,
$dragVisibility: dragVisibility,
children: [
/*#__PURE__*/ jsx(DragIconButton, {
tag: "div",
contentEditable: false,
role: "button",
tabIndex: 0,
withTooltip: false,
label: formatMessage({
id: getTranslation('components.DragHandle-label'),
defaultMessage: 'Drag'
}),
onClick: (e)=>e.stopPropagation(),
"aria-disabled": disabled,
disabled: disabled,
draggable: true,
// For some blocks top margin added to drag handle to align at the text level
$dragHandleTopMargin: dragHandleTopMargin,
children: /*#__PURE__*/ jsx(Drag, {
color: "primary500"
})
}),
children
]
})
]
});
};
// To prevent applying opacity to the original item being dragged, display a cloned element without opacity.
const CloneDragItem = ({ children, dragHandleTopMargin })=>{
const { formatMessage } = useIntl();
return /*#__PURE__*/ jsxs(DragItem, {
gap: 2,
paddingLeft: 2,
alignItems: "start",
$dragVisibility: "visible",
children: [
/*#__PURE__*/ jsx(DragIconButton, {
tag: "div",
role: "button",
withTooltip: false,
label: formatMessage({
id: getTranslation('components.DragHandle-label'),
defaultMessage: 'Drag'
}),
$dragHandleTopMargin: dragHandleTopMargin,
children: /*#__PURE__*/ jsx(Drag, {
color: "neutral600"
})
}),
children
]
});
};
const baseRenderLeaf = (props, modifiers)=>{
// Recursively wrap the children for each active modifier
const wrappedChildren = getEntries(modifiers).reduce((currentChildren, modifierEntry)=>{
const [name1, modifier] = modifierEntry;
if (props.leaf[name1]) {
return modifier.renderLeaf(currentChildren);
}
return currentChildren;
}, props.children);
return /*#__PURE__*/ jsx("span", {
...props.attributes,
className: props.leaf.className,
children: wrappedChildren
});
};
const baseRenderElement = ({ props, blocks, editor, setDragDirection, dragDirection })=>{
const { element } = props;
const blockMatch = Object.values(blocks).find((block)=>block.matchNode(element));
const block = blockMatch || blocks.paragraph;
const nodePath = ReactEditor.findPath(editor, element);
// Link is inline block so it cannot be dragged
// List items and nested list blocks i.e. lists with indent level higher than 0 are skipped from dragged items
if (isLinkNode(element) || isListNode(element) && element.indentLevel && element.indentLevel > 0 || element.type === 'list-item') {
return block.renderElement(props);
}
return /*#__PURE__*/ jsx(DragAndDropElement, {
index: nodePath,
setDragDirection: setDragDirection,
dragDirection: dragDirection,
dragHandleTopMargin: block.dragHandleTopMargin,
children: block.renderElement(props)
});
};
const BlocksContent = ({ placeholder, ariaLabelId })=>{
const { editor, disabled, blocks, modifiers, setLiveText, isExpandedMode } = useBlocksEditorContext('BlocksContent');
const blocksRef = React.useRef(null);
const { formatMessage } = useIntl();
const [dragDirection, setDragDirection] = React.useState(null);
const { modalElement, handleConversionResult } = useConversionModal();
// Create renderLeaf function based on the modifiers store
const renderLeaf = React.useCallback((props)=>baseRenderLeaf(props, modifiers), [
modifiers
]);
const handleMoveBlocks = (editor, event)=>{
if (!editor.selection) return;
const start = Range.start(editor.selection);
const currentIndex = [
start.path[0]
];
let newIndexPosition = 0;
if (event.key === 'ArrowUp') {
newIndexPosition = currentIndex[0] > 0 ? currentIndex[0] - 1 : currentIndex[0];
} else {
newIndexPosition = currentIndex[0] < editor.children.length - 1 ? currentIndex[0] + 1 : currentIndex[0];
}
const newIndex = [
newIndexPosition
];
if (newIndexPosition !== currentIndex[0]) {
Transforms.moveNodes(editor, {
at: currentIndex,
to: newIndex
});
setLiveText(formatMessage({
id: getTranslation('components.Blocks.dnd.reorder'),
defaultMessage: '{item}, moved. New position in the editor: {position}.'
}, {
item: `${name}.${currentIndex[0] + 1}`,
position: `${newIndex[0] + 1} of ${editor.children.length}`
}));
event.preventDefault();
}
};
// Create renderElement function base on the blocks store
const renderElement = React.useCallback((props)=>baseRenderElement({
props,
blocks,
editor,
dragDirection,
setDragDirection
}), [
blocks,
editor,
dragDirection,
setDragDirection
]);
const checkSnippet = (event)=>{
// Get current text block
if (!editor.selection) {
return;
}
const [textNode, textNodePath] = Editor.node(editor, editor.selection.anchor.path);
// Narrow the type to a text node
if (Editor.isEditor(textNode) || textNode.type !== 'text') {
return;
}
// Don't check for snippets if we're not at the start of a block
if (textNodePath.at(-1) !== 0) {
return;
}
// Check if the text node starts with a known snippet
const blockMatchingSnippet = Object.values(blocks).find((block)=>{
return block.snippets?.includes(textNode.text);
});
if (blockMatchingSnippet?.handleConvert) {
// Prevent the space from being created and delete the snippet
event.preventDefault();
Transforms.delete(editor, {
distance: textNode.text.length,
unit: 'character',
reverse: true
});
// Convert the selected block
const maybeRenderModal = blockMatchingSnippet.handleConvert(editor);
handleConversionResult(maybeRenderModal);
}
};
const handleEnter = (event)=>{
if (!editor.selection) {
return;
}
const selectedNode = editor.children[editor.selection.anchor.path[0]];
const selectedBlock = Object.values(blocks).find((block)=>block.matchNode(selectedNode));
if (!selectedBlock) {
return;
}
// Allow forced line breaks when shift is pressed
if (event.shiftKey && selectedNode.type !== 'image') {
Transforms.insertText(editor, '\n');
return;
}
// Check if there's an enter handler for the selected block
if (selectedBlock.handleEnterKey) {
selectedBlock.handleEnterKey(editor);
} else {
blocks.paragraph.handleEnterKey(editor);
}
};
const handleBackspaceEvent = (event)=>{
if (!editor.selection) {
return;
}
const selectedNode = editor.children[editor.selection.anchor.path[0]];
const selectedBlock = Object.values(blocks).find((block)=>block.matchNode(selectedNode));
if (!selectedBlock) {
return;
}
if (selectedBlock.handleBackspaceKey) {
selectedBlock.handleBackspaceKey(editor, event);
}
};
const handleTab = (event)=>{
if (!editor.selection) {
return;
}
const selectedNode = editor.children[editor.selection.anchor.path[0]];
const selectedBlock = Object.values(blocks).find((block)=>block.matchNode(selectedNode));
if (!selectedBlock) {
return;
}
if (selectedBlock.handleTab) {
event.preventDefault();
selectedBlock.handleTab(editor);
}
};
const handleKeyboardShortcuts = (event)=>{
const isCtrlOrCmd = event.metaKey || event.ctrlKey;
if (isCtrlOrCmd) {
// Check if there's a modifier to toggle
Object.values(modifiers).forEach((value)=>{
if (value.isValidEventKey(event)) {
value.handleToggle(editor);
return;
}
});
if (event.shiftKey && [
'ArrowUp',
'ArrowDown'
].includes(event.key)) {
handleMoveBlocks(editor, event);
}
}
};
const handleKeyDown = (event)=>{
// Find the right block-specific handlers for enter and backspace key presses
switch(event.key){
case 'Enter':
event.preventDefault();
return handleEnter(event);
case 'Backspace':
return handleBackspaceEvent(event);
case 'Tab':
return handleTab(event);
case 'Escape':
return ReactEditor.blur(editor);
}
handleKeyboardShortcuts(event);
// Check if a snippet was triggered
if (event.key === ' ') {
checkSnippet(event);
}
};
/**
* scrollSelectionIntoView : Slate's default method to scroll a DOM selection into the view,
* thats shifting layout for us when there is a overflowY:scroll on the viewport.
* We are overriding it to check if the selection is not fully within the visible area of the editor,
* we use scrollBy one line to the bottom
*/ const handleScrollSelectionIntoView = ()=>{
if (!editor.selection) return;
const domRange = ReactEditor.toDOMRange(editor, editor.selection);
const domRect = domRange.getBoundingClientRect();
const blocksInput = blocksRef.current;
if (!blocksInput) {
return;
}
const editorRect = blocksInput.getBoundingClientRect();
// Check if the selection is not fully within the visible area of the editor
if (domRect.top < editorRect.top || domRect.bottom > editorRect.bottom) {
// Scroll by one line to the bottom
blocksInput.scrollBy({
top: 28,
behavior: 'smooth'
});
}
};
return /*#__PURE__*/ jsxs(Box, {
ref: blocksRef,
grow: 1,
width: "100%",
overflow: "auto",
fontSize: 2,
background: "neutral0",
color: "neutral800",
lineHeight: 6,
paddingRight: 7,
paddingTop: 6,
paddingBottom: 3,
children: [
/*#__PURE__*/ jsx(StyledEditable, {
"aria-labelledby": ariaLabelId,
readOnly: disabled,
placeholder: placeholder,
isExpandedMode: isExpandedMode,
decorate: decorateCode,
renderElement: renderElement,
renderLeaf: renderLeaf,
onKeyDown: handleKeyDown,
scrollSelectionIntoView: handleScrollSelectionIntoView,
// As we have our own handler to drag and drop the elements returing true will skip slate's own event handler
onDrop: ()=>{
return true;
},
onDragStart: ()=>{
return true;
}
}),
modalElement
]
});
};
export { BlocksContent };
//# sourceMappingURL=BlocksContent.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"BlocksContent.mjs","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport {\n Box,\n BoxComponent,\n Flex,\n FlexComponent,\n IconButton,\n IconButtonComponent,\n useComposedRefs,\n} from '@strapi/design-system';\nimport { Drag } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\nimport { Editor, Range, Transforms } from 'slate';\nimport { ReactEditor, type RenderElementProps, type RenderLeafProps, Editable } from 'slate-react';\nimport { styled, CSSProperties, css } from 'styled-components';\n\nimport { ItemTypes } from '../../../../../constants/dragAndDrop';\nimport { useDragAndDrop, DIRECTIONS } from '../../../../../hooks/useDragAndDrop';\nimport { getTranslation } from '../../../../../utils/translations';\n\nimport { decorateCode } from './Blocks/Code';\nimport { type BlocksStore, useBlocksEditorContext } from './BlocksEditor';\nimport { useConversionModal } from './BlocksToolbar';\nimport { type ModifiersStore } from './Modifiers';\nimport { getEntries, isLinkNode, isListNode } from './utils/types';\n\nconst StyledEditable = styled(Editable)<{ isExpandedMode: boolean }>`\n // The outline style is set on the wrapper with :focus-within\n outline: none;\n display: flex;\n flex-direction: column;\n gap: ${({ theme }) => theme.spaces[3]};\n height: 100%;\n // For fullscreen align input in the center with fixed width\n width: ${(props) => (props.isExpandedMode ? '512px' : '100%')};\n margin: auto;\n\n > *:last-child {\n padding-bottom: ${({ theme }) => theme.spaces[3]};\n }\n`;\n\nconst Wrapper = styled<BoxComponent>(Box)<{ isOverDropTarget: boolean }>`\n position: ${({ isOverDropTarget }) => isOverDropTarget && 'relative'};\n`;\n\ntype DragDirection = (typeof DIRECTIONS)[keyof typeof DIRECTIONS];\n\nconst DropPlaceholder = styled<BoxComponent>(Box)<{\n dragDirection: DragDirection | null;\n placeholderMargin: 1 | 2;\n}>`\n position: absolute;\n right: 0;\n\n // Show drop placeholder 8px above or below the drop target\n ${({ dragDirection, theme, placeholderMargin }) => css`\n top: ${dragDirection === DIRECTIONS.UPWARD && `-${theme.spaces[placeholderMargin]}`};\n bottom: ${dragDirection === DIRECTIONS.DOWNWARD && `-${theme.spaces[placeholderMargin]}`};\n `}\n`;\n\nconst DragItem = styled<FlexComponent>(Flex)<{ $dragVisibility: CSSProperties['visibility'] }>`\n // Style each block rendered using renderElement()\n & > [data-slate-node='element'] {\n width: 100%;\n opacity: inherit;\n }\n\n // Set the visibility of drag button\n [role='button'] {\n visibility: ${(props) => props.$dragVisibility};\n opacity: inherit;\n }\n &[aria-disabled='true'] {\n user-drag: none;\n }\n`;\n\nconst DragIconButton = styled<IconButtonComponent<'div'>>(IconButton)<{\n $dragHandleTopMargin?: CSSProperties['marginTop'];\n}>`\n user-select: none;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: ${({ theme }) => theme.borderRadius};\n padding-left: ${({ theme }) => theme.spaces[0]};\n padding-right: ${({ theme }) => theme.spaces[0]};\n padding-top: ${({ theme }) => theme.spaces[1]};\n padding-bottom: ${({ theme }) => theme.spaces[1]};\n visibility: hidden;\n cursor: grab;\n opacity: inherit;\n margin-top: ${(props) => props.$dragHandleTopMargin ?? 0};\n\n &:hover {\n background: ${({ theme }) => theme.colors.neutral100};\n }\n &:active {\n cursor: grabbing;\n background: ${({ theme }) => theme.colors.neutral150};\n }\n &[aria-disabled='true'] {\n visibility: hidden;\n }\n svg {\n min-width: ${({ theme }) => theme.spaces[3]};\n\n path {\n fill: ${({ theme }) => theme.colors.neutral500};\n }\n }\n`;\n\ntype Direction = {\n setDragDirection: (direction: DragDirection) => void;\n dragDirection: DragDirection | null;\n};\n\ntype DragAndDropElementProps = Direction & {\n children: RenderElementProps['children'];\n index: Array<number>;\n dragHandleTopMargin?: CSSProperties['marginTop'];\n};\n\nconst DragAndDropElement = ({\n children,\n index,\n setDragDirection,\n dragDirection,\n dragHandleTopMargin,\n}: DragAndDropElementProps) => {\n const { editor, disabled, name, setLiveText } = useBlocksEditorContext('drag-and-drop');\n const { formatMessage } = useIntl();\n const [dragVisibility, setDragVisibility] = React.useState<CSSProperties['visibility']>('hidden');\n\n const handleMoveBlock = React.useCallback(\n (newIndex: Array<number>, currentIndex: Array<number>) => {\n Transforms.moveNodes(editor, {\n at: currentIndex,\n to: newIndex,\n });\n\n // Add 1 to the index for the live text message\n const currentIndexPosition = [currentIndex[0] + 1, ...currentIndex.slice(1)];\n const newIndexPosition = [newIndex[0] + 1, ...newIndex.slice(1)];\n\n setLiveText(\n formatMessagLine truncated
@@ -0,0 +1,217 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var strapiAdmin = require('@strapi/admin/strapi-admin');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var reactIntl = require('react-intl');
var slate = require('slate');
var slateHistory = require('slate-history');
var slateReact = require('slate-react');
var styledComponents = require('styled-components');
var translations = require('../../../../../utils/translations.js');
var Code = require('./Blocks/Code.js');
var Heading = require('./Blocks/Heading.js');
var Image = require('./Blocks/Image.js');
var Link = require('./Blocks/Link.js');
var List = require('./Blocks/List.js');
var Paragraph = require('./Blocks/Paragraph.js');
var Quote = require('./Blocks/Quote.js');
var BlocksContent = require('./BlocksContent.js');
var BlocksToolbar = require('./BlocksToolbar.js');
var EditorLayout = require('./EditorLayout.js');
var Modifiers = require('./Modifiers.js');
var withImages = require('./plugins/withImages.js');
var withLinks = require('./plugins/withLinks.js');
var withStrapiSchema = require('./plugins/withStrapiSchema.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const selectorBlockKeys = [
'paragraph',
'heading-one',
'heading-two',
'heading-three',
'heading-four',
'heading-five',
'heading-six',
'list-ordered',
'list-unordered',
'image',
'quote',
'code'
];
const isSelectorBlockKey = (key)=>{
return typeof key === 'string' && selectorBlockKeys.includes(key);
};
const [BlocksEditorProvider, usePartialBlocksEditorContext] = strapiAdmin.createContext('BlocksEditor');
function useBlocksEditorContext(consumerName) {
const context = usePartialBlocksEditorContext(consumerName, (state)=>state);
const editor = slateReact.useSlate();
return {
...context,
editor
};
}
/* -------------------------------------------------------------------------------------------------
* BlocksEditor
* -----------------------------------------------------------------------------------------------*/ const EditorDivider = styledComponents.styled(designSystem.Divider)`
background: ${({ theme })=>theme.colors.neutral200};
`;
/**
* Forces an update of the Slate editor when the value prop changes from outside of Slate.
* The root cause is that Slate is not a controlled component: https://github.com/ianstormtaylor/slate/issues/4612
* Why not use JSON.stringify(value) as the key?
* Because it would force a rerender of the entire editor every time the user types a character.
* Why not use the entity id as the key, since it's unique for each locale?
* Because it would not solve the problem when using the "fill in from other locale" feature
*/ function useResetKey(value) {
// Keep track how many times Slate detected a change from a user interaction in the editor
const slateUpdatesCount = React__namespace.useRef(0);
// Keep track of how many times the value prop was updated, whether from within editor or from outside
const valueUpdatesCount = React__namespace.useRef(0);
// Use a key to force a rerender of the Slate editor when needed
const [key, setKey] = React__namespace.useState(0);
React__namespace.useEffect(()=>{
valueUpdatesCount.current += 1;
// If the 2 refs are not equal, it means the value was updated from outside
if (valueUpdatesCount.current !== slateUpdatesCount.current) {
// So we change the key to force a rerender of the Slate editor,
// which will pick up the new value through its initialValue prop
setKey((previousKey)=>previousKey + 1);
// Then bring the 2 refs back in sync
slateUpdatesCount.current = valueUpdatesCount.current;
}
}, [
value
]);
return {
key,
incrementSlateUpdatesCount: ()=>slateUpdatesCount.current += 1
};
}
const pipe = (...fns)=>(value)=>fns.reduce((prev, fn)=>fn(prev), value);
const BlocksEditor = /*#__PURE__*/ React__namespace.forwardRef(({ disabled = false, name, onChange, value, error, ...contentProps }, forwardedRef)=>{
const { formatMessage } = reactIntl.useIntl();
const [editor] = React__namespace.useState(()=>pipe(slateHistory.withHistory, withImages.withImages, withStrapiSchema.withStrapiSchema, slateReact.withReact, withLinks.withLinks)(slate.createEditor()));
const [liveText, setLiveText] = React__namespace.useState('');
const ariaDescriptionId = React__namespace.useId();
const [isExpandedMode, handleToggleExpand] = React__namespace.useReducer((prev)=>!prev, false);
/**
* Editable is not able to hold the ref, https://github.com/ianstormtaylor/slate/issues/4082
* so with "useImperativeHandle" we can use ReactEditor methods to expose to the parent above
* also not passing forwarded ref here, gives console warning.
*/ React__namespace.useImperativeHandle(forwardedRef, ()=>({
focus () {
slateReact.ReactEditor.focus(editor);
}
}), [
editor
]);
const { key, incrementSlateUpdatesCount } = useResetKey(value);
const handleSlateChange = (state)=>{
const isAstChange = editor.operations.some((op)=>op.type !== 'set_selection');
if (isAstChange) {
incrementSlateUpdatesCount();
onChange(name, state);
}
};
const blocks = {
...Paragraph.paragraphBlocks,
...Heading.headingBlocks,
...List.listBlocks,
...Link.linkBlocks,
...Image.imageBlocks,
...Quote.quoteBlocks,
...Code.codeBlocks
};
return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.VisuallyHidden, {
id: ariaDescriptionId,
children: formatMessage({
id: translations.getTranslation('components.Blocks.dnd.instruction'),
defaultMessage: `To reorder blocks, press Command or Control along with Shift and the Up or Down arrow keys`
})
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.VisuallyHidden, {
"aria-live": "assertive",
children: liveText
}),
/*#__PURE__*/ jsxRuntime.jsx(slateReact.Slate, {
editor: editor,
initialValue: value || [
{
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
}
],
onChange: handleSlateChange,
children: /*#__PURE__*/ jsxRuntime.jsx(BlocksEditorProvider, {
blocks: blocks,
modifiers: Modifiers.modifiers,
disabled: disabled,
name: name,
setLiveText: setLiveText,
isExpandedMode: isExpandedMode,
children: /*#__PURE__*/ jsxRuntime.jsxs(EditorLayout.EditorLayout, {
error: error,
disabled: disabled,
onToggleExpand: handleToggleExpand,
ariaDescriptionId: ariaDescriptionId,
children: [
/*#__PURE__*/ jsxRuntime.jsx(BlocksToolbar.BlocksToolbar, {}),
/*#__PURE__*/ jsxRuntime.jsx(EditorDivider, {
width: "100%"
}),
/*#__PURE__*/ jsxRuntime.jsx(BlocksContent.BlocksContent, {
...contentProps
}),
!isExpandedMode && /*#__PURE__*/ jsxRuntime.jsx(designSystem.IconButton, {
position: "absolute",
bottom: "1.2rem",
right: "1.2rem",
shadow: "filterShadow",
label: formatMessage({
id: translations.getTranslation('components.Blocks.expand'),
defaultMessage: 'Expand'
}),
onClick: handleToggleExpand,
children: /*#__PURE__*/ jsxRuntime.jsx(Icons.Expand, {})
})
]
})
})
}, key)
]
});
});
exports.BlocksEditor = BlocksEditor;
exports.BlocksEditorProvider = BlocksEditorProvider;
exports.isSelectorBlockKey = isSelectorBlockKey;
exports.useBlocksEditorContext = useBlocksEditorContext;
//# sourceMappingURL=BlocksEditor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"BlocksEditor.js","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { createContext, type FieldValue } from '@strapi/admin/strapi-admin';\nimport { IconButton, Divider, VisuallyHidden } from '@strapi/design-system';\nimport { Expand } from '@strapi/icons';\nimport { MessageDescriptor, useIntl } from 'react-intl';\nimport { Editor, type Descendant, createEditor } from 'slate';\nimport { withHistory } from 'slate-history';\nimport { type RenderElementProps, Slate, withReact, ReactEditor, useSlate } from 'slate-react';\nimport { styled, type CSSProperties } from 'styled-components';\n\nimport { getTranslation } from '../../../../../utils/translations';\n\nimport { codeBlocks } from './Blocks/Code';\nimport { headingBlocks } from './Blocks/Heading';\nimport { imageBlocks } from './Blocks/Image';\nimport { linkBlocks } from './Blocks/Link';\nimport { listBlocks } from './Blocks/List';\nimport { paragraphBlocks } from './Blocks/Paragraph';\nimport { quoteBlocks } from './Blocks/Quote';\nimport { BlocksContent, type BlocksContentProps } from './BlocksContent';\nimport { BlocksToolbar } from './BlocksToolbar';\nimport { EditorLayout } from './EditorLayout';\nimport { type ModifiersStore, modifiers } from './Modifiers';\nimport { withImages } from './plugins/withImages';\nimport { withLinks } from './plugins/withLinks';\nimport { withStrapiSchema } from './plugins/withStrapiSchema';\n\nimport type { Schema } from '@strapi/types';\n\n/* -------------------------------------------------------------------------------------------------\n * BlocksEditorProvider\n * -----------------------------------------------------------------------------------------------*/\n\ninterface BaseBlock {\n renderElement: (props: RenderElementProps) => React.JSX.Element;\n matchNode: (node: Schema.Attribute.BlocksNode) => boolean;\n handleConvert?: (editor: Editor) => void | (() => React.JSX.Element);\n handleEnterKey?: (editor: Editor) => void;\n handleBackspaceKey?: (editor: Editor, event: React.KeyboardEvent<HTMLElement>) => void;\n handleTab?: (editor: Editor) => void;\n snippets?: string[];\n dragHandleTopMargin?: CSSProperties['marginTop'];\n}\n\ninterface NonSelectorBlock extends BaseBlock {\n isInBlocksSelector: false;\n}\n\ninterface SelectorBlock extends BaseBlock {\n isInBlocksSelector: true;\n icon: React.ComponentType;\n label: MessageDescriptor;\n}\n\ntype NonSelectorBlockKey = 'list-item' | 'link';\n\nconst selectorBlockKeys = [\n 'paragraph',\n 'heading-one',\n 'heading-two',\n 'heading-three',\n 'heading-four',\n 'heading-five',\n 'heading-six',\n 'list-ordered',\n 'list-unordered',\n 'image',\n 'quote',\n 'code',\n] as const;\n\ntype SelectorBlockKey = (typeof selectorBlockKeys)[number];\n\nconst isSelectorBlockKey = (key: unknown): key is SelectorBlockKey => {\n return typeof key === 'string' && selectorBlockKeys.includes(key as SelectorBlockKey);\n};\n\ntype BlocksStore = {\n [K in SelectorBlockKey]: SelectorBlock;\n} & {\n [K in NonSelectorBlockKey]: NonSelectorBlock;\n};\n\ninterface BlocksEditorContextValue {\n blocks: BlocksStore;\n modifiers: ModifiersStore;\n disabled: boolean;\n name: string;\n setLiveText: (text: string) => void;\n isExpandedMode: boolean;\n}\n\nconst [BlocksEditorProvider, usePartialBlocksEditorContext] =\n createContext<BlocksEditorContextValue>('BlocksEditor');\n\nfunction useBlocksEditorContext(\n consumerName: string\n): BlocksEditorContextValue & { editor: Editor } {\n const context = usePartialBlocksEditorContext(consumerName, (state) => state);\n const editor = useSlate();\n\n return {\n ...context,\n editor,\n };\n}\n\n/* -------------------------------------------------------------------------------------------------\n * BlocksEditor\n * -----------------------------------------------------------------------------------------------*/\n\nconst EditorDivider = styled(Divider)`\n background: ${({ theme }) => theme.colors.neutral200};\n`;\n\n/**\n * Forces an update of the Slate editor when the value prop changes from outside of Slate.\n * The root cause is that Slate is not a controlled component: https://github.com/ianstormtaylor/slate/issues/4612\n * Why not use JSON.stringify(value) as the key?\n * Because it would force a rerender of the entire editor every time the user types a character.\n * Why not use the entity id as the key, since it's unique for each locale?\n * Because it would not solve the problem when using the \"fill in from other locale\" feature\n */\nfunction useResetKey(value?: Schema.Attribute.BlocksValue): {\n key: number;\n incrementSlateUpdatesCount: () => void;\n} {\n // Keep track how many times Slate detected a change from a user interaction in the editor\n const slateUpdatesCount = React.useRef(0);\n // Keep track of how many times the value prop was updated, whether fromLine truncated
@@ -0,0 +1,193 @@
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import * as React from 'react';
import { createContext } from '@strapi/admin/strapi-admin';
import { Divider, VisuallyHidden, IconButton } from '@strapi/design-system';
import { Expand } from '@strapi/icons';
import { useIntl } from 'react-intl';
import { createEditor } from 'slate';
import { withHistory } from 'slate-history';
import { ReactEditor, Slate, useSlate, withReact } from 'slate-react';
import { styled } from 'styled-components';
import { getTranslation } from '../../../../../utils/translations.mjs';
import { codeBlocks } from './Blocks/Code.mjs';
import { headingBlocks } from './Blocks/Heading.mjs';
import { imageBlocks } from './Blocks/Image.mjs';
import { linkBlocks } from './Blocks/Link.mjs';
import { listBlocks } from './Blocks/List.mjs';
import { paragraphBlocks } from './Blocks/Paragraph.mjs';
import { quoteBlocks } from './Blocks/Quote.mjs';
import { BlocksContent } from './BlocksContent.mjs';
import { BlocksToolbar } from './BlocksToolbar.mjs';
import { EditorLayout } from './EditorLayout.mjs';
import { modifiers } from './Modifiers.mjs';
import { withImages } from './plugins/withImages.mjs';
import { withLinks } from './plugins/withLinks.mjs';
import { withStrapiSchema } from './plugins/withStrapiSchema.mjs';
const selectorBlockKeys = [
'paragraph',
'heading-one',
'heading-two',
'heading-three',
'heading-four',
'heading-five',
'heading-six',
'list-ordered',
'list-unordered',
'image',
'quote',
'code'
];
const isSelectorBlockKey = (key)=>{
return typeof key === 'string' && selectorBlockKeys.includes(key);
};
const [BlocksEditorProvider, usePartialBlocksEditorContext] = createContext('BlocksEditor');
function useBlocksEditorContext(consumerName) {
const context = usePartialBlocksEditorContext(consumerName, (state)=>state);
const editor = useSlate();
return {
...context,
editor
};
}
/* -------------------------------------------------------------------------------------------------
* BlocksEditor
* -----------------------------------------------------------------------------------------------*/ const EditorDivider = styled(Divider)`
background: ${({ theme })=>theme.colors.neutral200};
`;
/**
* Forces an update of the Slate editor when the value prop changes from outside of Slate.
* The root cause is that Slate is not a controlled component: https://github.com/ianstormtaylor/slate/issues/4612
* Why not use JSON.stringify(value) as the key?
* Because it would force a rerender of the entire editor every time the user types a character.
* Why not use the entity id as the key, since it's unique for each locale?
* Because it would not solve the problem when using the "fill in from other locale" feature
*/ function useResetKey(value) {
// Keep track how many times Slate detected a change from a user interaction in the editor
const slateUpdatesCount = React.useRef(0);
// Keep track of how many times the value prop was updated, whether from within editor or from outside
const valueUpdatesCount = React.useRef(0);
// Use a key to force a rerender of the Slate editor when needed
const [key, setKey] = React.useState(0);
React.useEffect(()=>{
valueUpdatesCount.current += 1;
// If the 2 refs are not equal, it means the value was updated from outside
if (valueUpdatesCount.current !== slateUpdatesCount.current) {
// So we change the key to force a rerender of the Slate editor,
// which will pick up the new value through its initialValue prop
setKey((previousKey)=>previousKey + 1);
// Then bring the 2 refs back in sync
slateUpdatesCount.current = valueUpdatesCount.current;
}
}, [
value
]);
return {
key,
incrementSlateUpdatesCount: ()=>slateUpdatesCount.current += 1
};
}
const pipe = (...fns)=>(value)=>fns.reduce((prev, fn)=>fn(prev), value);
const BlocksEditor = /*#__PURE__*/ React.forwardRef(({ disabled = false, name, onChange, value, error, ...contentProps }, forwardedRef)=>{
const { formatMessage } = useIntl();
const [editor] = React.useState(()=>pipe(withHistory, withImages, withStrapiSchema, withReact, withLinks)(createEditor()));
const [liveText, setLiveText] = React.useState('');
const ariaDescriptionId = React.useId();
const [isExpandedMode, handleToggleExpand] = React.useReducer((prev)=>!prev, false);
/**
* Editable is not able to hold the ref, https://github.com/ianstormtaylor/slate/issues/4082
* so with "useImperativeHandle" we can use ReactEditor methods to expose to the parent above
* also not passing forwarded ref here, gives console warning.
*/ React.useImperativeHandle(forwardedRef, ()=>({
focus () {
ReactEditor.focus(editor);
}
}), [
editor
]);
const { key, incrementSlateUpdatesCount } = useResetKey(value);
const handleSlateChange = (state)=>{
const isAstChange = editor.operations.some((op)=>op.type !== 'set_selection');
if (isAstChange) {
incrementSlateUpdatesCount();
onChange(name, state);
}
};
const blocks = {
...paragraphBlocks,
...headingBlocks,
...listBlocks,
...linkBlocks,
...imageBlocks,
...quoteBlocks,
...codeBlocks
};
return /*#__PURE__*/ jsxs(Fragment, {
children: [
/*#__PURE__*/ jsx(VisuallyHidden, {
id: ariaDescriptionId,
children: formatMessage({
id: getTranslation('components.Blocks.dnd.instruction'),
defaultMessage: `To reorder blocks, press Command or Control along with Shift and the Up or Down arrow keys`
})
}),
/*#__PURE__*/ jsx(VisuallyHidden, {
"aria-live": "assertive",
children: liveText
}),
/*#__PURE__*/ jsx(Slate, {
editor: editor,
initialValue: value || [
{
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
}
],
onChange: handleSlateChange,
children: /*#__PURE__*/ jsx(BlocksEditorProvider, {
blocks: blocks,
modifiers: modifiers,
disabled: disabled,
name: name,
setLiveText: setLiveText,
isExpandedMode: isExpandedMode,
children: /*#__PURE__*/ jsxs(EditorLayout, {
error: error,
disabled: disabled,
onToggleExpand: handleToggleExpand,
ariaDescriptionId: ariaDescriptionId,
children: [
/*#__PURE__*/ jsx(BlocksToolbar, {}),
/*#__PURE__*/ jsx(EditorDivider, {
width: "100%"
}),
/*#__PURE__*/ jsx(BlocksContent, {
...contentProps
}),
!isExpandedMode && /*#__PURE__*/ jsx(IconButton, {
position: "absolute",
bottom: "1.2rem",
right: "1.2rem",
shadow: "filterShadow",
label: formatMessage({
id: getTranslation('components.Blocks.expand'),
defaultMessage: 'Expand'
}),
onClick: handleToggleExpand,
children: /*#__PURE__*/ jsx(Expand, {})
})
]
})
})
}, key)
]
});
});
export { BlocksEditor, BlocksEditorProvider, isSelectorBlockKey, useBlocksEditorContext };
//# sourceMappingURL=BlocksEditor.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"BlocksEditor.mjs","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { createContext, type FieldValue } from '@strapi/admin/strapi-admin';\nimport { IconButton, Divider, VisuallyHidden } from '@strapi/design-system';\nimport { Expand } from '@strapi/icons';\nimport { MessageDescriptor, useIntl } from 'react-intl';\nimport { Editor, type Descendant, createEditor } from 'slate';\nimport { withHistory } from 'slate-history';\nimport { type RenderElementProps, Slate, withReact, ReactEditor, useSlate } from 'slate-react';\nimport { styled, type CSSProperties } from 'styled-components';\n\nimport { getTranslation } from '../../../../../utils/translations';\n\nimport { codeBlocks } from './Blocks/Code';\nimport { headingBlocks } from './Blocks/Heading';\nimport { imageBlocks } from './Blocks/Image';\nimport { linkBlocks } from './Blocks/Link';\nimport { listBlocks } from './Blocks/List';\nimport { paragraphBlocks } from './Blocks/Paragraph';\nimport { quoteBlocks } from './Blocks/Quote';\nimport { BlocksContent, type BlocksContentProps } from './BlocksContent';\nimport { BlocksToolbar } from './BlocksToolbar';\nimport { EditorLayout } from './EditorLayout';\nimport { type ModifiersStore, modifiers } from './Modifiers';\nimport { withImages } from './plugins/withImages';\nimport { withLinks } from './plugins/withLinks';\nimport { withStrapiSchema } from './plugins/withStrapiSchema';\n\nimport type { Schema } from '@strapi/types';\n\n/* -------------------------------------------------------------------------------------------------\n * BlocksEditorProvider\n * -----------------------------------------------------------------------------------------------*/\n\ninterface BaseBlock {\n renderElement: (props: RenderElementProps) => React.JSX.Element;\n matchNode: (node: Schema.Attribute.BlocksNode) => boolean;\n handleConvert?: (editor: Editor) => void | (() => React.JSX.Element);\n handleEnterKey?: (editor: Editor) => void;\n handleBackspaceKey?: (editor: Editor, event: React.KeyboardEvent<HTMLElement>) => void;\n handleTab?: (editor: Editor) => void;\n snippets?: string[];\n dragHandleTopMargin?: CSSProperties['marginTop'];\n}\n\ninterface NonSelectorBlock extends BaseBlock {\n isInBlocksSelector: false;\n}\n\ninterface SelectorBlock extends BaseBlock {\n isInBlocksSelector: true;\n icon: React.ComponentType;\n label: MessageDescriptor;\n}\n\ntype NonSelectorBlockKey = 'list-item' | 'link';\n\nconst selectorBlockKeys = [\n 'paragraph',\n 'heading-one',\n 'heading-two',\n 'heading-three',\n 'heading-four',\n 'heading-five',\n 'heading-six',\n 'list-ordered',\n 'list-unordered',\n 'image',\n 'quote',\n 'code',\n] as const;\n\ntype SelectorBlockKey = (typeof selectorBlockKeys)[number];\n\nconst isSelectorBlockKey = (key: unknown): key is SelectorBlockKey => {\n return typeof key === 'string' && selectorBlockKeys.includes(key as SelectorBlockKey);\n};\n\ntype BlocksStore = {\n [K in SelectorBlockKey]: SelectorBlock;\n} & {\n [K in NonSelectorBlockKey]: NonSelectorBlock;\n};\n\ninterface BlocksEditorContextValue {\n blocks: BlocksStore;\n modifiers: ModifiersStore;\n disabled: boolean;\n name: string;\n setLiveText: (text: string) => void;\n isExpandedMode: boolean;\n}\n\nconst [BlocksEditorProvider, usePartialBlocksEditorContext] =\n createContext<BlocksEditorContextValue>('BlocksEditor');\n\nfunction useBlocksEditorContext(\n consumerName: string\n): BlocksEditorContextValue & { editor: Editor } {\n const context = usePartialBlocksEditorContext(consumerName, (state) => state);\n const editor = useSlate();\n\n return {\n ...context,\n editor,\n };\n}\n\n/* -------------------------------------------------------------------------------------------------\n * BlocksEditor\n * -----------------------------------------------------------------------------------------------*/\n\nconst EditorDivider = styled(Divider)`\n background: ${({ theme }) => theme.colors.neutral200};\n`;\n\n/**\n * Forces an update of the Slate editor when the value prop changes from outside of Slate.\n * The root cause is that Slate is not a controlled component: https://github.com/ianstormtaylor/slate/issues/4612\n * Why not use JSON.stringify(value) as the key?\n * Because it would force a rerender of the entire editor every time the user types a character.\n * Why not use the entity id as the key, since it's unique for each locale?\n * Because it would not solve the problem when using the \"fill in from other locale\" feature\n */\nfunction useResetKey(value?: Schema.Attribute.BlocksValue): {\n key: number;\n incrementSlateUpdatesCount: () => void;\n} {\n // Keep track how many times Slate detected a change from a user interaction in the editor\n const slateUpdatesCount = React.useRef(0);\n // Keep track of how many times the value prop was updated, whether froLine truncated
@@ -0,0 +1,64 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var strapiAdmin = require('@strapi/admin/strapi-admin');
var designSystem = require('@strapi/design-system');
var BlocksEditor = require('./BlocksEditor.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const BlocksInput = /*#__PURE__*/ React__namespace.forwardRef(({ label, name, required = false, hint, labelAction, ...editorProps }, forwardedRef)=>{
const id = React__namespace.useId();
const field = strapiAdmin.useField(name);
return /*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Root, {
id: id,
name: name,
hint: hint,
error: field.error,
required: required,
children: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
direction: "column",
alignItems: "stretch",
gap: 1,
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Label, {
action: labelAction,
children: label
}),
/*#__PURE__*/ jsxRuntime.jsx(BlocksEditor.BlocksEditor, {
name: name,
error: field.error,
ref: forwardedRef,
value: field.value,
onChange: field.onChange,
ariaLabelId: id,
...editorProps
}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Hint, {}),
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Error, {})
]
})
});
});
const MemoizedBlocksInput = /*#__PURE__*/ React__namespace.memo(BlocksInput);
exports.BlocksInput = MemoizedBlocksInput;
//# sourceMappingURL=BlocksInput.js.map
@@ -0,0 +1 @@
{"version":3,"file":"BlocksInput.js","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { useField, type InputProps } from '@strapi/admin/strapi-admin';\nimport { Field, Flex } from '@strapi/design-system';\n\nimport { BlocksEditor } from './BlocksEditor';\n\nimport type { Schema } from '@strapi/types';\n\ninterface BlocksInputProps extends Omit<InputProps, 'type'> {\n labelAction?: React.ReactNode;\n type: Schema.Attribute.Blocks['type'];\n}\n\nconst BlocksInput = React.forwardRef<{ focus: () => void }, BlocksInputProps>(\n ({ label, name, required = false, hint, labelAction, ...editorProps }, forwardedRef) => {\n const id = React.useId();\n const field = useField(name);\n\n return (\n <Field.Root id={id} name={name} hint={hint} error={field.error} required={required}>\n <Flex direction=\"column\" alignItems=\"stretch\" gap={1}>\n <Field.Label action={labelAction}>{label}</Field.Label>\n <BlocksEditor\n name={name}\n error={field.error}\n ref={forwardedRef}\n value={field.value}\n onChange={field.onChange}\n ariaLabelId={id}\n {...editorProps}\n />\n <Field.Hint />\n <Field.Error />\n </Flex>\n </Field.Root>\n );\n }\n);\n\nconst MemoizedBlocksInput = React.memo(BlocksInput);\n\nexport { MemoizedBlocksInput as BlocksInput };\n"],"names":["BlocksInput","React","forwardRef","label","name","required","hint","labelAction","editorProps","forwardedRef","id","useId","field","useField","_jsx","Field","Root","error","_jsxs","Flex","direction","alignItems","gap","Label","action","BlocksEditor","ref","value","onChange","ariaLabelId","Hint","Error","MemoizedBlocksInput","memo"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,MAAMA,4BAAcC,gBAAMC,CAAAA,UAAU,CAClC,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAEC,QAAW,GAAA,KAAK,EAAEC,IAAI,EAAEC,WAAW,EAAE,GAAGC,aAAa,EAAEC,YAAAA,GAAAA;IACrE,MAAMC,EAAAA,GAAKT,iBAAMU,KAAK,EAAA;AACtB,IAAA,MAAMC,QAAQC,oBAAST,CAAAA,IAAAA,CAAAA;IAEvB,qBACEU,cAAA,CAACC,mBAAMC,IAAI,EAAA;QAACN,EAAIA,EAAAA,EAAAA;QAAIN,IAAMA,EAAAA,IAAAA;QAAME,IAAMA,EAAAA,IAAAA;AAAMW,QAAAA,KAAAA,EAAOL,MAAMK,KAAK;QAAEZ,QAAUA,EAAAA,QAAAA;AACxE,QAAA,QAAA,gBAAAa,eAACC,CAAAA,iBAAAA,EAAAA;YAAKC,SAAU,EAAA,QAAA;YAASC,UAAW,EAAA,SAAA;YAAUC,GAAK,EAAA,CAAA;;AACjD,8BAAAR,cAAA,CAACC,mBAAMQ,KAAK,EAAA;oBAACC,MAAQjB,EAAAA,WAAAA;AAAcJ,oBAAAA,QAAAA,EAAAA;;8BACnCW,cAACW,CAAAA,yBAAAA,EAAAA;oBACCrB,IAAMA,EAAAA,IAAAA;AACNa,oBAAAA,KAAAA,EAAOL,MAAMK,KAAK;oBAClBS,GAAKjB,EAAAA,YAAAA;AACLkB,oBAAAA,KAAAA,EAAOf,MAAMe,KAAK;AAClBC,oBAAAA,QAAAA,EAAUhB,MAAMgB,QAAQ;oBACxBC,WAAanB,EAAAA,EAAAA;AACZ,oBAAA,GAAGF;;AAEN,8BAAAM,cAAA,CAACC,mBAAMe,IAAI,EAAA,EAAA,CAAA;AACX,8BAAAhB,cAAA,CAACC,mBAAMgB,KAAK,EAAA,EAAA;;;;AAIpB,CAAA,CAAA;AAGIC,MAAAA,mBAAAA,iBAAsB/B,gBAAMgC,CAAAA,IAAI,CAACjC,WAAAA;;;;"}
@@ -0,0 +1,43 @@
import { jsx, jsxs } from 'react/jsx-runtime';
import * as React from 'react';
import { useField } from '@strapi/admin/strapi-admin';
import { Field, Flex } from '@strapi/design-system';
import { BlocksEditor } from './BlocksEditor.mjs';
const BlocksInput = /*#__PURE__*/ React.forwardRef(({ label, name, required = false, hint, labelAction, ...editorProps }, forwardedRef)=>{
const id = React.useId();
const field = useField(name);
return /*#__PURE__*/ jsx(Field.Root, {
id: id,
name: name,
hint: hint,
error: field.error,
required: required,
children: /*#__PURE__*/ jsxs(Flex, {
direction: "column",
alignItems: "stretch",
gap: 1,
children: [
/*#__PURE__*/ jsx(Field.Label, {
action: labelAction,
children: label
}),
/*#__PURE__*/ jsx(BlocksEditor, {
name: name,
error: field.error,
ref: forwardedRef,
value: field.value,
onChange: field.onChange,
ariaLabelId: id,
...editorProps
}),
/*#__PURE__*/ jsx(Field.Hint, {}),
/*#__PURE__*/ jsx(Field.Error, {})
]
})
});
});
const MemoizedBlocksInput = /*#__PURE__*/ React.memo(BlocksInput);
export { MemoizedBlocksInput as BlocksInput };
//# sourceMappingURL=BlocksInput.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"BlocksInput.mjs","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { useField, type InputProps } from '@strapi/admin/strapi-admin';\nimport { Field, Flex } from '@strapi/design-system';\n\nimport { BlocksEditor } from './BlocksEditor';\n\nimport type { Schema } from '@strapi/types';\n\ninterface BlocksInputProps extends Omit<InputProps, 'type'> {\n labelAction?: React.ReactNode;\n type: Schema.Attribute.Blocks['type'];\n}\n\nconst BlocksInput = React.forwardRef<{ focus: () => void }, BlocksInputProps>(\n ({ label, name, required = false, hint, labelAction, ...editorProps }, forwardedRef) => {\n const id = React.useId();\n const field = useField(name);\n\n return (\n <Field.Root id={id} name={name} hint={hint} error={field.error} required={required}>\n <Flex direction=\"column\" alignItems=\"stretch\" gap={1}>\n <Field.Label action={labelAction}>{label}</Field.Label>\n <BlocksEditor\n name={name}\n error={field.error}\n ref={forwardedRef}\n value={field.value}\n onChange={field.onChange}\n ariaLabelId={id}\n {...editorProps}\n />\n <Field.Hint />\n <Field.Error />\n </Flex>\n </Field.Root>\n );\n }\n);\n\nconst MemoizedBlocksInput = React.memo(BlocksInput);\n\nexport { MemoizedBlocksInput as BlocksInput };\n"],"names":["BlocksInput","React","forwardRef","label","name","required","hint","labelAction","editorProps","forwardedRef","id","useId","field","useField","_jsx","Field","Root","error","_jsxs","Flex","direction","alignItems","gap","Label","action","BlocksEditor","ref","value","onChange","ariaLabelId","Hint","Error","MemoizedBlocksInput","memo"],"mappings":";;;;;;AAcA,MAAMA,4BAAcC,KAAMC,CAAAA,UAAU,CAClC,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAEC,QAAW,GAAA,KAAK,EAAEC,IAAI,EAAEC,WAAW,EAAE,GAAGC,aAAa,EAAEC,YAAAA,GAAAA;IACrE,MAAMC,EAAAA,GAAKT,MAAMU,KAAK,EAAA;AACtB,IAAA,MAAMC,QAAQC,QAAST,CAAAA,IAAAA,CAAAA;IAEvB,qBACEU,GAAA,CAACC,MAAMC,IAAI,EAAA;QAACN,EAAIA,EAAAA,EAAAA;QAAIN,IAAMA,EAAAA,IAAAA;QAAME,IAAMA,EAAAA,IAAAA;AAAMW,QAAAA,KAAAA,EAAOL,MAAMK,KAAK;QAAEZ,QAAUA,EAAAA,QAAAA;AACxE,QAAA,QAAA,gBAAAa,IAACC,CAAAA,IAAAA,EAAAA;YAAKC,SAAU,EAAA,QAAA;YAASC,UAAW,EAAA,SAAA;YAAUC,GAAK,EAAA,CAAA;;AACjD,8BAAAR,GAAA,CAACC,MAAMQ,KAAK,EAAA;oBAACC,MAAQjB,EAAAA,WAAAA;AAAcJ,oBAAAA,QAAAA,EAAAA;;8BACnCW,GAACW,CAAAA,YAAAA,EAAAA;oBACCrB,IAAMA,EAAAA,IAAAA;AACNa,oBAAAA,KAAAA,EAAOL,MAAMK,KAAK;oBAClBS,GAAKjB,EAAAA,YAAAA;AACLkB,oBAAAA,KAAAA,EAAOf,MAAMe,KAAK;AAClBC,oBAAAA,QAAAA,EAAUhB,MAAMgB,QAAQ;oBACxBC,WAAanB,EAAAA,EAAAA;AACZ,oBAAA,GAAGF;;AAEN,8BAAAM,GAAA,CAACC,MAAMe,IAAI,EAAA,EAAA,CAAA;AACX,8BAAAhB,GAAA,CAACC,MAAMgB,KAAK,EAAA,EAAA;;;;AAIpB,CAAA,CAAA;AAGIC,MAAAA,mBAAAA,iBAAsB/B,KAAMgC,CAAAA,IAAI,CAACjC,WAAAA;;;;"}
@@ -0,0 +1,635 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var Toolbar = require('@radix-ui/react-toolbar');
require('@strapi/admin/strapi-admin');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var reactIntl = require('react-intl');
var slate = require('slate');
var slateReact = require('slate-react');
var styledComponents = require('styled-components');
var EditorToolbarObserver = require('../../EditorToolbarObserver.js');
var BlocksEditor = require('./BlocksEditor.js');
var links = require('./utils/links.js');
var types = require('./utils/types.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
var Toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(Toolbar);
const ToolbarWrapper = styledComponents.styled(designSystem.Flex)`
&[aria-disabled='true'] {
cursor: not-allowed;
background: ${({ theme })=>theme.colors.neutral150};
}
`;
const ToolbarSeparator = styledComponents.styled(Toolbar__namespace.Separator)`
background: ${({ theme })=>theme.colors.neutral150};
width: 1px;
height: 2.4rem;
`;
const FlexButton = styledComponents.styled(designSystem.Flex)`
// Inherit the not-allowed cursor from ToolbarWrapper when disabled
&[aria-disabled] {
cursor: not-allowed;
}
&[aria-disabled='false'] {
cursor: pointer;
// Only apply hover styles if the button is enabled
&:hover {
background: ${({ theme })=>theme.colors.primary100};
}
}
`;
const SelectWrapper = styledComponents.styled(designSystem.Box)`
// Styling changes to SingleSelect component don't work, so adding wrapper to target SingleSelect
div[role='combobox'] {
border: none;
cursor: pointer;
min-height: unset;
padding-top: 6px;
padding-bottom: 6px;
&[aria-disabled='false']:hover {
cursor: pointer;
background: ${({ theme })=>theme.colors.primary100};
}
&[aria-disabled] {
background: transparent;
cursor: inherit;
// Select text and icons should also have disabled color
span {
color: ${({ theme })=>theme.colors.neutral600};
}
}
}
`;
/**
* Handles the modal component that may be returned by a block when converting it
*/ function useConversionModal() {
const [modalElement, setModalComponent] = React__namespace.useState(null);
const handleConversionResult = (renderModal)=>{
// Not all blocks return a modal
if (renderModal) {
// Use cloneElement to apply a key because to create a new instance of the component
// Without the new key, the state is kept from previous times that option was picked
setModalComponent(/*#__PURE__*/ React__namespace.cloneElement(renderModal(), {
key: Date.now()
}));
}
};
return {
modalElement,
handleConversionResult
};
}
const ToolbarButton = ({ icon: Icon, name, label, isActive, disabled, handleClick })=>{
const { editor } = BlocksEditor.useBlocksEditorContext('ToolbarButton');
const { formatMessage } = reactIntl.useIntl();
const labelMessage = formatMessage(label);
const enabledColor = isActive ? 'primary600' : 'neutral600';
return /*#__PURE__*/ jsxRuntime.jsx(designSystem.Tooltip, {
label: labelMessage,
children: /*#__PURE__*/ jsxRuntime.jsx(Toolbar__namespace.ToggleItem, {
value: name,
"data-state": isActive ? 'on' : 'off',
onMouseDown: (e)=>{
e.preventDefault();
handleClick();
slateReact.ReactEditor.focus(editor);
},
"aria-disabled": disabled,
disabled: disabled,
"aria-label": labelMessage,
asChild: true,
children: /*#__PURE__*/ jsxRuntime.jsx(FlexButton, {
tag: "button",
background: isActive ? 'primary100' : '',
alignItems: "center",
justifyContent: "center",
width: 7,
height: 7,
hasRadius: true,
children: /*#__PURE__*/ jsxRuntime.jsx(Icon, {
fill: disabled ? 'neutral300' : enabledColor
})
})
})
});
};
const BlocksDropdown = ()=>{
const { editor, blocks, disabled } = BlocksEditor.useBlocksEditorContext('BlocksDropdown');
const { formatMessage } = reactIntl.useIntl();
const { modalElement, handleConversionResult } = useConversionModal();
const blockKeysToInclude = types.getEntries(blocks).reduce((currentKeys, entry)=>{
const [key, block] = entry;
return block.isInBlocksSelector ? [
...currentKeys,
key
] : currentKeys;
}, []);
const [blockSelected, setBlockSelected] = React__namespace.useState('paragraph');
const handleSelect = (optionKey)=>{
if (!BlocksEditor.isSelectorBlockKey(optionKey)) {
return;
}
const editorIsEmpty = editor.children.length === 1 && slate.Editor.isEmpty(editor, editor.children[0]);
if (!editor.selection && !editorIsEmpty) {
// When there is no selection, create an empty block at the end of the editor
// so that it can be converted to the selected block
slate.Transforms.insertNodes(editor, {
type: 'quote',
children: [
{
type: 'text',
text: ''
}
]
}, {
select: true
});
} else if (!editor.selection && editorIsEmpty) {
// When there is no selection and the editor is empty,
// select the empty paragraph from Slate's initialValue so it gets converted
slate.Transforms.select(editor, slate.Editor.start(editor, [
0,
0
]));
}
// If selection is already a list block, toggle its format
const currentListEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list'
});
if (currentListEntry && [
'list-ordered',
'list-unordered'
].includes(optionKey)) {
const [currentList, currentListPath] = currentListEntry;
const format = optionKey === 'list-ordered' ? 'ordered' : 'unordered';
if (!slate.Editor.isEditor(currentList) && isListNode(currentList)) {
// Format is different, toggle list format
if (currentList.format !== format) {
slate.Transforms.setNodes(editor, {
format
}, {
at: currentListPath
});
}
}
return;
}
// Let the block handle the Slate conversion logic
const maybeRenderModal = blocks[optionKey].handleConvert?.(editor);
handleConversionResult(maybeRenderModal);
setBlockSelected(optionKey);
slateReact.ReactEditor.focus(editor);
};
/**
* Prevent the select from focusing itself so ReactEditor.focus(editor) can focus the editor instead.
*
* The editor first loses focus to a blur event when clicking the select button. However,
* refocusing the editor is not enough since the select's default behavior is to refocus itself
* after an option is selected.
*
*/ const preventSelectFocus = (e)=>e.preventDefault();
// Listen to the selection change and update the selected block in the dropdown
React__namespace.useEffect(()=>{
if (editor.selection) {
let selectedNode;
// If selection anchor is a list-item, get its parent
const currentListEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list',
at: editor.selection.anchor
});
if (currentListEntry) {
const [currentList] = currentListEntry;
selectedNode = currentList;
} else {
// Get the parent node of the anchor other than list-item
const [anchorNode] = slate.Editor.parent(editor, editor.selection.anchor, {
edge: 'start',
depth: 2
});
// @ts-expect-error slate's delete behaviour creates an exceptional type
if (anchorNode.type === 'list-item') {
// When the last node in the selection is a list item,
// slate's default delete operation leaves an empty list-item instead of converting it into a paragraph.
// Issue: https://github.com/ianstormtaylor/slate/issues/2500
slate.Transforms.setNodes(editor, {
type: 'paragraph'
});
// @ts-expect-error convert explicitly type to paragraph
selectedNode = {
...anchorNode,
type: 'paragraph'
};
} else {
selectedNode = anchorNode;
}
}
// Find the block key that matches the anchor node
const anchorBlockKey = types.getKeys(blocks).find((blockKey)=>!slate.Editor.isEditor(selectedNode) && blocks[blockKey].matchNode(selectedNode));
// Change the value selected in the dropdown if it doesn't match the anchor block key
if (anchorBlockKey && anchorBlockKey !== blockSelected) {
setBlockSelected(anchorBlockKey);
}
}
}, [
editor.selection,
editor,
blocks,
blockSelected
]);
const Icon = blocks[blockSelected].icon;
return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
/*#__PURE__*/ jsxRuntime.jsx(SelectWrapper, {
children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.SingleSelect, {
startIcon: /*#__PURE__*/ jsxRuntime.jsx(Icon, {}),
onChange: handleSelect,
placeholder: formatMessage(blocks[blockSelected].label),
value: blockSelected,
onCloseAutoFocus: preventSelectFocus,
"aria-label": formatMessage({
id: 'components.Blocks.blocks.selectBlock',
defaultMessage: 'Select a block'
}),
disabled: disabled,
children: blockKeysToInclude.map((key)=>/*#__PURE__*/ jsxRuntime.jsx(BlockOption, {
value: key,
label: blocks[key].label,
icon: blocks[key].icon,
blockSelected: blockSelected
}, key))
})
}),
modalElement
]
});
};
const BlockOption = ({ value, icon: Icon, label, blockSelected })=>{
const { formatMessage } = reactIntl.useIntl();
const isSelected = value === blockSelected;
return /*#__PURE__*/ jsxRuntime.jsx(designSystem.SingleSelectOption, {
startIcon: /*#__PURE__*/ jsxRuntime.jsx(Icon, {
fill: isSelected ? 'primary600' : 'neutral600'
}),
value: value,
children: formatMessage(label)
});
};
const isListNode = (node)=>{
return slate.Node.isNode(node) && !slate.Editor.isEditor(node) && node.type === 'list';
};
const ListButton = ({ block, format, location = 'toolbar' })=>{
const { editor, disabled, blocks } = BlocksEditor.useBlocksEditorContext('ListButton');
const { formatMessage } = reactIntl.useIntl();
const isListActive = ()=>{
if (!editor.selection) return false;
// Get the parent list at selection anchor node
const currentListEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list',
at: editor.selection.anchor
});
if (currentListEntry) {
const [currentList] = currentListEntry;
if (!slate.Editor.isEditor(currentList) && isListNode(currentList) && currentList.format === format) return true;
}
return false;
};
/**
* @TODO: Currently, applying list while multiple blocks are selected is not supported.
* We should implement this feature in the future.
*/ const isListDisabled = ()=>{
// Always disabled when the whole editor is disabled
if (disabled) {
return true;
}
// Always enabled when there's no selection
if (!editor.selection) {
return false;
}
// Get the block node closest to the anchor and focus
const anchorNodeEntry = slate.Editor.above(editor, {
at: editor.selection.anchor,
match: (node)=>!slate.Editor.isEditor(node) && node.type !== 'text'
});
const focusNodeEntry = slate.Editor.above(editor, {
at: editor.selection.focus,
match: (node)=>!slate.Editor.isEditor(node) && node.type !== 'text'
});
if (!anchorNodeEntry || !focusNodeEntry) {
return false;
}
// Disabled if the anchor and focus are not in the same block
return anchorNodeEntry[0] !== focusNodeEntry[0];
};
const toggleList = (format)=>{
let currentListEntry;
if (editor.selection) {
currentListEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list'
});
} else {
// If no selection, toggle last inserted node
const [_, lastNodePath] = slate.Editor.last(editor, []);
currentListEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list',
at: lastNodePath
});
}
if (!currentListEntry) {
// If selection is not a list then convert it to list
blocks[`list-${format}`].handleConvert(editor);
return;
}
// If selection is already a list then toggle format
const [currentList, currentListPath] = currentListEntry;
if (!slate.Editor.isEditor(currentList) && isListNode(currentList)) {
if (currentList.format !== format) {
// Format is different, toggle list format
slate.Transforms.setNodes(editor, {
format
}, {
at: currentListPath
});
} else {
// Format is same, convert selected list-item to paragraph
blocks['paragraph'].handleConvert(editor);
}
}
};
if (location === 'menu') {
const Icon = block.icon;
return /*#__PURE__*/ jsxRuntime.jsxs(StyledMenuItem, {
onSelect: ()=>toggleList(format),
isActive: isListActive(),
disabled: isListDisabled(),
children: [
/*#__PURE__*/ jsxRuntime.jsx(Icon, {}),
formatMessage(block.label)
]
});
}
return /*#__PURE__*/ jsxRuntime.jsx(ToolbarButton, {
icon: block.icon,
name: format,
label: block.label,
isActive: isListActive(),
disabled: isListDisabled(),
handleClick: ()=>toggleList(format)
});
};
const LinkButton = ({ disabled, location = 'toolbar' })=>{
const { editor } = BlocksEditor.useBlocksEditorContext('LinkButton');
const { formatMessage } = reactIntl.useIntl();
const isLinkActive = ()=>{
const { selection } = editor;
if (!selection) return false;
const [match] = Array.from(slate.Editor.nodes(editor, {
at: slate.Editor.unhangRange(editor, selection),
match: (node)=>slate.Element.isElement(node) && node.type === 'link'
}));
return Boolean(match);
};
const isLinkDisabled = ()=>{
// Always disabled when the whole editor is disabled
if (disabled) {
return true;
}
// Always enabled when there's no selection
if (!editor.selection) {
return false;
}
// Get the block node closest to the anchor and focus
const anchorNodeEntry = slate.Editor.above(editor, {
at: editor.selection.anchor,
match: (node)=>!slate.Editor.isEditor(node) && node.type !== 'text'
});
const focusNodeEntry = slate.Editor.above(editor, {
at: editor.selection.focus,
match: (node)=>!slate.Editor.isEditor(node) && node.type !== 'text'
});
if (!anchorNodeEntry || !focusNodeEntry) {
return false;
}
// Disabled if the anchor and focus are not in the same block
return anchorNodeEntry[0] !== focusNodeEntry[0];
};
const addLink = ()=>{
editor.shouldSaveLinkPath = true;
// We insert an empty anchor, so we split the DOM to have a element we can use as reference for the popover
links.insertLink(editor, {
url: ''
});
};
const label = {
id: 'components.Blocks.link',
defaultMessage: 'Link'
};
if (location === 'menu') {
return /*#__PURE__*/ jsxRuntime.jsxs(StyledMenuItem, {
onSelect: addLink,
isActive: isLinkActive(),
disabled: isLinkDisabled(),
children: [
/*#__PURE__*/ jsxRuntime.jsx(Icons.Link, {}),
formatMessage(label)
]
});
}
return /*#__PURE__*/ jsxRuntime.jsx(ToolbarButton, {
icon: Icons.Link,
name: "link",
label: label,
isActive: isLinkActive(),
handleClick: addLink,
disabled: isLinkDisabled()
});
};
const StyledMenuItem = styledComponents.styled(designSystem.Menu.Item)`
&:hover {
background-color: ${({ theme })=>theme.colors.primary100};
}
${(props)=>props.isActive && styledComponents.css`
font-weight: bold;
background-color: ${({ theme })=>theme.colors.primary100};
color: ${({ theme })=>theme.colors.primary600};
font-weight: bold;
`}
> span {
display: inline-flex;
gap: ${({ theme })=>theme.spaces[2]};
align-items: center;
}
svg {
fill: ${({ theme, isActive })=>isActive ? theme.colors.primary600 : theme.colors.neutral600};
}
`;
const BlocksToolbar = ()=>{
const { editor, blocks, modifiers, disabled } = BlocksEditor.useBlocksEditorContext('BlocksToolbar');
const { formatMessage } = reactIntl.useIntl();
/**
* The modifier buttons are disabled when an image is selected.
*/ const checkButtonDisabled = ()=>{
// Always disabled when the whole editor is disabled
if (disabled) {
return true;
}
if (!editor.selection) {
return false;
}
const selectedNode = editor.children[editor.selection.anchor.path[0]];
if ([
'image',
'code'
].includes(selectedNode.type)) {
return true;
}
return false;
};
const isButtonDisabled = checkButtonDisabled();
/**
* Observed components are ones that may or may not be visible in the toolbar, depending on the
* available space. They provide two render props:
* - renderInToolbar: for when we try to render the component in the toolbar (may be hidden)
* - renderInMenu: for when the component didn't fit in the toolbar and is relegated
* to the "more" menu
*/ const observedComponents = [
...Object.entries(modifiers).map(([name, modifier])=>{
const Icon = modifier.icon;
const isActive = modifier.checkIsActive(editor);
const handleSelect = ()=>modifier.handleToggle(editor);
return {
toolbar: /*#__PURE__*/ jsxRuntime.jsx(ToolbarButton, {
name: name,
icon: modifier.icon,
label: modifier.label,
isActive: modifier.checkIsActive(editor),
handleClick: handleSelect,
disabled: isButtonDisabled
}, name),
menu: /*#__PURE__*/ jsxRuntime.jsxs(StyledMenuItem, {
onSelect: handleSelect,
isActive: isActive,
children: [
/*#__PURE__*/ jsxRuntime.jsx(Icon, {}),
formatMessage(modifier.label)
]
}),
key: `modifier.${name}`
};
}),
{
toolbar: /*#__PURE__*/ jsxRuntime.jsx(LinkButton, {
disabled: isButtonDisabled,
location: "toolbar"
}),
menu: /*#__PURE__*/ jsxRuntime.jsx(LinkButton, {
disabled: isButtonDisabled,
location: "menu"
}),
key: 'block.link'
},
{
// List buttons can only be rendered together when in the toolbar
toolbar: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
direction: "row",
gap: 1,
children: [
/*#__PURE__*/ jsxRuntime.jsx(ToolbarSeparator, {}),
/*#__PURE__*/ jsxRuntime.jsx(Toolbar__namespace.ToggleGroup, {
type: "single",
asChild: true,
children: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
gap: 1,
children: [
/*#__PURE__*/ jsxRuntime.jsx(ListButton, {
block: blocks['list-unordered'],
format: "unordered",
location: "toolbar"
}),
/*#__PURE__*/ jsxRuntime.jsx(ListButton, {
block: blocks['list-ordered'],
format: "ordered",
location: "toolbar"
})
]
})
})
]
}),
menu: /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
/*#__PURE__*/ jsxRuntime.jsx(designSystem.Menu.Separator, {}),
/*#__PURE__*/ jsxRuntime.jsx(ListButton, {
block: blocks['list-unordered'],
format: "unordered",
location: "menu"
}),
/*#__PURE__*/ jsxRuntime.jsx(ListButton, {
block: blocks['list-ordered'],
format: "ordered",
location: "menu"
})
]
}),
key: 'block.list'
}
];
return /*#__PURE__*/ jsxRuntime.jsx(Toolbar__namespace.Root, {
"aria-disabled": disabled,
asChild: true,
children: /*#__PURE__*/ jsxRuntime.jsxs(ToolbarWrapper, {
gap: 2,
padding: 2,
width: "100%",
children: [
/*#__PURE__*/ jsxRuntime.jsx(BlocksDropdown, {}),
/*#__PURE__*/ jsxRuntime.jsx(ToolbarSeparator, {}),
/*#__PURE__*/ jsxRuntime.jsx(Toolbar__namespace.ToggleGroup, {
type: "multiple",
asChild: true,
children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Flex, {
direction: "row",
gap: 1,
grow: 1,
overflow: "hidden",
children: /*#__PURE__*/ jsxRuntime.jsx(EditorToolbarObserver.EditorToolbarObserver, {
observedComponents: observedComponents
})
})
})
]
})
});
};
exports.BlocksToolbar = BlocksToolbar;
exports.useConversionModal = useConversionModal;
//# sourceMappingURL=BlocksToolbar.js.map
@@ -0,0 +1 @@
{"version":3,"file":"BlocksToolbar.js","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksToolbar.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport * as Toolbar from '@radix-ui/react-toolbar';\nimport { useElementOnScreen } from '@strapi/admin/strapi-admin';\nimport {\n Flex,\n Tooltip,\n SingleSelect,\n SingleSelectOption,\n Box,\n FlexComponent,\n BoxComponent,\n Menu,\n IconButton,\n} from '@strapi/design-system';\nimport { Link, More } from '@strapi/icons';\nimport { MessageDescriptor, useIntl } from 'react-intl';\nimport { Editor, Transforms, Element as SlateElement, Node, type Ancestor } from 'slate';\nimport { ReactEditor } from 'slate-react';\nimport { css, styled } from 'styled-components';\n\nimport { EditorToolbarObserver } from '../../EditorToolbarObserver';\n\nimport {\n type BlocksStore,\n type SelectorBlockKey,\n isSelectorBlockKey,\n useBlocksEditorContext,\n} from './BlocksEditor';\nimport { insertLink } from './utils/links';\nimport { type Block, getEntries, getKeys } from './utils/types';\n\nconst ToolbarWrapper = styled<FlexComponent>(Flex)`\n &[aria-disabled='true'] {\n cursor: not-allowed;\n background: ${({ theme }) => theme.colors.neutral150};\n }\n`;\n\nconst ToolbarSeparator = styled(Toolbar.Separator)`\n background: ${({ theme }) => theme.colors.neutral150};\n width: 1px;\n height: 2.4rem;\n`;\n\nconst FlexButton = styled<FlexComponent<'button'>>(Flex)`\n // Inherit the not-allowed cursor from ToolbarWrapper when disabled\n &[aria-disabled] {\n cursor: not-allowed;\n }\n\n &[aria-disabled='false'] {\n cursor: pointer;\n\n // Only apply hover styles if the button is enabled\n &:hover {\n background: ${({ theme }) => theme.colors.primary100};\n }\n }\n`;\n\nconst SelectWrapper = styled<BoxComponent>(Box)`\n // Styling changes to SingleSelect component don't work, so adding wrapper to target SingleSelect\n div[role='combobox'] {\n border: none;\n cursor: pointer;\n min-height: unset;\n padding-top: 6px;\n padding-bottom: 6px;\n\n &[aria-disabled='false']:hover {\n cursor: pointer;\n background: ${({ theme }) => theme.colors.primary100};\n }\n\n &[aria-disabled] {\n background: transparent;\n cursor: inherit;\n\n // Select text and icons should also have disabled color\n span {\n color: ${({ theme }) => theme.colors.neutral600};\n }\n }\n }\n`;\n\n/**\n * Handles the modal component that may be returned by a block when converting it\n */\nfunction useConversionModal() {\n const [modalElement, setModalComponent] = React.useState<React.JSX.Element | null>(null);\n\n const handleConversionResult = (renderModal: void | (() => React.JSX.Element) | undefined) => {\n // Not all blocks return a modal\n if (renderModal) {\n // Use cloneElement to apply a key because to create a new instance of the component\n // Without the new key, the state is kept from previous times that option was picked\n setModalComponent(React.cloneElement(renderModal(), { key: Date.now() }));\n }\n };\n\n return { modalElement, handleConversionResult };\n}\n\ninterface ToolbarButtonProps {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n name: string;\n label: MessageDescriptor;\n isActive: boolean;\n disabled: boolean;\n handleClick: () => void;\n}\n\nconst ToolbarButton = ({\n icon: Icon,\n name,\n label,\n isActive,\n disabled,\n handleClick,\n}: ToolbarButtonProps) => {\n const { editor } = useBlocksEditorContext('ToolbarButton');\n const { formatMessage } = useIntl();\n const labelMessage = formatMessage(label);\n\n const enabledColor = isActive ? 'primary600' : 'neutral600';\n\n return (\n <Tooltip label={labelMessage}>\n <Toolbar.ToggleItem\n value={name}\n data-state={isActive ? 'on' : 'off'}\n onMouseDown={(e) => {\n e.preventDefault();\n handleClick();\n ReactEditor.focus(editor);\n }}\n aria-disabled={disabled}\n disabled={disabled}\n aria-label={labelMessage}\n asChild\n >\n <FlexButton\n tag=\"button\"\n background={isActive ? 'primary100' : ''}\n alignItems=\"center\"\n justifyContent=\"center\"\n width={7}\n height={7}\n hasRadius\n >\n <Icon fill={disabled ? 'neutral300' : enabledColor} />\n </FlexButton>\n </Toolbar.ToggleItem>\n </Tooltip>\n );\n};\n\nconst BlocksDropdown = () => {\n const { editor, blocks, disabled } = useBlocksEditorContext('BlocksDropdown');\n const { formatMessage } = useIntl();\n const { modalElement, handleConversionResult } = useConversionModal();\n\n const blockKeysToInclude: SelectorBlockKey[] = getEntries(blocks).reduce<\n ReturnType<typeof getEntries>\n >((currentKeys, entry) => {\n const [key, block] = entry;\n\n return blocLine truncated
@@ -0,0 +1,612 @@
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import * as React from 'react';
import * as Toolbar from '@radix-ui/react-toolbar';
import '@strapi/admin/strapi-admin';
import { Flex, Box, Menu, Tooltip, SingleSelect, SingleSelectOption } from '@strapi/design-system';
import { Link } from '@strapi/icons';
import { useIntl } from 'react-intl';
import { Editor, Transforms, Node, Element } from 'slate';
import { ReactEditor } from 'slate-react';
import { styled, css } from 'styled-components';
import { EditorToolbarObserver } from '../../EditorToolbarObserver.mjs';
import { useBlocksEditorContext, isSelectorBlockKey } from './BlocksEditor.mjs';
import { insertLink } from './utils/links.mjs';
import { getEntries, getKeys } from './utils/types.mjs';
const ToolbarWrapper = styled(Flex)`
&[aria-disabled='true'] {
cursor: not-allowed;
background: ${({ theme })=>theme.colors.neutral150};
}
`;
const ToolbarSeparator = styled(Toolbar.Separator)`
background: ${({ theme })=>theme.colors.neutral150};
width: 1px;
height: 2.4rem;
`;
const FlexButton = styled(Flex)`
// Inherit the not-allowed cursor from ToolbarWrapper when disabled
&[aria-disabled] {
cursor: not-allowed;
}
&[aria-disabled='false'] {
cursor: pointer;
// Only apply hover styles if the button is enabled
&:hover {
background: ${({ theme })=>theme.colors.primary100};
}
}
`;
const SelectWrapper = styled(Box)`
// Styling changes to SingleSelect component don't work, so adding wrapper to target SingleSelect
div[role='combobox'] {
border: none;
cursor: pointer;
min-height: unset;
padding-top: 6px;
padding-bottom: 6px;
&[aria-disabled='false']:hover {
cursor: pointer;
background: ${({ theme })=>theme.colors.primary100};
}
&[aria-disabled] {
background: transparent;
cursor: inherit;
// Select text and icons should also have disabled color
span {
color: ${({ theme })=>theme.colors.neutral600};
}
}
}
`;
/**
* Handles the modal component that may be returned by a block when converting it
*/ function useConversionModal() {
const [modalElement, setModalComponent] = React.useState(null);
const handleConversionResult = (renderModal)=>{
// Not all blocks return a modal
if (renderModal) {
// Use cloneElement to apply a key because to create a new instance of the component
// Without the new key, the state is kept from previous times that option was picked
setModalComponent(/*#__PURE__*/ React.cloneElement(renderModal(), {
key: Date.now()
}));
}
};
return {
modalElement,
handleConversionResult
};
}
const ToolbarButton = ({ icon: Icon, name, label, isActive, disabled, handleClick })=>{
const { editor } = useBlocksEditorContext('ToolbarButton');
const { formatMessage } = useIntl();
const labelMessage = formatMessage(label);
const enabledColor = isActive ? 'primary600' : 'neutral600';
return /*#__PURE__*/ jsx(Tooltip, {
label: labelMessage,
children: /*#__PURE__*/ jsx(Toolbar.ToggleItem, {
value: name,
"data-state": isActive ? 'on' : 'off',
onMouseDown: (e)=>{
e.preventDefault();
handleClick();
ReactEditor.focus(editor);
},
"aria-disabled": disabled,
disabled: disabled,
"aria-label": labelMessage,
asChild: true,
children: /*#__PURE__*/ jsx(FlexButton, {
tag: "button",
background: isActive ? 'primary100' : '',
alignItems: "center",
justifyContent: "center",
width: 7,
height: 7,
hasRadius: true,
children: /*#__PURE__*/ jsx(Icon, {
fill: disabled ? 'neutral300' : enabledColor
})
})
})
});
};
const BlocksDropdown = ()=>{
const { editor, blocks, disabled } = useBlocksEditorContext('BlocksDropdown');
const { formatMessage } = useIntl();
const { modalElement, handleConversionResult } = useConversionModal();
const blockKeysToInclude = getEntries(blocks).reduce((currentKeys, entry)=>{
const [key, block] = entry;
return block.isInBlocksSelector ? [
...currentKeys,
key
] : currentKeys;
}, []);
const [blockSelected, setBlockSelected] = React.useState('paragraph');
const handleSelect = (optionKey)=>{
if (!isSelectorBlockKey(optionKey)) {
return;
}
const editorIsEmpty = editor.children.length === 1 && Editor.isEmpty(editor, editor.children[0]);
if (!editor.selection && !editorIsEmpty) {
// When there is no selection, create an empty block at the end of the editor
// so that it can be converted to the selected block
Transforms.insertNodes(editor, {
type: 'quote',
children: [
{
type: 'text',
text: ''
}
]
}, {
select: true
});
} else if (!editor.selection && editorIsEmpty) {
// When there is no selection and the editor is empty,
// select the empty paragraph from Slate's initialValue so it gets converted
Transforms.select(editor, Editor.start(editor, [
0,
0
]));
}
// If selection is already a list block, toggle its format
const currentListEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list'
});
if (currentListEntry && [
'list-ordered',
'list-unordered'
].includes(optionKey)) {
const [currentList, currentListPath] = currentListEntry;
const format = optionKey === 'list-ordered' ? 'ordered' : 'unordered';
if (!Editor.isEditor(currentList) && isListNode(currentList)) {
// Format is different, toggle list format
if (currentList.format !== format) {
Transforms.setNodes(editor, {
format
}, {
at: currentListPath
});
}
}
return;
}
// Let the block handle the Slate conversion logic
const maybeRenderModal = blocks[optionKey].handleConvert?.(editor);
handleConversionResult(maybeRenderModal);
setBlockSelected(optionKey);
ReactEditor.focus(editor);
};
/**
* Prevent the select from focusing itself so ReactEditor.focus(editor) can focus the editor instead.
*
* The editor first loses focus to a blur event when clicking the select button. However,
* refocusing the editor is not enough since the select's default behavior is to refocus itself
* after an option is selected.
*
*/ const preventSelectFocus = (e)=>e.preventDefault();
// Listen to the selection change and update the selected block in the dropdown
React.useEffect(()=>{
if (editor.selection) {
let selectedNode;
// If selection anchor is a list-item, get its parent
const currentListEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list',
at: editor.selection.anchor
});
if (currentListEntry) {
const [currentList] = currentListEntry;
selectedNode = currentList;
} else {
// Get the parent node of the anchor other than list-item
const [anchorNode] = Editor.parent(editor, editor.selection.anchor, {
edge: 'start',
depth: 2
});
// @ts-expect-error slate's delete behaviour creates an exceptional type
if (anchorNode.type === 'list-item') {
// When the last node in the selection is a list item,
// slate's default delete operation leaves an empty list-item instead of converting it into a paragraph.
// Issue: https://github.com/ianstormtaylor/slate/issues/2500
Transforms.setNodes(editor, {
type: 'paragraph'
});
// @ts-expect-error convert explicitly type to paragraph
selectedNode = {
...anchorNode,
type: 'paragraph'
};
} else {
selectedNode = anchorNode;
}
}
// Find the block key that matches the anchor node
const anchorBlockKey = getKeys(blocks).find((blockKey)=>!Editor.isEditor(selectedNode) && blocks[blockKey].matchNode(selectedNode));
// Change the value selected in the dropdown if it doesn't match the anchor block key
if (anchorBlockKey && anchorBlockKey !== blockSelected) {
setBlockSelected(anchorBlockKey);
}
}
}, [
editor.selection,
editor,
blocks,
blockSelected
]);
const Icon = blocks[blockSelected].icon;
return /*#__PURE__*/ jsxs(Fragment, {
children: [
/*#__PURE__*/ jsx(SelectWrapper, {
children: /*#__PURE__*/ jsx(SingleSelect, {
startIcon: /*#__PURE__*/ jsx(Icon, {}),
onChange: handleSelect,
placeholder: formatMessage(blocks[blockSelected].label),
value: blockSelected,
onCloseAutoFocus: preventSelectFocus,
"aria-label": formatMessage({
id: 'components.Blocks.blocks.selectBlock',
defaultMessage: 'Select a block'
}),
disabled: disabled,
children: blockKeysToInclude.map((key)=>/*#__PURE__*/ jsx(BlockOption, {
value: key,
label: blocks[key].label,
icon: blocks[key].icon,
blockSelected: blockSelected
}, key))
})
}),
modalElement
]
});
};
const BlockOption = ({ value, icon: Icon, label, blockSelected })=>{
const { formatMessage } = useIntl();
const isSelected = value === blockSelected;
return /*#__PURE__*/ jsx(SingleSelectOption, {
startIcon: /*#__PURE__*/ jsx(Icon, {
fill: isSelected ? 'primary600' : 'neutral600'
}),
value: value,
children: formatMessage(label)
});
};
const isListNode = (node)=>{
return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'list';
};
const ListButton = ({ block, format, location = 'toolbar' })=>{
const { editor, disabled, blocks } = useBlocksEditorContext('ListButton');
const { formatMessage } = useIntl();
const isListActive = ()=>{
if (!editor.selection) return false;
// Get the parent list at selection anchor node
const currentListEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list',
at: editor.selection.anchor
});
if (currentListEntry) {
const [currentList] = currentListEntry;
if (!Editor.isEditor(currentList) && isListNode(currentList) && currentList.format === format) return true;
}
return false;
};
/**
* @TODO: Currently, applying list while multiple blocks are selected is not supported.
* We should implement this feature in the future.
*/ const isListDisabled = ()=>{
// Always disabled when the whole editor is disabled
if (disabled) {
return true;
}
// Always enabled when there's no selection
if (!editor.selection) {
return false;
}
// Get the block node closest to the anchor and focus
const anchorNodeEntry = Editor.above(editor, {
at: editor.selection.anchor,
match: (node)=>!Editor.isEditor(node) && node.type !== 'text'
});
const focusNodeEntry = Editor.above(editor, {
at: editor.selection.focus,
match: (node)=>!Editor.isEditor(node) && node.type !== 'text'
});
if (!anchorNodeEntry || !focusNodeEntry) {
return false;
}
// Disabled if the anchor and focus are not in the same block
return anchorNodeEntry[0] !== focusNodeEntry[0];
};
const toggleList = (format)=>{
let currentListEntry;
if (editor.selection) {
currentListEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list'
});
} else {
// If no selection, toggle last inserted node
const [_, lastNodePath] = Editor.last(editor, []);
currentListEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list',
at: lastNodePath
});
}
if (!currentListEntry) {
// If selection is not a list then convert it to list
blocks[`list-${format}`].handleConvert(editor);
return;
}
// If selection is already a list then toggle format
const [currentList, currentListPath] = currentListEntry;
if (!Editor.isEditor(currentList) && isListNode(currentList)) {
if (currentList.format !== format) {
// Format is different, toggle list format
Transforms.setNodes(editor, {
format
}, {
at: currentListPath
});
} else {
// Format is same, convert selected list-item to paragraph
blocks['paragraph'].handleConvert(editor);
}
}
};
if (location === 'menu') {
const Icon = block.icon;
return /*#__PURE__*/ jsxs(StyledMenuItem, {
onSelect: ()=>toggleList(format),
isActive: isListActive(),
disabled: isListDisabled(),
children: [
/*#__PURE__*/ jsx(Icon, {}),
formatMessage(block.label)
]
});
}
return /*#__PURE__*/ jsx(ToolbarButton, {
icon: block.icon,
name: format,
label: block.label,
isActive: isListActive(),
disabled: isListDisabled(),
handleClick: ()=>toggleList(format)
});
};
const LinkButton = ({ disabled, location = 'toolbar' })=>{
const { editor } = useBlocksEditorContext('LinkButton');
const { formatMessage } = useIntl();
const isLinkActive = ()=>{
const { selection } = editor;
if (!selection) return false;
const [match] = Array.from(Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: (node)=>Element.isElement(node) && node.type === 'link'
}));
return Boolean(match);
};
const isLinkDisabled = ()=>{
// Always disabled when the whole editor is disabled
if (disabled) {
return true;
}
// Always enabled when there's no selection
if (!editor.selection) {
return false;
}
// Get the block node closest to the anchor and focus
const anchorNodeEntry = Editor.above(editor, {
at: editor.selection.anchor,
match: (node)=>!Editor.isEditor(node) && node.type !== 'text'
});
const focusNodeEntry = Editor.above(editor, {
at: editor.selection.focus,
match: (node)=>!Editor.isEditor(node) && node.type !== 'text'
});
if (!anchorNodeEntry || !focusNodeEntry) {
return false;
}
// Disabled if the anchor and focus are not in the same block
return anchorNodeEntry[0] !== focusNodeEntry[0];
};
const addLink = ()=>{
editor.shouldSaveLinkPath = true;
// We insert an empty anchor, so we split the DOM to have a element we can use as reference for the popover
insertLink(editor, {
url: ''
});
};
const label = {
id: 'components.Blocks.link',
defaultMessage: 'Link'
};
if (location === 'menu') {
return /*#__PURE__*/ jsxs(StyledMenuItem, {
onSelect: addLink,
isActive: isLinkActive(),
disabled: isLinkDisabled(),
children: [
/*#__PURE__*/ jsx(Link, {}),
formatMessage(label)
]
});
}
return /*#__PURE__*/ jsx(ToolbarButton, {
icon: Link,
name: "link",
label: label,
isActive: isLinkActive(),
handleClick: addLink,
disabled: isLinkDisabled()
});
};
const StyledMenuItem = styled(Menu.Item)`
&:hover {
background-color: ${({ theme })=>theme.colors.primary100};
}
${(props)=>props.isActive && css`
font-weight: bold;
background-color: ${({ theme })=>theme.colors.primary100};
color: ${({ theme })=>theme.colors.primary600};
font-weight: bold;
`}
> span {
display: inline-flex;
gap: ${({ theme })=>theme.spaces[2]};
align-items: center;
}
svg {
fill: ${({ theme, isActive })=>isActive ? theme.colors.primary600 : theme.colors.neutral600};
}
`;
const BlocksToolbar = ()=>{
const { editor, blocks, modifiers, disabled } = useBlocksEditorContext('BlocksToolbar');
const { formatMessage } = useIntl();
/**
* The modifier buttons are disabled when an image is selected.
*/ const checkButtonDisabled = ()=>{
// Always disabled when the whole editor is disabled
if (disabled) {
return true;
}
if (!editor.selection) {
return false;
}
const selectedNode = editor.children[editor.selection.anchor.path[0]];
if ([
'image',
'code'
].includes(selectedNode.type)) {
return true;
}
return false;
};
const isButtonDisabled = checkButtonDisabled();
/**
* Observed components are ones that may or may not be visible in the toolbar, depending on the
* available space. They provide two render props:
* - renderInToolbar: for when we try to render the component in the toolbar (may be hidden)
* - renderInMenu: for when the component didn't fit in the toolbar and is relegated
* to the "more" menu
*/ const observedComponents = [
...Object.entries(modifiers).map(([name, modifier])=>{
const Icon = modifier.icon;
const isActive = modifier.checkIsActive(editor);
const handleSelect = ()=>modifier.handleToggle(editor);
return {
toolbar: /*#__PURE__*/ jsx(ToolbarButton, {
name: name,
icon: modifier.icon,
label: modifier.label,
isActive: modifier.checkIsActive(editor),
handleClick: handleSelect,
disabled: isButtonDisabled
}, name),
menu: /*#__PURE__*/ jsxs(StyledMenuItem, {
onSelect: handleSelect,
isActive: isActive,
children: [
/*#__PURE__*/ jsx(Icon, {}),
formatMessage(modifier.label)
]
}),
key: `modifier.${name}`
};
}),
{
toolbar: /*#__PURE__*/ jsx(LinkButton, {
disabled: isButtonDisabled,
location: "toolbar"
}),
menu: /*#__PURE__*/ jsx(LinkButton, {
disabled: isButtonDisabled,
location: "menu"
}),
key: 'block.link'
},
{
// List buttons can only be rendered together when in the toolbar
toolbar: /*#__PURE__*/ jsxs(Flex, {
direction: "row",
gap: 1,
children: [
/*#__PURE__*/ jsx(ToolbarSeparator, {}),
/*#__PURE__*/ jsx(Toolbar.ToggleGroup, {
type: "single",
asChild: true,
children: /*#__PURE__*/ jsxs(Flex, {
gap: 1,
children: [
/*#__PURE__*/ jsx(ListButton, {
block: blocks['list-unordered'],
format: "unordered",
location: "toolbar"
}),
/*#__PURE__*/ jsx(ListButton, {
block: blocks['list-ordered'],
format: "ordered",
location: "toolbar"
})
]
})
})
]
}),
menu: /*#__PURE__*/ jsxs(Fragment, {
children: [
/*#__PURE__*/ jsx(Menu.Separator, {}),
/*#__PURE__*/ jsx(ListButton, {
block: blocks['list-unordered'],
format: "unordered",
location: "menu"
}),
/*#__PURE__*/ jsx(ListButton, {
block: blocks['list-ordered'],
format: "ordered",
location: "menu"
})
]
}),
key: 'block.list'
}
];
return /*#__PURE__*/ jsx(Toolbar.Root, {
"aria-disabled": disabled,
asChild: true,
children: /*#__PURE__*/ jsxs(ToolbarWrapper, {
gap: 2,
padding: 2,
width: "100%",
children: [
/*#__PURE__*/ jsx(BlocksDropdown, {}),
/*#__PURE__*/ jsx(ToolbarSeparator, {}),
/*#__PURE__*/ jsx(Toolbar.ToggleGroup, {
type: "multiple",
asChild: true,
children: /*#__PURE__*/ jsx(Flex, {
direction: "row",
gap: 1,
grow: 1,
overflow: "hidden",
children: /*#__PURE__*/ jsx(EditorToolbarObserver, {
observedComponents: observedComponents
})
})
})
]
})
});
};
export { BlocksToolbar, useConversionModal };
//# sourceMappingURL=BlocksToolbar.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"BlocksToolbar.mjs","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksToolbar.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport * as Toolbar from '@radix-ui/react-toolbar';\nimport { useElementOnScreen } from '@strapi/admin/strapi-admin';\nimport {\n Flex,\n Tooltip,\n SingleSelect,\n SingleSelectOption,\n Box,\n FlexComponent,\n BoxComponent,\n Menu,\n IconButton,\n} from '@strapi/design-system';\nimport { Link, More } from '@strapi/icons';\nimport { MessageDescriptor, useIntl } from 'react-intl';\nimport { Editor, Transforms, Element as SlateElement, Node, type Ancestor } from 'slate';\nimport { ReactEditor } from 'slate-react';\nimport { css, styled } from 'styled-components';\n\nimport { EditorToolbarObserver } from '../../EditorToolbarObserver';\n\nimport {\n type BlocksStore,\n type SelectorBlockKey,\n isSelectorBlockKey,\n useBlocksEditorContext,\n} from './BlocksEditor';\nimport { insertLink } from './utils/links';\nimport { type Block, getEntries, getKeys } from './utils/types';\n\nconst ToolbarWrapper = styled<FlexComponent>(Flex)`\n &[aria-disabled='true'] {\n cursor: not-allowed;\n background: ${({ theme }) => theme.colors.neutral150};\n }\n`;\n\nconst ToolbarSeparator = styled(Toolbar.Separator)`\n background: ${({ theme }) => theme.colors.neutral150};\n width: 1px;\n height: 2.4rem;\n`;\n\nconst FlexButton = styled<FlexComponent<'button'>>(Flex)`\n // Inherit the not-allowed cursor from ToolbarWrapper when disabled\n &[aria-disabled] {\n cursor: not-allowed;\n }\n\n &[aria-disabled='false'] {\n cursor: pointer;\n\n // Only apply hover styles if the button is enabled\n &:hover {\n background: ${({ theme }) => theme.colors.primary100};\n }\n }\n`;\n\nconst SelectWrapper = styled<BoxComponent>(Box)`\n // Styling changes to SingleSelect component don't work, so adding wrapper to target SingleSelect\n div[role='combobox'] {\n border: none;\n cursor: pointer;\n min-height: unset;\n padding-top: 6px;\n padding-bottom: 6px;\n\n &[aria-disabled='false']:hover {\n cursor: pointer;\n background: ${({ theme }) => theme.colors.primary100};\n }\n\n &[aria-disabled] {\n background: transparent;\n cursor: inherit;\n\n // Select text and icons should also have disabled color\n span {\n color: ${({ theme }) => theme.colors.neutral600};\n }\n }\n }\n`;\n\n/**\n * Handles the modal component that may be returned by a block when converting it\n */\nfunction useConversionModal() {\n const [modalElement, setModalComponent] = React.useState<React.JSX.Element | null>(null);\n\n const handleConversionResult = (renderModal: void | (() => React.JSX.Element) | undefined) => {\n // Not all blocks return a modal\n if (renderModal) {\n // Use cloneElement to apply a key because to create a new instance of the component\n // Without the new key, the state is kept from previous times that option was picked\n setModalComponent(React.cloneElement(renderModal(), { key: Date.now() }));\n }\n };\n\n return { modalElement, handleConversionResult };\n}\n\ninterface ToolbarButtonProps {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n name: string;\n label: MessageDescriptor;\n isActive: boolean;\n disabled: boolean;\n handleClick: () => void;\n}\n\nconst ToolbarButton = ({\n icon: Icon,\n name,\n label,\n isActive,\n disabled,\n handleClick,\n}: ToolbarButtonProps) => {\n const { editor } = useBlocksEditorContext('ToolbarButton');\n const { formatMessage } = useIntl();\n const labelMessage = formatMessage(label);\n\n const enabledColor = isActive ? 'primary600' : 'neutral600';\n\n return (\n <Tooltip label={labelMessage}>\n <Toolbar.ToggleItem\n value={name}\n data-state={isActive ? 'on' : 'off'}\n onMouseDown={(e) => {\n e.preventDefault();\n handleClick();\n ReactEditor.focus(editor);\n }}\n aria-disabled={disabled}\n disabled={disabled}\n aria-label={labelMessage}\n asChild\n >\n <FlexButton\n tag=\"button\"\n background={isActive ? 'primary100' : ''}\n alignItems=\"center\"\n justifyContent=\"center\"\n width={7}\n height={7}\n hasRadius\n >\n <Icon fill={disabled ? 'neutral300' : enabledColor} />\n </FlexButton>\n </Toolbar.ToggleItem>\n </Tooltip>\n );\n};\n\nconst BlocksDropdown = () => {\n const { editor, blocks, disabled } = useBlocksEditorContext('BlocksDropdown');\n const { formatMessage } = useIntl();\n const { modalElement, handleConversionResult } = useConversionModal();\n\n const blockKeysToInclude: SelectorBlockKey[] = getEntries(blocks).reduce<\n ReturnType<typeof getEntries>\n >((currentKeys, entry) => {\n const [key, block] = entry;\n\n return bloLine truncated
@@ -0,0 +1,89 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
require('react');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var reactIntl = require('react-intl');
var styledComponents = require('styled-components');
var translations = require('../../../../../utils/translations.js');
var BlocksEditor = require('./BlocksEditor.js');
const EditorLayout = ({ children, error, disabled, onToggleExpand, ariaDescriptionId })=>{
const { formatMessage } = reactIntl.useIntl();
const { isExpandedMode } = BlocksEditor.useBlocksEditorContext('editorLayout');
return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
children: [
isExpandedMode && /*#__PURE__*/ jsxRuntime.jsx(designSystem.Modal.Root, {
open: isExpandedMode,
onOpenChange: onToggleExpand,
children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Modal.Content, {
style: {
maxWidth: 'unset',
width: 'unset'
},
children: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
height: "90vh",
width: "90vw",
alignItems: "flex-start",
direction: "column",
children: [
children,
/*#__PURE__*/ jsxRuntime.jsx(designSystem.IconButton, {
position: "absolute",
bottom: "1.2rem",
right: "1.2rem",
shadow: "filterShadow",
label: formatMessage({
id: translations.getTranslation('components.Blocks.collapse'),
defaultMessage: 'Collapse'
}),
onClick: onToggleExpand,
children: /*#__PURE__*/ jsxRuntime.jsx(Icons.Collapse, {})
})
]
})
})
}),
/*#__PURE__*/ jsxRuntime.jsx(InputWrapper, {
direction: "column",
alignItems: "flex-start",
height: "512px",
$disabled: disabled,
$hasError: Boolean(error),
style: {
overflow: 'hidden'
},
"aria-describedby": ariaDescriptionId,
position: "relative",
children: !isExpandedMode && children
})
]
});
};
const InputWrapper = styledComponents.styled(designSystem.Flex)`
border: 1px solid
${({ theme, $hasError })=>$hasError ? theme.colors.danger600 : theme.colors.neutral200};
border-radius: ${({ theme })=>theme.borderRadius};
background: ${({ theme })=>theme.colors.neutral0};
${({ theme, $hasError = false })=>styledComponents.css`
outline: none;
box-shadow: 0;
transition-property: border-color, box-shadow, fill;
transition-duration: 0.2s;
&:focus-within {
border: 1px solid ${$hasError ? theme.colors.danger600 : theme.colors.primary600};
box-shadow: ${$hasError ? theme.colors.danger600 : theme.colors.primary600} 0px 0px 0px 2px;
}
`}
${({ theme, $disabled })=>$disabled ? styledComponents.css`
color: ${theme.colors.neutral600};
background: ${theme.colors.neutral150};
` : undefined}
`;
exports.EditorLayout = EditorLayout;
//# sourceMappingURL=EditorLayout.js.map
@@ -0,0 +1 @@
{"version":3,"file":"EditorLayout.js","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/EditorLayout.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Flex, IconButton, FlexComponent, Modal } from '@strapi/design-system';\nimport { Collapse } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\nimport { css, styled } from 'styled-components';\n\nimport { getTranslation } from '../../../../../utils/translations';\n\nimport { useBlocksEditorContext } from './BlocksEditor';\n\ninterface EditorLayoutProps {\n children: React.ReactNode;\n error?: string;\n onToggleExpand: () => void;\n disabled: boolean;\n ariaDescriptionId: string;\n}\n\nconst EditorLayout = ({\n children,\n error,\n disabled,\n onToggleExpand,\n ariaDescriptionId,\n}: EditorLayoutProps) => {\n const { formatMessage } = useIntl();\n const { isExpandedMode } = useBlocksEditorContext('editorLayout');\n\n return (\n <>\n {isExpandedMode && (\n <Modal.Root open={isExpandedMode} onOpenChange={onToggleExpand}>\n <Modal.Content style={{ maxWidth: 'unset', width: 'unset' }}>\n <Flex height=\"90vh\" width=\"90vw\" alignItems=\"flex-start\" direction=\"column\">\n {children}\n <IconButton\n position=\"absolute\"\n bottom=\"1.2rem\"\n right=\"1.2rem\"\n shadow=\"filterShadow\"\n label={formatMessage({\n id: getTranslation('components.Blocks.collapse'),\n defaultMessage: 'Collapse',\n })}\n onClick={onToggleExpand}\n >\n <Collapse />\n </IconButton>\n </Flex>\n </Modal.Content>\n </Modal.Root>\n )}\n <InputWrapper\n direction=\"column\"\n alignItems=\"flex-start\"\n height=\"512px\"\n $disabled={disabled}\n $hasError={Boolean(error)}\n style={{ overflow: 'hidden' }}\n aria-describedby={ariaDescriptionId}\n position=\"relative\"\n >\n {!isExpandedMode && children}\n </InputWrapper>\n </>\n );\n};\n\nconst InputWrapper = styled<FlexComponent>(Flex)<{ $disabled?: boolean; $hasError?: boolean }>`\n border: 1px solid\n ${({ theme, $hasError }) => ($hasError ? theme.colors.danger600 : theme.colors.neutral200)};\n border-radius: ${({ theme }) => theme.borderRadius};\n background: ${({ theme }) => theme.colors.neutral0};\n\n ${({ theme, $hasError = false }) => css`\n outline: none;\n box-shadow: 0;\n transition-property: border-color, box-shadow, fill;\n transition-duration: 0.2s;\n\n &:focus-within {\n border: 1px solid ${$hasError ? theme.colors.danger600 : theme.colors.primary600};\n box-shadow: ${$hasError ? theme.colors.danger600 : theme.colors.primary600} 0px 0px 0px 2px;\n }\n `}\n\n ${({ theme, $disabled }) =>\n $disabled\n ? css`\n color: ${theme.colors.neutral600};\n background: ${theme.colors.neutral150};\n `\n : undefined}\n`;\n\nexport { EditorLayout };\n"],"names":["EditorLayout","children","error","disabled","onToggleExpand","ariaDescriptionId","formatMessage","useIntl","isExpandedMode","useBlocksEditorContext","_jsxs","_Fragment","_jsx","Modal","Root","open","onOpenChange","Content","style","maxWidth","width","Flex","height","alignItems","direction","IconButton","position","bottom","right","shadow","label","id","getTranslation","defaultMessage","onClick","Collapse","InputWrapper","$disabled","$hasError","Boolean","overflow","aria-describedby","styled","theme","colors","danger600","neutral200","borderRadius","neutral0","css","primary600","neutral600","neutral150","undefined"],"mappings":";;;;;;;;;;;AAmBA,MAAMA,YAAe,GAAA,CAAC,EACpBC,QAAQ,EACRC,KAAK,EACLC,QAAQ,EACRC,cAAc,EACdC,iBAAiB,EACC,GAAA;IAClB,MAAM,EAAEC,aAAa,EAAE,GAAGC,iBAAAA,EAAAA;AAC1B,IAAA,MAAM,EAAEC,cAAc,EAAE,GAAGC,mCAAuB,CAAA,cAAA,CAAA;IAElD,qBACEC,eAAA,CAAAC,mBAAA,EAAA;;YACGH,cACC,kBAAAI,cAAA,CAACC,mBAAMC,IAAI,EAAA;gBAACC,IAAMP,EAAAA,cAAAA;gBAAgBQ,YAAcZ,EAAAA,cAAAA;wCAC9CQ,cAAA,CAACC,mBAAMI,OAAO,EAAA;oBAACC,KAAO,EAAA;wBAAEC,QAAU,EAAA,OAAA;wBAASC,KAAO,EAAA;AAAQ,qBAAA;AACxD,oBAAA,QAAA,gBAAAV,eAACW,CAAAA,iBAAAA,EAAAA;wBAAKC,MAAO,EAAA,MAAA;wBAAOF,KAAM,EAAA,MAAA;wBAAOG,UAAW,EAAA,YAAA;wBAAaC,SAAU,EAAA,QAAA;;AAChEvB,4BAAAA,QAAAA;0CACDW,cAACa,CAAAA,uBAAAA,EAAAA;gCACCC,QAAS,EAAA,UAAA;gCACTC,MAAO,EAAA,QAAA;gCACPC,KAAM,EAAA,QAAA;gCACNC,MAAO,EAAA,cAAA;AACPC,gCAAAA,KAAAA,EAAOxB,aAAc,CAAA;AACnByB,oCAAAA,EAAAA,EAAIC,2BAAe,CAAA,4BAAA,CAAA;oCACnBC,cAAgB,EAAA;AAClB,iCAAA,CAAA;gCACAC,OAAS9B,EAAAA,cAAAA;AAET,gCAAA,QAAA,gBAAAQ,cAACuB,CAAAA,cAAAA,EAAAA,EAAAA;;;;;;0BAMXvB,cAACwB,CAAAA,YAAAA,EAAAA;gBACCZ,SAAU,EAAA,QAAA;gBACVD,UAAW,EAAA,YAAA;gBACXD,MAAO,EAAA,OAAA;gBACPe,SAAWlC,EAAAA,QAAAA;AACXmC,gBAAAA,SAAAA,EAAWC,OAAQrC,CAAAA,KAAAA,CAAAA;gBACnBgB,KAAO,EAAA;oBAAEsB,QAAU,EAAA;AAAS,iBAAA;gBAC5BC,kBAAkBpC,EAAAA,Line truncated
@@ -0,0 +1,87 @@
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import 'react';
import { Flex, Modal, IconButton } from '@strapi/design-system';
import { Collapse } from '@strapi/icons';
import { useIntl } from 'react-intl';
import { styled, css } from 'styled-components';
import { getTranslation } from '../../../../../utils/translations.mjs';
import { useBlocksEditorContext } from './BlocksEditor.mjs';
const EditorLayout = ({ children, error, disabled, onToggleExpand, ariaDescriptionId })=>{
const { formatMessage } = useIntl();
const { isExpandedMode } = useBlocksEditorContext('editorLayout');
return /*#__PURE__*/ jsxs(Fragment, {
children: [
isExpandedMode && /*#__PURE__*/ jsx(Modal.Root, {
open: isExpandedMode,
onOpenChange: onToggleExpand,
children: /*#__PURE__*/ jsx(Modal.Content, {
style: {
maxWidth: 'unset',
width: 'unset'
},
children: /*#__PURE__*/ jsxs(Flex, {
height: "90vh",
width: "90vw",
alignItems: "flex-start",
direction: "column",
children: [
children,
/*#__PURE__*/ jsx(IconButton, {
position: "absolute",
bottom: "1.2rem",
right: "1.2rem",
shadow: "filterShadow",
label: formatMessage({
id: getTranslation('components.Blocks.collapse'),
defaultMessage: 'Collapse'
}),
onClick: onToggleExpand,
children: /*#__PURE__*/ jsx(Collapse, {})
})
]
})
})
}),
/*#__PURE__*/ jsx(InputWrapper, {
direction: "column",
alignItems: "flex-start",
height: "512px",
$disabled: disabled,
$hasError: Boolean(error),
style: {
overflow: 'hidden'
},
"aria-describedby": ariaDescriptionId,
position: "relative",
children: !isExpandedMode && children
})
]
});
};
const InputWrapper = styled(Flex)`
border: 1px solid
${({ theme, $hasError })=>$hasError ? theme.colors.danger600 : theme.colors.neutral200};
border-radius: ${({ theme })=>theme.borderRadius};
background: ${({ theme })=>theme.colors.neutral0};
${({ theme, $hasError = false })=>css`
outline: none;
box-shadow: 0;
transition-property: border-color, box-shadow, fill;
transition-duration: 0.2s;
&:focus-within {
border: 1px solid ${$hasError ? theme.colors.danger600 : theme.colors.primary600};
box-shadow: ${$hasError ? theme.colors.danger600 : theme.colors.primary600} 0px 0px 0px 2px;
}
`}
${({ theme, $disabled })=>$disabled ? css`
color: ${theme.colors.neutral600};
background: ${theme.colors.neutral150};
` : undefined}
`;
export { EditorLayout };
//# sourceMappingURL=EditorLayout.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"EditorLayout.mjs","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/EditorLayout.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Flex, IconButton, FlexComponent, Modal } from '@strapi/design-system';\nimport { Collapse } from '@strapi/icons';\nimport { useIntl } from 'react-intl';\nimport { css, styled } from 'styled-components';\n\nimport { getTranslation } from '../../../../../utils/translations';\n\nimport { useBlocksEditorContext } from './BlocksEditor';\n\ninterface EditorLayoutProps {\n children: React.ReactNode;\n error?: string;\n onToggleExpand: () => void;\n disabled: boolean;\n ariaDescriptionId: string;\n}\n\nconst EditorLayout = ({\n children,\n error,\n disabled,\n onToggleExpand,\n ariaDescriptionId,\n}: EditorLayoutProps) => {\n const { formatMessage } = useIntl();\n const { isExpandedMode } = useBlocksEditorContext('editorLayout');\n\n return (\n <>\n {isExpandedMode && (\n <Modal.Root open={isExpandedMode} onOpenChange={onToggleExpand}>\n <Modal.Content style={{ maxWidth: 'unset', width: 'unset' }}>\n <Flex height=\"90vh\" width=\"90vw\" alignItems=\"flex-start\" direction=\"column\">\n {children}\n <IconButton\n position=\"absolute\"\n bottom=\"1.2rem\"\n right=\"1.2rem\"\n shadow=\"filterShadow\"\n label={formatMessage({\n id: getTranslation('components.Blocks.collapse'),\n defaultMessage: 'Collapse',\n })}\n onClick={onToggleExpand}\n >\n <Collapse />\n </IconButton>\n </Flex>\n </Modal.Content>\n </Modal.Root>\n )}\n <InputWrapper\n direction=\"column\"\n alignItems=\"flex-start\"\n height=\"512px\"\n $disabled={disabled}\n $hasError={Boolean(error)}\n style={{ overflow: 'hidden' }}\n aria-describedby={ariaDescriptionId}\n position=\"relative\"\n >\n {!isExpandedMode && children}\n </InputWrapper>\n </>\n );\n};\n\nconst InputWrapper = styled<FlexComponent>(Flex)<{ $disabled?: boolean; $hasError?: boolean }>`\n border: 1px solid\n ${({ theme, $hasError }) => ($hasError ? theme.colors.danger600 : theme.colors.neutral200)};\n border-radius: ${({ theme }) => theme.borderRadius};\n background: ${({ theme }) => theme.colors.neutral0};\n\n ${({ theme, $hasError = false }) => css`\n outline: none;\n box-shadow: 0;\n transition-property: border-color, box-shadow, fill;\n transition-duration: 0.2s;\n\n &:focus-within {\n border: 1px solid ${$hasError ? theme.colors.danger600 : theme.colors.primary600};\n box-shadow: ${$hasError ? theme.colors.danger600 : theme.colors.primary600} 0px 0px 0px 2px;\n }\n `}\n\n ${({ theme, $disabled }) =>\n $disabled\n ? css`\n color: ${theme.colors.neutral600};\n background: ${theme.colors.neutral150};\n `\n : undefined}\n`;\n\nexport { EditorLayout };\n"],"names":["EditorLayout","children","error","disabled","onToggleExpand","ariaDescriptionId","formatMessage","useIntl","isExpandedMode","useBlocksEditorContext","_jsxs","_Fragment","_jsx","Modal","Root","open","onOpenChange","Content","style","maxWidth","width","Flex","height","alignItems","direction","IconButton","position","bottom","right","shadow","label","id","getTranslation","defaultMessage","onClick","Collapse","InputWrapper","$disabled","$hasError","Boolean","overflow","aria-describedby","styled","theme","colors","danger600","neutral200","borderRadius","neutral0","css","primary600","neutral600","neutral150","undefined"],"mappings":";;;;;;;;;AAmBA,MAAMA,YAAe,GAAA,CAAC,EACpBC,QAAQ,EACRC,KAAK,EACLC,QAAQ,EACRC,cAAc,EACdC,iBAAiB,EACC,GAAA;IAClB,MAAM,EAAEC,aAAa,EAAE,GAAGC,OAAAA,EAAAA;AAC1B,IAAA,MAAM,EAAEC,cAAc,EAAE,GAAGC,sBAAuB,CAAA,cAAA,CAAA;IAElD,qBACEC,IAAA,CAAAC,QAAA,EAAA;;YACGH,cACC,kBAAAI,GAAA,CAACC,MAAMC,IAAI,EAAA;gBAACC,IAAMP,EAAAA,cAAAA;gBAAgBQ,YAAcZ,EAAAA,cAAAA;wCAC9CQ,GAAA,CAACC,MAAMI,OAAO,EAAA;oBAACC,KAAO,EAAA;wBAAEC,QAAU,EAAA,OAAA;wBAASC,KAAO,EAAA;AAAQ,qBAAA;AACxD,oBAAA,QAAA,gBAAAV,IAACW,CAAAA,IAAAA,EAAAA;wBAAKC,MAAO,EAAA,MAAA;wBAAOF,KAAM,EAAA,MAAA;wBAAOG,UAAW,EAAA,YAAA;wBAAaC,SAAU,EAAA,QAAA;;AAChEvB,4BAAAA,QAAAA;0CACDW,GAACa,CAAAA,UAAAA,EAAAA;gCACCC,QAAS,EAAA,UAAA;gCACTC,MAAO,EAAA,QAAA;gCACPC,KAAM,EAAA,QAAA;gCACNC,MAAO,EAAA,cAAA;AACPC,gCAAAA,KAAAA,EAAOxB,aAAc,CAAA;AACnByB,oCAAAA,EAAAA,EAAIC,cAAe,CAAA,4BAAA,CAAA;oCACnBC,cAAgB,EAAA;AAClB,iCAAA,CAAA;gCACAC,OAAS9B,EAAAA,cAAAA;AAET,gCAAA,QAAA,gBAAAQ,GAACuB,CAAAA,QAAAA,EAAAA,EAAAA;;;;;;0BAMXvB,GAACwB,CAAAA,YAAAA,EAAAA;gBACCZ,SAAU,EAAA,QAAA;gBACVD,UAAW,EAAA,YAAA;gBACXD,MAAO,EAAA,OAAA;gBACPe,SAAWlC,EAAAA,QAAAA;AACXmC,gBAAAA,SAAAA,EAAWC,OAAQrC,CAAAA,KAAAA,CAAAA;gBACnBgB,KAAO,EAAA;oBAAEsB,QAAU,EAAA;AAAS,iBAAA;gBAC5BC,kBAAkBpC,EAAAA,iBAAAA;gLine truncated
@@ -0,0 +1,134 @@
'use strict';
var jsxRuntime = require('react/jsx-runtime');
require('react');
var designSystem = require('@strapi/design-system');
var Icons = require('@strapi/icons');
var slate = require('slate');
var styledComponents = require('styled-components');
const stylesToInherit = styledComponents.css`
font-size: inherit;
color: inherit;
line-height: inherit;
`;
const BoldText = styledComponents.styled(designSystem.Typography).attrs({
fontWeight: 'bold'
})`
${stylesToInherit}
`;
const ItalicText = styledComponents.styled(designSystem.Typography)`
font-style: italic;
${stylesToInherit}
`;
const UnderlineText = styledComponents.styled(designSystem.Typography).attrs({
textDecoration: 'underline'
})`
${stylesToInherit}
`;
const StrikeThroughText = styledComponents.styled(designSystem.Typography).attrs({
textDecoration: 'line-through'
})`
${stylesToInherit}
`;
const InlineCode = styledComponents.styled.code`
background-color: ${({ theme })=>theme.colors.neutral150};
border-radius: ${({ theme })=>theme.borderRadius};
padding: ${({ theme })=>`0 ${theme.spaces[2]}`};
font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas,
monospace;
color: inherit;
`;
/**
* The default handler for checking if a modifier is active
*/ const baseCheckIsActive = (editor, name)=>{
const marks = slate.Editor.marks(editor);
if (!marks) return false;
return Boolean(marks[name]);
};
/**
* The default handler for toggling a modifier
*/ const baseHandleToggle = (editor, name)=>{
const marks = slate.Editor.marks(editor);
// If there is no selection, set selection to the end of line
if (!editor.selection) {
const endOfEditor = slate.Editor.end(editor, []);
slate.Transforms.select(editor, endOfEditor);
}
// Toggle the modifier
if (marks?.[name]) {
slate.Editor.removeMark(editor, name);
} else {
slate.Editor.addMark(editor, name, true);
}
};
const modifiers = {
bold: {
icon: Icons.Bold,
isValidEventKey: (event)=>event.key === 'b',
label: {
id: 'components.Blocks.modifiers.bold',
defaultMessage: 'Bold'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'bold'),
handleToggle: (editor)=>baseHandleToggle(editor, 'bold'),
renderLeaf: (children)=>/*#__PURE__*/ jsxRuntime.jsx(BoldText, {
children: children
})
},
italic: {
icon: Icons.Italic,
isValidEventKey: (event)=>event.key === 'i',
label: {
id: 'components.Blocks.modifiers.italic',
defaultMessage: 'Italic'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'italic'),
handleToggle: (editor)=>baseHandleToggle(editor, 'italic'),
renderLeaf: (children)=>/*#__PURE__*/ jsxRuntime.jsx(ItalicText, {
children: children
})
},
underline: {
icon: Icons.Underline,
isValidEventKey: (event)=>event.key === 'u',
label: {
id: 'components.Blocks.modifiers.underline',
defaultMessage: 'Underline'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'underline'),
handleToggle: (editor)=>baseHandleToggle(editor, 'underline'),
renderLeaf: (children)=>/*#__PURE__*/ jsxRuntime.jsx(UnderlineText, {
children: children
})
},
strikethrough: {
icon: Icons.StrikeThrough,
isValidEventKey: (event)=>event.key === 'S' && event.shiftKey,
label: {
id: 'components.Blocks.modifiers.strikethrough',
defaultMessage: 'Strikethrough'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'strikethrough'),
handleToggle: (editor)=>baseHandleToggle(editor, 'strikethrough'),
renderLeaf: (children)=>/*#__PURE__*/ jsxRuntime.jsx(StrikeThroughText, {
children: children
})
},
code: {
icon: Icons.Code,
isValidEventKey: (event)=>event.key === 'e',
label: {
id: 'components.Blocks.modifiers.code',
defaultMessage: 'Inline code'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'code'),
handleToggle: (editor)=>baseHandleToggle(editor, 'code'),
renderLeaf: (children)=>/*#__PURE__*/ jsxRuntime.jsx(InlineCode, {
children: children
})
}
};
exports.modifiers = modifiers;
//# sourceMappingURL=Modifiers.js.map
@@ -0,0 +1 @@
{"version":3,"file":"Modifiers.js","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Modifiers.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Typography, TypographyComponent } from '@strapi/design-system';\nimport { Bold, Italic, Underline, StrikeThrough, Code } from '@strapi/icons';\nimport { type MessageDescriptor } from 'react-intl';\nimport { Editor, Text, Transforms } from 'slate';\nimport { styled, css } from 'styled-components';\n\nconst stylesToInherit = css`\n font-size: inherit;\n color: inherit;\n line-height: inherit;\n`;\n\nconst BoldText = styled<TypographyComponent>(Typography).attrs({ fontWeight: 'bold' })`\n ${stylesToInherit}\n`;\n\nconst ItalicText = styled<TypographyComponent>(Typography)`\n font-style: italic;\n ${stylesToInherit}\n`;\n\nconst UnderlineText = styled<TypographyComponent>(Typography).attrs({\n textDecoration: 'underline',\n})`\n ${stylesToInherit}\n`;\n\nconst StrikeThroughText = styled<TypographyComponent>(Typography).attrs({\n textDecoration: 'line-through',\n})`\n ${stylesToInherit}\n`;\n\nconst InlineCode = styled.code`\n background-color: ${({ theme }) => theme.colors.neutral150};\n border-radius: ${({ theme }) => theme.borderRadius};\n padding: ${({ theme }) => `0 ${theme.spaces[2]}`};\n font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas,\n monospace;\n color: inherit;\n`;\n\ntype ModifierKey = Exclude<keyof Text, 'type' | 'text'>;\n\ntype ModifiersStore = {\n [K in ModifierKey]: {\n icon: React.ComponentType;\n isValidEventKey: (event: React.KeyboardEvent<HTMLElement>) => boolean;\n label: MessageDescriptor;\n checkIsActive: (editor: Editor) => boolean;\n handleToggle: (editor: Editor) => void;\n renderLeaf: (children: React.JSX.Element | string) => React.JSX.Element;\n };\n};\n\n/**\n * The default handler for checking if a modifier is active\n */\nconst baseCheckIsActive = (editor: Editor, name: ModifierKey) => {\n const marks = Editor.marks(editor);\n if (!marks) return false;\n\n return Boolean(marks[name]);\n};\n\n/**\n * The default handler for toggling a modifier\n */\nconst baseHandleToggle = (editor: Editor, name: ModifierKey) => {\n const marks = Editor.marks(editor);\n\n // If there is no selection, set selection to the end of line\n if (!editor.selection) {\n const endOfEditor = Editor.end(editor, []);\n Transforms.select(editor, endOfEditor);\n }\n\n // Toggle the modifier\n if (marks?.[name]) {\n Editor.removeMark(editor, name);\n } else {\n Editor.addMark(editor, name, true);\n }\n};\n\nconst modifiers: ModifiersStore = {\n bold: {\n icon: Bold,\n isValidEventKey: (event) => event.key === 'b',\n label: { id: 'components.Blocks.modifiers.bold', defaultMessage: 'Bold' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'bold'),\n handleToggle: (editor) => baseHandleToggle(editor, 'bold'),\n renderLeaf: (children) => <BoldText>{children}</BoldText>,\n },\n italic: {\n icon: Italic,\n isValidEventKey: (event) => event.key === 'i',\n label: { id: 'components.Blocks.modifiers.italic', defaultMessage: 'Italic' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'italic'),\n handleToggle: (editor) => baseHandleToggle(editor, 'italic'),\n renderLeaf: (children) => <ItalicText>{children}</ItalicText>,\n },\n underline: {\n icon: Underline,\n isValidEventKey: (event) => event.key === 'u',\n label: { id: 'components.Blocks.modifiers.underline', defaultMessage: 'Underline' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'underline'),\n handleToggle: (editor) => baseHandleToggle(editor, 'underline'),\n renderLeaf: (children) => <UnderlineText>{children}</UnderlineText>,\n },\n strikethrough: {\n icon: StrikeThrough,\n isValidEventKey: (event) => event.key === 'S' && event.shiftKey,\n label: { id: 'components.Blocks.modifiers.strikethrough', defaultMessage: 'Strikethrough' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'strikethrough'),\n handleToggle: (editor) => baseHandleToggle(editor, 'strikethrough'),\n renderLeaf: (children) => <StrikeThroughText>{children}</StrikeThroughText>,\n },\n code: {\n icon: Code,\n isValidEventKey: (event) => event.key === 'e',\n label: { id: 'components.Blocks.modifiers.code', defaultMessage: 'Inline code' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'code'),\n handleToggle: (editor) => baseHandleToggle(editor, 'code'),\n renderLeaf: (children) => <InlineCode>{children}</InlineCode>,\n },\n};\n\nexport { type ModifiersStore, modifiers };\n"],"names":["stylesToInherit","css","BoldText","styled","Typography","attrs","fontWeight","ItalicText","UnderlineText","textDecoration","StrikeThroughText","InlineCode","code","theme","colors","neutral150","borderRadius","spaces","baseCheckIsActive","editor","name","marks","Editor","Boolean",Line truncated
@@ -0,0 +1,132 @@
import { jsx } from 'react/jsx-runtime';
import 'react';
import { Typography } from '@strapi/design-system';
import { Bold, Italic, Underline, StrikeThrough, Code } from '@strapi/icons';
import { Editor, Transforms } from 'slate';
import { css, styled } from 'styled-components';
const stylesToInherit = css`
font-size: inherit;
color: inherit;
line-height: inherit;
`;
const BoldText = styled(Typography).attrs({
fontWeight: 'bold'
})`
${stylesToInherit}
`;
const ItalicText = styled(Typography)`
font-style: italic;
${stylesToInherit}
`;
const UnderlineText = styled(Typography).attrs({
textDecoration: 'underline'
})`
${stylesToInherit}
`;
const StrikeThroughText = styled(Typography).attrs({
textDecoration: 'line-through'
})`
${stylesToInherit}
`;
const InlineCode = styled.code`
background-color: ${({ theme })=>theme.colors.neutral150};
border-radius: ${({ theme })=>theme.borderRadius};
padding: ${({ theme })=>`0 ${theme.spaces[2]}`};
font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas,
monospace;
color: inherit;
`;
/**
* The default handler for checking if a modifier is active
*/ const baseCheckIsActive = (editor, name)=>{
const marks = Editor.marks(editor);
if (!marks) return false;
return Boolean(marks[name]);
};
/**
* The default handler for toggling a modifier
*/ const baseHandleToggle = (editor, name)=>{
const marks = Editor.marks(editor);
// If there is no selection, set selection to the end of line
if (!editor.selection) {
const endOfEditor = Editor.end(editor, []);
Transforms.select(editor, endOfEditor);
}
// Toggle the modifier
if (marks?.[name]) {
Editor.removeMark(editor, name);
} else {
Editor.addMark(editor, name, true);
}
};
const modifiers = {
bold: {
icon: Bold,
isValidEventKey: (event)=>event.key === 'b',
label: {
id: 'components.Blocks.modifiers.bold',
defaultMessage: 'Bold'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'bold'),
handleToggle: (editor)=>baseHandleToggle(editor, 'bold'),
renderLeaf: (children)=>/*#__PURE__*/ jsx(BoldText, {
children: children
})
},
italic: {
icon: Italic,
isValidEventKey: (event)=>event.key === 'i',
label: {
id: 'components.Blocks.modifiers.italic',
defaultMessage: 'Italic'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'italic'),
handleToggle: (editor)=>baseHandleToggle(editor, 'italic'),
renderLeaf: (children)=>/*#__PURE__*/ jsx(ItalicText, {
children: children
})
},
underline: {
icon: Underline,
isValidEventKey: (event)=>event.key === 'u',
label: {
id: 'components.Blocks.modifiers.underline',
defaultMessage: 'Underline'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'underline'),
handleToggle: (editor)=>baseHandleToggle(editor, 'underline'),
renderLeaf: (children)=>/*#__PURE__*/ jsx(UnderlineText, {
children: children
})
},
strikethrough: {
icon: StrikeThrough,
isValidEventKey: (event)=>event.key === 'S' && event.shiftKey,
label: {
id: 'components.Blocks.modifiers.strikethrough',
defaultMessage: 'Strikethrough'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'strikethrough'),
handleToggle: (editor)=>baseHandleToggle(editor, 'strikethrough'),
renderLeaf: (children)=>/*#__PURE__*/ jsx(StrikeThroughText, {
children: children
})
},
code: {
icon: Code,
isValidEventKey: (event)=>event.key === 'e',
label: {
id: 'components.Blocks.modifiers.code',
defaultMessage: 'Inline code'
},
checkIsActive: (editor)=>baseCheckIsActive(editor, 'code'),
handleToggle: (editor)=>baseHandleToggle(editor, 'code'),
renderLeaf: (children)=>/*#__PURE__*/ jsx(InlineCode, {
children: children
})
}
};
export { modifiers };
//# sourceMappingURL=Modifiers.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"Modifiers.mjs","sources":["../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/Modifiers.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { Typography, TypographyComponent } from '@strapi/design-system';\nimport { Bold, Italic, Underline, StrikeThrough, Code } from '@strapi/icons';\nimport { type MessageDescriptor } from 'react-intl';\nimport { Editor, Text, Transforms } from 'slate';\nimport { styled, css } from 'styled-components';\n\nconst stylesToInherit = css`\n font-size: inherit;\n color: inherit;\n line-height: inherit;\n`;\n\nconst BoldText = styled<TypographyComponent>(Typography).attrs({ fontWeight: 'bold' })`\n ${stylesToInherit}\n`;\n\nconst ItalicText = styled<TypographyComponent>(Typography)`\n font-style: italic;\n ${stylesToInherit}\n`;\n\nconst UnderlineText = styled<TypographyComponent>(Typography).attrs({\n textDecoration: 'underline',\n})`\n ${stylesToInherit}\n`;\n\nconst StrikeThroughText = styled<TypographyComponent>(Typography).attrs({\n textDecoration: 'line-through',\n})`\n ${stylesToInherit}\n`;\n\nconst InlineCode = styled.code`\n background-color: ${({ theme }) => theme.colors.neutral150};\n border-radius: ${({ theme }) => theme.borderRadius};\n padding: ${({ theme }) => `0 ${theme.spaces[2]}`};\n font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas,\n monospace;\n color: inherit;\n`;\n\ntype ModifierKey = Exclude<keyof Text, 'type' | 'text'>;\n\ntype ModifiersStore = {\n [K in ModifierKey]: {\n icon: React.ComponentType;\n isValidEventKey: (event: React.KeyboardEvent<HTMLElement>) => boolean;\n label: MessageDescriptor;\n checkIsActive: (editor: Editor) => boolean;\n handleToggle: (editor: Editor) => void;\n renderLeaf: (children: React.JSX.Element | string) => React.JSX.Element;\n };\n};\n\n/**\n * The default handler for checking if a modifier is active\n */\nconst baseCheckIsActive = (editor: Editor, name: ModifierKey) => {\n const marks = Editor.marks(editor);\n if (!marks) return false;\n\n return Boolean(marks[name]);\n};\n\n/**\n * The default handler for toggling a modifier\n */\nconst baseHandleToggle = (editor: Editor, name: ModifierKey) => {\n const marks = Editor.marks(editor);\n\n // If there is no selection, set selection to the end of line\n if (!editor.selection) {\n const endOfEditor = Editor.end(editor, []);\n Transforms.select(editor, endOfEditor);\n }\n\n // Toggle the modifier\n if (marks?.[name]) {\n Editor.removeMark(editor, name);\n } else {\n Editor.addMark(editor, name, true);\n }\n};\n\nconst modifiers: ModifiersStore = {\n bold: {\n icon: Bold,\n isValidEventKey: (event) => event.key === 'b',\n label: { id: 'components.Blocks.modifiers.bold', defaultMessage: 'Bold' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'bold'),\n handleToggle: (editor) => baseHandleToggle(editor, 'bold'),\n renderLeaf: (children) => <BoldText>{children}</BoldText>,\n },\n italic: {\n icon: Italic,\n isValidEventKey: (event) => event.key === 'i',\n label: { id: 'components.Blocks.modifiers.italic', defaultMessage: 'Italic' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'italic'),\n handleToggle: (editor) => baseHandleToggle(editor, 'italic'),\n renderLeaf: (children) => <ItalicText>{children}</ItalicText>,\n },\n underline: {\n icon: Underline,\n isValidEventKey: (event) => event.key === 'u',\n label: { id: 'components.Blocks.modifiers.underline', defaultMessage: 'Underline' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'underline'),\n handleToggle: (editor) => baseHandleToggle(editor, 'underline'),\n renderLeaf: (children) => <UnderlineText>{children}</UnderlineText>,\n },\n strikethrough: {\n icon: StrikeThrough,\n isValidEventKey: (event) => event.key === 'S' && event.shiftKey,\n label: { id: 'components.Blocks.modifiers.strikethrough', defaultMessage: 'Strikethrough' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'strikethrough'),\n handleToggle: (editor) => baseHandleToggle(editor, 'strikethrough'),\n renderLeaf: (children) => <StrikeThroughText>{children}</StrikeThroughText>,\n },\n code: {\n icon: Code,\n isValidEventKey: (event) => event.key === 'e',\n label: { id: 'components.Blocks.modifiers.code', defaultMessage: 'Inline code' },\n checkIsActive: (editor) => baseCheckIsActive(editor, 'code'),\n handleToggle: (editor) => baseHandleToggle(editor, 'code'),\n renderLeaf: (children) => <InlineCode>{children}</InlineCode>,\n },\n};\n\nexport { type ModifiersStore, modifiers };\n"],"names":["stylesToInherit","css","BoldText","styled","Typography","attrs","fontWeight","ItalicText","UnderlineText","textDecoration","StrikeThroughText","InlineCode","code","theme","colors","neutral150","borderRadius","spaces","baseCheckIsActive","editor","name","marks","Editor","Boolean"Line truncated
@@ -0,0 +1,17 @@
'use strict';
/**
* Images are void elements. They handle the rendering of their children instead of Slate.
* See the Slate documentation for more information:
* - https://docs.slatejs.org/api/nodes/element#void-vs-not-void
* - https://docs.slatejs.org/api/nodes/element#rendering-void-elements
*/ const withImages = (editor)=>{
const { isVoid } = editor;
editor.isVoid = (element)=>{
return element.type === 'image' ? true : isVoid(element);
};
return editor;
};
exports.withImages = withImages;
//# sourceMappingURL=withImages.js.map
@@ -0,0 +1 @@
{"version":3,"file":"withImages.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/plugins/withImages.ts"],"sourcesContent":["import { type Editor } from 'slate';\n\n/**\n * Images are void elements. They handle the rendering of their children instead of Slate.\n * See the Slate documentation for more information:\n * - https://docs.slatejs.org/api/nodes/element#void-vs-not-void\n * - https://docs.slatejs.org/api/nodes/element#rendering-void-elements\n */\nconst withImages = (editor: Editor) => {\n const { isVoid } = editor;\n\n editor.isVoid = (element) => {\n return element.type === 'image' ? true : isVoid(element);\n };\n\n return editor;\n};\n\nexport { withImages };\n"],"names":["withImages","editor","isVoid","element","type"],"mappings":";;AAEA;;;;;IAMA,MAAMA,aAAa,CAACC,MAAAA,GAAAA;IAClB,MAAM,EAAEC,MAAM,EAAE,GAAGD,MAAAA;IAEnBA,MAAOC,CAAAA,MAAM,GAAG,CAACC,OAAAA,GAAAA;AACf,QAAA,OAAOA,OAAQC,CAAAA,IAAI,KAAK,OAAA,GAAU,OAAOF,MAAOC,CAAAA,OAAAA,CAAAA;AAClD,KAAA;IAEA,OAAOF,MAAAA;AACT;;;;"}
@@ -0,0 +1,15 @@
/**
* Images are void elements. They handle the rendering of their children instead of Slate.
* See the Slate documentation for more information:
* - https://docs.slatejs.org/api/nodes/element#void-vs-not-void
* - https://docs.slatejs.org/api/nodes/element#rendering-void-elements
*/ const withImages = (editor)=>{
const { isVoid } = editor;
editor.isVoid = (element)=>{
return element.type === 'image' ? true : isVoid(element);
};
return editor;
};
export { withImages };
//# sourceMappingURL=withImages.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"withImages.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/plugins/withImages.ts"],"sourcesContent":["import { type Editor } from 'slate';\n\n/**\n * Images are void elements. They handle the rendering of their children instead of Slate.\n * See the Slate documentation for more information:\n * - https://docs.slatejs.org/api/nodes/element#void-vs-not-void\n * - https://docs.slatejs.org/api/nodes/element#rendering-void-elements\n */\nconst withImages = (editor: Editor) => {\n const { isVoid } = editor;\n\n editor.isVoid = (element) => {\n return element.type === 'image' ? true : isVoid(element);\n };\n\n return editor;\n};\n\nexport { withImages };\n"],"names":["withImages","editor","isVoid","element","type"],"mappings":"AAEA;;;;;IAMA,MAAMA,aAAa,CAACC,MAAAA,GAAAA;IAClB,MAAM,EAAEC,MAAM,EAAE,GAAGD,MAAAA;IAEnBA,MAAOC,CAAAA,MAAM,GAAG,CAACC,OAAAA,GAAAA;AACf,QAAA,OAAOA,OAAQC,CAAAA,IAAI,KAAK,OAAA,GAAU,OAAOF,MAAOC,CAAAA,OAAAA,CAAAA;AAClD,KAAA;IAEA,OAAOF,MAAAA;AACT;;;;"}
@@ -0,0 +1,75 @@
'use strict';
var slate = require('slate');
var links = require('../utils/links.js');
const withLinks = (editor)=>{
const { isInline, apply, insertText, insertData } = editor;
// Links are inline elements, so we need to override the isInline method for slate
editor.isInline = (element)=>{
return element.type === 'link' ? true : isInline(element);
};
// We keep a track of the last inserted link path
// So we can show the popover on the link component if that link is the last one inserted
editor.lastInsertedLinkPath = null;
// We intercept the apply method, so everytime we insert a new link, we save its path
editor.apply = (operation)=>{
if (operation.type === 'insert_node') {
if (!slate.Editor.isEditor(operation.node) && operation.node.type === 'link' && editor.shouldSaveLinkPath) {
editor.lastInsertedLinkPath = operation.path;
}
} else if (operation.type === 'move_node') {
// We need to update the last inserted link path when link is moved
// If link is the first word in the paragraph we dont need to update the path
if (slate.Path.hasPrevious(operation.path) && editor.lastInsertedLinkPath && editor.shouldSaveLinkPath) {
editor.lastInsertedLinkPath = slate.Path.transform(editor.lastInsertedLinkPath, operation);
}
}
apply(operation);
};
editor.insertText = (text)=>{
// When selection is at the end of a link and user types a space, we want to break the link
if (editor.selection && slate.Range.isCollapsed(editor.selection) && text === ' ') {
const linksInSelection = Array.from(slate.Editor.nodes(editor, {
at: editor.selection,
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'link'
}));
const selectionIsInLink = editor.selection && linksInSelection.length > 0;
const selectionIsAtEndOfLink = selectionIsInLink && slate.Point.equals(editor.selection.anchor, slate.Editor.end(editor, linksInSelection[0][1]));
if (selectionIsAtEndOfLink) {
slate.Transforms.insertNodes(editor, {
text: ' ',
type: 'text'
}, {
at: slate.Path.next(linksInSelection[0][1]),
select: true
});
return;
}
}
insertText(text);
};
// Add data as a clickable link if its a valid URL
editor.insertData = (data)=>{
const pastedText = data.getData('text/plain');
if (pastedText) {
try {
// eslint-disable-next-line no-new
new URL(pastedText);
// Do not show link popup on copy-paste a link, so do not save its path
editor.shouldSaveLinkPath = false;
links.insertLink(editor, {
url: pastedText
});
return;
} catch (error) {
// continue normal data insertion
}
}
insertData(data);
};
return editor;
};
exports.withLinks = withLinks;
//# sourceMappingURL=withLinks.js.map
@@ -0,0 +1 @@
{"version":3,"file":"withLinks.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/plugins/withLinks.ts"],"sourcesContent":["import { type BaseEditor, Path, Transforms, Range, Point, Editor } from 'slate';\n\nimport { insertLink } from '../utils/links';\n\ninterface LinkEditor extends BaseEditor {\n lastInsertedLinkPath: Path | null;\n shouldSaveLinkPath: boolean;\n}\n\nconst withLinks = (editor: Editor) => {\n const { isInline, apply, insertText, insertData } = editor;\n\n // Links are inline elements, so we need to override the isInline method for slate\n editor.isInline = (element) => {\n return element.type === 'link' ? true : isInline(element);\n };\n\n // We keep a track of the last inserted link path\n // So we can show the popover on the link component if that link is the last one inserted\n editor.lastInsertedLinkPath = null;\n\n // We intercept the apply method, so everytime we insert a new link, we save its path\n editor.apply = (operation) => {\n if (operation.type === 'insert_node') {\n if (\n !Editor.isEditor(operation.node) &&\n operation.node.type === 'link' &&\n editor.shouldSaveLinkPath\n ) {\n editor.lastInsertedLinkPath = operation.path;\n }\n } else if (operation.type === 'move_node') {\n // We need to update the last inserted link path when link is moved\n // If link is the first word in the paragraph we dont need to update the path\n if (\n Path.hasPrevious(operation.path) &&\n editor.lastInsertedLinkPath &&\n editor.shouldSaveLinkPath\n ) {\n editor.lastInsertedLinkPath = Path.transform(editor.lastInsertedLinkPath, operation);\n }\n }\n\n apply(operation);\n };\n\n editor.insertText = (text) => {\n // When selection is at the end of a link and user types a space, we want to break the link\n if (editor.selection && Range.isCollapsed(editor.selection) && text === ' ') {\n const linksInSelection = Array.from(\n Editor.nodes(editor, {\n at: editor.selection,\n match: (node) => !Editor.isEditor(node) && node.type === 'link',\n })\n );\n\n const selectionIsInLink = editor.selection && linksInSelection.length > 0;\n const selectionIsAtEndOfLink =\n selectionIsInLink &&\n Point.equals(editor.selection.anchor, Editor.end(editor, linksInSelection[0][1]));\n\n if (selectionIsAtEndOfLink) {\n Transforms.insertNodes(\n editor,\n { text: ' ', type: 'text' },\n { at: Path.next(linksInSelection[0][1]), select: true }\n );\n\n return;\n }\n }\n\n insertText(text);\n };\n\n // Add data as a clickable link if its a valid URL\n editor.insertData = (data) => {\n const pastedText = data.getData('text/plain');\n\n if (pastedText) {\n try {\n // eslint-disable-next-line no-new\n new URL(pastedText);\n // Do not show link popup on copy-paste a link, so do not save its path\n editor.shouldSaveLinkPath = false;\n insertLink(editor, { url: pastedText });\n return;\n } catch (error) {\n // continue normal data insertion\n }\n }\n\n insertData(data);\n };\n\n return editor;\n};\n\nexport { withLinks, type LinkEditor };\n"],"names":["withLinks","editor","isInline","apply","insertText","insertData","element","type","lastInsertedLinkPath","operation","Editor","isEditor","node","shouldSaveLinkPath","path","Path","hasPrevious","transform","text","selection","Range","isCollapsed","linksInSelection","Array","from","nodes","at","match","selectionIsInLink","length","selectionIsAtEndOfLink","Point","equals","anchor","end","Transforms","insertNodes","next","select","data","pastedText","getData","URL","insertLink","url","error"],"mappings":";;;;;AASA,MAAMA,YAAY,CAACC,MAAAA,GAAAA;IACjB,MAAM,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,UAAU,EAAEC,UAAU,EAAE,GAAGJ,MAAAA;;IAGpDA,MAAOC,CAAAA,QAAQ,GAAG,CAACI,OAAAA,GAAAA;AACjB,QAAA,OAAOA,OAAQC,CAAAA,IAAI,KAAK,MAAA,GAAS,OAAOL,QAASI,CAAAA,OAAAA,CAAAA;AACnD,KAAA;;;AAIAL,IAAAA,MAAAA,CAAOO,oBAAoB,GAAG,IAAA;;IAG9BP,MAAOE,CAAAA,KAAK,GAAG,CAACM,SAAAA,GAAAA;QACd,IAAIA,SAAAA,CAAUF,IAAI,KAAK,aAAe,EAAA;AACpC,YAAA,IACE,CAACG,YAAAA,CAAOC,QAAQ,CAACF,UAAUG,IAAI,CAAA,IAC/BH,SAAUG,CAAAA,IAAI,CAACL,IAAI,KAAK,MACxBN,IAAAA,MAAAA,CAAOY,kBAAkB,EACzB;gBACAZ,MAAOO,CAAAA,oBAAoB,GAAGC,SAAAA,CAAUK,IAAI;AAC9C;AACF,SAAA,MAAO,IAAIL,SAAAA,CAAUF,IAAI,KAAK,WAAa,EAAA;;;YAGzC,IACEQ,UAAAA,CAAKC,WAAW,CAACP,SAAUK,CAAAA,IAAI,CAC/Bb,IAAAA,MAAAA,CAAOO,oBAAoB,IAC3BP,MAAOY,CAAAA,kBAAkB,EACzB;AACAZ,gBAAAA,MAAAA,CAAOO,oBAAoB,GAAGO,UAAAA,CAAKE,SAAS,CAAChB,MAAAA,CAAOO,oBAAoB,EAAEC,SAAAA,CAAAA;AAC5E;AACF;QAEAN,KAAMM,CAAAA,SAAAA,CAAAA;AACR,KAAA;IAEAR,MAAOG,CAAAA,UAAU,GAAG,CAACc,IAAAA,GAAAA;;QAEnB,IAAIjB,MAAAA,CAAOkB,SAAS,IAAIC,WAAMC,CAAAA,WAAW,CAACpB,MAAOkB,CAAAA,SAAS,CAAKD,IAAAA,IAAAA,KAAS,GAAK,EAAA;AAC3E,YAAA,MAAMI,mBAAmBC,KAAMC,CAAAA,IAAI,CACjCd,YAAOe,CAAAA,KAAK,CLine truncated
@@ -0,0 +1,73 @@
import { Editor, Path, Range, Point, Transforms } from 'slate';
import { insertLink } from '../utils/links.mjs';
const withLinks = (editor)=>{
const { isInline, apply, insertText, insertData } = editor;
// Links are inline elements, so we need to override the isInline method for slate
editor.isInline = (element)=>{
return element.type === 'link' ? true : isInline(element);
};
// We keep a track of the last inserted link path
// So we can show the popover on the link component if that link is the last one inserted
editor.lastInsertedLinkPath = null;
// We intercept the apply method, so everytime we insert a new link, we save its path
editor.apply = (operation)=>{
if (operation.type === 'insert_node') {
if (!Editor.isEditor(operation.node) && operation.node.type === 'link' && editor.shouldSaveLinkPath) {
editor.lastInsertedLinkPath = operation.path;
}
} else if (operation.type === 'move_node') {
// We need to update the last inserted link path when link is moved
// If link is the first word in the paragraph we dont need to update the path
if (Path.hasPrevious(operation.path) && editor.lastInsertedLinkPath && editor.shouldSaveLinkPath) {
editor.lastInsertedLinkPath = Path.transform(editor.lastInsertedLinkPath, operation);
}
}
apply(operation);
};
editor.insertText = (text)=>{
// When selection is at the end of a link and user types a space, we want to break the link
if (editor.selection && Range.isCollapsed(editor.selection) && text === ' ') {
const linksInSelection = Array.from(Editor.nodes(editor, {
at: editor.selection,
match: (node)=>!Editor.isEditor(node) && node.type === 'link'
}));
const selectionIsInLink = editor.selection && linksInSelection.length > 0;
const selectionIsAtEndOfLink = selectionIsInLink && Point.equals(editor.selection.anchor, Editor.end(editor, linksInSelection[0][1]));
if (selectionIsAtEndOfLink) {
Transforms.insertNodes(editor, {
text: ' ',
type: 'text'
}, {
at: Path.next(linksInSelection[0][1]),
select: true
});
return;
}
}
insertText(text);
};
// Add data as a clickable link if its a valid URL
editor.insertData = (data)=>{
const pastedText = data.getData('text/plain');
if (pastedText) {
try {
// eslint-disable-next-line no-new
new URL(pastedText);
// Do not show link popup on copy-paste a link, so do not save its path
editor.shouldSaveLinkPath = false;
insertLink(editor, {
url: pastedText
});
return;
} catch (error) {
// continue normal data insertion
}
}
insertData(data);
};
return editor;
};
export { withLinks };
//# sourceMappingURL=withLinks.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"withLinks.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/plugins/withLinks.ts"],"sourcesContent":["import { type BaseEditor, Path, Transforms, Range, Point, Editor } from 'slate';\n\nimport { insertLink } from '../utils/links';\n\ninterface LinkEditor extends BaseEditor {\n lastInsertedLinkPath: Path | null;\n shouldSaveLinkPath: boolean;\n}\n\nconst withLinks = (editor: Editor) => {\n const { isInline, apply, insertText, insertData } = editor;\n\n // Links are inline elements, so we need to override the isInline method for slate\n editor.isInline = (element) => {\n return element.type === 'link' ? true : isInline(element);\n };\n\n // We keep a track of the last inserted link path\n // So we can show the popover on the link component if that link is the last one inserted\n editor.lastInsertedLinkPath = null;\n\n // We intercept the apply method, so everytime we insert a new link, we save its path\n editor.apply = (operation) => {\n if (operation.type === 'insert_node') {\n if (\n !Editor.isEditor(operation.node) &&\n operation.node.type === 'link' &&\n editor.shouldSaveLinkPath\n ) {\n editor.lastInsertedLinkPath = operation.path;\n }\n } else if (operation.type === 'move_node') {\n // We need to update the last inserted link path when link is moved\n // If link is the first word in the paragraph we dont need to update the path\n if (\n Path.hasPrevious(operation.path) &&\n editor.lastInsertedLinkPath &&\n editor.shouldSaveLinkPath\n ) {\n editor.lastInsertedLinkPath = Path.transform(editor.lastInsertedLinkPath, operation);\n }\n }\n\n apply(operation);\n };\n\n editor.insertText = (text) => {\n // When selection is at the end of a link and user types a space, we want to break the link\n if (editor.selection && Range.isCollapsed(editor.selection) && text === ' ') {\n const linksInSelection = Array.from(\n Editor.nodes(editor, {\n at: editor.selection,\n match: (node) => !Editor.isEditor(node) && node.type === 'link',\n })\n );\n\n const selectionIsInLink = editor.selection && linksInSelection.length > 0;\n const selectionIsAtEndOfLink =\n selectionIsInLink &&\n Point.equals(editor.selection.anchor, Editor.end(editor, linksInSelection[0][1]));\n\n if (selectionIsAtEndOfLink) {\n Transforms.insertNodes(\n editor,\n { text: ' ', type: 'text' },\n { at: Path.next(linksInSelection[0][1]), select: true }\n );\n\n return;\n }\n }\n\n insertText(text);\n };\n\n // Add data as a clickable link if its a valid URL\n editor.insertData = (data) => {\n const pastedText = data.getData('text/plain');\n\n if (pastedText) {\n try {\n // eslint-disable-next-line no-new\n new URL(pastedText);\n // Do not show link popup on copy-paste a link, so do not save its path\n editor.shouldSaveLinkPath = false;\n insertLink(editor, { url: pastedText });\n return;\n } catch (error) {\n // continue normal data insertion\n }\n }\n\n insertData(data);\n };\n\n return editor;\n};\n\nexport { withLinks, type LinkEditor };\n"],"names":["withLinks","editor","isInline","apply","insertText","insertData","element","type","lastInsertedLinkPath","operation","Editor","isEditor","node","shouldSaveLinkPath","path","Path","hasPrevious","transform","text","selection","Range","isCollapsed","linksInSelection","Array","from","nodes","at","match","selectionIsInLink","length","selectionIsAtEndOfLink","Point","equals","anchor","end","Transforms","insertNodes","next","select","data","pastedText","getData","URL","insertLink","url","error"],"mappings":";;;AASA,MAAMA,YAAY,CAACC,MAAAA,GAAAA;IACjB,MAAM,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,UAAU,EAAEC,UAAU,EAAE,GAAGJ,MAAAA;;IAGpDA,MAAOC,CAAAA,QAAQ,GAAG,CAACI,OAAAA,GAAAA;AACjB,QAAA,OAAOA,OAAQC,CAAAA,IAAI,KAAK,MAAA,GAAS,OAAOL,QAASI,CAAAA,OAAAA,CAAAA;AACnD,KAAA;;;AAIAL,IAAAA,MAAAA,CAAOO,oBAAoB,GAAG,IAAA;;IAG9BP,MAAOE,CAAAA,KAAK,GAAG,CAACM,SAAAA,GAAAA;QACd,IAAIA,SAAAA,CAAUF,IAAI,KAAK,aAAe,EAAA;AACpC,YAAA,IACE,CAACG,MAAAA,CAAOC,QAAQ,CAACF,UAAUG,IAAI,CAAA,IAC/BH,SAAUG,CAAAA,IAAI,CAACL,IAAI,KAAK,MACxBN,IAAAA,MAAAA,CAAOY,kBAAkB,EACzB;gBACAZ,MAAOO,CAAAA,oBAAoB,GAAGC,SAAAA,CAAUK,IAAI;AAC9C;AACF,SAAA,MAAO,IAAIL,SAAAA,CAAUF,IAAI,KAAK,WAAa,EAAA;;;YAGzC,IACEQ,IAAAA,CAAKC,WAAW,CAACP,SAAUK,CAAAA,IAAI,CAC/Bb,IAAAA,MAAAA,CAAOO,oBAAoB,IAC3BP,MAAOY,CAAAA,kBAAkB,EACzB;AACAZ,gBAAAA,MAAAA,CAAOO,oBAAoB,GAAGO,IAAAA,CAAKE,SAAS,CAAChB,MAAAA,CAAOO,oBAAoB,EAAEC,SAAAA,CAAAA;AAC5E;AACF;QAEAN,KAAMM,CAAAA,SAAAA,CAAAA;AACR,KAAA;IAEAR,MAAOG,CAAAA,UAAU,GAAG,CAACc,IAAAA,GAAAA;;QAEnB,IAAIjB,MAAAA,CAAOkB,SAAS,IAAIC,KAAMC,CAAAA,WAAW,CAACpB,MAAOkB,CAAAA,SAAS,CAAKD,IAAAA,IAAAA,KAAS,GAAK,EAAA;AAC3E,YAAA,MAAMI,mBAAmBC,KAAMC,CAAAA,IAAI,CACjCd,MAAOe,CAAAA,KAAK,CALine truncated
@@ -0,0 +1,32 @@
'use strict';
var slate = require('slate');
const isText = (node)=>{
return slate.Node.isNode(node) && !slate.Editor.isEditor(node) && node.type === 'text';
};
/**
* This plugin is used to normalize the Slate document to match the Strapi schema.
*/ const withStrapiSchema = (editor)=>{
const { normalizeNode } = editor;
/**
* On the strapi schema, we want text nodes to have type: text
* By default, Slate add text nodes without type: text
* So we add this normalization for the cases when Slate add text nodes automatically
*/ editor.normalizeNode = (entry)=>{
const [node, path] = entry;
if (!slate.Element.isElement(node) && !isText(node)) {
slate.Transforms.setNodes(editor, {
type: 'text'
}, {
at: path
});
return;
}
normalizeNode(entry);
};
return editor;
};
exports.withStrapiSchema = withStrapiSchema;
//# sourceMappingURL=withStrapiSchema.js.map
@@ -0,0 +1 @@
{"version":3,"file":"withStrapiSchema.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.ts"],"sourcesContent":["import { type Text, Node, Editor, Element, Transforms } from 'slate';\n\nconst isText = (node: unknown): node is Text => {\n return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';\n};\n\n/**\n * This plugin is used to normalize the Slate document to match the Strapi schema.\n */\nconst withStrapiSchema = (editor: Editor) => {\n const { normalizeNode } = editor;\n\n /**\n * On the strapi schema, we want text nodes to have type: text\n * By default, Slate add text nodes without type: text\n * So we add this normalization for the cases when Slate add text nodes automatically\n */\n editor.normalizeNode = (entry) => {\n const [node, path] = entry;\n\n if (!Element.isElement(node) && !isText(node)) {\n Transforms.setNodes(editor, { type: 'text' }, { at: path });\n\n return;\n }\n\n normalizeNode(entry);\n };\n\n return editor;\n};\n\nexport { withStrapiSchema };\n"],"names":["isText","node","Node","isNode","Editor","isEditor","type","withStrapiSchema","editor","normalizeNode","entry","path","Element","isElement","Transforms","setNodes","at"],"mappings":";;;;AAEA,MAAMA,SAAS,CAACC,IAAAA,GAAAA;IACd,OAAOC,UAAAA,CAAKC,MAAM,CAACF,IAAS,CAAA,IAAA,CAACG,YAAOC,CAAAA,QAAQ,CAACJ,IAAAA,CAAAA,IAASA,IAAKK,CAAAA,IAAI,KAAK,MAAA;AACtE,CAAA;AAEA;;IAGA,MAAMC,mBAAmB,CAACC,MAAAA,GAAAA;IACxB,MAAM,EAAEC,aAAa,EAAE,GAAGD,MAAAA;AAE1B;;;;MAKAA,MAAAA,CAAOC,aAAa,GAAG,CAACC,KAAAA,GAAAA;QACtB,MAAM,CAACT,IAAMU,EAAAA,IAAAA,CAAK,GAAGD,KAAAA;AAErB,QAAA,IAAI,CAACE,aAAQC,CAAAA,SAAS,CAACZ,IAAS,CAAA,IAAA,CAACD,OAAOC,IAAO,CAAA,EAAA;YAC7Ca,gBAAWC,CAAAA,QAAQ,CAACP,MAAQ,EAAA;gBAAEF,IAAM,EAAA;aAAU,EAAA;gBAAEU,EAAIL,EAAAA;AAAK,aAAA,CAAA;AAEzD,YAAA;AACF;QAEAF,aAAcC,CAAAA,KAAAA,CAAAA;AAChB,KAAA;IAEA,OAAOF,MAAAA;AACT;;;;"}
@@ -0,0 +1,30 @@
import { Element, Transforms, Node, Editor } from 'slate';
const isText = (node)=>{
return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';
};
/**
* This plugin is used to normalize the Slate document to match the Strapi schema.
*/ const withStrapiSchema = (editor)=>{
const { normalizeNode } = editor;
/**
* On the strapi schema, we want text nodes to have type: text
* By default, Slate add text nodes without type: text
* So we add this normalization for the cases when Slate add text nodes automatically
*/ editor.normalizeNode = (entry)=>{
const [node, path] = entry;
if (!Element.isElement(node) && !isText(node)) {
Transforms.setNodes(editor, {
type: 'text'
}, {
at: path
});
return;
}
normalizeNode(entry);
};
return editor;
};
export { withStrapiSchema };
//# sourceMappingURL=withStrapiSchema.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"withStrapiSchema.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.ts"],"sourcesContent":["import { type Text, Node, Editor, Element, Transforms } from 'slate';\n\nconst isText = (node: unknown): node is Text => {\n return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';\n};\n\n/**\n * This plugin is used to normalize the Slate document to match the Strapi schema.\n */\nconst withStrapiSchema = (editor: Editor) => {\n const { normalizeNode } = editor;\n\n /**\n * On the strapi schema, we want text nodes to have type: text\n * By default, Slate add text nodes without type: text\n * So we add this normalization for the cases when Slate add text nodes automatically\n */\n editor.normalizeNode = (entry) => {\n const [node, path] = entry;\n\n if (!Element.isElement(node) && !isText(node)) {\n Transforms.setNodes(editor, { type: 'text' }, { at: path });\n\n return;\n }\n\n normalizeNode(entry);\n };\n\n return editor;\n};\n\nexport { withStrapiSchema };\n"],"names":["isText","node","Node","isNode","Editor","isEditor","type","withStrapiSchema","editor","normalizeNode","entry","path","Element","isElement","Transforms","setNodes","at"],"mappings":";;AAEA,MAAMA,SAAS,CAACC,IAAAA,GAAAA;IACd,OAAOC,IAAAA,CAAKC,MAAM,CAACF,IAAS,CAAA,IAAA,CAACG,MAAOC,CAAAA,QAAQ,CAACJ,IAAAA,CAAAA,IAASA,IAAKK,CAAAA,IAAI,KAAK,MAAA;AACtE,CAAA;AAEA;;IAGA,MAAMC,mBAAmB,CAACC,MAAAA,GAAAA;IACxB,MAAM,EAAEC,aAAa,EAAE,GAAGD,MAAAA;AAE1B;;;;MAKAA,MAAAA,CAAOC,aAAa,GAAG,CAACC,KAAAA,GAAAA;QACtB,MAAM,CAACT,IAAMU,EAAAA,IAAAA,CAAK,GAAGD,KAAAA;AAErB,QAAA,IAAI,CAACE,OAAQC,CAAAA,SAAS,CAACZ,IAAS,CAAA,IAAA,CAACD,OAAOC,IAAO,CAAA,EAAA;YAC7Ca,UAAWC,CAAAA,QAAQ,CAACP,MAAQ,EAAA;gBAAEF,IAAM,EAAA;aAAU,EAAA;gBAAEU,EAAIL,EAAAA;AAAK,aAAA,CAAA;AAEzD,YAAA;AACF;QAEAF,aAAcC,CAAAA,KAAAA,CAAAA;AAChB,KAAA;IAEA,OAAOF,MAAAA;AACT;;;;"}
@@ -0,0 +1,223 @@
'use strict';
const codeLanguages = [
{
value: 'asm',
label: 'Assembly',
decorate: 'asmatmel'
},
{
value: 'bash',
label: 'Bash'
},
{
value: 'c',
label: 'C'
},
{
value: 'clojure',
label: 'Clojure'
},
{
value: 'cobol',
label: 'COBOL'
},
{
value: 'cpp',
label: 'C++'
},
{
value: 'csharp',
label: 'C#'
},
{
value: 'css',
label: 'CSS'
},
{
value: 'dart',
label: 'Dart'
},
{
value: 'dockerfile',
label: 'Dockerfile',
decorate: 'docker'
},
{
value: 'elixir',
label: 'Elixir'
},
{
value: 'erlang',
label: 'Erlang'
},
{
value: 'fortran',
label: 'Fortran'
},
{
value: 'fsharp',
label: 'F#'
},
{
value: 'go',
label: 'Go'
},
{
value: 'graphql',
label: 'GraphQL'
},
{
value: 'groovy',
label: 'Groovy'
},
{
value: 'haskell',
label: 'Haskell'
},
{
value: 'haxe',
label: 'Haxe'
},
{
value: 'html',
label: 'HTML'
},
{
value: 'ini',
label: 'INI'
},
{
value: 'java',
label: 'Java'
},
{
value: 'javascript',
label: 'JavaScript'
},
{
value: 'jsx',
label: 'JavaScript (React)'
},
{
value: 'json',
label: 'JSON'
},
{
value: 'julia',
label: 'Julia'
},
{
value: 'kotlin',
label: 'Kotlin'
},
{
value: 'latex',
label: 'LaTeX'
},
{
value: 'lua',
label: 'Lua'
},
{
value: 'markdown',
label: 'Markdown'
},
{
value: 'matlab',
label: 'MATLAB'
},
{
value: 'makefile',
label: 'Makefile'
},
{
value: 'objectivec',
label: 'Objective-C'
},
{
value: 'perl',
label: 'Perl'
},
{
value: 'php',
label: 'PHP'
},
{
value: 'plaintext',
label: 'Plain text'
},
{
value: 'powershell',
label: 'PowerShell'
},
{
value: 'python',
label: 'Python'
},
{
value: 'r',
label: 'R'
},
{
value: 'ruby',
label: 'Ruby'
},
{
value: 'rust',
label: 'Rust'
},
{
value: 'sas',
label: 'SAS'
},
{
value: 'scala',
label: 'Scala'
},
{
value: 'scheme',
label: 'Scheme'
},
{
value: 'shell',
label: 'Shell'
},
{
value: 'sql',
label: 'SQL'
},
{
value: 'stata',
label: 'Stata'
},
{
value: 'swift',
label: 'Swift'
},
{
value: 'typescript',
label: 'TypeScript',
decorate: 'ts'
},
{
value: 'tsx',
label: 'TypeScript (React)'
},
{
value: 'vbnet',
label: 'VB.NET'
},
{
value: 'xml',
label: 'XML'
},
{
value: 'yaml',
label: 'YAML',
decorate: 'yml'
}
];
exports.codeLanguages = codeLanguages;
//# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
{"version":3,"file":"constants.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/utils/constants.ts"],"sourcesContent":["export const codeLanguages: {\n value: string;\n label: string;\n decorate?: string;\n}[] = [\n {\n value: 'asm',\n label: 'Assembly',\n decorate: 'asmatmel',\n },\n {\n value: 'bash',\n label: 'Bash',\n },\n {\n value: 'c',\n label: 'C',\n },\n {\n value: 'clojure',\n label: 'Clojure',\n },\n {\n value: 'cobol',\n label: 'COBOL',\n },\n {\n value: 'cpp',\n label: 'C++',\n },\n {\n value: 'csharp',\n label: 'C#',\n },\n {\n value: 'css',\n label: 'CSS',\n },\n {\n value: 'dart',\n label: 'Dart',\n },\n {\n value: 'dockerfile',\n label: 'Dockerfile',\n decorate: 'docker',\n },\n {\n value: 'elixir',\n label: 'Elixir',\n },\n {\n value: 'erlang',\n label: 'Erlang',\n },\n {\n value: 'fortran',\n label: 'Fortran',\n },\n {\n value: 'fsharp',\n label: 'F#',\n },\n {\n value: 'go',\n label: 'Go',\n },\n {\n value: 'graphql',\n label: 'GraphQL',\n },\n {\n value: 'groovy',\n label: 'Groovy',\n },\n {\n value: 'haskell',\n label: 'Haskell',\n },\n {\n value: 'haxe',\n label: 'Haxe',\n },\n {\n value: 'html',\n label: 'HTML',\n },\n {\n value: 'ini',\n label: 'INI',\n },\n {\n value: 'java',\n label: 'Java',\n },\n {\n value: 'javascript',\n label: 'JavaScript',\n },\n {\n value: 'jsx',\n label: 'JavaScript (React)',\n },\n {\n value: 'json',\n label: 'JSON',\n },\n {\n value: 'julia',\n label: 'Julia',\n },\n {\n value: 'kotlin',\n label: 'Kotlin',\n },\n {\n value: 'latex',\n label: 'LaTeX',\n },\n {\n value: 'lua',\n label: 'Lua',\n },\n {\n value: 'markdown',\n label: 'Markdown',\n },\n {\n value: 'matlab',\n label: 'MATLAB',\n },\n {\n value: 'makefile',\n label: 'Makefile',\n },\n {\n value: 'objectivec',\n label: 'Objective-C',\n },\n {\n value: 'perl',\n label: 'Perl',\n },\n {\n value: 'php',\n label: 'PHP',\n },\n {\n value: 'plaintext',\n label: 'Plain text',\n },\n {\n value: 'powershell',\n label: 'PowerShell',\n },\n {\n value: 'python',\n label: 'Python',\n },\n {\n value: 'r',\n label: 'R',\n },\n {\n value: 'ruby',\n label: 'Ruby',\n },\n {\n value: 'rust',\n label: 'Rust',\n },\n {\n value: 'sas',\n label: 'SAS',\n },\n {\n value: 'scala',\n label: 'Scala',\n },\n {\n value: 'scheme',\n label: 'Scheme',\n },\n {\n value: 'shell',\n label: 'Shell',\n },\n {\n value: 'sql',\n label: 'SQL',\n },\n {\n value: 'stata',\n label: 'Stata',\n },\n {\n value: 'swift',\n label: 'Swift',\n },\n {\n value: 'typescript',\n label: 'TypeScript',\n decorate: 'ts',\n },\n {\n value: 'tsx',\n label: 'TypeScript (React)',\n },\n {\n value: 'vbnet',\n label: 'VB.NET',\n },\n {\n value: 'xml',\n label: 'XML',\n },\n {\n value: 'yaml',\n label: 'YAML',\n decorate: 'yml',\n },\n];\n"],"names":["codeLanguages","value","label","decorate"],"mappings":";;MAAaA,aAIP,GAAA;AACJ,IAAA;QACEC,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA,UAAA;QACPC,QAAU,EAAA;AACZ,KAAA;AACA,IAAA;QACEF,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,GAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,SAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,OAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,YAAA;QACPC,KAAO,EAAA,YAAA;QACPC,QAAU,EAAA;AACZ,KAAA;AACA,IAAA;QACEF,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,SAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,IAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,SAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,SAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,YAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,OAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,OAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACTLine truncated
@@ -0,0 +1,221 @@
const codeLanguages = [
{
value: 'asm',
label: 'Assembly',
decorate: 'asmatmel'
},
{
value: 'bash',
label: 'Bash'
},
{
value: 'c',
label: 'C'
},
{
value: 'clojure',
label: 'Clojure'
},
{
value: 'cobol',
label: 'COBOL'
},
{
value: 'cpp',
label: 'C++'
},
{
value: 'csharp',
label: 'C#'
},
{
value: 'css',
label: 'CSS'
},
{
value: 'dart',
label: 'Dart'
},
{
value: 'dockerfile',
label: 'Dockerfile',
decorate: 'docker'
},
{
value: 'elixir',
label: 'Elixir'
},
{
value: 'erlang',
label: 'Erlang'
},
{
value: 'fortran',
label: 'Fortran'
},
{
value: 'fsharp',
label: 'F#'
},
{
value: 'go',
label: 'Go'
},
{
value: 'graphql',
label: 'GraphQL'
},
{
value: 'groovy',
label: 'Groovy'
},
{
value: 'haskell',
label: 'Haskell'
},
{
value: 'haxe',
label: 'Haxe'
},
{
value: 'html',
label: 'HTML'
},
{
value: 'ini',
label: 'INI'
},
{
value: 'java',
label: 'Java'
},
{
value: 'javascript',
label: 'JavaScript'
},
{
value: 'jsx',
label: 'JavaScript (React)'
},
{
value: 'json',
label: 'JSON'
},
{
value: 'julia',
label: 'Julia'
},
{
value: 'kotlin',
label: 'Kotlin'
},
{
value: 'latex',
label: 'LaTeX'
},
{
value: 'lua',
label: 'Lua'
},
{
value: 'markdown',
label: 'Markdown'
},
{
value: 'matlab',
label: 'MATLAB'
},
{
value: 'makefile',
label: 'Makefile'
},
{
value: 'objectivec',
label: 'Objective-C'
},
{
value: 'perl',
label: 'Perl'
},
{
value: 'php',
label: 'PHP'
},
{
value: 'plaintext',
label: 'Plain text'
},
{
value: 'powershell',
label: 'PowerShell'
},
{
value: 'python',
label: 'Python'
},
{
value: 'r',
label: 'R'
},
{
value: 'ruby',
label: 'Ruby'
},
{
value: 'rust',
label: 'Rust'
},
{
value: 'sas',
label: 'SAS'
},
{
value: 'scala',
label: 'Scala'
},
{
value: 'scheme',
label: 'Scheme'
},
{
value: 'shell',
label: 'Shell'
},
{
value: 'sql',
label: 'SQL'
},
{
value: 'stata',
label: 'Stata'
},
{
value: 'swift',
label: 'Swift'
},
{
value: 'typescript',
label: 'TypeScript',
decorate: 'ts'
},
{
value: 'tsx',
label: 'TypeScript (React)'
},
{
value: 'vbnet',
label: 'VB.NET'
},
{
value: 'xml',
label: 'XML'
},
{
value: 'yaml',
label: 'YAML',
decorate: 'yml'
}
];
export { codeLanguages };
//# sourceMappingURL=constants.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"constants.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/utils/constants.ts"],"sourcesContent":["export const codeLanguages: {\n value: string;\n label: string;\n decorate?: string;\n}[] = [\n {\n value: 'asm',\n label: 'Assembly',\n decorate: 'asmatmel',\n },\n {\n value: 'bash',\n label: 'Bash',\n },\n {\n value: 'c',\n label: 'C',\n },\n {\n value: 'clojure',\n label: 'Clojure',\n },\n {\n value: 'cobol',\n label: 'COBOL',\n },\n {\n value: 'cpp',\n label: 'C++',\n },\n {\n value: 'csharp',\n label: 'C#',\n },\n {\n value: 'css',\n label: 'CSS',\n },\n {\n value: 'dart',\n label: 'Dart',\n },\n {\n value: 'dockerfile',\n label: 'Dockerfile',\n decorate: 'docker',\n },\n {\n value: 'elixir',\n label: 'Elixir',\n },\n {\n value: 'erlang',\n label: 'Erlang',\n },\n {\n value: 'fortran',\n label: 'Fortran',\n },\n {\n value: 'fsharp',\n label: 'F#',\n },\n {\n value: 'go',\n label: 'Go',\n },\n {\n value: 'graphql',\n label: 'GraphQL',\n },\n {\n value: 'groovy',\n label: 'Groovy',\n },\n {\n value: 'haskell',\n label: 'Haskell',\n },\n {\n value: 'haxe',\n label: 'Haxe',\n },\n {\n value: 'html',\n label: 'HTML',\n },\n {\n value: 'ini',\n label: 'INI',\n },\n {\n value: 'java',\n label: 'Java',\n },\n {\n value: 'javascript',\n label: 'JavaScript',\n },\n {\n value: 'jsx',\n label: 'JavaScript (React)',\n },\n {\n value: 'json',\n label: 'JSON',\n },\n {\n value: 'julia',\n label: 'Julia',\n },\n {\n value: 'kotlin',\n label: 'Kotlin',\n },\n {\n value: 'latex',\n label: 'LaTeX',\n },\n {\n value: 'lua',\n label: 'Lua',\n },\n {\n value: 'markdown',\n label: 'Markdown',\n },\n {\n value: 'matlab',\n label: 'MATLAB',\n },\n {\n value: 'makefile',\n label: 'Makefile',\n },\n {\n value: 'objectivec',\n label: 'Objective-C',\n },\n {\n value: 'perl',\n label: 'Perl',\n },\n {\n value: 'php',\n label: 'PHP',\n },\n {\n value: 'plaintext',\n label: 'Plain text',\n },\n {\n value: 'powershell',\n label: 'PowerShell',\n },\n {\n value: 'python',\n label: 'Python',\n },\n {\n value: 'r',\n label: 'R',\n },\n {\n value: 'ruby',\n label: 'Ruby',\n },\n {\n value: 'rust',\n label: 'Rust',\n },\n {\n value: 'sas',\n label: 'SAS',\n },\n {\n value: 'scala',\n label: 'Scala',\n },\n {\n value: 'scheme',\n label: 'Scheme',\n },\n {\n value: 'shell',\n label: 'Shell',\n },\n {\n value: 'sql',\n label: 'SQL',\n },\n {\n value: 'stata',\n label: 'Stata',\n },\n {\n value: 'swift',\n label: 'Swift',\n },\n {\n value: 'typescript',\n label: 'TypeScript',\n decorate: 'ts',\n },\n {\n value: 'tsx',\n label: 'TypeScript (React)',\n },\n {\n value: 'vbnet',\n label: 'VB.NET',\n },\n {\n value: 'xml',\n label: 'XML',\n },\n {\n value: 'yaml',\n label: 'YAML',\n decorate: 'yml',\n },\n];\n"],"names":["codeLanguages","value","label","decorate"],"mappings":"MAAaA,aAIP,GAAA;AACJ,IAAA;QACEC,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA,UAAA;QACPC,QAAU,EAAA;AACZ,KAAA;AACA,IAAA;QACEF,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,GAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,SAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,OAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,YAAA;QACPC,KAAO,EAAA,YAAA;QACPC,QAAU,EAAA;AACZ,KAAA;AACA,IAAA;QACEF,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,SAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,IAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,SAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,SAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,YAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,MAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,OAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,QAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,OAAA;QACPC,KAAO,EAAA;AACT,KAAA;AACA,IAAA;QACED,KAAO,EAAA,KAAA;QACPC,KAAO,EAAA;AACT,Line truncated
@@ -0,0 +1,48 @@
'use strict';
var slate = require('slate');
/**
* Extracts some logic that is common to most blocks' handleConvert functions.
* @returns The path of the converted block
*/ const baseHandleConvert = (editor, attributesToSet)=>{
// If there is no selection, convert last inserted node
const [_, lastNodePath] = slate.Editor.last(editor, []);
// If the selection is inside a list, split the list so that the modified block is outside of it
slate.Transforms.unwrapNodes(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type === 'list',
split: true,
at: editor.selection ?? lastNodePath
});
// Make sure we get a block node, not an inline node
const [, updatedLastNodePath] = slate.Editor.last(editor, []);
const entry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && node.type !== 'text' && node.type !== 'link',
at: editor.selection ?? updatedLastNodePath
});
if (!entry || slate.Editor.isEditor(entry[0])) {
return;
}
const [element, elementPath] = entry;
slate.Transforms.setNodes(editor, {
...getAttributesToClear(element),
...attributesToSet
}, {
at: elementPath
});
return elementPath;
};
/**
* Set all attributes except type and children to null so that Slate deletes them
*/ const getAttributesToClear = (element)=>{
const { children: _children, type: _type, ...extra } = element;
const attributesToClear = Object.keys(extra).reduce((currentAttributes, key)=>({
...currentAttributes,
[key]: null
}), {});
return attributesToClear;
};
exports.baseHandleConvert = baseHandleConvert;
exports.getAttributesToClear = getAttributesToClear;
//# sourceMappingURL=conversions.js.map
@@ -0,0 +1 @@
{"version":3,"file":"conversions.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/utils/conversions.ts"],"sourcesContent":["import { type Element, type Path, Editor, Transforms } from 'slate';\n\n/**\n * Extracts some logic that is common to most blocks' handleConvert functions.\n * @returns The path of the converted block\n */\nconst baseHandleConvert = <T extends Element>(\n editor: Editor,\n attributesToSet: Partial<T> & { type: T['type'] }\n): void | Path => {\n // If there is no selection, convert last inserted node\n const [_, lastNodePath] = Editor.last(editor, []);\n\n // If the selection is inside a list, split the list so that the modified block is outside of it\n Transforms.unwrapNodes(editor, {\n match: (node) => !Editor.isEditor(node) && node.type === 'list',\n split: true,\n at: editor.selection ?? lastNodePath,\n });\n\n // Make sure we get a block node, not an inline node\n const [, updatedLastNodePath] = Editor.last(editor, []);\n const entry = Editor.above(editor, {\n match: (node) => !Editor.isEditor(node) && node.type !== 'text' && node.type !== 'link',\n at: editor.selection ?? updatedLastNodePath,\n });\n\n if (!entry || Editor.isEditor(entry[0])) {\n return;\n }\n\n const [element, elementPath] = entry;\n\n Transforms.setNodes(\n editor,\n {\n ...getAttributesToClear(element),\n ...attributesToSet,\n } as Partial<Element>,\n { at: elementPath }\n );\n\n return elementPath;\n};\n\n/**\n * Set all attributes except type and children to null so that Slate deletes them\n */\nconst getAttributesToClear = (element: Element) => {\n const { children: _children, type: _type, ...extra } = element;\n\n const attributesToClear = Object.keys(extra).reduce(\n (currentAttributes, key) => ({ ...currentAttributes, [key]: null }),\n {}\n );\n\n return attributesToClear as Record<string, null>;\n};\n\nexport { baseHandleConvert, getAttributesToClear };\n"],"names":["baseHandleConvert","editor","attributesToSet","_","lastNodePath","Editor","last","Transforms","unwrapNodes","match","node","isEditor","type","split","at","selection","updatedLastNodePath","entry","above","element","elementPath","setNodes","getAttributesToClear","children","_children","_type","extra","attributesToClear","Object","keys","reduce","currentAttributes","key"],"mappings":";;;;AAEA;;;IAIA,MAAMA,iBAAoB,GAAA,CACxBC,MACAC,EAAAA,eAAAA,GAAAA;;IAGA,MAAM,CAACC,GAAGC,YAAa,CAAA,GAAGC,aAAOC,IAAI,CAACL,QAAQ,EAAE,CAAA;;IAGhDM,gBAAWC,CAAAA,WAAW,CAACP,MAAQ,EAAA;QAC7BQ,KAAO,EAAA,CAACC,OAAS,CAACL,YAAAA,CAAOM,QAAQ,CAACD,IAAAA,CAAAA,IAASA,IAAKE,CAAAA,IAAI,KAAK,MAAA;QACzDC,KAAO,EAAA,IAAA;QACPC,EAAIb,EAAAA,MAAAA,CAAOc,SAAS,IAAIX;AAC1B,KAAA,CAAA;;AAGA,IAAA,MAAM,GAAGY,mBAAoB,CAAA,GAAGX,aAAOC,IAAI,CAACL,QAAQ,EAAE,CAAA;AACtD,IAAA,MAAMgB,KAAQZ,GAAAA,YAAAA,CAAOa,KAAK,CAACjB,MAAQ,EAAA;AACjCQ,QAAAA,KAAAA,EAAO,CAACC,IAAAA,GAAS,CAACL,YAAAA,CAAOM,QAAQ,CAACD,IAASA,CAAAA,IAAAA,IAAAA,CAAKE,IAAI,KAAK,MAAUF,IAAAA,IAAAA,CAAKE,IAAI,KAAK,MAAA;QACjFE,EAAIb,EAAAA,MAAAA,CAAOc,SAAS,IAAIC;AAC1B,KAAA,CAAA;IAEA,IAAI,CAACC,SAASZ,YAAOM,CAAAA,QAAQ,CAACM,KAAK,CAAC,EAAE,CAAG,EAAA;AACvC,QAAA;AACF;IAEA,MAAM,CAACE,OAASC,EAAAA,WAAAA,CAAY,GAAGH,KAAAA;IAE/BV,gBAAWc,CAAAA,QAAQ,CACjBpB,MACA,EAAA;AACE,QAAA,GAAGqB,qBAAqBH,OAAQ,CAAA;AAChC,QAAA,GAAGjB;KAEL,EAAA;QAAEY,EAAIM,EAAAA;AAAY,KAAA,CAAA;IAGpB,OAAOA,WAAAA;AACT;AAEA;;IAGA,MAAME,uBAAuB,CAACH,OAAAA,GAAAA;IAC5B,MAAM,EAAEI,UAAUC,SAAS,EAAEZ,MAAMa,KAAK,EAAE,GAAGC,KAAAA,EAAO,GAAGP,OAAAA;IAEvD,MAAMQ,iBAAAA,GAAoBC,MAAOC,CAAAA,IAAI,CAACH,KAAAA,CAAAA,CAAOI,MAAM,CACjD,CAACC,iBAAmBC,EAAAA,GAAAA,IAAS;AAAE,YAAA,GAAGD,iBAAiB;AAAE,YAAA,CAACC,MAAM;AAAK,SAAA,GACjE,EAAC,CAAA;IAGH,OAAOL,iBAAAA;AACT;;;;;"}
@@ -0,0 +1,45 @@
import { Editor, Transforms } from 'slate';
/**
* Extracts some logic that is common to most blocks' handleConvert functions.
* @returns The path of the converted block
*/ const baseHandleConvert = (editor, attributesToSet)=>{
// If there is no selection, convert last inserted node
const [_, lastNodePath] = Editor.last(editor, []);
// If the selection is inside a list, split the list so that the modified block is outside of it
Transforms.unwrapNodes(editor, {
match: (node)=>!Editor.isEditor(node) && node.type === 'list',
split: true,
at: editor.selection ?? lastNodePath
});
// Make sure we get a block node, not an inline node
const [, updatedLastNodePath] = Editor.last(editor, []);
const entry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && node.type !== 'text' && node.type !== 'link',
at: editor.selection ?? updatedLastNodePath
});
if (!entry || Editor.isEditor(entry[0])) {
return;
}
const [element, elementPath] = entry;
Transforms.setNodes(editor, {
...getAttributesToClear(element),
...attributesToSet
}, {
at: elementPath
});
return elementPath;
};
/**
* Set all attributes except type and children to null so that Slate deletes them
*/ const getAttributesToClear = (element)=>{
const { children: _children, type: _type, ...extra } = element;
const attributesToClear = Object.keys(extra).reduce((currentAttributes, key)=>({
...currentAttributes,
[key]: null
}), {});
return attributesToClear;
};
export { baseHandleConvert, getAttributesToClear };
//# sourceMappingURL=conversions.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"conversions.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/utils/conversions.ts"],"sourcesContent":["import { type Element, type Path, Editor, Transforms } from 'slate';\n\n/**\n * Extracts some logic that is common to most blocks' handleConvert functions.\n * @returns The path of the converted block\n */\nconst baseHandleConvert = <T extends Element>(\n editor: Editor,\n attributesToSet: Partial<T> & { type: T['type'] }\n): void | Path => {\n // If there is no selection, convert last inserted node\n const [_, lastNodePath] = Editor.last(editor, []);\n\n // If the selection is inside a list, split the list so that the modified block is outside of it\n Transforms.unwrapNodes(editor, {\n match: (node) => !Editor.isEditor(node) && node.type === 'list',\n split: true,\n at: editor.selection ?? lastNodePath,\n });\n\n // Make sure we get a block node, not an inline node\n const [, updatedLastNodePath] = Editor.last(editor, []);\n const entry = Editor.above(editor, {\n match: (node) => !Editor.isEditor(node) && node.type !== 'text' && node.type !== 'link',\n at: editor.selection ?? updatedLastNodePath,\n });\n\n if (!entry || Editor.isEditor(entry[0])) {\n return;\n }\n\n const [element, elementPath] = entry;\n\n Transforms.setNodes(\n editor,\n {\n ...getAttributesToClear(element),\n ...attributesToSet,\n } as Partial<Element>,\n { at: elementPath }\n );\n\n return elementPath;\n};\n\n/**\n * Set all attributes except type and children to null so that Slate deletes them\n */\nconst getAttributesToClear = (element: Element) => {\n const { children: _children, type: _type, ...extra } = element;\n\n const attributesToClear = Object.keys(extra).reduce(\n (currentAttributes, key) => ({ ...currentAttributes, [key]: null }),\n {}\n );\n\n return attributesToClear as Record<string, null>;\n};\n\nexport { baseHandleConvert, getAttributesToClear };\n"],"names":["baseHandleConvert","editor","attributesToSet","_","lastNodePath","Editor","last","Transforms","unwrapNodes","match","node","isEditor","type","split","at","selection","updatedLastNodePath","entry","above","element","elementPath","setNodes","getAttributesToClear","children","_children","_type","extra","attributesToClear","Object","keys","reduce","currentAttributes","key"],"mappings":";;AAEA;;;IAIA,MAAMA,iBAAoB,GAAA,CACxBC,MACAC,EAAAA,eAAAA,GAAAA;;IAGA,MAAM,CAACC,GAAGC,YAAa,CAAA,GAAGC,OAAOC,IAAI,CAACL,QAAQ,EAAE,CAAA;;IAGhDM,UAAWC,CAAAA,WAAW,CAACP,MAAQ,EAAA;QAC7BQ,KAAO,EAAA,CAACC,OAAS,CAACL,MAAAA,CAAOM,QAAQ,CAACD,IAAAA,CAAAA,IAASA,IAAKE,CAAAA,IAAI,KAAK,MAAA;QACzDC,KAAO,EAAA,IAAA;QACPC,EAAIb,EAAAA,MAAAA,CAAOc,SAAS,IAAIX;AAC1B,KAAA,CAAA;;AAGA,IAAA,MAAM,GAAGY,mBAAoB,CAAA,GAAGX,OAAOC,IAAI,CAACL,QAAQ,EAAE,CAAA;AACtD,IAAA,MAAMgB,KAAQZ,GAAAA,MAAAA,CAAOa,KAAK,CAACjB,MAAQ,EAAA;AACjCQ,QAAAA,KAAAA,EAAO,CAACC,IAAAA,GAAS,CAACL,MAAAA,CAAOM,QAAQ,CAACD,IAASA,CAAAA,IAAAA,IAAAA,CAAKE,IAAI,KAAK,MAAUF,IAAAA,IAAAA,CAAKE,IAAI,KAAK,MAAA;QACjFE,EAAIb,EAAAA,MAAAA,CAAOc,SAAS,IAAIC;AAC1B,KAAA,CAAA;IAEA,IAAI,CAACC,SAASZ,MAAOM,CAAAA,QAAQ,CAACM,KAAK,CAAC,EAAE,CAAG,EAAA;AACvC,QAAA;AACF;IAEA,MAAM,CAACE,OAASC,EAAAA,WAAAA,CAAY,GAAGH,KAAAA;IAE/BV,UAAWc,CAAAA,QAAQ,CACjBpB,MACA,EAAA;AACE,QAAA,GAAGqB,qBAAqBH,OAAQ,CAAA;AAChC,QAAA,GAAGjB;KAEL,EAAA;QAAEY,EAAIM,EAAAA;AAAY,KAAA,CAAA;IAGpB,OAAOA,WAAAA;AACT;AAEA;;IAGA,MAAME,uBAAuB,CAACH,OAAAA,GAAAA;IAC5B,MAAM,EAAEI,UAAUC,SAAS,EAAEZ,MAAMa,KAAK,EAAE,GAAGC,KAAAA,EAAO,GAAGP,OAAAA;IAEvD,MAAMQ,iBAAAA,GAAoBC,MAAOC,CAAAA,IAAI,CAACH,KAAAA,CAAAA,CAAOI,MAAM,CACjD,CAACC,iBAAmBC,EAAAA,GAAAA,IAAS;AAAE,YAAA,GAAGD,iBAAiB;AAAE,YAAA,CAACC,MAAM;AAAK,SAAA,GACjE,EAAC,CAAA;IAGH,OAAOL,iBAAAA;AACT;;;;"}
@@ -0,0 +1,64 @@
'use strict';
var slate = require('slate');
const isText = (node)=>{
return slate.Node.isNode(node) && !slate.Editor.isEditor(node) && node.type === 'text';
};
/**
* Inserts a line break the first time the user presses enter, and exits the node the second time.
*/ const pressEnterTwiceToExit = (editor)=>{
/**
* To determine if we should break out of the node, check 2 things:
* 1. If the cursor is at the end of the node
* 2. If the last line of the node is empty
*/ const nodeEntry = slate.Editor.above(editor, {
match: (node)=>!slate.Editor.isEditor(node) && ![
'link',
'text'
].includes(node.type)
});
if (!nodeEntry || !editor.selection) {
return;
}
const [node, nodePath] = nodeEntry;
const isNodeEnd = slate.Editor.isEnd(editor, editor.selection.anchor, nodePath);
const lastTextNode = node.children.at(-1);
const isEmptyLine = isText(lastTextNode) && lastTextNode.text.endsWith('\n');
if (isNodeEnd && isEmptyLine) {
// Remove the last line break
slate.Transforms.delete(editor, {
distance: 1,
unit: 'character',
reverse: true
});
// Break out of the node by creating a new paragraph
slate.Transforms.insertNodes(editor, {
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
});
return;
}
// Otherwise insert a new line within the node
slate.Transforms.insertText(editor, '\n');
// If there's nothing after the cursor, disable modifiers
if (isNodeEnd) {
[
'bold',
'italic',
'underline',
'strikethrough',
'code'
].forEach((modifier)=>{
slate.Editor.removeMark(editor, modifier);
});
}
};
exports.pressEnterTwiceToExit = pressEnterTwiceToExit;
//# sourceMappingURL=enterKey.js.map
@@ -0,0 +1 @@
{"version":3,"file":"enterKey.js","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/utils/enterKey.ts"],"sourcesContent":["import { type Text, Editor, Node, Transforms } from 'slate';\n\nconst isText = (node: unknown): node is Text => {\n return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';\n};\n\n/**\n * Inserts a line break the first time the user presses enter, and exits the node the second time.\n */\nconst pressEnterTwiceToExit = (editor: Editor) => {\n /**\n * To determine if we should break out of the node, check 2 things:\n * 1. If the cursor is at the end of the node\n * 2. If the last line of the node is empty\n */\n const nodeEntry = Editor.above(editor, {\n match: (node) => !Editor.isEditor(node) && !['link', 'text'].includes(node.type),\n });\n if (!nodeEntry || !editor.selection) {\n return;\n }\n const [node, nodePath] = nodeEntry;\n const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, nodePath);\n const lastTextNode = node.children.at(-1);\n const isEmptyLine = isText(lastTextNode) && lastTextNode.text.endsWith('\\n');\n\n if (isNodeEnd && isEmptyLine) {\n // Remove the last line break\n Transforms.delete(editor, { distance: 1, unit: 'character', reverse: true });\n // Break out of the node by creating a new paragraph\n Transforms.insertNodes(editor, {\n type: 'paragraph',\n children: [{ type: 'text', text: '' }],\n });\n return;\n }\n\n // Otherwise insert a new line within the node\n Transforms.insertText(editor, '\\n');\n\n // If there's nothing after the cursor, disable modifiers\n if (isNodeEnd) {\n ['bold', 'italic', 'underline', 'strikethrough', 'code'].forEach((modifier) => {\n Editor.removeMark(editor, modifier);\n });\n }\n};\n\nexport { pressEnterTwiceToExit };\n"],"names":["isText","node","Node","isNode","Editor","isEditor","type","pressEnterTwiceToExit","editor","nodeEntry","above","match","includes","selection","nodePath","isNodeEnd","isEnd","anchor","lastTextNode","children","at","isEmptyLine","text","endsWith","Transforms","delete","distance","unit","reverse","insertNodes","insertText","forEach","modifier","removeMark"],"mappings":";;;;AAEA,MAAMA,SAAS,CAACC,IAAAA,GAAAA;IACd,OAAOC,UAAAA,CAAKC,MAAM,CAACF,IAAS,CAAA,IAAA,CAACG,YAAOC,CAAAA,QAAQ,CAACJ,IAAAA,CAAAA,IAASA,IAAKK,CAAAA,IAAI,KAAK,MAAA;AACtE,CAAA;AAEA;;IAGA,MAAMC,wBAAwB,CAACC,MAAAA,GAAAA;AAC7B;;;;AAIC,MACD,MAAMC,SAAAA,GAAYL,YAAOM,CAAAA,KAAK,CAACF,MAAQ,EAAA;AACrCG,QAAAA,KAAAA,EAAO,CAACV,IAAS,GAAA,CAACG,aAAOC,QAAQ,CAACJ,SAAS,CAAC;AAAC,gBAAA,MAAA;AAAQ,gBAAA;aAAO,CAACW,QAAQ,CAACX,IAAAA,CAAKK,IAAI;AACjF,KAAA,CAAA;AACA,IAAA,IAAI,CAACG,SAAAA,IAAa,CAACD,MAAAA,CAAOK,SAAS,EAAE;AACnC,QAAA;AACF;IACA,MAAM,CAACZ,IAAMa,EAAAA,QAAAA,CAAS,GAAGL,SAAAA;IACzB,MAAMM,SAAAA,GAAYX,aAAOY,KAAK,CAACR,QAAQA,MAAOK,CAAAA,SAAS,CAACI,MAAM,EAAEH,QAAAA,CAAAA;AAChE,IAAA,MAAMI,eAAejB,IAAKkB,CAAAA,QAAQ,CAACC,EAAE,CAAC,CAAC,CAAA,CAAA;AACvC,IAAA,MAAMC,cAAcrB,MAAOkB,CAAAA,YAAAA,CAAAA,IAAiBA,aAAaI,IAAI,CAACC,QAAQ,CAAC,IAAA,CAAA;AAEvE,IAAA,IAAIR,aAAaM,WAAa,EAAA;;QAE5BG,gBAAWC,CAAAA,MAAM,CAACjB,MAAQ,EAAA;YAAEkB,QAAU,EAAA,CAAA;YAAGC,IAAM,EAAA,WAAA;YAAaC,OAAS,EAAA;AAAK,SAAA,CAAA;;QAE1EJ,gBAAWK,CAAAA,WAAW,CAACrB,MAAQ,EAAA;YAC7BF,IAAM,EAAA,WAAA;YACNa,QAAU,EAAA;AAAC,gBAAA;oBAAEb,IAAM,EAAA,MAAA;oBAAQgB,IAAM,EAAA;AAAG;AAAE;AACxC,SAAA,CAAA;AACA,QAAA;AACF;;IAGAE,gBAAWM,CAAAA,UAAU,CAACtB,MAAQ,EAAA,IAAA,CAAA;;AAG9B,IAAA,IAAIO,SAAW,EAAA;AACb,QAAA;AAAC,YAAA,MAAA;AAAQ,YAAA,QAAA;AAAU,YAAA,WAAA;AAAa,YAAA,eAAA;AAAiB,YAAA;SAAO,CAACgB,OAAO,CAAC,CAACC,QAAAA,GAAAA;YAChE5B,YAAO6B,CAAAA,UAAU,CAACzB,MAAQwB,EAAAA,QAAAA,CAAAA;AAC5B,SAAA,CAAA;AACF;AACF;;;;"}
@@ -0,0 +1,62 @@
import { Editor, Transforms, Node } from 'slate';
const isText = (node)=>{
return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';
};
/**
* Inserts a line break the first time the user presses enter, and exits the node the second time.
*/ const pressEnterTwiceToExit = (editor)=>{
/**
* To determine if we should break out of the node, check 2 things:
* 1. If the cursor is at the end of the node
* 2. If the last line of the node is empty
*/ const nodeEntry = Editor.above(editor, {
match: (node)=>!Editor.isEditor(node) && ![
'link',
'text'
].includes(node.type)
});
if (!nodeEntry || !editor.selection) {
return;
}
const [node, nodePath] = nodeEntry;
const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, nodePath);
const lastTextNode = node.children.at(-1);
const isEmptyLine = isText(lastTextNode) && lastTextNode.text.endsWith('\n');
if (isNodeEnd && isEmptyLine) {
// Remove the last line break
Transforms.delete(editor, {
distance: 1,
unit: 'character',
reverse: true
});
// Break out of the node by creating a new paragraph
Transforms.insertNodes(editor, {
type: 'paragraph',
children: [
{
type: 'text',
text: ''
}
]
});
return;
}
// Otherwise insert a new line within the node
Transforms.insertText(editor, '\n');
// If there's nothing after the cursor, disable modifiers
if (isNodeEnd) {
[
'bold',
'italic',
'underline',
'strikethrough',
'code'
].forEach((modifier)=>{
Editor.removeMark(editor, modifier);
});
}
};
export { pressEnterTwiceToExit };
//# sourceMappingURL=enterKey.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"enterKey.mjs","sources":["../../../../../../../../admin/src/pages/EditView/components/FormInputs/BlocksInput/utils/enterKey.ts"],"sourcesContent":["import { type Text, Editor, Node, Transforms } from 'slate';\n\nconst isText = (node: unknown): node is Text => {\n return Node.isNode(node) && !Editor.isEditor(node) && node.type === 'text';\n};\n\n/**\n * Inserts a line break the first time the user presses enter, and exits the node the second time.\n */\nconst pressEnterTwiceToExit = (editor: Editor) => {\n /**\n * To determine if we should break out of the node, check 2 things:\n * 1. If the cursor is at the end of the node\n * 2. If the last line of the node is empty\n */\n const nodeEntry = Editor.above(editor, {\n match: (node) => !Editor.isEditor(node) && !['link', 'text'].includes(node.type),\n });\n if (!nodeEntry || !editor.selection) {\n return;\n }\n const [node, nodePath] = nodeEntry;\n const isNodeEnd = Editor.isEnd(editor, editor.selection.anchor, nodePath);\n const lastTextNode = node.children.at(-1);\n const isEmptyLine = isText(lastTextNode) && lastTextNode.text.endsWith('\\n');\n\n if (isNodeEnd && isEmptyLine) {\n // Remove the last line break\n Transforms.delete(editor, { distance: 1, unit: 'character', reverse: true });\n // Break out of the node by creating a new paragraph\n Transforms.insertNodes(editor, {\n type: 'paragraph',\n children: [{ type: 'text', text: '' }],\n });\n return;\n }\n\n // Otherwise insert a new line within the node\n Transforms.insertText(editor, '\\n');\n\n // If there's nothing after the cursor, disable modifiers\n if (isNodeEnd) {\n ['bold', 'italic', 'underline', 'strikethrough', 'code'].forEach((modifier) => {\n Editor.removeMark(editor, modifier);\n });\n }\n};\n\nexport { pressEnterTwiceToExit };\n"],"names":["isText","node","Node","isNode","Editor","isEditor","type","pressEnterTwiceToExit","editor","nodeEntry","above","match","includes","selection","nodePath","isNodeEnd","isEnd","anchor","lastTextNode","children","at","isEmptyLine","text","endsWith","Transforms","delete","distance","unit","reverse","insertNodes","insertText","forEach","modifier","removeMark"],"mappings":";;AAEA,MAAMA,SAAS,CAACC,IAAAA,GAAAA;IACd,OAAOC,IAAAA,CAAKC,MAAM,CAACF,IAAS,CAAA,IAAA,CAACG,MAAOC,CAAAA,QAAQ,CAACJ,IAAAA,CAAAA,IAASA,IAAKK,CAAAA,IAAI,KAAK,MAAA;AACtE,CAAA;AAEA;;IAGA,MAAMC,wBAAwB,CAACC,MAAAA,GAAAA;AAC7B;;;;AAIC,MACD,MAAMC,SAAAA,GAAYL,MAAOM,CAAAA,KAAK,CAACF,MAAQ,EAAA;AACrCG,QAAAA,KAAAA,EAAO,CAACV,IAAS,GAAA,CAACG,OAAOC,QAAQ,CAACJ,SAAS,CAAC;AAAC,gBAAA,MAAA;AAAQ,gBAAA;aAAO,CAACW,QAAQ,CAACX,IAAAA,CAAKK,IAAI;AACjF,KAAA,CAAA;AACA,IAAA,IAAI,CAACG,SAAAA,IAAa,CAACD,MAAAA,CAAOK,SAAS,EAAE;AACnC,QAAA;AACF;IACA,MAAM,CAACZ,IAAMa,EAAAA,QAAAA,CAAS,GAAGL,SAAAA;IACzB,MAAMM,SAAAA,GAAYX,OAAOY,KAAK,CAACR,QAAQA,MAAOK,CAAAA,SAAS,CAACI,MAAM,EAAEH,QAAAA,CAAAA;AAChE,IAAA,MAAMI,eAAejB,IAAKkB,CAAAA,QAAQ,CAACC,EAAE,CAAC,CAAC,CAAA,CAAA;AACvC,IAAA,MAAMC,cAAcrB,MAAOkB,CAAAA,YAAAA,CAAAA,IAAiBA,aAAaI,IAAI,CAACC,QAAQ,CAAC,IAAA,CAAA;AAEvE,IAAA,IAAIR,aAAaM,WAAa,EAAA;;QAE5BG,UAAWC,CAAAA,MAAM,CAACjB,MAAQ,EAAA;YAAEkB,QAAU,EAAA,CAAA;YAAGC,IAAM,EAAA,WAAA;YAAaC,OAAS,EAAA;AAAK,SAAA,CAAA;;QAE1EJ,UAAWK,CAAAA,WAAW,CAACrB,MAAQ,EAAA;YAC7BF,IAAM,EAAA,WAAA;YACNa,QAAU,EAAA;AAAC,gBAAA;oBAAEb,IAAM,EAAA,MAAA;oBAAQgB,IAAM,EAAA;AAAG;AAAE;AACxC,SAAA,CAAA;AACA,QAAA;AACF;;IAGAE,UAAWM,CAAAA,UAAU,CAACtB,MAAQ,EAAA,IAAA,CAAA;;AAG9B,IAAA,IAAIO,SAAW,EAAA;AACb,QAAA;AAAC,YAAA,MAAA;AAAQ,YAAA,QAAA;AAAU,YAAA,WAAA;AAAa,YAAA,eAAA;AAAiB,YAAA;SAAO,CAACgB,OAAO,CAAC,CAACC,QAAAA,GAAAA;YAChE5B,MAAO6B,CAAAA,UAAU,CAACzB,MAAQwB,EAAAA,QAAAA,CAAAA;AAC5B,SAAA,CAAA;AACF;AACF;;;;"}
Loaded 100 of 65041 files, more files were not shown because too many files have changed in this diff. Show more