Copy past
This feature allows you to easily copy a paragraph from one page and paste it into different pages, enhancing content management and facilitating efficient pages editing.
Overview
This guide explains how to upgrade your existing paragraphs implementation to include copy, paste, and delete functionality.
We'll break this down into the original implementation and the enhanced version with the new features.
This feature is enabled only for content types that include paragraphs. The field used to determine this is field_vactory_paragraph, which is mandatory for the feature to function.
Prerequisites
Before configuring the Copy-Paste feature, ensure the following requirements are met:
The project uses @vactorynext/core and voidagency/vactory_starter_kit, which support this functionality.
Original Implementation
The original implementation simply maps through paragraphs and renders them using a ParagraphsController:
./components/modules/contrib/page/PageNode.jsx
{node?.field_vactory_paragraphs.map((paragraph) => ( <React.Fragment key={paragraph.id}> <ParagraphsController key={paragraph.id} data={paragraph} hasAMP={false} isAmp={isAmp} DF_DEV_MAPPING={DF_DEV_MAPPING} ContainerComponent={Container} ImageComponent={Image} Widgets={Widgets} IconComponent={Icon} HeadingComponent={Heading} TextComponent={Text} TabsComponent={Tabs} AccordionComponent={Accordion} isAnimated={true} AnimationComponent={CustomAnimation} animationKeyframe={fadeInRightAnimation} /> </React.Fragment>))}
Enhanced Implementation
To enable copy-paste functionality, we’ll introduce a ParagraphWrapper and ParagraphToolbox components that handles the state for the copy, paste, delete and insert paragraph actions for each paragraph.
Step 1: Adding the Wrapper Component
To add copy-paste functionality, wrap the entire paragraph section in a ParagraphWrapper. This component will manage state for the actions (copy, paste, delete), enabling each paragraph to be individually controlled and modified.
Replace your existing paragraph rendering code with the following:
Navigate to the Node Template file in your Next.js project. For example, for the content type vactory_page, edit the file located at:
components/modules/contrib/page/PageNode.jsx
./components/modules/contrib/page/PageNode.jsx
import { ParagraphsController, ParagraphWrapper } from "@vactorynext/core/paragraphs"import { toast } from "react-toastify"
<> <ParagraphWrapper initialParagraphs={node?.field_vactory_paragraphs} nid={node.drupal_internal__nid} toast={toast} > {({ paragraphs, handleCopy, handlePaste, handleDelete, handleInsert, templates, isLiveMode = false, showPasteButtons }) => ( <> {paragraphs?.map((paragraph, index) => ( <React.Fragment key={paragraph.id}> <ParagraphsController key={paragraph.id} data={paragraph} hasAMP={false} isAmp={isAmp} DF_DEV_MAPPING={DF_DEV_MAPPING} ContainerComponent={Container} ImageComponent={Image} Widgets={Widgets} IconComponent={Icon} HeadingComponent={Heading} TextComponent={Text} TabsComponent={Tabs} AccordionComponent={Accordion} isAnimated={true} AnimationComponent={CustomAnimation} animationKeyframe={fadeInRightAnimation} weight={index} /> </React.Fragment> ))} </> )} </ParagraphWrapper></>
Step 2: Adding Action Buttons for Each Paragraph
Now, add ParagraphToolbox component which is had the action buttons for copy, paste, delete and insert template actions. These buttons will allow users to copy a paragraph, paste and insert a new paragraph before or after, and delete paragraphs as needed.
Updated Code with Action Buttons:
- 1. import ParagraphToolbox component
./components/modules/contrib/page/PageNode.jsx
import { ParagraphsController, ParagraphWrapper, ParagraphToolbox,} from "@vactorynext/core/paragraphs"
- 1. Add ParagraphToolbox component to your code
./components/modules/contrib/page/PageNode.jsx
<ParagraphWrapper initialParagraphs={node?.field_vactory_paragraphs} nid={node.drupal_internal__nid} toast={toast} > {({ paragraphs, handleCopy, handlePaste, handleDelete, handleInsert, templates, isLiveMode = false, showPasteButtons, }) => ( <> {paragraphs?.map((paragraph, index) => ( <React.Fragment key={paragraph.id}> {isLiveMode && ( <ParagraphToolbox onHandleCopy={handleCopy} onHandlePast={handlePaste} onHandleDelete={handleDelete} handleInsert={handleInsert} props={{ parent_id: node.drupal_internal__nid, pid: paragraph.drupal_internal__id, type: paragraph.type, weight: index, isFirst: index === 0, templates, }} showPasteButtons={showPasteButtons} /> )} <ParagraphsController key={paragraph.id} data={paragraph} hasAMP={false} isAmp={isAmp} DF_DEV_MAPPING={DF_DEV_MAPPING} ContainerComponent={Container} ImageComponent={Image} Widgets={Widgets} IconComponent={Icon} HeadingComponent={Heading} TextComponent={Text} TabsComponent={Tabs} AccordionComponent={Accordion} isAnimated={true} AnimationComponent={CustomAnimation} animationKeyframe={fadeInRightAnimation} weight={index} /> {isLiveMode && paragraphs?.length === index + 1 && ( <ParagraphToolbox onHandleCopy={handleCopy} onHandlePast={handlePaste} onHandleDelete={handleDelete} handleInsert={handleInsert} props={{ parent_id: node.drupal_internal__nid, pid: paragraph.drupal_internal__id, type: paragraph.type, weight: paragraphs?.length === index + 1 ? index + 1 : index, isFirst: false, isLast: paragraphs?.length === index + 1, templates, }} showPasteButtons={showPasteButtons} /> )} </React.Fragment> ))} </> )} </ParagraphWrapper>
Step 3: Adding the field_live_mode field to the configuration object
Edit the configuration constants in the Node Template to include the field_live_mode field.
Update the configuration object as follows:
export const config = { id: "node--vactory_page", params: { fields: { "node--vactory_page": "field_vactory_paragraphs,node_bg_image,node_class,node_body_class,node_banner_title,node_banner_image,node_banner_mobile_image,node_banner_description,node_banner_showbreadcrumb,field_sticky_ancre,field_df_sticky,field_live_mode", "media--image": "thumbnail", "file--image": "uri", "paragraph--vactory_component": "drupal_internal__id,paragraph_section,paragraph_identifier,paragraph_container,field_animation,container_spacing,paragraph_css_class,paragraph_background_color,paragraph_background_image,field_vactory_component,field_vactory_title,field_background_color,field_paragraph_hide_lg,field_paragraph_hide_sm,field_position_image_x,field_position_image_y,field_size_image,field_vactory_flag,field_vactory_flag_2,paragraph_background_parallax", "paragraph--vactory_paragraph_multi_template": "drupal_internal__id,paragraph_section,paragraph_identifier,paragraph_container,field_animation,container_spacing,paragraph_css_class,paragraph_background_color,paragraph_background_image,field_vactory_component,field_vactory_title,field_paragraph_introduction,field_vactory_paragraph_tab,field_background_color,field_multi_paragraph_type,field_paragraph_hide_lg,field_paragraph_hide_sm,field_position_image_x,field_position_image_y,field_size_image,field_vactory_flag,field_vactory_flag_2,paragraph_background_parallax", }, include: "field_vactory_paragraphs,field_vactory_paragraphs.field_vactory_paragraph_tab,field_vactory_paragraphs.paragraph_background_image,field_vactory_paragraphs.paragraph_background_image.thumbnail,node_bg_image,node_bg_image.thumbnail,node_banner_image,node_banner_mobile_image,node_banner_image.thumbnail,node_banner_mobile_image.thumbnail", },};