diff --git a/package.json b/package.json index 7ea1fb71..cdabe47e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "dependencies": { "@headlessui/react": "^1.7.17", - "@pzh-ui/components": "^0.0.546", + "@pzh-ui/components": "^0.0.548", "@pzh-ui/config": "^0.0.70", "@pzh-ui/css": "^0.0.97", "@pzh-ui/icons": "^0.0.61", diff --git a/src/components/DynamicObject/DynamicObjectForm/DynamicField/DynamicField.tsx b/src/components/DynamicObject/DynamicObjectForm/DynamicField/DynamicField.tsx index de39b20d..40da54e7 100644 --- a/src/components/DynamicObject/DynamicObjectForm/DynamicField/DynamicField.tsx +++ b/src/components/DynamicObject/DynamicObjectForm/DynamicField/DynamicField.tsx @@ -4,7 +4,9 @@ import { FormikRte, FormikSelect, FormikTextArea, + RteMenuButton, } from '@pzh-ui/components' +import { DrawPolygon } from '@pzh-ui/icons' import { useFormikContext } from 'formik' import FieldArray from '@/components/Form/FieldArray' @@ -12,9 +14,11 @@ import FieldConnections from '@/components/Form/FieldConnections' import FieldSelectArea from '@/components/Form/FieldSelectArea' import { Model } from '@/config/objects/types' import { DynamicField as DynamicFieldProps } from '@/config/types' +import useModalStore from '@/store/modalStore' import { fileToBase64 } from '@/utils/file' import DynamicObjectSearch from '../../DynamicObjectSearch' +import { Area } from './extensions/area' const inputFieldMap = { text: FormikInput, @@ -40,6 +44,7 @@ const DynamicField = ({ model?: Model }) => { const { setFieldValue, values } = useFormikContext() + const setActiveModal = useModalStore(state => state.setActiveModal) const InputField = inputFieldMap[type] if (!InputField) { @@ -85,6 +90,24 @@ const DynamicField = ({ {...(type === 'select' && { blurInputOnSelect: true, })} + {...(type === 'wysiwyg' && { + customExtensions: [Area], + customMenuButtons: editor => ( + + setActiveModal('objectAreaAnnotate', { + editor, + }) + } + aria-label="Gebiedsaanwijzing" + title="Gebiedsaanwijzing"> + + + ), + className: + '[&_[data-gebiedengroep]]:text-pzh-blue-900 [&_[data-gebiedengroep]]:bg-pzh-blue-10 [&_[data-gebiedengroep]]:inline-block', + })} {...field} /> diff --git a/src/components/DynamicObject/DynamicObjectForm/DynamicField/extensions/area.ts b/src/components/DynamicObject/DynamicObjectForm/DynamicField/extensions/area.ts new file mode 100644 index 00000000..31adbca8 --- /dev/null +++ b/src/components/DynamicObject/DynamicObjectForm/DynamicField/extensions/area.ts @@ -0,0 +1,87 @@ +import { Mark, mergeAttributes } from '@tiptap/core' + +declare module '@tiptap/core' { + interface Commands { + area: { + setArea: (attributes: { + 'data-gebiedengroep': string + 'data-type': string + 'data-gebiedsaanwijzing': string + text?: string + }) => ReturnType + } + } +} + +/** + * This extension allows you to insert areas. + */ +export const Area = Mark.create({ + name: 'area', + + addOptions() { + return { + HTMLAttributes: { + href: '#', + 'data-gebiedengroep': null, + 'data-type': null, + 'data-gebiedsaanwijzing': null, + }, + } + }, + + addAttributes() { + return { + href: { + default: this.options.HTMLAttributes.href, + }, + 'data-gebiedengroep': { + default: this.options.HTMLAttributes['data-gebiedengroep'], + }, + 'data-type': { + default: this.options.HTMLAttributes['data-type'], + }, + 'data-gebiedsaanwijzing': { + default: this.options.HTMLAttributes['data-gebiedsaanwijzing'], + }, + } + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'a', + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + 0, + ] + }, + + addCommands() { + return { + setArea: + attributes => + ({ chain, state }) => { + const { empty } = state.selection + + if (empty) { + const { text = 'default text', ...rest } = attributes + return chain() + .insertContentAt(state.selection.anchor, [ + { + type: 'text', + text, + marks: [ + { + type: this.name, + attrs: rest, + }, + ], + }, + ]) + .run() + } else { + return chain().setMark(this.name, attributes).run() + } + }, + } + }, +}) diff --git a/src/components/DynamicObject/DynamicObjectForm/DynamicObjectForm.tsx b/src/components/DynamicObject/DynamicObjectForm/DynamicObjectForm.tsx index adb3a2d4..c988088c 100644 --- a/src/components/DynamicObject/DynamicObjectForm/DynamicObjectForm.tsx +++ b/src/components/DynamicObject/DynamicObjectForm/DynamicObjectForm.tsx @@ -1,9 +1,11 @@ import { FieldSelectProps } from '@pzh-ui/components' import { Form, Formik, FormikHelpers, FormikProps, FormikValues } from 'formik' +import { useMemo } from 'react' import { toFormikValidationSchema } from 'zod-formik-adapter' import ButtonSubmitFixed from '@/components/ButtonSubmitFixed' import { LoaderSpinner } from '@/components/Loader' +import ObjectAreaAnnotateModal from '@/components/Modals/ObjectModals/ObjectAreaAnnotateModal' import ScrollToFieldError from '@/components/ScrollToFieldError' import { Model } from '@/config/objects/types' import { usePrompt } from '@/hooks/usePrompt' @@ -72,6 +74,14 @@ const ObjectForm = ({ FormikProps) => { const sections = model.dynamicSections + const containsRteField = useMemo( + () => + sections.some(section => + section.fields.some(field => field.type === 'wysiwyg') + ), + [sections] + ) + /** * Show prompt message when leaving the page without saving changes */ @@ -81,28 +91,32 @@ const ObjectForm = ({ ) return ( -
-
- {sections?.map((section, index) => ( - - ))} -
+ <> + +
+ {sections?.map((section, index) => ( + + ))} +
+ + - + + - - + {containsRteField && } + ) } diff --git a/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/ObjectAreaAnnotateModal.tsx b/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/ObjectAreaAnnotateModal.tsx new file mode 100644 index 00000000..1adf103a --- /dev/null +++ b/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/ObjectAreaAnnotateModal.tsx @@ -0,0 +1,165 @@ +import { Button, FormikSelect } from '@pzh-ui/components' +import { Form, Formik } from 'formik' +import { useMemo } from 'react' +import { z } from 'zod' +import { toFormikValidationSchema } from 'zod-formik-adapter' + +import Modal from '@/components/Modal' +import useModalStore from '@/store/modalStore' +import { SCHEMA_OBJECT_ANNOTATE_AREA } from '@/validation/objectAnnotate' + +import { ModalStateMap } from '../../types' + +type Values = z.infer + +const ObjectAreaAnnotateModal = () => { + const setActiveModal = useModalStore(state => state.setActiveModal) + const modalState = useModalStore( + state => state.modalStates['objectAreaAnnotate'] + ) as ModalStateMap['objectAreaAnnotate'] + + const groupOptions = [ + { + label: 'Gebiedengroep 1', + value: 'group-1', + }, + ] + + const areaTypeOptions = [{ label: 'Type 1', value: 'type-1' }] + + const areaGroupOptions = [ + { + label: 'Gebiedengroep 1', + value: 'group-1', + }, + ] + + const isEmptySelection = useMemo( + () => modalState?.editor.state.selection.empty, + [modalState?.editor.state.selection.empty] + ) + + const hasValues = useMemo( + () => { + const values = modalState?.editor?.getAttributes('area') + + return !!values && !!Object.keys(values).length + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [modalState?.editor, modalState?.editor?.state.selection] + ) + + const initialValues: Values = useMemo(() => { + const previousValues = modalState?.editor?.getAttributes('area') + + return { + group: previousValues?.['data-gebiedengroep'] ?? '', + areaType: previousValues?.['data-type'] ?? '', + areaGroup: previousValues?.['data-gebiedsaanwijzing'] ?? '', + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [modalState?.editor, modalState?.editor?.state.selection]) + + const handleSubmit = (payload: Values) => { + const groupName = groupOptions.find( + option => option.value === payload.group + )?.label + + modalState?.editor + ?.chain() + .focus() + .extendMarkRange('area') + .setArea({ + 'data-gebiedengroep': payload.group, + 'data-type': payload.areaType, + 'data-gebiedsaanwijzing': payload.areaGroup, + text: isEmptySelection ? groupName : undefined, + }) + .run() + + setActiveModal(null) + } + + return ( + + + {({ isValid, isSubmitting, dirty }) => ( +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ {!hasValues ? ( + + ) : ( + + )} + +
+
+ )} +
+
+ ) +} + +export default ObjectAreaAnnotateModal diff --git a/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/index.ts b/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/index.ts new file mode 100644 index 00000000..fc0b1424 --- /dev/null +++ b/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/index.ts @@ -0,0 +1 @@ +export { default } from './ObjectAreaAnnotateModal' diff --git a/src/components/Modals/types.ts b/src/components/Modals/types.ts index f2440ead..dfe46655 100644 --- a/src/components/Modals/types.ts +++ b/src/components/Modals/types.ts @@ -1,3 +1,5 @@ +import { Editor } from '@tiptap/core' + import { DocumentType, Module, @@ -23,6 +25,7 @@ export type ModalType = | 'moduleEditObject' | 'moduleDeleteObject' | 'areaAdd' + | 'objectAreaAnnotate' | 'objectDetails' | 'objectAddConnection' | 'objectDelete' @@ -50,6 +53,9 @@ export interface ModalStateMap { object: ModuleObjectShort module: Module } + objectAreaAnnotate: { + editor: Editor + } publicationAdd: { documentType: DocumentType procedureType: ProcedureType diff --git a/src/validation/objectAnnotate.ts b/src/validation/objectAnnotate.ts new file mode 100644 index 00000000..700294ea --- /dev/null +++ b/src/validation/objectAnnotate.ts @@ -0,0 +1,9 @@ +import { object } from 'zod' + +import { schemaDefaults } from './zodSchema' + +export const SCHEMA_OBJECT_ANNOTATE_AREA = object({ + group: schemaDefaults.requiredString(), + areaType: schemaDefaults.requiredString(), + areaGroup: schemaDefaults.requiredString(), +}) diff --git a/yarn.lock b/yarn.lock index ed81bff7..7497f691 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2815,9 +2815,9 @@ __metadata: languageName: node linkType: hard -"@pzh-ui/components@npm:^0.0.546": - version: 0.0.546 - resolution: "@pzh-ui/components@npm:0.0.546" +"@pzh-ui/components@npm:^0.0.548": + version: 0.0.548 + resolution: "@pzh-ui/components@npm:0.0.548" dependencies: "@floating-ui/react": "npm:^0.24.8" "@headlessui/react": "npm:^1.5.0" @@ -2869,7 +2869,7 @@ __metadata: react-stately: "npm:^3.30.1" react-toastify: "npm:^9.1.2" tailwind-merge: "npm:^2.2.2" - checksum: 0511a073e52dee587cffaa3b88d67996ab6bd1cb16d4f965ddd02507c4026448338d940130adc5e060b3f8e522b5692a0286b0964fcc08aa6caf42841872d465 + checksum: 5677bf6d9a836e21ffb76b8b89b2603722c11208f32553bae81f3430125f26bf15ab052be23d905ca9e10eca05d693ca4589c43163985c8baf9ce1d26d67cc0d languageName: node linkType: hard @@ -13064,7 +13064,7 @@ __metadata: "@axe-core/react": "npm:^4.8.2" "@faker-js/faker": "npm:^8.3.1" "@headlessui/react": "npm:^1.7.17" - "@pzh-ui/components": "npm:^0.0.546" + "@pzh-ui/components": "npm:^0.0.548" "@pzh-ui/config": "npm:^0.0.70" "@pzh-ui/css": "npm:^0.0.97" "@pzh-ui/icons": "npm:^0.0.61"