├── 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 |
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 |
--------------------------------------------------------------------------------