/* |------------------------------------------------------------------- | jQuery Metal Clone Plugins |------------------------------------------------------------------- | http://thunderwide.com | | @category Plugins | @author Norlihazmey Ghazali | @license Licensed under MIT (http://www.opensource.org/licenses/mit-license.php). | @copyright (c) 2015 Norlihazmey(metallurgical) | @version 1.3.0 | @Github https://github.com/metallurgical/jquery-metal-clone |------------------------------------------------------------------- */ (function($) { $.fn.metalClone = function(options, callback) { var opt = cloned = $.extend({}, $.fn.metalClone.defaults, options); var base = clonedElement = this; return base.each(function(index, elem) { // if already defined or register // clone plugin inside current selector // then no need to redefined it var classOrId = $(elem).attr('id') || $(elem).attr('class'); if (!classOrId) { classOrId = elem; } var generatedReferenceNo = Math.floor(Math.random() * 99999999999 + 1); $(elem).data('metalCloneRef', generatedReferenceNo); $(elem).addClass('metalElement' + generatedReferenceNo); if (undefined == $(elem).data('metalClone-' + classOrId)) { $(elem).data('metalClone-' + classOrId, 'metalClone'); } else { return; } // Get the selector // To see either class or ids were used var typeSelector = base.selector || '.' + classOrId, generatedSelectorClass = '.metalElement' + generatedReferenceNo, generatedSelectorClassForRemoveBtn = generatedSelectorClass.replace('.', '') + 'BtnRemove', // remove either . or # for class and ID respectively newTypeSelector = generatedSelectorClass, nodeType = base[0].nodeName, // Capture the configuration options currentCopyValue = opt.copyValue, currentClearExceptions = opt.clearExceptions, currentPosition = opt.position, currentNumberToClone = opt.numberToClone, currentDestination = opt.destination, currentIds = opt.ids, currentBtnRemoveText = opt.btnRemoveText, destinationNodeType = (currentDestination) ? $(currentDestination)[0].nodeName : 'none', cloneLimit = opt.cloneLimit, cloneLimitText = opt.cloneLimitText, cloneLimitClass = opt.cloneLimitClass, enableIcon = opt.enableIcon, fontAwesomeTheme = opt.fontAwesomeTheme, fontAwesomeRemoveClass = opt.fontAwesomeRemoveClass, fontAwesomeAddClass = opt.fontAwesomeAddClass, fontAwesomeAddDataTransform = opt.fontAwesomeAddDataTransform, fontAwesomeAddDataMask = opt.fontAwesomeAddDataMask, fontAwesomeRemoveDataTransform = opt.fontAwesomeRemoveDataTransform, fontAwesomeRemoveDataMask = opt.fontAwesomeRemoveDataMask, enableConfirmMessage = opt.enableConfirmMessage, confirmMessageText = opt.confirmMessageText, enableAnimation = opt.enableAnimation, animationSpeed = opt.animationSpeed, enableScrollTop = opt.enableScrollTop, scrollTopSpeed = opt.scrollTopSpeed, defaultFontAwesomeTheme = ['regular', 'solid', 'brand', 'light', 'basic'], onStart = opt.onStart, onClone = opt.onClone, onComplete = opt.onComplete, onClonedRemoved = opt.onClonedRemoved, metalRowElementWrapper = 'metalRowElementWrapper', // Table list(match with selection) allNodeTableWithout = [ 'TABLE', 'TR', 'TD', 'TBODY', 'TFOOT', 'THEAD', 'TH' ], element, flagClass = false, tdCloseParent, firstTdChild; if (typeSelector.match(/[.]/)) { // if the selector is a class, // then take the first element only flagClass = true; element = $(this).first(); } else { // If the selector is an ID // return its object element = $(this); } // if onstart callback was called // provide them self paramater if ($.isFunction(onStart)) onStart.call(base, base); /*=================== parent[table] ===================*/ // only for table if ($.inArray(nodeType, allNodeTableWithout) !== -1) { tdCloseParent = element.closest('table'); firstTdChild = tdCloseParent.find('tr').first(); } /* |------------------------------------------------ | Default clone button |------------------------------------------------ | If user did't not provided the class or id name for | cloned button, then system will provided one | */ // initialize global variable for clone button var currentBtnClone; var btnClonePosition; // If user not defined clone button, // then make new one if (opt.btnClone === null) { // create new clone button with unique id currentBtnClone = "metalBtnClone" + Math.floor(Math.random() * 99999999999 + 1); // if selector is a table and destination not table if (($.inArray(nodeType, allNodeTableWithout) !== -1) && ($.inArray(destinationNodeType, allNodeTableWithout) == -1)) { btnClonePosition = tdCloseParent; } // if selector is a table element and destination is a table else if (($.inArray(nodeType, allNodeTableWithout) !== -1) && ($.inArray(destinationNodeType, allNodeTableWithout) !== -1)) { btnClonePosition = tdCloseParent; } // if a selector is not a table && destination not a table else if (($.inArray(nodeType, allNodeTableWithout) == -1) && ($.inArray(destinationNodeType, allNodeTableWithout) == -1)) { btnClonePosition = elem; } // if selector is not a table element and destination is a table else if (($.inArray(nodeType, allNodeTableWithout) == -1) && ($.inArray(destinationNodeType, allNodeTableWithout) !== -1)) { btnClonePosition = elem; } $(''; } return output; } /** * Get font awesome theme. * * @returns {string} */ function getFontAwesomeThemeType() { if ($.inArray(fontAwesomeTheme, defaultFontAwesomeTheme) === -1) { return 'far'; } var theme; switch (fontAwesomeTheme) { case 'solid': theme = 'fas'; break; case 'brand': theme = 'fab'; break; case 'light': theme = 'fal'; break; case 'basic': theme = 'fa'; break; default: theme = 'far'; } return theme; } /** * Increment an ID to be unique. * * @param arr * @returns {Array} */ function idIncreament(arr) { var ids_value, clonedElement = []; // Check if the paramter passed // has *(all) symbol // if yes, then find all element if ($.inArray('*', arr)) { ids_value = '*'; } // find element provided in array // list only else { ids_value = arr.join(','); } // iterate throught cloned container $(generatedSelectorClass).not(':first').each(function(inc, e) { // then find the element either * or a few // depend on user defined and default value $(this).find(ids_value).each(function(i, ee) { // looking for and ID(S) // if found, then increament its value // to ensure all the same clone element // have unique id value if ($(this).attr('id')) { // Get the original value var oldValue = $(this).attr('id'); var newValue = oldValue.replace(/\d+/g, ''); // Set the new id(s) value $(this).attr('id', newValue + parseInt(inc)); } }); clonedElement.push($(this).get(0)); }); return clonedElement; } /** * Check no of element was cloned. This function for limit * @returns {*} */ function checkLimit() { var numberOfCloneElementExisted; // if the selector is a class // then no problem, just get length // of all the element with same class existed // if (flagClass) // numberOfCloneElementExisted = $(generatedSelectorClass).length; // if the selector is an IDs // then, find all the element with // same id with same name // else // numberOfCloneElementExisted = $('[id="' + base[0].id + '"]').length; numberOfCloneElementExisted = $(generatedSelectorClass).length; // if the clone limit option provided by users // and the input is a number if (cloneLimit != "infinity" && typeof cloneLimit == "number") { // if number of clone element more than limit provided // return false(not possible to clone element exceed limit) // then return false if (currentNumberToClone > cloneLimit) { return false; // if meet the condition, then return the length // of clone element existed } else { return numberOfCloneElementExisted; } } // user not provided the limit // then just use the default value // default value is infinity else { return 'no limit'; } } /** * Get selector name being select to clone. * @returns {string} */ function getSelectorName() { var name; if (flagClass) name = generatedSelectorClass.replace('.', ''); else name = generatedSelectorClass.replace('#', ''); return name; } /** * Check the cloned element meet the condition or not. * @returns {boolean} */ function limitHandler() { // get length of cloned element var flagLimit = checkLimit(), // store length canProceed, // flag bool value flagProceed = false; // if number to clone more than limit // return to true if (!flagLimit) { console.error('MetalClone Error: numberToClone option defined is more than cloneLimit option'); alert('MetalClone Error: numberToClone option defined is more than cloneLimit option'); flagProceed = true; } // if no limit, then assign 0 value as a default // otherwise use the current length else { canProceed = (flagLimit == "no limit") ? 0 : flagLimit; } // if cloned element already exceed limit provided // stop current process if (canProceed > cloneLimit) { console.log("Can't clone more than limit provided"); if ($(currentBtnClone).next().is('span')) { $(currentBtnClone).next().html(cloneLimitText); } else { // call function to get selector name // without .(class) or #(id) symbols var selectorName = getSelectorName(); // create span element for error_limit message // after clone button $('', { 'data-clone-reference': selectorName, class: 'metal-error-limit' + ((cloneLimitClass && typeof cloneLimitClass !== 'number') ? ' ' + cloneLimitClass : ''), text: cloneLimitText }).insertAfter(currentBtnClone); } flagProceed = true; } return flagProceed; } /* |------------------------------------------------- | When Remove button was clicked |------------------------------------------------- */ $(document).on('click', 'button.' + generatedSelectorClassForRemoveBtn + ',div.' + generatedSelectorClassForRemoveBtn, function() { var selectorName, parentToRemove, className = $(this).data('metalRef').replace('.', ''), parentTr = $(this).closest('tr.' + className); if (typeof enableConfirmMessage === 'boolean' && enableConfirmMessage) { if (!confirm(confirmMessageText)) { return false; } } // without .(class) or #(id) symbols selectorName = getSelectorName(); // call function to get selector name if (parentTr.length) { if (opt.enableAnimation) { parentTr.children('td').wrapInner('
'); parentTr.children('td').children('div').each(function (index, elem) { $(elem).slideUp(animationSpeed, function() { if (index === (parentTr.children('td').children('div').length - 1)) { $(document).triggerHandler('metal-event:onClonedRemoved', { base: base, callback: onClonedRemoved, toRemoveElement: parentTr }); } }); }); } else { $(document).triggerHandler('metal-event:onClonedRemoved', { base: base, callback: onClonedRemoved, toRemoveElement: parentTr }); } } else { if (opt.enableAnimation) { $(this).closest(generatedSelectorClass).slideUp(animationSpeed, function () { // onClonedRemoved callback accept 1 paramater // param1 - removed element $(document).triggerHandler('metal-event:onClonedRemoved', { base: base, callback: onClonedRemoved, toRemoveElement: $(this).closest(generatedSelectorClass) }); }); } else { $(document).triggerHandler('metal-event:onClonedRemoved', { base: base, callback: onClonedRemoved, toRemoveElement: $(this).closest(generatedSelectorClass) }); } } // remove error_limit message after remove // current deleted element $('body').find('[data-clone-reference="' + selectorName + '"]').remove(); }); }); }; $.fn.metalClone.defaults = { destination: false, // Put your selector(parent container) eg : .myContainer | #myContainer position: 'after', // Available in two option : after & before numberToClone: 1, // Number of element to clone ids: [], // Element to increase the id(s) value, unique purpose // eg : ['input','select','textarea'] // Available options : // - input // - select // - textarea // - div // - span // - i // - strong // - h1-h6 // * -> find all element inside container btnClone: null, // Put your selector(button class or id name) eg : .clickMe | #clickMe copyValue: false, // Clone together the previous element value - available for form element only clearExceptions: '', // Make exception for elements that you do not want to clear: eg: '.my_exception_element, #my_exception_element' btnRemoveText: 'Remove', // Text appear on remove button btnRemoveClass: null, // Adding user defined class name for remove button btnRemoveId: null, // Adding user defined id for remove button btnCloneText: 'Create New Element', // Text appear on clone button btnCloneClass: null, // Adding user defined class name for clone button cloneLimit: 'infinity', // limit the element that want to clone, cloneLimitText: 'Clone limit reached', cloneLimitClass: null, // Adding user defined class name for clone limit message onStart: null, // on start plugin initialization onClone: null, // on cloned element(when cloned button clicked) onComplete: null, // on success/complete cloned element render into page onClonedRemoved: null, // on delete/remove cloned element, enableIcon: true, // Set to false to hide button's icon // These sections only available when "enableIcon" set to true" fontAwesomeTheme: 'solid', // Available option "regular", "solid", "brand", "light", "basic" fontAwesomeRemoveClass: 'fa-trash-alt', // Reference: https://fontawesome.com/icons?d=gallery fontAwesomeAddClass: 'fa-plus', // Reference: https://fontawesome.com/icons?d=gallery fontAwesomeAddDataTransform: '', // Reference: https://fontawesome.com/how-to-use/svg-with-js fontAwesomeAddDataMask: '', // Reference: https://fontawesome.com/how-to-use/svg-with-js fontAwesomeRemoveDataTransform: '', // Reference: https://fontawesome.com/how-to-use/svg-with-js fontAwesomeRemoveDataMask: '', // Reference: https://fontawesome.com/how-to-use/svg-with-js enableConfirmMessage: true, // Set to false to disable confirmation message when remove action triggered confirmMessageText: 'Are you sure?', // Set your custom message[only available when enableConfirmMessage is set to true] enableAnimation: true, // Set to false to disable animation on clone and remove element animationSpeed: 400, // Duration speed in milliseconds, enableScrollTop: true, // Set to false to disable page from scrolling to newly created element scrollTopSpeed: 1000, // Default speed scroll top // Please wait for more callback option.. coming soon.. }; /** * [Event listener] Triggered when remove cloned element. */ $(document).on('metal-event:onClonedRemoved', function (event, data) { data.toRemoveElement.remove(); if ($.isFunction(data.callback)) data.callback.call(data.base, data.toRemoveElement); }); /** * [Event Listener] Triggered when "enableScrollTop" option is enable. */ $(document).on('metal-event:scrollTop', function (event, data) { $('html,body').animate({ scrollTop: $(data.cloned_element).offset().top }, data.scroll_top_speed); }); })(jQuery);