/* formBuilder - git@github.com:kevinchappell/formBuilder.git Version: 1.6.2 Author: Kevin Chappell */ 'use strict'; (function($) { 'use strict'; var Toggle = function Toggle(element, options) { var defaults = { theme: 'fresh', labels: { off: 'Off', on: 'On' } }; var opts = $.extend(defaults, options), $kcToggle = $('
').insertAfter(element).append(element); $kcToggle.toggleClass('on', element.is(':checked')); var kctOn = '
' + opts.labels.on + '
', kctOff = '
' + opts.labels.off + '
', kctHandle = '
', kctInner = '
' + kctOn + kctHandle + kctOff + '
'; $kcToggle.append(kctInner); $kcToggle.click(function() { element.attr('checked', !element.attr('checked')); $(this).toggleClass('on'); }); }; $.fn.kcToggle = function(options) { var toggle = this; return toggle.each(function() { var element = $(this); if (element.data('kcToggle')) { return; } var kcToggle = new Toggle(element, options); element.data('kcToggle', kcToggle); }); }; })(jQuery); 'use strict'; (function($) { 'use strict'; var FormBuilder = function FormBuilder(element, options) { var defaults = { // Uneditable fields or other content you would like to // appear before and after regular fields. disableFields: { // before: '

Header

', // after: '

Footer

' }, // array of objects with fields values // ex: // defaultFields: [{ // label: 'First Name', // name: 'first-name', // required: 'true', // description: 'Your first name', // type: 'text' // }, { // label: 'Phone', // name: 'phone', // description: 'How can we reach you?', // type: 'text' // }], defaultFields: [], roles: { 1 : 'Administrator' }, showWarning: false, serializePrefix: 'frmb', messages: { add: 'Add Item', allowSelect: 'Allow Select', autocomplete: 'Autocomplete', cannotBeEmpty: 'This field cannot be empty', checkboxGroup: 'Checkbox Group', checkbox: 'Checkbox', checkboxes: 'Checkboxes', clearAllMessage: 'Are you sure you want to remove all items?', clearAll: 'Clear All', close: 'Close', copy: 'Copy To Clipboard', dateField: 'Date Field', description: 'Help Text', descriptionField: 'Description', devMode: 'Developer Mode', disableFields: 'These fields cannot be moved.', editNames: 'Edit Names', editorTitle: 'Form Elements', editXML: 'Edit XML', fieldVars: 'Field Variables', fieldRemoveWarning: 'Are you sure you want to remove this field?', getStarted: 'Drag a field from the right to this area', hide: 'Edit', hidden: 'Hidden Input', label: 'Label', labelEmpty: 'Field Label cannot be empty', limitRole: 'Limit access to one or more of the following roles:', mandatory: 'Mandatory', maxLength: 'Max Length', minOptionMessage: 'This field requires a minimum of 2 options', name: 'Name', no: 'No', off: 'Off', on: 'On', optional: 'optional', optionLabelPlaceholder: 'Label', optionValuePlaceholder: 'Value', optionEmpty: 'Option value required', paragraph: 'Paragraph', preview: 'Preview', radioGroup: 'Radio Group', radio: 'Radio', removeMessage: 'Remove Element', remove: '×', required: 'Required', richText: 'Rich Text Editor', roles: 'Access', save: 'Save Template', selectOptions: 'Select Items', select: 'Select', selectionsMessage: 'Allow Multiple Selections', text: 'Text Field', textLabel: 'Title', textIntroduce: 'Description', toggle: 'Toggle', warning: 'Warning!', viewXML: 'View XML', yes: 'Yes', defaultValue : 'Default Value', } }; var startIndex, doCancel, _helpers = {}; /** * Callback for when a drag begins * @param {object} event * @param {object} ui */ _helpers.startMoving = function(event, ui) { event = event; ui.item.addClass('moving'); startIndex = $('li', this).index(ui.item); }; /** * Callback for when a drag ends * @param {object} event * @param {object} ui */ _helpers.stopMoving = function(event, ui) { event = event; ui.item.removeClass('moving'); if (doCancel) { $(ui.sender).sortable('cancel'); $(this).sortable('cancel'); } }; /** * Make strings safe to be used as classes * @param {string} str string to be converted * @return {string} converter string */ _helpers.safename = function(str) { return str.replace(/\s/g, '-').replace(/[^a-zA-Z0-9\-\_]/g, '').toLowerCase(); }; /** * Strips non-numbers from a number only input * @param {string} str string with possible number * @return {string} string without numbers */ _helpers.forceNumber = function(str) { return str.replace(/[^0-9]/g, ''); }; /** * hide and show mouse tracking tooltips, only used for disabled * fields in the editor. * @todo remove or refactor to make better use * @param {object} tt jQuery option with nexted tooltip * @return {void} */ _helpers.initTooltip = function(tt) { var tooltip = tt.find('.tooltip'); tt.mouseenter(function() { if (tooltip.outerWidth() > 200) { tooltip.addClass('max-width'); } tooltip.css('left', tt.width() + 14); tooltip.stop(true, true).fadeIn('fast'); }).mouseleave(function() { tt.find('.tooltip').stop(true, true).fadeOut('fast'); }); tooltip.hide(); }; // saves the field data to our canvas (elem) _helpers.save = function() { $sortableFields.children('li').not('.disabled').each(function() { _helpers.updatePreview($(this)); }); elem.val($sortableFields.toXML()); }; // updatePreview will generate the preview for radio and checkbox groups _helpers.updatePreview = function(field) { var fieldClass = field.attr('class'), $prevHolder = $('.prev-holder', field); if (fieldClass.indexOf('ui-sortable-handle') !== -1) { return; } fieldClass = fieldClass.replace(' form-field', ''); var preview, previewData = { type: fieldClass, label: $('.fld-label', field).val() }; if (fieldClass === 'checkbox') { previewData.toggle = $('.checkbox-toggle', field).is(':checked'); } if (fieldClass.match(/(select|checkbox-group|radio-group)/)) { previewData.values = []; $('.sortable-options li', field).each(function() { var option = {}; option.selected = $('.select-option', $(this)).is(':checked'); option.value = $('.option-value', $(this)).val(); option.label = $('.option-label', $(this)).val(); previewData.values.push(option); }); } preview = fieldPreview(previewData); $prevHolder.html(preview); $('input[toggle]', $prevHolder).kcToggle(); }; // update preview to label _helpers.updateMultipleSelect = function() { $sortableFields.delegate('input[name="multiple"]', 'change', function() { var options = $(this).parents('.fields:eq(0)').find('.sortable-options input.select-option'); if (this.checked) { options.each(function() { $(this).prop('type', 'checkbox'); }); } else { options.each(function() { $(this).removeAttr('checked').prop('type', 'radio'); }); } }); }; _helpers.htmlEncode = function(value) { return $('
').text(value).html(); }; _helpers.htmlDecode = function(value) { return $('
').html(value).text(); }; _helpers.validateForm = function() { var errors = []; // check for empty field labels $('input[name="label"], input[type="text"].option', $sortableFields).each(function() { if ($(this).val() === '') { var field = $(this).parents('li.form-field'), fieldAttr = $(this); errors.push({ field: field, error: opts.messages.labelEmpty, attribute: fieldAttr }); } }); // @todo add error = { noVal: opts.messages.labelEmpty } if (errors.length) { alert('Error: ' + errors[0].error); $('html, body').animate({ scrollTop: errors[0].field.offset().top }, 1000, function() { var targetID = $('.toggle-form', errors[0].field).attr('id'); $('.toggle-form', errors[0].field).addClass('open').parent().next('.prev-holder').slideUp(250); $('#' + targetID + '-fld').slideDown(250, function() { errors[0].attribute.addClass('error'); }); }); } }; _helpers.disabledTT = function(field) { var title = field.attr('data-tooltip'); if (title) { field.removeAttr('title').data('tip_text', title); var tt = $('

', { 'class': 'frmb-tt' }).html(title); field.append(tt); tt.css({ top: -tt.outerHeight(), left: -15 }); field.mouseleave(function() { $(this).attr('data-tooltip', field.data('tip_text')); $('.frmb-tt').remove(); }); } }; var opts = $.extend(defaults, options), elem = $(element), frmbID = 'frmb-' + $('ul[id^=frmb-]').length++; var field = '', lastID = 1, boxID = frmbID + '-control-box'; // create array of field objects to cycle through var frmbFields = [{ label: opts.messages.text, attrs: { type: 'text', className: 'text-input', name: 'text-input' } }, { label: opts.messages.select, attrs: { type: 'select', className: 'select', name: 'select' } }, { label: opts.messages.richText, attrs: { type: 'rich-text', className: 'rich-text', name: 'rich-text' } }, { label: opts.messages.radioGroup, attrs: { type: 'radio-group', className: 'radio-group', name: 'radio-group' } }, { label: opts.messages.hidden, attrs: { type: 'hidden', className: 'hidden-input', name: 'hidden-input' } }, { label: opts.messages.dateField, attrs: { type: 'date', className: 'calendar', name: 'date-input' } }, { label: opts.messages.checkboxGroup, attrs: { type: 'checkbox-group', className: 'checkbox-group', name: 'checkbox-group' } }, { label: opts.messages.checkbox, attrs: { type: 'checkbox', className: 'checkbox', name: 'checkbox' } }, { label: opts.messages.autocomplete, attrs: { type: 'autocomplete', className: 'autocomplete', name: 'autocomplete' } }, { label: opts.messages.textLabel, attrs: { type: 'text-label', className: 'text-label', name: 'text' } }]; // Create draggable fields for formBuilder var cbUL = $('

    ', { id: boxID, 'class': 'frmb-control' }); // Loop through for (var i = frmbFields.length - 1; i >= 0; i--) { var $field = $('
  • ', { 'class': 'icon-' + frmbFields[i].attrs.className, 'type': frmbFields[i].type, 'name': frmbFields[i].className, 'label': frmbFields[i].label }); for (var attr in frmbFields[i]) { if (frmbFields[i].hasOwnProperty(attr)) { $field.data(attr, frmbFields[i][attr]); } } $field.html(frmbFields[i].label).appendTo(cbUL); } // Build our headers and action links var cbHeader = $('

    ').html(opts.messages.editorTitle), viewXML = $('', { id: frmbID + '-export-xml', text: opts.messages.viewXML, href: '#', 'class': 'view-xml' }), allowSelect = $('', { id: frmbID + '-allow-select', text: opts.messages.allowSelect, href: '#', 'class': 'allow-select' }).prop('checked', 'checked'), editXML = $('', { id: frmbID + '-edit-xml', text: opts.messages.editXML, href: '#', 'class': 'edit-xml' }), editNames = $('', { id: frmbID + '-edit-names', text: opts.messages.editNames, href: '#', 'class': 'edit-names' }), clearAll = $('', { id: frmbID + '-clear-all', text: opts.messages.clearAll, href: '#', 'class': 'clear-all' }), saveAll = $('
    ', { id: frmbID + '-save', href: '#', 'class': 'save-btn-wrap', title: opts.messages.save }).html('' + opts.messages.save + ''), actionLinksInner = $('
    ', { id: frmbID + '-action-links-inner', 'class': 'action-links-inner' }).append(editXML, ' | ', editNames, ' | ', allowSelect, ' | ', clearAll, ' | '), devMode = $('', { 'class': 'dev-mode-link' }).html(opts.messages.devMode + ' ' + opts.messages.off), actionLinks = $('
    ', { id: frmbID + '-action-links', 'class': 'action-links' }).append(actionLinksInner, devMode); // Sortable fields var $sortableFields = $('
      ').attr('id', frmbID).addClass('frmb').sortable({ cursor: 'move', opacity: 0.9, beforeStop: function beforeStop(event, ui) { var lastIndex = $('> li', $sortableFields).length - 1, curIndex = ui.placeholder.index(); doCancel = curIndex <= 1 || curIndex === lastIndex; }, start: _helpers.startMoving, stop: _helpers.stopMoving, cancel: 'input, .disabled, .sortable-options, .add, .btn, .no-drag', // items: 'li:not(.no-fields)', receive: function receive(event, ui) { // if (doCancel) { // $('li:nth-child(' + curIndex + ')', $(this)).remove(); // } }, placeholder: 'frmb-placeholder' }); // ControlBox with different fields cbUL.sortable({ helper: 'clone', opacity: 0.9, connectWith: $sortableFields, cursor: 'move', placeholder: 'ui-state-highlight', start: _helpers.startMoving, stop: _helpers.stopMoving, revert: 150, change: function change(event, ui) { //fix the logic on this to only hide placeholder for disabledFields.before and after // if (ui.placeholder.index() === 0 || ui.placeholder.index() === $('> li', $sortableFields).last().index()) { // $(ui.placeholder).hide(); // } else { // $(ui.placeholder).show(); // } }, remove: function remove(event, ui) { if (startIndex === 0) { cbUL.prepend(ui.item); } else { $('li:nth-child(' + startIndex + ')', cbUL).after(ui.item); } }, beforeStop: function beforeStop(event, ui) { var lastIndex = $('> li', $sortableFields).length - 1, curIndex = ui.placeholder.index(); doCancel = curIndex <= 1 || curIndex === lastIndex ? true: false; if (ui.placeholder.parent().hasClass('frmb-control')) { doCancel = true; } }, update: function update(event, ui) { // _helpers.stopMoving; elem.stopIndex = $('li', $sortableFields).index(ui.item) === 0 ? '0': $('li', $sortableFields).index(ui.item); if ($('li', $sortableFields).index(ui.item) < 0) { $(this).sortable('cancel'); } else { prepFieldVars($(ui.item[0]), true); } }, receive: function receive(event, ui) { if (ui.sender.hasClass('frmb') || ui.sender.hasClass('frmb-control')) { $(ui.sender).sortable('cancel'); } } }); var $stageWrap = $('
      ', { id: frmbID + '-stage-wrap', 'class': 'stage-wrap' }); var $formWrap = $('
      ', { id: frmbID + '-form-wrap', 'class': 'form-wrap' }); elem.before($stageWrap).appendTo($stageWrap); // Replace the textarea with sortable list. //elem.before($sortableFields).parent().prepend(frmbHeader).addClass('frmb-wrap').append(actionLinks, viewXML, saveAll); var cbWrap = $('
      ', { id: frmbID + '-cb-wrap', 'class': 'cb-wrap' }).append(cbHeader, cbUL); $stageWrap.append($sortableFields, cbWrap, actionLinks, viewXML, saveAll); $stageWrap.before($formWrap); $formWrap.append($stageWrap, cbWrap); var doSave = function doSave() { if ($(this).parents('li.disabled').length === 0) { if ($(this).name === 'label' && $(this).val() === '') { return alert('Error: ' + opts.messages.labelEmpty); } _helpers.save(); } }; // Not pretty but we need to save a lot so users don't have to keep clicking a save button $('input, select', $sortableFields).on('change', doSave); $('input, select', $sortableFields).on('blur', doSave); // Parse saved XML template data elem.getTemplate = function() { var xml = elem.val() !== '' ? $.parseXML(elem.val()) : false, fields = $(xml).find('field'); if (fields.length > 0) { fields.each(function() { prepFieldVars($(this)); }); } else if (!xml) { // Load default fields if none are set if (opts.defaultFields.length) { for (var i = opts.defaultFields.length - 1; i >= 0; i--) { appendNewField(opts.defaultFields[i]); } } else { $stageWrap.addClass('empty').attr('data-content', opts.messages.getStarted); } disabledBeforeAfter(); } }; var disabledBeforeAfter = function disabledBeforeAfter() { var li = '
    • __CONTENT__
    • '; if (opts.disableFields.before && !$('.disabled.before', $sortableFields).length) { $sortableFields.prepend(li.replace('__POSITION__', 'before').replace('__CONTENT__', opts.disableFields.before)); } if (opts.disableFields.after && !$('.disabled.after', $sortableFields).length) { $sortableFields.append(li.replace('__POSITION__', 'after').replace('__CONTENT__', opts.disableFields.after)); } }; var nameAttr = function nameAttr(field) { var epoch = new Date().getTime(); return field.data('attrs').name + '-' + epoch; }; var prepFieldVars = function prepFieldVars($field, isNew) { isNew = isNew || false; var fieldAttrs = $field.data('attrs') || {}, fType = fieldAttrs.type || $field.attr('type'), isMultiple = fType.match(/(select|checkbox-group|radio-group)/), values = {}; values.label = _helpers.htmlEncode($field.attr('label')); values.name = isNew ? nameAttr($field) : fieldAttrs.name || $field.attr('name'); values.name = typeof values.name == 'undefined' ? '' : values.name; values.defaultVal = $field.attr('defaultVal') !== undefined ? _helpers.htmlEncode($field.attr('defaultVal')) : ''; values.role = $field.attr('role'); values.required = $field.attr('required'); values.maxLength = $field.attr('max-length'); values.toggle = $field.attr('toggle'); values.type = fType; values.description = $field.attr('description') !== undefined ? _helpers.htmlEncode($field.attr('description')) : ''; values.introduce = $field.attr('introduce') !== undefined ? _helpers.htmlEncode($field.attr('introduce')) : ''; if (isMultiple) { values.multiple = true; values.values = []; $field.children().each(function(i) { var value = { label: $(this).text(), value: $(this).attr('value'), selected: $field.attr('default') === i ? true: false }; values.values.push(value); }); } appendNewField(values); $stageWrap.removeClass('empty'); disabledBeforeAfter(); }; // multi-line textarea var appendTextarea = function appendTextarea(values) { appendFieldLi(opts.messages.richText, advFields(values), values); }; var appendInput = function appendInput(values) { var type = values.type || 'text'; appendFieldLi(opts.messages[type], advFields(values), values); }; var textLabel = function textLabel(values) { var type = values.type || 'text'; values.introduce = values.introduce || opts.messages.textIntroduce; appendFieldLi(opts.messages[type], advFields(values), values); } // add select dropdown var appendSelectList = function appendSelectList(values) { if (!values.values || !values.values.length) { values.values = [{ selected: 'false', label: 'Option 1', value: 'option-1' }, { selected: 'false', label: 'Option 2', value: 'option-2' }]; } var field = '', name = _helpers.safename(values.name), multiDisplay = values.type === 'checkbox-group' ? 'none': 'none'; field += advFields(values); field += '
      ' + opts.messages.selectOptions + '
      '; field += '
      '; field += '
      '; field += ''; field += ''; field += '
      '; field += '
        '; for (i = 0; i < values.values.length; i++) { field += selectFieldOptions(values.values[i], name, values.values[i].selected, values.multiple); } field += '
      '; field += ''; field += '
      '; appendFieldLi(opts.messages.select, field, values); $('.sortable-options').sortable(); // making the dynamically added option fields sortable. }; var appendNewField = function appendNewField(values) { if (values === undefined) { values = ''; } // TODO: refactor to move functions into this object var appendFieldType = { // 'text': appendTextInput(values), // 'checkbox': appendCheckbox(values), // 'select': appendSelectList(values), // 'textarea': appendTextarea(values), 'text-label': textLabel, '2': appendInput, 'date': appendInput, 'autocomplete': appendInput, 'checkbox': appendInput, 'select': appendSelectList, 'rich-text': appendTextarea, 'textarea': appendTextarea, 'radio-group': appendSelectList, 'checkbox-group': appendSelectList, 'text': appendInput, 'hidden': appendInput }; if (typeof appendFieldType[values.type] === 'function') { appendFieldType[values.type](values); } }; /** * Build the editable properties for the field * @param {object} values configuration object for advanced fields * @return {string} markup for advanced fields */ var advFields = function advFields(values) { var advFields = '', key, roles = values.role !== undefined ? values.role.split(',') : []; var fieldLabel = $('
      ', { 'class': 'frm-fld label-wrap' }); $('