|
| 1 | +import React, { useEffect, useState } from 'react'; |
| 2 | +// eslint-disable-next-line import/no-unresolved |
| 3 | +import { renderToStaticMarkup } from 'react-dom/server'; |
| 4 | +import { useCMEditViewDataManager } from '@strapi/helper-plugin'; |
| 5 | +import { Duplicate } from '@strapi/icons'; |
| 6 | + |
| 7 | +const Button = () => ( |
| 8 | + <button |
| 9 | + aria-disabled="false" |
| 10 | + type="button" |
| 11 | + className="sc-aXZVg sc-gEvEer sc-cwHptR bzWqhm bYXTJs ksKyfS sc-cfxfcM bNDrnU sc-cPrPEB gsfWfo duplicator-button" |
| 12 | + tabIndex="0" |
| 13 | + aria-labelledby=":r1n:" |
| 14 | + style={{ display: 'inline-block', width: '2rem', height: '2rem', padding: '8px' }} |
| 15 | + > |
| 16 | + <Duplicate width={12} fill="#666687" /> |
| 17 | + </button> |
| 18 | +); |
| 19 | + |
| 20 | +const insertDuplicateButton = (node) => { |
| 21 | + const deleteButtons = node.querySelectorAll('button'); |
| 22 | + deleteButtons.forEach((button) => { |
| 23 | + const span = button.querySelector('span'); |
| 24 | + if (span && span.textContent.trim() === 'Delete') { |
| 25 | + |
| 26 | + const buttonContainer = button.parentElement.parentElement; |
| 27 | + |
| 28 | + // Check if there already is a duplicate button in the container. |
| 29 | + // If so we should not attempt to add a new one. |
| 30 | + if (buttonContainer && !buttonContainer.querySelector('.duplicator-span')) { |
| 31 | + |
| 32 | + const duplicatorSpan = document.createElement('span'); |
| 33 | + duplicatorSpan.classList.add('duplicator-span'); |
| 34 | + duplicatorSpan.style.display = 'inline-block'; |
| 35 | + |
| 36 | + const duplicatorButtonHTML = renderToStaticMarkup(<Button />); |
| 37 | + duplicatorSpan.innerHTML = duplicatorButtonHTML; |
| 38 | + |
| 39 | + buttonContainer.insertBefore(duplicatorSpan, button.parentElement.nextSibling); |
| 40 | + } |
| 41 | + } |
| 42 | + }); |
| 43 | +}; |
| 44 | + |
| 45 | + |
| 46 | +const DuplicateButton = () => { |
| 47 | + const { modifiedData, onChange, allLayoutData } = useCMEditViewDataManager(); |
| 48 | + const [count, setCount] = useState(0); |
| 49 | + const visibleFields = allLayoutData.contentType.layouts.edit; |
| 50 | + const repeatableComponentFields = visibleFields.filter((field) => field[0].fieldSchema.type === 'component' && field[0].fieldSchema.repeatable); |
| 51 | + |
| 52 | + // Initially insert the duplicate buttons for all existing repeatable components. |
| 53 | + useEffect(() => { |
| 54 | + repeatableComponentFields.forEach((field) => { |
| 55 | + const { label } = field[0].metadatas; |
| 56 | + |
| 57 | + for (const labelElement of document.querySelectorAll('label')) { |
| 58 | + if (labelElement.textContent?.startsWith(`${label}`)) { |
| 59 | + const wrapperElement = labelElement.parentElement.parentElement.parentElement; |
| 60 | + insertDuplicateButton(wrapperElement); |
| 61 | + } |
| 62 | + } |
| 63 | + }); |
| 64 | + }, [allLayoutData]); |
| 65 | + |
| 66 | + // Insert the duplicate button for any new repeatable components that are added to the DOM. |
| 67 | + // This happens when somebody adds a new component to the list, or drags it to a new position. |
| 68 | + useEffect(() => { |
| 69 | + const targetNode = document.body; |
| 70 | + const config = { childList: true, subtree: true }; |
| 71 | + |
| 72 | + const callback = (mutationsList, observer) => { |
| 73 | + mutationsList.forEach((mutation) => { |
| 74 | + if (mutation.type === 'childList') { |
| 75 | + mutation.addedNodes.forEach((node) => { |
| 76 | + if (node.nodeType === Node.ELEMENT_NODE) { |
| 77 | + insertDuplicateButton(node); |
| 78 | + setCount((prevCount) => prevCount + 1); |
| 79 | + } |
| 80 | + }); |
| 81 | + } |
| 82 | + }); |
| 83 | + }; |
| 84 | + |
| 85 | + const observer = new MutationObserver(callback); |
| 86 | + observer.observe(targetNode, config); |
| 87 | + }, []); |
| 88 | + |
| 89 | + // Add a click event listener to all the duplicate buttons. |
| 90 | + useEffect(() => { |
| 91 | + const duplicatorButtons = document.querySelectorAll('.duplicator-button'); |
| 92 | + |
| 93 | + if (!duplicatorButtons || duplicatorButtons.length === 0) { |
| 94 | + return () => {}; |
| 95 | + } |
| 96 | + |
| 97 | + const clickFunction = (e) => { |
| 98 | + const button = e.target.nodeName === 'BUTTON' ? e.target : e.target.closest('button'); |
| 99 | + const listItem = button.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement; |
| 100 | + const siblings = Array.from(listItem.parentElement.children); |
| 101 | + const index = siblings.indexOf(listItem); |
| 102 | + |
| 103 | + const wrapper = listItem.parentElement.parentElement.parentElement.parentElement.parentElement; |
| 104 | + const componentLabel = wrapper.querySelector('label').textContent; |
| 105 | + const componentInfo = repeatableComponentFields.filter((field) => componentLabel?.startsWith(`${field[0].metadatas.label}`)); |
| 106 | + const componentName = componentInfo[0][0].name; |
| 107 | + |
| 108 | + const componentData = modifiedData[componentName][index]; |
| 109 | + |
| 110 | + const currentComponents = [...modifiedData[componentName]] || []; |
| 111 | + const newComponent = { ...componentData, __temp_key__: Date.now() }; |
| 112 | + delete newComponent.id; |
| 113 | + |
| 114 | + currentComponents.splice(index + 1, 0, newComponent); |
| 115 | + |
| 116 | + onChange({ target: { name: componentName, value: currentComponents } }); |
| 117 | + }; |
| 118 | + |
| 119 | + duplicatorButtons.forEach((button) => { |
| 120 | + button.addEventListener('click', clickFunction); |
| 121 | + }); |
| 122 | + |
| 123 | + return () => { |
| 124 | + duplicatorButtons.forEach((button) => { |
| 125 | + button.removeEventListener('click', clickFunction); |
| 126 | + }); |
| 127 | + }; |
| 128 | + }, [modifiedData, count]); |
| 129 | +}; |
| 130 | + |
| 131 | +export default DuplicateButton; |
0 commit comments