(function ($, api) {
'use strict'
/**
* Simple Form for Tabbed Builder
*/
$.fn.formtab = function () {
var parent = this
var li = $(this).find('.tabbed-list > li')
var body = $(this).find('.jeg_tabbed_body')
var active = 'active'
li.on('click', function () {
if (!$(this).hasClass(active)) {
var link = $(this).attr('href').substr(1)
// Active Tab
li.removeClass(active)
$(this).addClass(active)
// Active Body
body.removeClass(active)
$(parent).find('#' + link).addClass(active)
}
return false
})
}
/**
* Global helper for form builder
*
* @type {{}}
*/
api.helper = {}
/**
* Sort element by priority
*
* @param data
* @returns {Array}
*/
api.helper.prioritySort = function (data) {
var sortedData = []
var sortedKey = Object.keys(data).sort(function (a, b) {
return data[a]['priority'] - data[b]['priority']
})
_.each(sortedKey, function (key) {
sortedData.push(data[key])
})
return sortedData
}
/**
* String Format functionality to replicate sprintf on php
* @param format
*/
api.helper.format = function (format) {
var args = Array.prototype.slice.call(arguments, 1)
return format.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] !== 'undefined' ? args[number] : match
})
}
/**
* Base container to extended by type of container
*/
api.BaseContainer = api.Class.extend({
/**
* ID of this container
*/
id: null,
/**
* Element that hold this menu
*/
element: null,
/**
* Collection segments attached for every single menu
*/
segments: null,
/**
* Collection fields attached for every single field
*/
fields: null,
/**
* Menu container loaded
*/
loaded: false,
/**
* Container pane. Here to inject new element
*/
containerPaneParent: '',
/**
* Container Holder
*/
containerHolder: null,
/**
* Initialize Menu
*
* @param id
* @param element
* @param option
*/
initialize: function (id, element, option) {
this.id = id
this.element = element
this.option = option
// This variable need to be assigned right here to prevent merging variable with previous instantiate class
this.segments = new api.Values({ defaultConstructor: api.Segment })
this.setContainerHolder()
this.loadContainer()
},
/**
* Load Container Content
*/
loadContainer: function () {
// populate segment
this.populateSegments(this.option.segments)
// Need to trigger Widget Container Loaded so both segment & field can be resolved
this.triggerFinish()
// Attach event if available
this.attachEvent()
},
/**
* Trigger when container already finish rendered
*/
triggerFinish: function () {
this.loaded = true
this.containerHolder.trigger(this.id, this)
},
/**
* Populate Segment
*
* @param segments
*/
populateSegments: function (segments) {
segments = this.prepareSegment(segments)
this.setupSegment(this.id, segments)
},
/**
* Setup Setting Segment
*
* @param segments
*/
prepareSegment: function (segments) {
var index = 0
_.each(segments, function (data, key) {
segments[key] = this.prepareSegmentData(segments[key], data, index++)
}.bind(this))
return api.helper.prioritySort(segments)
},
/**
* Inject segment into MenuContainer
*
* @param id
* @param segments
*/
setupSegment: function (id, segments) {
var Constructor = null
_.each(segments, function (data) {
if (_.has(api.segmentConstructor, data.type)) {
Constructor = api.segmentConstructor[data.type]
} else {
Constructor = api.segmentConstructor.normal
}
if (!this.segments.has(data.id)) {
this.segments.add(data.id, new Constructor(data.id, data))
}
}.bind(this))
},
/**
* Assign additional Segment Data
*
* @param segment
*
* @returns {*}
*/
prepareSegmentData: function (segment) {
segment.parent = this.id
segment.container = this
return segment
},
/**
* Set Container Holder
*/
setContainerHolder: function () { },
/**
* Attach event
*/
attachEvent: function () { },
})
/**
* Main class for Segment type, will be extended by menu, widget, category, etc
*/
api.Segment = api.Class.extend({
/**
* Segment Type
*/
segmentType: 'segment',
/**
* Segment not loaded
*/
loaded: false,
/**
* Default parameters
*/
defaults: {
name: '',
type: 'default',
active: true,
parent: '',
priority: 10,
},
/**
* Initialize segment
*
* @param id
* @param options
*/
initialize: function (id, options) {
this.id = id
this.params = _.extend(
{},
this.defaults,
this.params || {},
options || {}
)
this.priority = new api.Value()
this.priority.set(this.params.priority)
// holding fields
this.fields = new api.Values({ defaultConstructor: api.Fields })
this.deferred = {
embedded: new $.Deferred(),
}
this.embed()
this.setContainerHolder()
this.deferred.embedded.done(function () {
this.attachEvent()
this.ready()
}.bind(this))
this.populateFields()
this.loadState()
},
/**
* Set loaded segment
*/
triggerLoaded: function () {
this.loaded = true
this.containerHolder.trigger(this.id, this)
},
/**
* Render Content
*/
renderContent: function () {
var template
template = wp.template('form-segment-' + this.segmentType)
if (template) {
return template(this.params)
}
return '
'
},
/***
* Set Container Holder
*/
setContainerHolder: function () {
this.containerHolder = this.params.container.segments
},
/**
* Get selector where to append segment
*
* @return string
*/
getParentSegmentHolder: function () {
return this.params.container.containerPaneParent
},
/**
* Get parent container element
*
* @return string
*/
getParentContainer: function () {
return this.params.container.element
},
/**
* attach event
*/
attachEvent: function () { },
/**
* Ready State
*/
ready: function () { },
/**
* set load state for segment
*/
loadState: function () { },
/**
* Populare Fields
*/
populateFields: function () {
var fields = this.prepareField(this.params.container.option.fields)
this.setupField(this.id, fields)
},
/**
* Setup Setting Field
*
* @param fields
* @returns {*}
*/
prepareField: function (fields) {
var index = 0
var resultFields = []
_.each(fields, function (data, key) {
if (this.id === fields[key].segment) {
resultFields[key] = this.prepareFieldData(fields[key], data, index++)
}
}.bind(this))
return api.helper.prioritySort(resultFields)
},
/**
* Inject field into Menu Container
*
* @param id
* @param fields
*/
setupField: function (id, fields) {
var Constructor = null
_.each(fields, function (data) {
if (_.has(api.fieldConstructor, data.type)) {
Constructor = api.fieldConstructor[data.type]
} else {
Constructor = api.fieldConstructor.standart
}
if (!this.fields.has(data.id)) {
this.fields.add(data.id, new Constructor(data.id, data))
}
}.bind(this))
},
/**
* Prepare Field Data
*
* @param field
* @param data
*
* @returns {*}
*/
prepareFieldData: function (field, data) {
field.container = this
return field
},
/**
* Embed element
*/
embed: function () { },
/**
* Listen Field Change
*/
listenFieldChange: function (id, value) { },
})
/**
* Normal Default Segment
*/
api.normalSegment = api.Segment.extend({
segmentType: 'normal',
segmentContentParent: '.jeg_accordion_body',
/**
* Initialize Menu segment
*
* @param id
* @param params
*/
initialize: function (id, params) {
var normalSegment = this
api.Segment.prototype.initialize.call(normalSegment, id, params)
},
/**
* modify load state
*/
loadState: function () {
var normalSegment = this
var container = normalSegment.params.container
var containerID = container.id
container.containerHolder.bind(containerID, function (container) {
if (container.loaded) {
normalSegment.loaded = true
normalSegment.params.container.segments.trigger(normalSegment.id, normalSegment)
}
})
},
/**
* attach event
*/
attachEvent: function () {
var segment = this
segment.element.find('.jeg_accordion_heading').on('click', function (e) {
e.preventDefault()
var parent = $(this).parent('.jeg_accordion_wrapper')
var body = $(parent).find('.jeg_accordion_body')
if ($(parent).hasClass('open')) {
$(body).slideUp('fast')
$(parent).removeClass('open').addClass('close')
} else {
$(body).slideDown('fast')
$(parent).removeClass('close').addClass('open')
}
})
},
/**
* Embed element
*/
embed: function () {
var segment = this
segment.element = $(segment.renderContent())
segment.holder = segment.getParentContainer()
segment.holder.append(segment.element)
segment.deferred.embedded.resolve()
},
})
/**
* Metabox Segment
*/
api.noWrapSegment = api.Segment.extend({
segmentType: 'nowrap',
segmentContentParent: '.jeg_metabox_body',
/**
* modify load state
*/
loadState: function () {
var segment = this
var containerID = segment.params.container.id
segment.params.container.containerHolder.bind(containerID, function (container) {
if (container.loaded) {
segment.loaded = true
segment.params.container.segments.trigger(segment.id, segment)
}
})
},
/**
* Embed element
*/
embed: function () {
this.element = $(this.renderContent())
this.holder = this.getParentContainer()
this.holder.append(this.element)
this.deferred.embedded.resolve()
},
})
/**
* Tabbed Segment
*/
api.TabbedSegment = api.Segment.extend({
segmentType: 'tabbed',
segmentContentParent: '.jeg_metabox_body',
/**
* modify load state
*/
loadState: function () {
var tabbedSegment = this
var container = tabbedSegment.params.container
var containerID = container.id
container.containerHolder.bind(containerID, function (container) {
if (container.loaded) {
tabbedSegment.loaded = true
tabbedSegment.params.container.segments.trigger(tabbedSegment.id, tabbedSegment)
}
})
},
/**
* Embed element
*/
embed: function () {
var segment = this
// Navigation
segment.navigationElement = $(segment.renderNavigationContent())
segment.navigationHolder = segment.getParentContainer().find('.tabbed-list')
segment.navigationHolder.append(segment.navigationElement)
// Body
segment.element = $(segment.renderContent())
segment.holder = segment.getParentContainer().find('.tabbed-body')
segment.holder.append(segment.element)
segment.deferred.embedded.resolve()
},
/**
* Navigation Content
*/
renderNavigationContent: function () {
var template,
segment = this
template = wp.template('form-segment-' + segment.segmentType + '-tab')
if (template) {
return template(segment.params)
}
return ''
},
/**
* attach event
*/
attachEvent: function () { },
/**
* Prepare Field Data
*
* @param field
* @param data
* @param index
*
* @returns {*}
*/
prepareFieldData: function (field, data, index) {
field.container = this
field.index = index
return field
},
})
/**
* List of Segment Type
*
* @type {{menu: *}}
*/
api.segmentConstructor = {
normal: api.normalSegment,
nowrap: api.noWrapSegment,
tabbed: api.TabbedSegment,
}
/**
* Main class for Field will be extended by every text
*/
api.Fields = api.Class.extend({
defaults: {
title: '',
desc: '',
default: '',
type: 'text',
active: true,
parent: '',
priority: 10,
},
/**
* Initialize
*
* @param id
* @param options
*/
initialize: function (id, options) {
var field = this
field.id = id
field.params = _.extend(
{},
field.defaults,
field.params || {},
options || {}
)
// setup link field
field.params.link = 'data-link="true"'
// Setup field Value
field.value = new api.Value()
field.value.set(field.params.value)
field.value.bind(function (value) {
this.params.container.listenFieldChange(field.id, value)
}.bind(this))
// Setup Field Active State
field.active = new api.Value()
field.active.set(true)
field.active.bind(function (active) {
field.onChangeActive(active)
})
field.deferred = {
embedded: new $.Deferred(),
}
field.embed()
field.deferred.embedded.done(function () {
field.listenLink()
field.attachEvent()
field.ready()
})
this.params.container.containerHolder.bind(this.params.container.id, function (segment) {
if (segment.loaded) {
this.loaded(segment)
}
}.bind(this))
},
/**
* When active status changed
*
* @param active
*/
onChangeActive: function (active) {
var field = this
if (active) {
field.element.slideDown('fast')
} else {
field.element.slideUp('fast')
}
},
/**
* Embed element
*/
embed: function () {
var field = this
var segment = field.params.container
var container = segment.element.find(segment.segmentContentParent)
field.element = $(field.renderContent())
container.append(field.element)
field.deferred.embedded.resolve()
},
/**
* Render Content
*/
renderContent: function () {
var template,
field = this,
type = field.params.type
if (0 !== $('#tmpl-form-field-' + type).length) {
template = wp.template('form-field-' + type)
} else {
template = wp.template('form-field-standart')
}
if (template) {
return template(field.params)
}
},
/**
* Listen input change, then setup those field
*/
listenLink: function () {
var field = this
$(field.element).find('[data-link]').change(function () {
field.value.set($(this).val())
})
},
/**
* attach event
*/
attachEvent: function () { },
/**
* Ready State
*/
ready: function () { },
/**
* Field loaded
*
* @param segment
*/
loaded: function (segment) {
var field = this
field.activeField()
},
/**
* Compare between two value
*
* @param value1
* @param value2
* @param compare
* @returns {boolean}
*/
compare: function (value1, value2, compare) {
if (compare === '===') {
return value1 === value2
}
if (compare === '=' || compare === '==' || compare === 'equals' || compare === 'equal') {
return value1 == value2
}
if (compare === '!=') {
return value1 !== value2
}
if (compare === '!=' || compare === 'not equal') {
return value1 !== value2
}
if (compare === '>=' || compare === 'greater or equal' || compare === 'equal or greater') {
return value1 >= value2
}
if (compare === '<=' || compare === 'smaller or equal' || compare === 'equal or smaller') {
return value1 <= value2
}
if (compare === '>' || compare === 'greater') {
return value1 > value2
}
if (compare === '<' || compare === 'smaller') {
return value1 < value2
}
if (compare === 'in' || compare === 'contains') {
var result = value1.indexOf(value2)
return result >= 0
}
if (compare === 'not in' || compare === 'not contains') {
var result = value1.indexOf(value2)
return result < 0
}
},
/**
* Examine status
*
* @param dependencies
* @returns {boolean}
*/
getStatus: function (dependencies) {
var field = this
var fields = field.params.container.fields
var flag = true
_.each(dependencies, function (dependency) {
var parent = fields(dependency.field)
flag = flag && field.compare(dependency.value, parent.value.get(), dependency.operator)
})
return flag
},
/**
* Set active status for this field
*
* @param field
* @param dependencies
*/
setActiveStatus: function (field, dependencies) {
var activeStatus = field.getStatus(dependencies)
field.active.set(activeStatus)
},
/**
* Setup field active field
*/
activeField: function () {
var field = this
var fields = field.params.container.fields
if (undefined !== field.params.dependency) {
var dependencies = field.params.dependency
if (dependencies.length > 0) {
field.setActiveStatus(field, dependencies)
_.each(dependencies, function (dependency) {
var parent = fields(dependency.field)
parent.value.bind(function () {
field.setActiveStatus(field, dependencies)
})
})
}
}
},
})
/**
* Radio Image Field
*/
api.radioImageField = api.Fields.extend({})
/**
* Color Field Type
*/
api.colorField = api.Fields.extend({
attachEvent: function () {
var element = $(this.element)
var clone = $(element).find('.jeg-color-picker-clone')
var input = $(element).find('.jeg-color-picker')
clone.wpColorPicker({
change: function (event, ui) {
var color = ui.color.toString()
$(input).val(color).trigger('change')
},
clear: function () {
$(input).val('').trigger('change')
},
})
},
})
/**
* Slider Field Type
*/
api.sliderField = api.Fields.extend({
attachEvent: function () {
var element = $(this.element).find('input[type=range]'),
value = element.val()
element.closest('div').find('.jeg_range_value .value').text(value)
element.on('mousedown', function () {
$(this).mousemove(function () {
var value = $(this).val()
$(this).closest('div').find('.jeg_range_value .value').text(value)
})
})
element.on('click', function () {
var value = $(this).val()
$(this).closest('div').find('.jeg_range_value .value').text(value)
})
$(this.element).find('.jeg-slider-reset').on('click', function () {
var thisInput = element
var inputDefault = thisInput.data('reset_value')
thisInput.val(inputDefault)
thisInput.change()
$(this).closest('div.wrapper').find('.jeg_range_value .value').text(inputDefault)
})
},
})
/**
* Icon Picker
*/
api.iconPickerField = api.Fields.extend({
attachEvent: function () {
var field = this
$(field.element).find('.iconpicker').iconpicker({
arrowPrevIconClass: 'fa fa-chevron-left',
arrowNextIconClass: 'fa fa-chevron-right',
iconset: 'fontawesome',
}).on('change', function (e) {
field.value.set(e.icon)
})
},
})
/**
* Select Field
*/
api.selectField = api.Fields.extend({
/**
* Check if valid option passed
*
* @param options
*/
isValidOption: function (options) {
if (undefined !== options[0]) {
if (undefined !== options[0]['value'] &&
undefined !== options[0]['text']) {
return true
}
}
return false
},
/**
* Setup select option for Selectize
*
* @param options
* @returns {Array}
*/
setupOption: function (options) {
if (this.isValidOption(options)) {
return options
} else {
var newOption = []
_.each(options, function (text, value) {
newOption.push({
'value': value,
'text': text,
})
})
return newOption
}
},
/**
* Call ajax if required
*
* @param query
* @param callback
* @returns {*}
*/
ajaxCall: function (query, callback) {
var field = this, slug = ''
if (!query.length || query.length < 3) return callback()
if ('' !== field.params.slug) {
slug = field.params.slug
}
var request = wp.ajax.send(field.params.ajax, {
data: {
query: query,
nonce: field.params.nonce,
slug: slug,
},
})
request.done(function (response) {
callback(response)
})
},
/**
* Attach event for both single & multi
*/
attachEvent: function () {
var field = this,
options = this.setupOption(field.params.options),
value = field.params.value,
slug = ''
if ('' !== field.params.slug) {
slug = field.params.slug
}
if (('' !== value && undefined !== value) && options.length === 0) {
wp.ajax.send(field.params.ajaxoptions, {
data: {
value: value,
nonce: field.params.nonce,
slug: slug
},
}).done(function (response) {
field.renderSelect(response)
})
} else {
field.renderSelect(options)
}
},
/**
* Render select
*
* @return {*}
*/
renderSelect: function (options) {
var field = this,
setting = {}
var input = $(field.element).find('select')
if (input.length === 0) {
input = $(field.element).find('input')
}
if ((field.params.multiple && field.params.multiple > 1) || field.params.ajax) {
// Multi.
setting = {
plugins: ['drag_drop', 'remove_button'],
multiple: field.params.multiple,
hideSelected: true,
options: options,
render: {
option: function (item) {
return '
' + item.text + '
'
},
},
onChange: function (e) {
field.value.set(e)
},
onItemAdd: function () {
if (!field.params.multiple) {
var value = this.items
if (value.length > 1) {
for (var a = 0; a < value.length; a++) {
this.removeItem(value[a])
this.refreshOptions()
}
}
}
},
}
} else {
// Single.
setting = {
allowEmptyOption: true,
onChange: function (e) {
field.value.set(e)
},
}
}
if (field.params.ajax !== '') {
setting.load = field.ajaxCall.bind(field)
setting.create = true
}
$(input).selectize(setting)
},
})
/**
* Checkbox Field
*/
api.checkboxField = api.Fields.extend({
ready: function () {
var field = this,
checkboxValue
$(field.element).find('input').change(function () {
checkboxValue = $(this).is(':checked')
field.value.set(checkboxValue)
})
},
})
/**
* Text Field
*/
api.textField = api.Fields.extend({
ready: function () {
var field = this
$(field.element).on('change click keyup paste', 'input', function () {
field.value.set($(this).val())
})
},
})
/**
* Text Area
*/
api.textareaField = api.textField.extend({
ready: function () {
var field = this
$(field.element).on('change click keyup paste', 'textarea', function () {
field.value.set($(this).val())
})
},
})
/**
* Type Number
*/
api.numberField = api.Fields.extend({
ready: function () {
var field = this,
element = $(this.element).find('input'),
min = $(this).attr('min'),
max = $(this).attr('max'),
step = $(this).attr('step')
$(element).spinner({
min: min,
max: max,
step: step,
stop: function () {
field.value.set($(this).val())
},
})
},
})
api.uploadField = api.Fields.extend({
ready: function () {
var field = this;
field.createMediaUploadInstance(false);
field.element.on('click keypress', '.remove-button', function (e) {
e.preventDefault()
field.removeFile(e)
});
field.element.on('click keypress', '.upload-button', function (e) {
e.preventDefault()
field.openDialog(e)
});
},
createMediaUploadInstance: function () {
var libMediaType = this.getMimeType()
var library = null
if (libMediaType === 'image') {
library = wp.media.query({ type: libMediaType })
} else {
library = wp.media.query({ type: JSON.stringify(libMediaType.split(',')) })
}
this.frame = wp.media({
states: [
new wp.media.controller.Library({
library: library,
multiple: false,
date: false,
}),
],
})
// When a file is selected, run a callback.
this.frame.on('select', this.onSelect, this)
},
onSelect: function () {
var attachment = this.frame.state().get('selection').first().toJSON();
var $uploadButton = this.element.find('.upload-button');
this.element.find('.jeg-file-attachment').
html(' ' + attachment.filename +
'').
hide().
slideDown('slow');
this.element.find('.hidden-field').val(attachment.id);
$uploadButton.text($uploadButton.data('alt-label'));
$uploadButton.show();
this.element.find('.remove-button').show();
//This will activate the save button
this.element.find('input, textarea, select').trigger('change')
this.frame.close()
},
getMimeType: function () {
return this.params.mime_type;
},
openDialog: function () {
var field = this
this.frame.open()
},
removeFile: function () {
var $uploadButton = this.element.find('.upload-button');
this.element.find('.jeg-file-attachment').slideUp('fast', function () {
$(this).show().html($(this).data('placeholder'));
})
this.element.find('.hidden-field').val('');
$uploadButton.text($uploadButton.data('label'));
this.element.find('.remove-button').hide();
this.element.find('input, textarea, select').trigger('change')
}
});
/**
* Image
*/
api.imageField = api.Fields.extend({
ready: function () {
var field = this,
addButton = $(field.element).find('.add-button'),
removeButton = $(field.element).find('.remove-button'),
multiple = ($(field.element).find('.image-content').attr('data-multiple') == 'true')
field.createMediaUploadInstance(multiple)
$(addButton).on('click', field.openDialog.bind(this))
$(removeButton).on('click', field.removeImage.bind(this))
$(field.element).on('click', '.image-wrapper .remove', function (e) {
$(this).parent().fadeOut(200, function () {
$(this).remove();
$(field.element).trigger('change');
field.setupMultiple();
});
});
if (multiple) {
$(field.element).find('.image-wrapper ').sortable({
stop: function (e, ui) {
field.setupMultiple();
}
});
}
},
imageWrapperAction: function (src) {
var field = this
$(field.element).find('.image-wrapper').toggleClass('hide-image', src)
$(field.element).find('img').attr('src', src)
},
toogleButton: function (src) {
var field = this
var toggle = '' === src ? 1 : 0
$(field.element).find('.remove-button').toggleClass('hide-button', toggle)
$(field.element).find('.add-button').toggleClass('hide-button', !toggle)
},
setupInput: function (src) {
var field = this
$(field.element).find('.image-input').val(src).change()
field.value.set(src)
},
setupMultiple: function () {
var field = this,
images = [],
wrapper = $(field.element).find('.image-wrapper')
wrapper.find('input[name="' + field.params.fieldName + '[]"]').each(function () {
images.push($(this).val());
});
field.value.set(images);
},
removeImage: function () {
var field = this
field.imageWrapperAction('')
field.toogleButton('')
field.setupInput('')
},
addImage: function (image) {
var field = this
field.imageWrapperAction(image.url)
field.toogleButton(image.url)
field.setupInput(image.id)
},
addMultipleImage: function (images) {
var field = this,
output = '',
wrapper = $(field.element).find('.image-wrapper')
_.each(images, function (image) {
var thumbnail = image['url']
if (image['sizes'] !== undefined && image['sizes']['thumbnail'] !== undefined)
thumbnail = image['sizes']['thumbnail']['url']
output +=
"
" +
"" +
"" +
"" +
"
";
})
wrapper.removeClass('hide-image').append(output)
field.setupMultiple()
},
createMediaUploadInstance: function (multiple) {
var field = this
field.mediaUpload = wp.media({
frame: 'post',
state: 'insert',
multiple: multiple,
})
field.mediaUpload.on('insert', function () {
var json = field.mediaUpload.state().get('selection')
if (multiple) {
field.addMultipleImage(json.toJSON())
} else {
field.addImage(json.first().toJSON())
}
field.mediaUpload.close()
})
},
openDialog: function () {
var field = this
field.mediaUpload.open()
},
})
var RepeaterRow = function (rowIndex, container, label) {
'use strict'
var self = this
this.rowIndex = rowIndex
this.container = container
this.label = label
this.header = this.container.find('.repeater-row-header'),
this.header.on('click', function () {
self.toggleMinimize()
})
this.container.on('click', '.repeater-row-remove', function () {
self.remove()
self.container.trigger('change')
})
this.header.on('mousedown', function () {
self.container.trigger('row:start-dragging')
})
this.container.on('keyup change', 'input, select, textarea', function (e) {
self.container.trigger('row:update', [self.rowIndex, jQuery(e.target).data('field'), e.target])
})
this.setRowIndex = function (rowIndex) {
this.rowIndex = rowIndex
this.container.attr('data-row', rowIndex)
this.container.data('row', rowIndex)
this.updateLabel()
}
this.toggleMinimize = function () {
if (this.container.hasClass('minimized')) {
this.container.find('.repeater-row-content').slideDown('fast')
} else {
this.container.find('.repeater-row-content').slideUp('fast')
}
// Store the previous state.
this.container.toggleClass('minimized')
this.header.find('.dashicons').toggleClass('dashicons-arrow-up').toggleClass('dashicons-arrow-down')
}
this.remove = function () {
this.container.slideUp(300, function () {
jQuery(this).detach()
})
this.container.trigger('row:remove', [this.rowIndex])
}
this.updateLabel = function () {
var rowLabelField,
rowLabel
if ('field' === this.label.type) {
rowLabelField = this.container.find('.repeater-field [data-field="' + this.label.field + '"]')
if ('function' === typeof rowLabelField.val) {
rowLabel = rowLabelField.val()
if ('' !== rowLabel) {
this.header.find('.repeater-row-label').text(rowLabel)
return
}
}
}
this.header.find('.repeater-row-label').text(this.label.value + ' ' + (this.rowIndex + 1))
}
this.updateLabel()
}
/**
* Repeater Field
*/
api.repeaterField = api.Fields.extend({
ready: function () {
'use strict'
var control = this
var limit, theNewRow
// The current value set in Control Class (set in to_json() function)
var settingValue = $.isArray(control.params.value) ? control.params.value : JSON.parse(
decodeURIComponent(control.params.value))
// The hidden field that keeps the data saved (though we never update it)
control.settingField = control.element.find('[data-customize-setting-link]').first()
// Set the field value for the first time, we'll fill it up later
control.setValue([], false)
// The DIV that holds all the rows
control.repeaterFieldsContainer = control.element.find('.repeater-fields').first()
// Set number of rows to 0
control.currentIndex = 0
// Save the rows objects
control.rows = []
// Default limit choice
limit = false
if (control.params.choices) {
if (undefined !== control.params.choices.limit) {
limit = (0 >= control.params.choices.limit) ? false : parseInt(control.params.choices.limit)
}
}
control.element.on('click', 'button.repeater-add', function (e) {
e.preventDefault()
if (!limit || control.currentIndex < limit) {
theNewRow = control.addRow()
theNewRow.toggleMinimize()
control.initColorPicker()
control.initDropdownPages(theNewRow)
control.initSlider(theNewRow)
control.element.trigger('change')
} else {
jQuery(control.selector + ' .limit').addClass('highlight')
}
})
control.element.on('click', '.repeater-row-remove', function (e) {
control.currentIndex--
if (!limit || control.currentIndex < limit) {
jQuery(control.selector + ' .limit').removeClass('highlight')
}
})
control.element.on('click keypress',
'.repeater-field-image .upload-button,.repeater-field-cropped_image .upload-button,.repeater-field-upload .upload-button,.repeater-field-upload_file .upload-button',
function (e) {
e.preventDefault()
control.$thisButton = jQuery(this)
control.openFrame(e)
})
control.element.on('click keypress',
'.repeater-field-image .remove-button,.repeater-field-cropped_image .remove-button', function (e) {
e.preventDefault()
control.$thisButton = jQuery(this)
control.removeImage(e)
})
control.element.on('click keypress', '.repeater-field-upload .remove-button', function (e) {
e.preventDefault()
control.$thisButton = jQuery(this)
control.removeFile(e)
})
/**
* Function that loads the Mustache template
*/
control.repeaterTemplate = function () {
return wp.template('form-field-repeater-content')
}
// When we load the control, the fields have not been filled up
// This is the first time that we create all the rows
if (settingValue.length) {
_.each(settingValue, function (subValue) {
theNewRow = control.addRow(subValue)
control.initColorPicker()
control.initDropdownPages(theNewRow, subValue)
control.initSlider(theNewRow, subValue)
})
}
// Once we have displayed the rows, we cleanup the values
control.setValue(settingValue, true, true)
control.repeaterFieldsContainer.sortable({
handle: '.repeater-row-header',
update: function (e, ui) {
control.sort()
},
})
},
/**
* Open the media modal.
*/
openFrame: function (event) {
'use strict'
if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
return
}
if (this.$thisButton.closest('.repeater-field').hasClass('repeater-field-cropped_image')) {
this.initCropperFrame()
} else {
this.initFrame()
}
this.frame.open()
},
initFrame: function () {
'use strict'
var libMediaType = this.getMimeType()
var library = null
if (libMediaType === 'image') {
library = wp.media.query({ type: libMediaType })
} else {
library = wp.media.query({ type: JSON.stringify(libMediaType.split(',')) })
}
this.frame = wp.media({
states: [
new wp.media.controller.Library({
library: library,
multiple: false,
date: false,
}),
],
})
// When a file is selected, run a callback.
this.frame.on('select', this.onSelect, this)
},
/**
* Create a media modal select frame, and store it so the instance can be reused when needed.
* This is mostly a copy/paste of Core api.CroppedImageControl in /wp-admin/js/customize-control.js
*/
initCropperFrame: function () {
'use strict'
// We get the field id from which this was called
var currentFieldId = this.$thisButton.siblings('input.hidden-field').attr('data-field'),
attrs = ['width', 'height', 'flex_width', 'flex_height'], // A list of attributes to look for
libMediaType = this.getMimeType()
// Make sure we got it
if ('string' === typeof currentFieldId && '' !== currentFieldId) {
// Make fields is defined and only do the hack for cropped_image
if ('object' === typeof this.params.fields[currentFieldId] &&
'cropped_image' === this.params.fields[currentFieldId].type) {
//Iterate over the list of attributes
attrs.forEach(function (el, index) {
// If the attribute exists in the field
if ('undefined' !== typeof this.params.fields[currentFieldId][el]) {
// Set the attribute in the main object
this.params[el] = this.params.fields[currentFieldId][el]
}
}.bind(this))
}
}
this.frame = wp.media({
button: {
text: 'Select and Crop',
close: false,
},
states: [
new wp.media.controller.Library({
library: wp.media.query({ type: libMediaType }),
multiple: false,
date: false,
suggestedWidth: this.params.width,
suggestedHeight: this.params.height,
}),
new wp.media.controller.CustomizeImageCropper({
imgSelectOptions: this.calculateImageSelectOptions,
control: this,
}),
],
})
this.frame.on('select', this.onSelectForCrop, this)
this.frame.on('cropped', this.onCropped, this)
this.frame.on('skippedcrop', this.onSkippedCrop, this)
},
onSelect: function () {
'use strict'
var attachment = this.frame.state().get('selection').first().toJSON()
if (this.$thisButton.closest('.repeater-field').hasClass('repeater-field-upload')) {
this.setFileInRepeaterField(attachment)
} else {
this.setImageInRepeaterField(attachment)
}
},
/**
* After an image is selected in the media modal, switch to the cropper
* state if the image isn't the right size.
*/
onSelectForCrop: function () {
'use strict'
var attachment = this.frame.state().get('selection').first().toJSON()
if (this.params.width === attachment.width && this.params.height === attachment.height &&
!this.params.flex_width && !this.params.flex_height) {
this.setImageInRepeaterField(attachment)
} else {
this.frame.setState('cropper')
}
},
/**
* After the image has been cropped, apply the cropped image data to the setting.
*
* @param {object} croppedImage Cropped attachment data.
*/
onCropped: function (croppedImage) {
'use strict'
this.setImageInRepeaterField(croppedImage)
},
/**
* Returns a set of options, computed from the attached image data and
* control-specific data, to be fed to the imgAreaSelect plugin in
* wp.media.view.Cropper.
*
* @param {wp.media.model.Attachment} attachment
* @param {wp.media.controller.Cropper} controller
* @returns {Object} Options
*/
calculateImageSelectOptions: function (attachment, controller) {
'use strict'
var control = controller.get('control'),
flexWidth = !!parseInt(control.params.flex_width, 10),
flexHeight = !!parseInt(control.params.flex_height, 10),
realWidth = attachment.get('width'),
realHeight = attachment.get('height'),
xInit = parseInt(control.params.width, 10),
yInit = parseInt(control.params.height, 10),
ratio = xInit / yInit,
xImg = realWidth,
yImg = realHeight,
x1,
y1,
imgSelectOptions
controller.set('canSkipCrop', !control.mustBeCropped(flexWidth, flexHeight, xInit, yInit, realWidth, realHeight))
if (xImg / yImg > ratio) {
yInit = yImg
xInit = yInit * ratio
} else {
xInit = xImg
yInit = xInit / ratio
}
x1 = (xImg - xInit) / 2
y1 = (yImg - yInit) / 2
imgSelectOptions = {
handles: true,
keys: true,
instance: true,
persistent: true,
imageWidth: realWidth,
imageHeight: realHeight,
x1: x1,
y1: y1,
x2: xInit + x1,
y2: yInit + y1,
}
if (false === flexHeight && false === flexWidth) {
imgSelectOptions.aspectRatio = xInit + ':' + yInit
}
if (false === flexHeight) {
imgSelectOptions.maxHeight = yInit
}
if (false === flexWidth) {
imgSelectOptions.maxWidth = xInit
}
return imgSelectOptions
},
/**
* Return whether the image must be cropped, based on required dimensions.
*
* @param {bool} flexW
* @param {bool} flexH
* @param {int} dstW
* @param {int} dstH
* @param {int} imgW
* @param {int} imgH
* @return {bool}
*/
mustBeCropped: function (flexW, flexH, dstW, dstH, imgW, imgH) {
'use strict'
if (true === flexW && true === flexH) {
return false
}
if (true === flexW && dstH === imgH) {
return false
}
if (true === flexH && dstW === imgW) {
return false
}
if (dstW === imgW && dstH === imgH) {
return false
}
if (imgW <= dstW) {
return false
}
return true
},
/**
* If cropping was skipped, apply the image data directly to the setting.
*/
onSkippedCrop: function () {
'use strict'
var attachment = this.frame.state().get('selection').first().toJSON()
this.setImageInRepeaterField(attachment)
},
/**
* Updates the setting and re-renders the control UI.
*
* @param {object} attachment
*/
setImageInRepeaterField: function (attachment) {
'use strict'
var $targetDiv = this.$thisButton.closest('.repeater-field-image,.repeater-field-cropped_image')
$targetDiv.find('.jeg-image-attachment').html('').hide().slideDown('slow')
$targetDiv.find('.hidden-field').val(attachment.id)
this.$thisButton.text(this.$thisButton.data('alt-label'))
$targetDiv.find('.remove-button').show()
//This will activate the save button
$targetDiv.find('input, textarea, select').trigger('change')
this.frame.close()
},
/**
* Updates the setting and re-renders the control UI.
*
* @param {object} attachment
*/
setFileInRepeaterField: function (attachment) {
'use strict'
var $targetDiv = this.$thisButton.closest('.repeater-field-upload')
$targetDiv.find('.jeg-file-attachment').
html(' ' + attachment.filename +
'').
hide().
slideDown('slow')
$targetDiv.find('.hidden-field').val(attachment.id)
this.$thisButton.text(this.$thisButton.data('alt-label'))
$targetDiv.find('.upload-button').show()
$targetDiv.find('.remove-button').show()
//This will activate the save button
$targetDiv.find('input, textarea, select').trigger('change')
this.frame.close()
},
getMimeType: function () {
'use strict'
// We get the field id from which this was called
var currentFieldId = this.$thisButton.siblings('input.hidden-field').attr('data-field'),
attrs = ['mime_type'] // A list of attributes to look for
// Make sure we got it
if ('string' === typeof currentFieldId && '' !== currentFieldId) {
// Make fields is defined and only do the hack for cropped_image
if ('object' === typeof this.params.fields[currentFieldId] &&
('upload' === this.params.fields[currentFieldId].type || 'upload_file' === this.params.fields[currentFieldId].type)) {
// If the attribute exists in the field
if ('undefined' !== typeof this.params.fields[currentFieldId].mime_type) {
// Set the attribute in the main object
return this.params.fields[currentFieldId].mime_type
}
}
}
return 'image'
},
removeImage: function (event) {
'use strict'
var $targetDiv,
$uploadButton
if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
return
}
$targetDiv = this.$thisButton.closest(
'.repeater-field-image,.repeater-field-cropped_image,.repeater-field-upload')
$uploadButton = $targetDiv.find('.upload-button')
$targetDiv.find('.jeg-image-attachment').slideUp('fast', function () {
jQuery(this).show().html(jQuery(this).data('placeholder'))
})
$targetDiv.find('.hidden-field').val('')
$uploadButton.text($uploadButton.data('label'))
this.$thisButton.hide()
$targetDiv.find('input, textarea, select').trigger('change')
},
removeFile: function (event) {
'use strict'
var $targetDiv,
$uploadButton
if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
return
}
$targetDiv = this.$thisButton.closest('.repeater-field-upload')
$uploadButton = $targetDiv.find('.upload-button')
$targetDiv.find('.jeg-file-attachment').slideUp('fast', function () {
jQuery(this).show().html(jQuery(this).data('placeholder'))
})
$targetDiv.find('.hidden-field').val('')
$uploadButton.text($uploadButton.data('label'))
this.$thisButton.hide()
$targetDiv.find('input, textarea, select').trigger('change')
},
/**
* Get the current value of the setting
*
* @return Object
*/
getValue: function () {
'use strict'
// need to load the setting from JSON for first load
if (JSON.parse(decodeURIComponent($(this.element).find('.data-setting').attr('value'))).length <= 0) {
// The setting is saved in JSON
return $.isArray(this.params.value) ? this.params.value : JSON.parse(decodeURIComponent(this.params.value))
}
return $.isArray($(this.element).find('.data-setting').attr('value')) ? $(this.element).
find('.data-setting').
attr('value') : JSON.parse(decodeURIComponent($(this.element).find('.data-setting').attr('value')))
},
/**
* Set a new value for the setting
*
* @param newValue Object
* @param refresh If we want to refresh the previewer or not
* @param filtering
*/
setValue: function (newValue, refresh, filtering) {
'use strict'
// We need to filter the values after the first load to remove data requrired for diplay but that we don't want to save in DB
// need boolean filter to avoid null data on array after remove data
var filteredValue = newValue.filter(Boolean),
filter = []
if (filtering) {
jQuery.each(this.params.fields, function (index, value) {
if ('image' === value.type || 'cropped_image' === value.type || 'upload' === value.type) {
filter.push(index)
}
})
jQuery.each(newValue, function (index, value) {
jQuery.each(filter, function (ind, field) {
if ('undefined' !== typeof value[field] && 'undefined' !== typeof value[field].id) {
filteredValue[index][field] = value[field].id
}
})
})
}
this.value.set(filteredValue)
$(this.element).find('.data-setting').attr('value', encodeURI(JSON.stringify(filteredValue)))
},
/**
* Add a new row to repeater settings based on the structure.
*
* @param data (Optional) Object of field => value pairs (undefined if you want to get the default values)
*/
addRow: function (data) {
'use strict'
var control = this,
template = control.repeaterTemplate(), // The template for the new row (defined on render_content() ).
settingValue = this.getValue(), // Get the current setting value.
newRowSetting = {}, // Saves the new setting data.
templateData, // Data to pass to the template
newRow,
i
if (template) {
// The control structure is going to define the new fields
// We need to clone control.params.fields. Assigning it
// ould result in a reference assignment.
templateData = jQuery.extend(true, {}, control.params.fields)
// But if we have passed data, we'll use the data values instead
if (data) {
for (i in data) {
if (data.hasOwnProperty(i) && templateData.hasOwnProperty(i)) {
templateData[i]['default'] = data[i]
}
}
}
templateData.index = this.currentIndex
// Append the template content
template = template(templateData)
// Create a new row object and append the element
newRow = new RepeaterRow(
control.currentIndex,
jQuery(template).appendTo(control.repeaterFieldsContainer),
control.params.row_label
)
newRow.container.on('row:remove', function (e, rowIndex) {
control.deleteRow(rowIndex)
})
newRow.container.on('row:update', function (e, rowIndex, fieldName, element) {
control.updateField.call(control, e, rowIndex, fieldName, element)
newRow.updateLabel()
})
// Add the row to rows collection
this.rows[this.currentIndex] = newRow
for (i in templateData) {
if (templateData.hasOwnProperty(i)) {
newRowSetting[i] = templateData[i]['default']
}
}
settingValue[this.currentIndex] = newRowSetting
this.setValue(settingValue, true)
this.currentIndex++
return newRow
}
},
sort: function () {
'use strict'
var control = this,
$rows = this.repeaterFieldsContainer.find('.repeater-row'),
newOrder = [],
settings = control.getValue(),
newRows = [],
newSettings = []
$rows.each(function (i, element) {
newOrder.push(jQuery(element).data('row'))
})
jQuery.each(newOrder, function (newPosition, oldPosition) {
newRows[newPosition] = control.rows[oldPosition]
newRows[newPosition].setRowIndex(newPosition)
newSettings[newPosition] = settings[oldPosition]
})
control.rows = newRows
control.setValue(newSettings)
},
/**
* Delete a row in the repeater setting
*
* @param index Position of the row in the complete Setting Array
*/
deleteRow: function (index) {
'use strict'
var currentSettings = this.getValue(),
row,
i,
prop
if (currentSettings[index]) {
// Find the row
row = this.rows[index]
if (row) {
// The row exists, let's delete it
// Remove the row settings
delete currentSettings[index]
// Remove the row from the rows collection
delete this.rows[index]
// Update the new setting values
this.setValue(currentSettings, true)
}
}
// Remap the row numbers
i = 1
for (prop in this.rows) {
if (this.rows.hasOwnProperty(prop) && this.rows[prop]) {
this.rows[prop].updateLabel()
i++
}
}
},
/**
* Update a single field inside a row.
* Triggered when a field has changed
*
* @param e
* @param rowIndex
* @param fieldId
* @param element
*/
updateField: function (e, rowIndex, fieldId, element) {
'use strict'
var type,
row,
currentSettings
if (!this.rows[rowIndex]) {
return
}
if (!this.params.fields[fieldId]) {
return
}
type = this.params.fields[fieldId].type
row = this.rows[rowIndex]
currentSettings = this.getValue()
element = jQuery(element)
if (undefined === typeof currentSettings[row.rowIndex][fieldId]) {
return
}
if ('checkbox' === type) {
currentSettings[row.rowIndex][fieldId] = element.is(':checked')
} else {
// Update the settings
currentSettings[row.rowIndex][fieldId] = element.val()
}
this.setValue(currentSettings, true)
},
/**
* Init the color picker on color fields
* Called after AddRow
*
*/
initColorPicker: function () {
'use strict'
var control = this,
colorPicker = control.element.find('.color-picker-hex'),
options = {},
fieldId = colorPicker.data('field')
// We check if the color palette parameter is defined.
if ('undefined' !== typeof fieldId && 'undefined' !== typeof control.params.fields[fieldId] &&
'undefined' !== typeof control.params.fields[fieldId].palettes &&
'object' === typeof control.params.fields[fieldId].palettes) {
options.palettes = control.params.fields[fieldId].palettes
}
// When the color picker value is changed we update the value of the field
options.change = function (event, ui) {
var currentPicker = jQuery(event.target),
row = currentPicker.closest('.repeater-row'),
rowIndex = row.data('row'),
currentSettings = control.getValue()
currentSettings[rowIndex][currentPicker.data('field')] = ui.color.toString()
control.setValue(currentSettings, true)
}
// Init the color picker
if (0 !== colorPicker.length) {
colorPicker.wpColorPicker(options)
}
},
/**
* Init the dropdown-pages field with selectize
* Called after AddRow
*
* @param {object} theNewRow the row that was added to the repeater
* @param {object} data the data for the row if we're initializing a pre-existing row
*
*/
initDropdownPages: function (theNewRow, data) {
'use strict'
var control = this,
dropdown = theNewRow.container.find('.repeater-dropdown-pages select'),
$select,
selectize,
dataField
if (0 === dropdown.length) {
return
}
$select = jQuery(dropdown).selectize()
selectize = $select[0].selectize
dataField = dropdown.data('field')
if (data) {
selectize.setValue(data[dataField])
}
this.element.on('change', '.repeater-dropdown-pages select', function (event) {
var currentDropdown = jQuery(event.target),
row = currentDropdown.closest('.repeater-row'),
rowIndex = row.data('row'),
currentSettings = control.getValue()
currentSettings[rowIndex][currentDropdown.data('field')] = jQuery(this).val()
control.setValue(currentSettings)
})
},
initSlider: function (theNewRow, subValue) {
'use strict'
var control = this,
slider = theNewRow.container.find('.repeater-slider-wrapper .jeg-number-range'),
value
if (subValue !== undefined) {
if (jQuery.isEmptyObject(subValue['value'])) {
value = slider.data('reset_value')
}
else {
value = subValue['value']
}
}
else {
value = slider.data('reset_value')
}
slider.attr('value', value)
slider.closest('div').find('.jeg_range_value .value').text(value)
slider.on('mousedown', function () {
$(this).mousemove(function () {
var value = $(this).attr('value'),
currentSlider = jQuery(event.target),
row = currentSlider.closest('.repeater-row'),
rowIndex = row.data('row'),
currentSettings = control.getValue()
$(this).closest('div').find('.jeg_range_value .value').text(value)
$(this).attr('value', value)
currentSettings[rowIndex]['value'] = jQuery(this).val()
control.setValue(currentSettings)
})
})
theNewRow.container.find('.jeg-slider-reset').on('click', function () {
var thisInput = slider
var inputDefault = thisInput.data('reset_value')
thisInput.attr('value', inputDefault)
thisInput.change()
$(this).closest('div.repeater-slider-wrapper').find('.jeg_range_value .value').text(inputDefault)
})
},
})
//======================================//
api.standartField = api.Fields.extend({})
api.headingField = api.Fields.extend({})
api.alertField = api.Fields.extend({})
//======================================//
api.checkblockField = api.Fields.extend({})
//======================================//
api.fieldConstructor = {
text: api.textField,
password: api.textField,
color: api.colorField,
select: api.selectField,
checkbox: api.checkboxField,
radioimage: api.radioImageField,
slider: api.sliderField,
iconpicker: api.iconPickerField,
standart: api.standartField,
heading: api.headingField,
alert: api.alertField,
textarea: api.textareaField,
number: api.numberField,
image: api.imageField,
repeater: api.repeaterField,
upload: api.uploadField,
// Need to add this form
// checkblock: api.checkblockField,
}
})(jQuery, wp.customize)