# Copy Markup

Copy input, select or other markups.

[![npm](https://img.shields.io/badge/npm-v4.2.0-blue)](https://www.npmjs.com/package/@preline/copy-markup) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://preline.co/plugins/copy-markup.html)

## Contents

- [Overview](#overview)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Configuration Options](#configuration-options)
- [JavaScript API](#javascript-api)
- [Events](#events)
- [Common Patterns](#common-patterns)
- [License](#license)

## Overview

The Copy Markup component allows you to dynamically duplicate HTML elements (inputs, selects, or other markup) with a single click. It's useful for creating dynamic forms where users can add multiple instances of the same field.

**Key Features:**
- One-click element duplication
- Configurable copy limits
- Automatic element management
- Programmatic control via JavaScript API
- Event system for copy/delete tracking
- Support for any HTML markup

## Installation

To get started, install Copy Markup plugin via npm, else you can skip this step if you are already using Preline UI as a package.

```bash
npm i @preline/copy-markup
```

### CSS

[`@import`](https://tailwindcss.com/docs/functions-and-directives#import-directive) the plugin's CSS file into your Tailwind CSS file.

```css
@import "tailwindcss";

/* @preline/copy-markup */
@import "./node_modules/@preline/copy-markup/theme.css";
```

### JavaScript

Include the JavaScript that powers the interactive elements near the end of your `</body>` tag:

```html
<script src="./node_modules/@preline/copy-markup/index.js"></script>
```

### Manual Initialization

Use the `non-auto` entry if you need manual initialization. In this mode, automatic initialization on page load is not included, so the component should be initialized explicitly.

```html
<script type="module">
  import HSCopyMarkup from "@preline/copy-markup/non-auto.mjs";

  new HSCopyMarkup(document.querySelector("#copy-markup"));
</script>
```

### Via Bundler

When using a bundler (Vite, webpack, etc.), import the plugin directly as an ES module.

`@preline/copy-markup` is the auto-init entry: it scans the DOM and initializes matching elements automatically.

```js
import "@preline/copy-markup";
```

`@preline/copy-markup/non-auto` is the manual entry: use it when you want explicit control over when initialization happens, either via `autoInit()` or by creating a specific instance yourself.

```js
import HSCopyMarkup from "@preline/copy-markup/non-auto";

HSCopyMarkup.autoInit();

// Or initialize a specific element manually
const el = document.querySelector("#copy-markup");
if (el) new HSCopyMarkup(el);
```

### TypeScript

This package ships with TypeScript type definitions. No additional `@types/` package is needed.

## Basic usage

The following example demonstrates the minimal HTML structure required for a copy markup component. This is a base template without custom styling - you can apply your own CSS classes and styles as needed. Clicking the button will duplicate the target element.

```html
<div id="hs-wrapper-for-copy-first">
  <input id="hs-content-for-copy-first" type="text" class="py-3 px-4 block w-full border-gray-200 rounded-lg text-sm text-gray-800 focus:z-10 dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-500" placeholder="Enter Name">
</div>

<button type="button" data-hs-copy-markup='{
    "targetSelector": "#hs-content-for-copy-first",
    "wrapperSelector": "#hs-wrapper-for-copy-first",
    "limit": 3
  }' id="hs-copy-content-first">
  Add Name
</button>
```

**Structure Requirements:**
- `data-hs-copy-markup`: Required on the trigger button, contains configuration options as JSON
- `targetSelector`: Required, must be a valid CSS selector pointing to the element to copy
- `wrapperSelector`: Required, must be a valid CSS selector pointing to the container where copies will be added
- Target element must exist in the DOM

**Initial State:**
- One instance of the target element exists
- Copy button is enabled (unless limit is reached)

## Configuration Options

### Data Options

Data options are specified in the `data-hs-copy-markup` attribute as a JSON object.

| Option | Target Element | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `data-hs-copy-markup` | Trigger button | - | - | Activate a Copy Markup by specifying on an element. Should be added to the button (trigger). |
| `:targetSelector` | Inside `data-hs-copy-markup` | string (CSS selector) | - | Specifies the target element to copy. The value must be a valid CSS selector. Required. |
| `:wrapperSelector` | Inside `data-hs-copy-markup` | string (CSS selector) | - | Specifies which element should be used for copying. The value must be a valid CSS selector. Required. |
| `:limit` | Inside `data-hs-copy-markup` | number | `null` | Specifies how many items you can copy until the button is disabled. `null` means no limit. |

**Example:**
```html
<button data-hs-copy-markup='{
  "targetSelector": "#hs-input-field",
  "wrapperSelector": "#hs-wrapper",
  "limit": 5
}'>
  Add Field
</button>
```

## JavaScript API

The `HSCopyMarkup` object is available in the global `window` object after the plugin is loaded.

### Instance Methods

These methods are called on a copy markup instance.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `delete(target)` | `target`: `HTMLElement` | `void` | Remove the element associated to the target. The target should be a Node (HTMLElement). Use this to programmatically remove copied elements. |
| `destroy()` | None | `void` | Destroys the copy markup instance, removes all generated markup, classes, and event listeners. Use when removing copy markup from DOM. |

### Static Methods

These methods are called directly on the `HSCopyMarkup` class.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `HSCopyMarkup.getInstance(target, isInstance)` | `target`: `HTMLElement \| string` (CSS selector)<br>`isInstance`: `boolean` (optional) | `HSCopyMarkup \| { id: string \| number, element: HSCopyMarkup } \| null` | Returns the copy markup instance associated with the target. If `isInstance` is `true`, returns collection item object `{ id, element }` where `element` is the `HSCopyMarkup` instance. If `isInstance` is `false` or omitted, returns the `HSCopyMarkup` instance directly. Returns `null` if copy markup instance is not found. |

### Usage Examples

**Example 1: Deleting copied elements**
```javascript
// Get the copy markup instance
const instance = HSCopyMarkup.getInstance('#hs-copy-content', true);

if (instance) {
  const { element } = instance;
  const deleteBtn = document.querySelector('#hs-delete-btn');

  deleteBtn.addEventListener('click', () => {
    // Delete a specific copied element
    const copiedElement = document.querySelector('#hs-copy-markup-item-first');

    if (copiedElement) {
      element.delete(copiedElement);
    }
  });
}
```

**Example 2: Using instance methods (recommended pattern)**
```javascript
// Get the copy markup instance
const instance = HSCopyMarkup.getInstance('#hs-copy-content', true);

if (instance) {
  const { element } = instance;

  // Access instance properties
  console.log('Items:', element.items);
  console.log('Limit:', element.limit);

  // Delete a copied element programmatically
  function removeCopiedItem(itemElement) {
    element.delete(itemElement);
  }

  // Clean up when removing from DOM
  function removeCopyMarkup() {
    element.destroy();
  }
}
```

**Example 3: Destroying copy markup instance**
```javascript
const instance = HSCopyMarkup.getInstance('#hs-copy-content', true);

if (instance) {
  const { element } = instance;
  const removeBtn = document.querySelector('#hs-remove-btn');

  removeBtn.addEventListener('click', () => {
    // Clean up before removing from DOM
    element.destroy();
    // Now safe to remove the element
    document.querySelector('#hs-copy-content').remove();
  });
}
```

## Events

Copy markup instances emit events that can be listened to for copy/delete tracking and custom behavior.

| Event Name | When Fired | Callback Parameter | Description |
| --- | --- | --- | --- |
| `on:copy` | When target element was copied | `HTMLElement` (copied element) | Fires when a new copy of the target element is created. Returns the newly created element. |
| `on:delete` | When target element was deleted | `HTMLElement` (deleted element) | Fires when a copied element is deleted. Returns the deleted element. |

### Event Usage Example

```javascript
// Get copy markup instance
const instance = HSCopyMarkup.getInstance('#hs-copy-content', true);

if (instance) {
  const { element } = instance;

  // Listen to copy event
  element.on('copy', (copiedElement) => {
    console.log('Element copied:', copiedElement);
    // Perform actions after element is copied
    // e.g., initialize other plugins on the new element, update counters
  });

  // Listen to delete event
  element.on('delete', (deletedElement) => {
    console.log('Element deleted:', deletedElement);
    // Perform cleanup or state updates
  });
}
```

## Common Patterns

### Pattern 1: Limited Copies

Limit the number of copies that can be created.

```html
<button data-hs-copy-markup='{
  "targetSelector": "#hs-field",
  "wrapperSelector": "#hs-wrapper",
  "limit": 5
}'>
  Add Field (Max 5)
</button>
```

### Pattern 2: Dynamic Form Fields

Create dynamic form fields with copy functionality.

```html
<div id="hs-fields-wrapper">
  <input id="hs-field-template" type="text" placeholder="Field name">
</div>

<button data-hs-copy-markup='{
  "targetSelector": "#hs-field-template",
  "wrapperSelector": "#hs-fields-wrapper"
}'>
  Add Another Field
</button>
```

## License

Copyright (c) 2026 Preline Labs.

Licensed under the [MIT License](https://opensource.org/licenses/MIT).
