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>
</>
Don't forget to import the toast component and pass it to the ParagraphWrapper component as demonstrated above. This is essential for displaying notifications.

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",
},
};
The addition of the field_live_mode field is used to determine whether the current user has the permission to utilize the Copy-Paste feature.