├── index.html ├── .gitignore ├── bower.json ├── LICENSE ├── package.json ├── gulpfile.js ├── tests └── index.html ├── dist ├── jquery.cloner.min.js └── jquery.cloner.js ├── README.md └── src └── jquery.cloner.js /index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | vendor 4 | *.log -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-cloner", 3 | "version": "1.3.3", 4 | "description": "A jQuery plugin to clone HTML content", 5 | "main": "index.js", 6 | "authors": [ 7 | "John Lioneil Dionisio" 8 | ], 9 | "license": "MIT", 10 | "keywords": [ 11 | "jquery-plugin", 12 | "ecosystem:jquery", 13 | "cloner", 14 | "repeater" 15 | ], 16 | "homepage": "https://github.com/lioneil/jquery-cloner", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ], 24 | "dependencies": { 25 | "jquery": "^1.9.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 lioneil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-cloner", 3 | "version": "1.3.5", 4 | "description": "A jQuery plugin to clone HTML content", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/lioneil/jquery-cloner.git" 15 | }, 16 | "keywords": [ 17 | "jquery-plugin", 18 | "ecosystem:jquery", 19 | "cloner", 20 | "repeater" 21 | ], 22 | "author": "John Lioneil Dionisio", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/lioneil/jquery-cloner/issues" 26 | }, 27 | "homepage": "https://github.com/lioneil/jquery-cloner#readme", 28 | "dependencies": { 29 | "jquery": "^3.1.1" 30 | }, 31 | "devDependencies": { 32 | "del": "^2.2.0", 33 | "gulp": "^3.9.1", 34 | "gulp-autoprefixer": "^3.1.0", 35 | "gulp-cache": "^0.4.5", 36 | "gulp-concat": "^2.6.0", 37 | "gulp-cssnano": "^2.1.2", 38 | "gulp-imagemin": "^3.0.1", 39 | "gulp-jshint": "^2.0.1", 40 | "gulp-livereload": "^3.8.1", 41 | "gulp-notify": "^2.2.0", 42 | "gulp-rename": "^1.2.2", 43 | "gulp-ruby-sass": "^2.0.6", 44 | "gulp-uglify": "^1.5.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | autoprefixer = require('gulp-autoprefixer'), 3 | // jshint = require('gulp-jshint'), 4 | uglify = require('gulp-uglify'), 5 | rename = require('gulp-rename'), 6 | concat = require('gulp-concat'), 7 | notify = require('gulp-notify'), 8 | cache = require('gulp-cache'), 9 | // livereload = require('gulp-livereload'), 10 | del = require('del'); 11 | 12 | var directories = { 13 | assets: { 14 | js: 'assets', 15 | }, 16 | build: { 17 | js: 'build', 18 | }, 19 | dist: { 20 | js: 'dist', 21 | }, 22 | public: { 23 | js: 'public', 24 | }, 25 | root: { 26 | js: 'js', 27 | }, 28 | resources: { 29 | js: 'src' 30 | } 31 | } 32 | 33 | var _name = 'jquery.cloner.js'; 34 | 35 | /* 36 | | # Scripts 37 | | 38 | | The js files to be concatinated 39 | | and saved to different folders. 40 | | 41 | | @run gulp scripts 42 | | 43 | */ 44 | gulp.task('scripts', function () { 45 | return gulp.src(directories.resources.js + '/*.js') 46 | .pipe(concat(_name)) 47 | .pipe(gulp.dest(directories.dist.js)) 48 | .pipe(rename({suffix: '.min'})) 49 | .pipe(uglify()) 50 | // .pipe(gulp.dest(directories.js.build)) 51 | .pipe(gulp.dest(directories.dist.js)) 52 | .pipe(notify({ message: 'Completed compiling JS Files' })); 53 | }); 54 | 55 | /* 56 | | # Clean 57 | | 58 | | @run gulp clean 59 | */ 60 | gulp.task('clean', function () { 61 | return del(['js']); 62 | }); 63 | 64 | /* 65 | | # Default Task 66 | | 67 | | @run gulp default 68 | */ 69 | gulp.task('default', ['clean'], function () { 70 | gulp.start('scripts'); 71 | }); 72 | 73 | /* 74 | | # Watcher 75 | | 76 | | @run gulp watch 77 | */ 78 | gulp.task('watch', function () { 79 | // Create LiveReload server 80 | // livereload.listen(); 81 | // Watch any files in , reload on change 82 | // gulp.watch(['**']).on('change', livereload.changed); 83 | 84 | // Watch .js files 85 | gulp.watch(directories.resources + '/**/*.js', ['scripts']); 86 | }); -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Cloner Test 6 | 7 | 23 | 24 | 25 | 26 |
27 | 28 |
29 |
30 |
31 |
32 | 33 | 34 |
35 | 36 |
37 | 38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 |
63 | 64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 | 74 |

75 | 76 |
77 |
78 | 79 |
80 | 81 |
82 |
83 | 84 |
85 |
86 | 87 |
88 | 89 |
90 |
91 |
92 |
93 | 94 |
95 |
96 |
97 |
98 | 99 | 100 |
101 | 102 |
103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /dist/jquery.cloner.min.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"use strict";var t={init:function(n,t){var o=this;return o.elem=t,o.$elem=e(o.elem),o.options=e.extend({},e.fn.cloner.options,n),o.$container=o.$elem,o.$clonables=o.$container.closestChild(o.options.clonable),o.$closeButton=o.$container.closestChild(o.options.closeButton),o.$clonables.addClass(o.options.sourceName),o.$closeButton.first().hide(),this.debug("--------------------------------"),this.debug("[Cloner]: initialized"),o},toggle:function(e,n){this.debug("start click--------------------------------");var t=n.$clonables,o=t.length,l=t.last(),r=l.clone(!0);l[0].hasAttribute("data-clone-number")||l.attr("data-clone-number",o);var s=+l.attr("data-clone-number");return r.removeClass(n.options.sourceName).addClass(n.options.cloneName),n.$last=l,n.$clone=r,n.cloneNumber=s,!(!n.options.limitCloneNumbers||!n.$clone.hasClass(n.options.clonableCloneNumberDecrement)||1!==n.cloneNumber)||(1==n.cloneNumber?this.cloneNumberHandler(n.cloneNumber,n.$clone,"increment"):n.cloneNumber>1&&this.cloneNumberHandler(n.cloneNumber,n.$clone,"decrement"),n.debug("[Cloner]: start `beforeToggle` method"),n.options.beforeToggle(r,o,n),n.debug("[Cloner]: end `beforeToggle` method"),n.debug("[Cloner]: start `toggle` method"),n.options.clearValueOnClone&&(r.find("input, select").val(""),r.find("textarea").text("")),r.closestChild(n.options.closeButton).show(),this.increment(r,o,n),this.decrement(r,o,n),this.nestedClonesHandler(r,o,n),n.debug("[Cloner]: start clone append"),l.after(r),n.debug("[Cloner]: end clone append"),r.find(n.options.focusableElement).focus(),n.debug("[Cloner]: end `toggle` method"),n.debug("[Cloner]: start `afterToggle` method"),n.options.afterToggle(r,o,n),n.debug("[Cloner]: end `afterToggle` method"),this.debug("end click--------------------------------"),!0)},cloneNumberHandler:function(e,n,t){"increment"==t?n.attr("data-clone-number",e+1):"decrement"==t&&n.attr("data-clone-number",e-1)},increment:function(n,t,o){var l=this,r=n.find('[class*="'+o.options.incrementName+'"]').filter(function(t,l){return e(l).closest(o.options.clonable).get(0)==n.get(0)}),s=n[0].hasAttribute("clone-number")?n.data("clone-number"):t;l.debug("[Cloner]: start increment | Clone number: "+s+" | Index: "+t),r.each(function(){for(var n=e(this).attr("class").split(" "),t=n.length-1;t>=0;t--){var r=new RegExp(o.options.incrementName,"g");if(r.test(n[t])){var c=n[t].split(o.options.incrementName);switch(c=c[1].replace("-","")){case"value":var a=e(this).val(),i=a.replace(/-?\d+/g,function(e){return++e});e(this).val(i);break;case"html":var a=e(this).html(),i=a.replace(/-?\d+/g,function(e){return++e});e(this).html(i);break;case"text":var a=e(this).text(),i=a.replace(/-?\d+/g,function(e){return++e});e(this).text(i);break;case"for":case"id":case"class":default:if(!e(this)[0].hasAttribute(c))break;var a=e(this).attr(c),i=a.replace(/-?\d+/g,function(e){return++e});e(this).attr(c,i)}}}l.debug("[Cloner]: incrementing values... | Clone Number: "+s)}),l.debug("[Cloner]: end increment ")},decrement:function(n,t,o){var l=this,r=n.find('[class*="'+o.options.decrementName+'"]').filter(function(t,l){return e(l).closest(o.options.clonable).get(0)==n.get(0)}),s=n[0].hasAttribute("clone-number")?n.data("clone-number"):t;l.debug("[Cloner]: start increment | Clone number: "+s+" | Index: "+t),r.each(function(){for(var n=e(this).attr("class").split(" "),t=n.length-1;t>=0;t--){var r=new RegExp(o.options.decrementName,"g");if(r.test(n[t])){var c=n[t].split(o.options.decrementName);switch(c=c[1].replace("-","")){case"value":var a=e(this).val(),i=a.replace(/-?\d+/g,function(e){return console.log(e),--e});e(this).val(i);break;case"html":var a=e(this).html(),i=a.replace(/-?\d+/g,function(e){return console.log(e),--e});e(this).html(i);break;case"text":var a=e(this).text(),i=a.replace(/-?\d+/g,function(e){return console.log(e),--e});e(this).text(i);break;case"for":case"id":case"class":default:var a=e(this).attr(c),i=a.replace(/-?\d+/g,function(e){return console.log(e),--e});e(this).attr(i)}}}l.debug("[Cloner]: incrementing values... | Clone Number: "+s)}),l.debug("[Cloner]: end increment ")},nestedClonesHandler:function(e,n,t){if(t.options.removeNestedClonablesOnClone){var o=e.closestChild(t.options.clonable);o.not("."+t.options.sourceName).remove()}return t},destroy:function(){this.destroy(),this.element.unbind(this.eventNamespace),this.bindings.unbind(this.eventNamespace)},remove:function(e){var n=this;return n.$clonables=n.$container.find(n.options.clonable),e(n),!0},debug:function(e){var n=this;n.options.debug&&console.log(e)}};e.fn.cloner=function(o){var l=Object.create(t);return this.each(function(){var t=l.init(o,this),r=t.$elem.closestChild(t.options.addButton);e(r).on("click",function(n){t.$clonables=e(this).closest(t.options.clonableContainer).closestChild(t.options.clonable),l.toggle(t.options,t),n.preventDefault()}),l.remove(function(t){return e(n).on("click",t.options.closeButton,function(n){e(this).closest(t.options.clonable).remove()}),!0})})},e.fn.closestChild=function(n){var t,o;return t=this.children(),0===t.length?e():(o=t.filter(n),o.length>0?o:t.closestChild(n))},e.fn.cloner.options={clonableContainer:".clonable-block",clonable:".clonable",addButton:".clonable-button-add",closeButton:".clonable-button-close",focusableElement:":input:visible:enabled:first",clearValueOnClone:!0,removeNestedClonablesOnClone:!0,limitCloneNumbers:!0,debug:!1,cloneName:"clonable-clone",sourceName:"clonable-source",clonableCloneNumberDecrement:"clonable-clone-number-decrement",incrementName:"clonable-increment",decrementName:"clonable-decrement",beforeToggle:function(e,n,t){},afterToggle:function(e,n,t){}},e(n).find("[data-toggle=cloner]").each(function(){e(this).cloner(Object.assign({},e(this).data("options")||{}))})}(jQuery,document); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery Cloner 2 | A jQuery plugin to clone HTML content 3 | 4 | ## Getting Started 5 | This guide will help you install and use jQuery Cloner. See deployment for notes on how to deploy this plugin on frontend development on a live website. 6 | 7 | ### Installation 8 | via bower: 9 | ``` 10 | bower install jquery-cloner 11 | ``` 12 | 13 | via npm: 14 | ``` 15 | npm install jquery-cloner 16 | ``` 17 | 18 | or download or clone (pun!) on [GitHub](https://github.com/lioneil/jquery-cloner). 19 | 20 | 21 | ### Usage 22 | jQuery Cloner relies on classes and attributes to work. 23 | 24 | A simple sample markup: 25 | 26 | ```html 27 | 28 |
29 |
30 | 31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | ``` 42 | In the example above, the ```data-toggle="cloner"``` will automatically initialize our HTML. 43 | 44 | Manual initialization, then, is as easy as: 45 | 46 | ``` 47 | // main.js 48 | (function ($) { 49 | $('#my-clonable-block').cloner(); 50 | })(jQuery); 51 | ``` 52 | ### Options 53 | The plugin have options you can modify. Below is the list of options with their default values: 54 | 55 | ``` 56 | $('#my-clonable-block').cloner({ 57 | clonableContainer: '.clonable-block', 58 | clonable: '.clonable', 59 | addButton: '.clonable-button-add', 60 | closeButton: '.clonable-button-close', 61 | focusableElement: ':input:visible:enabled:first', 62 | 63 | clearValueOnClone: true, 64 | removeNestedClonablesOnClone: true, 65 | limitCloneNumbers: true, 66 | 67 | debug: false, 68 | 69 | cloneName: 'clonable-clone', 70 | sourceName: 'clonable-source', 71 | 72 | incrementName: 'clonable-increment', 73 | decrementName: 'clonable-decrement', 74 | }); 75 | ``` 76 | 77 | **clonableContainer** - The class that should contain all our clonable elements, including the Add Button 78 | 79 | **clonable** - The class of the clonable element. This is the html chunk that will be repeated. 80 | 81 | **addButton** - The class of the button that will fire the `toggle` method, prompting the cloning action. 82 | 83 | **closeButton** - The class of the button that will fire the `remove` method, prompting to remove the clonable element. *Important*: this element should be inside a `clonable` element. 84 | 85 | **focusableElement** - The attribute or input tag inside a newly cloned `clonable` to place the cursor over. 86 | 87 | **clearValueOnClone** - The plugin will clone the last instance of the `clonable` class. This option will toggle to remove or retain all previous input values. 88 | 89 | **removeNestedClonablesOnClone** - Toggle to remove all clone instances of the `clonableContainer`. 90 | 91 | **limitCloneNumbers** - Will only work for decrementing `clonables`. 92 | 93 | **debug** - Switch `console.log`ging on/off. 94 | 95 | **incrementName** - this option will increment all values inside a `clonable`. It uses suffixes (html, value, and any attribute like class, id, etc.) to know which integers will be incremented. 96 | Take the below as an example: 97 | 98 | ``` 99 | 100 | ``` 101 | 102 | In this example, we have classes of `clonable-increment`s with suffixes `-id` and `-name` which corresponds with the `input tag's` `id=attr_1` and `name="attr[0]"`. Performing a clone, therefore will result in 103 | 104 | 105 | ``` 106 | 107 | 108 | ``` 109 | It does this using `regex`. 110 | 111 | **decrementName** - The reverse of increment. 112 | 113 | **beforeToggle** - this is a function callback you can hook into before the `cloning` action is fired. It accepts parameters `$clone`( the clone of the last `clonable`), `index` (the `clonables`' length), and `self` (a catch-all reference of the jQuery-Cloner itself). An example use case: 114 | ``` 115 | $('#my-clonable-block').cloner({ 116 | beforeToggle: function ($clone, i, self) { 117 | // console.log(self); 118 | var $container = self.$container; 119 | if ($clone.find('input:last').val() == "") { 120 | $container.css({border:'1px solid red'}); 121 | } else { 122 | $container.css({border:'none'}); 123 | } 124 | }, 125 | }); 126 | ``` 127 | 128 | **afterToggle** - this will fire after the `cloning` action is triggered. 129 | 130 | 131 | ### Deployment 132 | Copy the /dist/\*.js folder to your project 133 | 134 | 135 | ### Versioning 136 | The project uses SemVer for versioning. For the versions available, see the tags on this repository. 137 | 138 | 139 | ### Authors 140 | * John Lioneil Dionisio 141 | 142 | See also the list of [contributors](#) who participated in this project. 143 | 144 | 145 | ### License 146 | [MIT License](https://raw.githubusercontent.com/lioneil/jquery-cloner/master/LICENSE) 147 | 148 | 149 | 150 | ### Acknowledgment 151 | * Andrey Mikhaylov (aka lolmaus) for his [jquery.closestchild](https://github.com/lolmaus/jquery.closestchild) 152 | * Everyone over at [stackoverflow](http://stackoverflow.com/tags/jquery), and other various resources. 153 | * to the Muses of Inspiration -------------------------------------------------------------------------------- /dist/jquery.cloner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Cloner 3 | * v1.3.3 4 | * 5 | * @param {Object} $ 6 | * @param {Object} document 7 | * @return 8 | */ 9 | (function ($, document) { 10 | 11 | 'use strict'; 12 | 13 | var Cloner = { 14 | init: function (options, elem) { 15 | /** 16 | * Store Cloner to a variable 17 | * 18 | * @type Object 19 | */ 20 | var self = this; 21 | self.elem = elem; 22 | self.$elem = $(self.elem); 23 | self.options = $.extend({}, $.fn.cloner.options, options); 24 | self.$container = self.$elem; 25 | self.$clonables = self.$container.closestChild(self.options.clonable); 26 | self.$closeButton = self.$container.closestChild(self.options.closeButton); 27 | 28 | /** 29 | * Add class to distinguish the `original clonables`. 30 | * This will be helpful later when removing clones. 31 | * 32 | */ 33 | self.$clonables.addClass(self.options.sourceName); 34 | 35 | /** 36 | * Hide All first instance of the closeButton 37 | * per Clonable Block. 38 | * 39 | */ 40 | self.$closeButton.first().hide(); 41 | 42 | this.debug("--------------------------------"); 43 | this.debug("[Cloner]: initialized"); 44 | 45 | return self; 46 | }, 47 | 48 | toggle: function (options, self) { 49 | this.debug("start click--------------------------------"); 50 | 51 | /** 52 | * Define variables for use 53 | * for this method. 54 | * 55 | */ 56 | var clonables = self.$clonables; 57 | var index = clonables.length; 58 | var $last = clonables.last(); 59 | var $clone = $last.clone(true); // true - clone even the bound events on the element 60 | 61 | /** 62 | * Check if the `data-clone-number` attribute is present. 63 | * If not the attribute is not found, then this adds it. 64 | * Spoiler Alert: This step is not important, really. But it might be! If you let it! 65 | * 66 | * @param !$last[0].hasAttribute('clone-number') check if attribute is present. 67 | * @return Boolean 68 | */ 69 | if (!$last[0].hasAttribute('data-clone-number')) { 70 | $last.attr('data-clone-number', index); 71 | } 72 | 73 | /** 74 | * Used .attr, because for some reason 75 | * using .data doesn't work. maybe because .data 76 | * is not getting the latest changes? 77 | * 78 | * Also, used the `+` sign to parse as int 79 | * 80 | */ 81 | var cloneNumber = +$last.attr('data-clone-number'); 82 | 83 | /** 84 | * Toggle class, flagging this as a replicant, I mean a clone. 85 | * 86 | */ 87 | $clone.removeClass(self.options.sourceName).addClass(self.options.cloneName); 88 | 89 | self.$last = $last; 90 | self.$clone = $clone; 91 | self.cloneNumber = cloneNumber; 92 | 93 | /** 94 | * If we are decrementing, 95 | * stop when limitCloneNumbers == true and 96 | * clone's clone-number is decrementable. 97 | * 98 | */ 99 | if (self.options.limitCloneNumbers && self.$clone.hasClass(self.options.clonableCloneNumberDecrement) && self.cloneNumber === 1) { 100 | return true; 101 | } 102 | 103 | /** 104 | * First, Let's check the `data-clone-number`, 105 | * If it a number greater than 1, then 106 | * it means we should be decrementing. 107 | * 108 | */ 109 | if (self.cloneNumber == 1) { 110 | this.cloneNumberHandler(self.cloneNumber, self.$clone, 'increment'); 111 | } else if (self.cloneNumber > 1) { 112 | this.cloneNumberHandler(self.cloneNumber, self.$clone, 'decrement'); 113 | } 114 | 115 | /** 116 | * Perform the `beforeToggle` method. 117 | * 118 | */ 119 | self.debug("[Cloner]: start `beforeToggle` method"); 120 | self.options.beforeToggle($clone, index, self); 121 | self.debug("[Cloner]: end `beforeToggle` method"); 122 | 123 | /** 124 | * Perform the start of `toggle` method. 125 | * 126 | */ 127 | self.debug("[Cloner]: start `toggle` method"); 128 | 129 | if (self.options.clearValueOnClone) { 130 | $clone.find('input, select').val(''); 131 | $clone.find('textarea').text(''); 132 | } 133 | 134 | /** 135 | * Show the Close button. 136 | * 137 | */ 138 | $clone.closestChild(self.options.closeButton).show(); 139 | 140 | /** 141 | * Perform the incrementations 142 | * and/or decrementations. 143 | * 144 | */ 145 | this.increment($clone, index, self); 146 | this.decrement($clone, index, self); 147 | 148 | /** 149 | * Perform values reset. 150 | * 151 | */ 152 | this.nestedClonesHandler($clone, index, self); 153 | 154 | self.debug("[Cloner]: start clone append"); 155 | 156 | /** 157 | * ------------------------------------- 158 | * THIS IS IT 159 | * ------------------------------------- 160 | * This is the magic line that adds the 161 | * cloned element next to the last instance 162 | * of the element on the DOM. 163 | * 164 | */ 165 | $last.after($clone); 166 | 167 | self.debug("[Cloner]: end clone append"); 168 | 169 | /** 170 | * Focus on the element specified. 171 | * 172 | */ 173 | $clone.find(self.options.focusableElement).focus(); 174 | 175 | self.debug("[Cloner]: end `toggle` method"); 176 | 177 | /** 178 | * Perform the `afterToggle` method. 179 | * 180 | */ 181 | self.debug("[Cloner]: start `afterToggle` method"); 182 | self.options.afterToggle($clone, index, self); 183 | self.debug("[Cloner]: end `afterToggle` method"); 184 | 185 | this.debug("end click--------------------------------"); 186 | 187 | return true; 188 | }, 189 | 190 | cloneNumberHandler: function (cloneNumber, $clone, type) { 191 | if (type == 'increment') { 192 | /** 193 | * Increment data-clone-number 194 | * If the attribute do not exist, create. 195 | * 196 | */ 197 | $clone.attr('data-clone-number', cloneNumber + 1); 198 | } else if (type == 'decrement') { 199 | /** 200 | * Decrement data-clone-number 201 | * If the attribute do not exist, create. 202 | * 203 | */ 204 | $clone.attr('data-clone-number', cloneNumber - 1); 205 | } 206 | }, 207 | 208 | increment: function ($clone, index, self) { 209 | /** 210 | * Instance of the Cloner Object 211 | * 212 | * @type Object 213 | */ 214 | var r = this; 215 | 216 | /** 217 | * All valid incrementables 218 | * 219 | * @type Object 220 | */ 221 | var incrementables = $clone.find('[class*="'+self.options.incrementName+'"]').filter(function (i, e) { 222 | return $(e).closest(self.options.clonable).get(0) == $clone.get(0); 223 | }); 224 | 225 | /** 226 | * The clone ID of the current Clone. 227 | * 228 | * @type int 229 | */ 230 | var _i = $clone[0].hasAttribute('clone-number') ? $clone.data('clone-number') : index; 231 | 232 | r.debug("[Cloner]: start increment | Clone number: " + _i + " | Index: " + index); 233 | 234 | incrementables.each(function () { 235 | var classes = $(this).attr('class').split(' '); 236 | for (var i = classes.length - 1; i >= 0; i--) { 237 | var reg = new RegExp(self.options.incrementName, "g"); 238 | if (reg.test(classes[i])) { 239 | var attr = classes[i].split(self.options.incrementName); 240 | attr = attr[1].replace('-', ''); 241 | switch (attr) { 242 | case 'value': 243 | var old_val = $(this).val(); 244 | var new_val = old_val.replace(/-?\d+/g, function (n) { return ++n; }); 245 | $(this).val(new_val); 246 | break; 247 | 248 | case 'html': 249 | var old_val = $(this).html(); 250 | var new_val = old_val.replace(/-?\d+/g, function (n) { return ++n; }); 251 | $(this).html(new_val); 252 | break; 253 | 254 | case 'text': 255 | var old_val = $(this).text(); 256 | var new_val = old_val.replace(/-?\d+/g, function (n) { return ++n; }); 257 | $(this).text(new_val); 258 | break; 259 | 260 | case 'for': 261 | case 'id': 262 | case 'class': 263 | default: 264 | if (!$(this)[0].hasAttribute(attr)) { 265 | break; 266 | } 267 | 268 | var old_val = $(this).attr(attr); 269 | var new_val = old_val.replace(/-?\d+/g, function (n) { return ++n; }); 270 | 271 | $(this).attr(attr, new_val); 272 | break; 273 | } 274 | } 275 | } 276 | 277 | r.debug("[Cloner]: incrementing values... | Clone Number: " +_i); 278 | 279 | }); 280 | 281 | r.debug("[Cloner]: end increment "); 282 | }, 283 | 284 | decrement: function ($clone, index, self) { 285 | /** 286 | * Instance of the Cloner Object 287 | * 288 | * @type Object 289 | */ 290 | var r = this; 291 | 292 | /** 293 | * All valid decrementables 294 | * 295 | * @type Object 296 | */ 297 | var decrementables = $clone.find('[class*="'+self.options.decrementName+'"]').filter(function (i, e) { 298 | return $(e).closest(self.options.clonable).get(0) == $clone.get(0); 299 | }); 300 | 301 | /** 302 | * The clone ID of the current Clone. 303 | * 304 | * @type int 305 | */ 306 | var _i = $clone[0].hasAttribute('clone-number') ? $clone.data('clone-number') : index; 307 | 308 | r.debug("[Cloner]: start increment | Clone number: " + _i + " | Index: " + index); 309 | 310 | decrementables.each(function () { 311 | var classes = $(this).attr('class').split(' '); 312 | for (var i = classes.length - 1; i >= 0; i--) { 313 | var reg = new RegExp(self.options.decrementName, "g"); 314 | if (reg.test(classes[i])) { 315 | var attr = classes[i].split(self.options.decrementName); 316 | attr = attr[1].replace('-', ''); 317 | switch (attr) { 318 | case 'value': 319 | var old_val = $(this).val(); 320 | var new_val = old_val.replace(/-?\d+/g, function (n) { console.log(n);return --n; }); 321 | $(this).val(new_val); 322 | break; 323 | 324 | case 'html': 325 | var old_val = $(this).html(); 326 | var new_val = old_val.replace(/-?\d+/g, function (n) { console.log(n);return --n; }); 327 | $(this).html(new_val); 328 | break; 329 | 330 | case 'text': 331 | var old_val = $(this).text(); 332 | var new_val = old_val.replace(/-?\d+/g, function (n) { console.log(n);return --n; }); 333 | $(this).text(new_val); 334 | break; 335 | 336 | case 'for': 337 | case 'id': 338 | case 'class': 339 | default: 340 | var old_val = $(this).attr(attr); 341 | var new_val = old_val.replace(/-?\d+/g, function (n) { console.log(n);return --n; }); 342 | $(this).attr(new_val); 343 | break; 344 | } 345 | } 346 | } 347 | 348 | r.debug("[Cloner]: incrementing values... | Clone Number: " +_i); 349 | 350 | }); 351 | 352 | r.debug("[Cloner]: end increment "); 353 | }, 354 | 355 | nestedClonesHandler: function ($clone, index, self) { 356 | /** 357 | * Remove all Nested Clones' clone. 358 | * This will revert the nested clone to it's original elements. 359 | * 360 | */ 361 | if (self.options.removeNestedClonablesOnClone) { 362 | var nestedClonables = $clone.closestChild(self.options.clonable); 363 | nestedClonables.not("." + self.options.sourceName).remove(); 364 | } 365 | 366 | return self; 367 | }, 368 | 369 | destroy: function () { 370 | this.destroy(); 371 | this.element.unbind( this.eventNamespace ) 372 | this.bindings.unbind( this.eventNamespace ); 373 | }, 374 | 375 | remove: function (callback) { 376 | var self = this; 377 | self.$clonables = self.$container.find(self.options.clonable); // Important: redo the clonables search here, so we know its the latest count 378 | callback(self); 379 | return true; 380 | }, 381 | 382 | debug: function ($d) { 383 | var self = this; 384 | if (self.options.debug) console.log($d); 385 | }, 386 | }; 387 | 388 | $.fn.cloner = function (options) { 389 | var cloner = Object.create(Cloner); 390 | 391 | return this.each(function () { 392 | var self = cloner.init(options, this); 393 | 394 | var addButton = self.$elem.closestChild(self.options.addButton); 395 | 396 | $(addButton).on('click', function (e) { 397 | // Important: redo the clonables search here, so we know its the latest count 398 | // Also it is crucial to make the `addButton` the starting point in finding the `clonables` 399 | // This makes multiple instance possible, coupled with the custom `closesChild` method. 400 | self.$clonables = $(this).closest(self.options.clonableContainer).closestChild(self.options.clonable); 401 | 402 | cloner.toggle(self.options, self); 403 | e.preventDefault(); 404 | }); 405 | 406 | cloner.remove(function (self) { 407 | $(document).on("click", self.options.closeButton, function (e) { 408 | $(this).closest(self.options.clonable).remove(); 409 | }); 410 | return true; 411 | }); 412 | 413 | }); 414 | }; 415 | 416 | /** 417 | * jquery.closestchild 0.1.1 418 | * 419 | * Author: Andrey Mikhaylov aka lolmaus 420 | * Email: lolmaus@gmail.com 421 | * 422 | */ 423 | $.fn.closestChild = function (selector) { 424 | var $children, $results; 425 | 426 | $children = this.children(); 427 | 428 | if ($children.length === 0) { 429 | return $(); 430 | } 431 | 432 | $results = $children.filter(selector); 433 | 434 | if ($results.length > 0) { 435 | return $results; 436 | } else { 437 | return $children.closestChild(selector); 438 | } 439 | }; 440 | 441 | $.fn.cloner.options = { 442 | clonableContainer: '.clonable-block', 443 | clonable: '.clonable', 444 | addButton: '.clonable-button-add', 445 | closeButton: '.clonable-button-close', 446 | focusableElement: ':input:visible:enabled:first', 447 | 448 | clearValueOnClone: true, 449 | removeNestedClonablesOnClone: true, 450 | limitCloneNumbers: true, 451 | 452 | debug: false, 453 | 454 | cloneName: 'clonable-clone', 455 | sourceName: 'clonable-source', 456 | 457 | clonableCloneNumberDecrement: 'clonable-clone-number-decrement', 458 | 459 | incrementName: 'clonable-increment', 460 | decrementName: 'clonable-decrement', 461 | 462 | beforeToggle: function (clone, index, self) {}, 463 | afterToggle: function (clone, index, self) {}, 464 | }; 465 | 466 | $(document).find('[data-toggle=cloner]').each(function () { 467 | $(this).cloner(Object.assign({}, $(this).data('options') || {})); 468 | }) 469 | 470 | })(jQuery, document); 471 | -------------------------------------------------------------------------------- /src/jquery.cloner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Cloner 3 | * v1.3.4 4 | * 5 | * @param {Object} $ 6 | * @param {Object} document 7 | * @return 8 | */ 9 | (function ($, document) { 10 | 11 | 'use strict'; 12 | 13 | var Cloner = { 14 | init: function (options, elem) { 15 | /** 16 | * Store Cloner to a variable 17 | * 18 | * @type Object 19 | */ 20 | var self = this; 21 | self.elem = elem; 22 | self.$elem = $(self.elem); 23 | self.options = $.extend({}, $.fn.cloner.options, options); 24 | self.$container = self.$elem; 25 | self.$clonables = self.$container.closestChild(self.options.clonable); 26 | self.$closeButton = self.$container.closestChild(self.options.closeButton); 27 | 28 | /** 29 | * Add class to distinguish the `original clonables`. 30 | * This will be helpful later when removing clones. 31 | * 32 | */ 33 | self.$clonables.addClass(self.options.sourceName); 34 | 35 | /** 36 | * Hide All first instance of the closeButton 37 | * per Clonable Block. 38 | * 39 | */ 40 | self.$closeButton.first().hide(); 41 | 42 | this.debug("--------------------------------"); 43 | this.debug("[Cloner]: initialized"); 44 | 45 | return self; 46 | }, 47 | 48 | toggle: function (options, self) { 49 | this.debug("start click--------------------------------"); 50 | 51 | /** 52 | * Define variables for use 53 | * for this method. 54 | * 55 | */ 56 | var clonables = self.$clonables; 57 | var index = clonables.length; 58 | var $last = clonables.last(); 59 | var $clone = $last.clone(true); // true - clone even the bound events on the element 60 | 61 | /** 62 | * Check if the `data-clone-number` attribute is present. 63 | * If not the attribute is not found, then this adds it. 64 | * Spoiler Alert: This step is not important, really. But it might be! If you let it! 65 | * 66 | * @param !$last[0].hasAttribute('clone-number') check if attribute is present. 67 | * @return Boolean 68 | */ 69 | if (!$last[0].hasAttribute('data-clone-number')) { 70 | $last.attr('data-clone-number', index); 71 | } 72 | 73 | /** 74 | * Used .attr, because for some reason 75 | * using .data doesn't work. maybe because .data 76 | * is not getting the latest changes? 77 | * 78 | * Also, used the `+` sign to parse as int 79 | * 80 | */ 81 | var cloneNumber = +$last.attr('data-clone-number'); 82 | 83 | /** 84 | * Toggle class, flagging this as a replicant, I mean a clone. 85 | * 86 | */ 87 | $clone.removeClass(self.options.sourceName).addClass(self.options.cloneName); 88 | 89 | self.$last = $last; 90 | self.$clone = $clone; 91 | self.cloneNumber = cloneNumber; 92 | 93 | /** 94 | * If we are decrementing, 95 | * stop when limitCloneNumbers == true and 96 | * clone's clone-number is decrementable. 97 | * 98 | */ 99 | if (self.options.limitCloneNumbers && self.$clone.hasClass(self.options.clonableCloneNumberDecrement) && self.cloneNumber === 1) { 100 | return true; 101 | } 102 | 103 | /** 104 | * First, Let's check the `data-clone-number`, 105 | * If it a number greater than 1, then 106 | * it means we should be decrementing. 107 | * 108 | */ 109 | if (self.cloneNumber == 1) { 110 | this.cloneNumberHandler(self.cloneNumber, self.$clone, 'increment'); 111 | } else if (self.cloneNumber > 1) { 112 | this.cloneNumberHandler(self.cloneNumber, self.$clone, 'decrement'); 113 | } 114 | 115 | /** 116 | * Perform the `beforeToggle` method. 117 | * 118 | */ 119 | self.debug("[Cloner]: start `beforeToggle` method"); 120 | self.options.beforeToggle($clone, index, self); 121 | self.debug("[Cloner]: end `beforeToggle` method"); 122 | 123 | /** 124 | * Perform the start of `toggle` method. 125 | * 126 | */ 127 | self.debug("[Cloner]: start `toggle` method"); 128 | 129 | if (self.options.clearValueOnClone) { 130 | $clone.find('input, select').val(''); 131 | $clone.find('textarea').val(''); 132 | $clone.find('input, radio').prop('checked', false); 133 | } 134 | 135 | /** 136 | * Show the Close button. 137 | * 138 | */ 139 | $clone.closestChild(self.options.closeButton).show(); 140 | 141 | /** 142 | * Perform the incrementations 143 | * and/or decrementations. 144 | * 145 | */ 146 | this.increment($clone, index, self); 147 | this.decrement($clone, index, self); 148 | 149 | /** 150 | * Perform values reset. 151 | * 152 | */ 153 | this.nestedClonesHandler($clone, index, self); 154 | 155 | self.debug("[Cloner]: start clone append"); 156 | 157 | /** 158 | * ------------------------------------- 159 | * THIS IS IT 160 | * ------------------------------------- 161 | * This is the magic line that adds the 162 | * cloned element next to the last instance 163 | * of the element on the DOM. 164 | * 165 | */ 166 | $last.after($clone); 167 | 168 | self.debug("[Cloner]: end clone append"); 169 | 170 | /** 171 | * Focus on the element specified. 172 | * 173 | */ 174 | $clone.find(self.options.focusableElement).focus(); 175 | 176 | self.debug("[Cloner]: end `toggle` method"); 177 | 178 | /** 179 | * Perform the `afterToggle` method. 180 | * 181 | */ 182 | self.debug("[Cloner]: start `afterToggle` method"); 183 | self.options.afterToggle($clone, index, self); 184 | self.debug("[Cloner]: end `afterToggle` method"); 185 | 186 | this.debug("end click--------------------------------"); 187 | 188 | return true; 189 | }, 190 | 191 | cloneNumberHandler: function (cloneNumber, $clone, type) { 192 | if (type == 'increment') { 193 | /** 194 | * Increment data-clone-number 195 | * If the attribute do not exist, create. 196 | * 197 | */ 198 | $clone.attr('data-clone-number', cloneNumber + 1); 199 | } else if (type == 'decrement') { 200 | /** 201 | * Decrement data-clone-number 202 | * If the attribute do not exist, create. 203 | * 204 | */ 205 | $clone.attr('data-clone-number', cloneNumber - 1); 206 | } 207 | }, 208 | 209 | increment: function ($clone, index, self) { 210 | /** 211 | * Instance of the Cloner Object 212 | * 213 | * @type Object 214 | */ 215 | var r = this; 216 | 217 | /** 218 | * All valid incrementables 219 | * 220 | * @type Object 221 | */ 222 | var incrementables = $clone.find('[class*="'+self.options.incrementName+'"]').filter(function (i, e) { 223 | return $(e).closest(self.options.clonable).get(0) == $clone.get(0); 224 | }); 225 | 226 | /** 227 | * The clone ID of the current Clone. 228 | * 229 | * @type int 230 | */ 231 | var _i = $clone[0].hasAttribute('clone-number') ? $clone.data('clone-number') : index; 232 | 233 | r.debug("[Cloner]: start increment | Clone number: " + _i + " | Index: " + index); 234 | 235 | incrementables.each(function () { 236 | var classes = $(this).attr('class').split(' '); 237 | for (var i = classes.length - 1; i >= 0; i--) { 238 | var reg = new RegExp(self.options.incrementName, "g"); 239 | if (reg.test(classes[i])) { 240 | var attr = classes[i].split(self.options.incrementName); 241 | attr = attr[1].replace('-', ''); 242 | switch (attr) { 243 | case 'value': 244 | var old_val = $(this).val(); 245 | var new_val = old_val.replace(/-?(\d+)(?!.*\d)/g, function (n) { return ++n; }); 246 | $(this).val(new_val); 247 | break; 248 | 249 | case 'html': 250 | var old_val = $(this).html(); 251 | var new_val = old_val.replace(/-?(\d+)(?!.*\d)/g, function (n) { return ++n; }); 252 | $(this).html(new_val); 253 | break; 254 | 255 | case 'text': 256 | var old_val = $(this).text(); 257 | var new_val = old_val.replace(/-?(\d+)(?!.*\d)/g, function (n) { return ++n; }); 258 | $(this).text(new_val); 259 | break; 260 | 261 | case 'for': 262 | case 'id': 263 | case 'class': 264 | default: 265 | if (!$(this)[0].hasAttribute(attr)) { 266 | break; 267 | } 268 | 269 | var old_val = $(this).attr(attr); 270 | var new_val = old_val.replace(/-?(\d+)(?!.*\d)/g, function (n) { return ++n; }); 271 | 272 | $(this).attr(attr, new_val); 273 | break; 274 | } 275 | } 276 | } 277 | 278 | r.debug("[Cloner]: incrementing values... | Clone Number: " +_i); 279 | 280 | }); 281 | 282 | r.debug("[Cloner]: end increment "); 283 | }, 284 | 285 | decrement: function ($clone, index, self) { 286 | /** 287 | * Instance of the Cloner Object 288 | * 289 | * @type Object 290 | */ 291 | var r = this; 292 | 293 | /** 294 | * All valid decrementables 295 | * 296 | * @type Object 297 | */ 298 | var decrementables = $clone.find('[class*="'+self.options.decrementName+'"]').filter(function (i, e) { 299 | return $(e).closest(self.options.clonable).get(0) == $clone.get(0); 300 | }); 301 | 302 | /** 303 | * The clone ID of the current Clone. 304 | * 305 | * @type int 306 | */ 307 | var _i = $clone[0].hasAttribute('clone-number') ? $clone.data('clone-number') : index; 308 | 309 | r.debug("[Cloner]: start increment | Clone number: " + _i + " | Index: " + index); 310 | 311 | decrementables.each(function () { 312 | var classes = $(this).attr('class').split(' '); 313 | for (var i = classes.length - 1; i >= 0; i--) { 314 | var reg = new RegExp(self.options.decrementName, "g"); 315 | if (reg.test(classes[i])) { 316 | var attr = classes[i].split(self.options.decrementName); 317 | attr = attr[1].replace('-', ''); 318 | switch (attr) { 319 | case 'value': 320 | var old_val = $(this).val(); 321 | var new_val = old_val.replace(/-?(\d+)(?!.*\d)/g, function (n) { return --n; }); 322 | $(this).val(new_val); 323 | break; 324 | 325 | case 'html': 326 | var old_val = $(this).html(); 327 | var new_val = old_val.replace(/-?(\d+)(?!.*\d)/g, function (n) { return --n; }); 328 | $(this).html(new_val); 329 | break; 330 | 331 | case 'text': 332 | var old_val = $(this).text(); 333 | var new_val = old_val.replace(/-?(\d+)(?!.*\d)/g, function (n) { return --n; }); 334 | $(this).text(new_val); 335 | break; 336 | 337 | case 'for': 338 | case 'id': 339 | case 'class': 340 | default: 341 | var old_val = $(this).attr(attr); 342 | var new_val = old_val.replace(/-?(\d+)(?!.*\d)/g, function (n) { return --n; }); 343 | $(this).attr(new_val); 344 | break; 345 | } 346 | } 347 | } 348 | 349 | r.debug("[Cloner]: incrementing values... | Clone Number: " +_i); 350 | 351 | }); 352 | 353 | r.debug("[Cloner]: end increment "); 354 | }, 355 | 356 | nestedClonesHandler: function ($clone, index, self) { 357 | /** 358 | * Remove all Nested Clones' clone. 359 | * This will revert the nested clone to it's original elements. 360 | * 361 | */ 362 | if (self.options.removeNestedClonablesOnClone) { 363 | var nestedClonables = $clone.closestChild(self.options.clonable); 364 | nestedClonables.not("." + self.options.sourceName).remove(); 365 | } 366 | 367 | return self; 368 | }, 369 | 370 | destroy: function () { 371 | this.destroy(); 372 | this.element.unbind( this.eventNamespace ) 373 | this.bindings.unbind( this.eventNamespace ); 374 | }, 375 | 376 | remove: function (callback) { 377 | var self = this; 378 | self.$clonables = self.$container.find(self.options.clonable); // Important: redo the clonables search here, so we know its the latest count 379 | callback(self); 380 | return true; 381 | }, 382 | 383 | debug: function ($d) { 384 | var self = this; 385 | if (self.options.debug) console.log($d); 386 | }, 387 | }; 388 | 389 | $.fn.cloner = function (options) { 390 | var cloner = Object.create(Cloner); 391 | 392 | return this.each(function () { 393 | var self = cloner.init(options, this); 394 | 395 | var addButton = self.$elem.closestChild(self.options.addButton); 396 | 397 | $(addButton).on('click', function (e) { 398 | // Important: redo the clonables search here, so we know its the latest count 399 | // Also it is crucial to make the `addButton` the starting point in finding the `clonables` 400 | // This makes multiple instance possible, coupled with the custom `closesChild` method. 401 | self.$clonables = $(this).closest(self.options.clonableContainer).closestChild(self.options.clonable); 402 | 403 | cloner.toggle(self.options, self); 404 | e.preventDefault(); 405 | }); 406 | 407 | cloner.remove(function (self) { 408 | $(document).on("click", self.options.closeButton, function (e) { 409 | $(this).closest(self.options.clonable).remove(); 410 | }); 411 | return true; 412 | }); 413 | 414 | }); 415 | }; 416 | 417 | /** 418 | * jquery.closestchild 0.1.1 419 | * 420 | * Author: Andrey Mikhaylov aka lolmaus 421 | * Email: lolmaus@gmail.com 422 | * 423 | */ 424 | $.fn.closestChild = function (selector) { 425 | var $children, $results; 426 | 427 | $children = this.children(); 428 | 429 | if ($children.length === 0) { 430 | return $(); 431 | } 432 | 433 | $results = $children.filter(selector); 434 | 435 | if ($results.length > 0) { 436 | return $results; 437 | } else { 438 | return $children.closestChild(selector); 439 | } 440 | }; 441 | 442 | $.fn.cloner.options = { 443 | clonableContainer: '.clonable-block', 444 | clonable: '.clonable', 445 | addButton: '.clonable-button-add', 446 | closeButton: '.clonable-button-close', 447 | focusableElement: ':input:visible:enabled:first', 448 | 449 | clearValueOnClone: true, 450 | removeNestedClonablesOnClone: true, 451 | limitCloneNumbers: true, 452 | 453 | debug: false, 454 | 455 | cloneName: 'clonable-clone', 456 | sourceName: 'clonable-source', 457 | 458 | clonableCloneNumberDecrement: 'clonable-clone-number-decrement', 459 | 460 | incrementName: 'clonable-increment', 461 | decrementName: 'clonable-decrement', 462 | 463 | beforeToggle: function (clone, index, self) {}, 464 | afterToggle: function (clone, index, self) {}, 465 | }; 466 | 467 | $(document).find('[data-toggle=cloner]').each(function () { 468 | $(this).cloner(Object.assign({}, $(this).data('options') || {})); 469 | }) 470 | 471 | })(jQuery, document); 472 | --------------------------------------------------------------------------------