assets/js/private/llms-metaboxes.js
/**
* LifterLMS Admin Panel Metabox Functions
*
* @since 3.0.0
* @version 7.1.1
*/
( function( $ ) {
/**
* jQuery plugin to allow "collapsible" sections
*
* @return jQuery object
* @since 3.0.0
* @version 3.29.0
*/
$.fn.llmsCollapsible = function() {
var $group = this;
this.on( 'click', '.llms-collapsible-header', function() {
var $parent = $( this ).closest( '.llms-collapsible' ),
$siblings = $parent.siblings( '.llms-collapsible' );
$parent.toggleClass( 'opened' ).trigger( 'llms-collapsible-toggled' );
$parent.find( '.llms-collapsible-body' ).slideToggle( 400 );
$siblings.each( function() {
$( this ).removeClass( 'opened' );
$( this ).find( '.llms-collapsible-body' ).slideUp( 400 );
} );
} );
return this;
};
window.llms = window.llms || {};
var Metaboxes = function() {
/**
* load all partials
*/
// = include ../partials/*.js
/**
* Initialize
*
* @since 3.0.0
* @since 3.13.0 Unknown.
* @since 4.19.0 Add `this.bind_mce_fixes()`.
* @since 5.3.0 Bind editables when editable buttons are present in addition to anchors.
*
* @return void
*/
this.init = function() {
var self = this;
$( '.llms-select2-post' ).each( function() {
self.post_select( $( this ) );
} );
$( '.llms-collapsible-group' ).llmsCollapsible();
this.bind_tabs();
this.bind_mce_fixes();
// bind everything better and less repetitively...
var bindings = [
{
selector: $( '.llms-datepicker' ),
func: 'bind_datepickers',
},
{
selector: $( '.llms-select2' ),
func: function( $selector ) {
$selector.llmsSelect2( {
width: '100%',
} );
},
},
{
selector: $( '.llms-select2-student' ),
func: function( $selector ) {
$selector.llmsStudentsSelect2();
}
},
{
selector: $( 'input[type="checkbox"][data-controls]' ),
func: 'bind_cb_controllers',
},
{
selector: $( '[data-is-controller]' ),
func: 'bind_controllers',
},
{
selector: $( '.llms-table' ),
func: 'bind_tables',
},
{
selector: $( '.llms-merge-code-wrapper' ),
func: 'bind_merge_code_buttons',
},
{
selector: $( 'a.llms-editable, button.llms-editable' ),
func: 'bind_editables',
},
];
// bind all the bindables but don't bind things in repeaters
$.each( bindings, function( index, obj ) {
if ( obj.selector.length ) {
// reduce the selector to exclude items in a repeater
var reduced = obj.selector.filter( function() {
return ( 0 === $( this ).closest( '.llms-repeater-model' ).length );
} );
// bind by string
if ( 'string' === typeof obj.func ) {
self[ obj.func ]( reduced );
}
// bind by an anonymous function
else if ( 'function' === typeof obj.func ) {
obj.func( reduced );
}
}
} );
// if a post type is set & a bind exists for it, bind it
if ( window.llms.post.post_type ) {
var func = 'bind_' + window.llms.post.post_type;
if ( 'function' === typeof this[func] ) {
this[func]();
}
}
};
/**
* Bind checkboxes that control the display of other elements
*
* @param obj $controllers jQuery selector for checkboxes to be bound as checkbox controllers
* @return void
* @since 3.0.0
* @version 3.11.0
*/
this.bind_cb_controllers = function( $controllers ) {
$controllers = $controllers || $( 'input[type="checkbox"][data-controls]' );
$controllers.each( function() {
var $cb = $( this ),
$controlled = $( $cb.attr( 'data-controls' ) ).closest( '.llms-mb-list' );
$cb.on( 'change', function() {
if ( $( this ).is( ':checked' ) ) {
$controlled.slideDown( 200 );
} else {
$controlled.slideUp( 200 );
}
} );
$cb.trigger( 'change' );
} );
};
/**
* Bind elements that control the display of other elements
*
* @param obj $controllers jQuery selector for elements to be bound as checkbox controllers
* @return void
* @since 3.0.0
* @version 3.11.0
*/
this.bind_controllers = function( $controllers ) {
$controllers = $controllers || $( '[data-is-controller]' );
$controllers.each( function() {
var $el = $( this ),
$controlled = $( '[data-controller="#' + $el.attr( 'id' ) + '"]' ),
val;
$el.on( 'change', function() {
if ( 'checkbox' === $el.attr( 'type' ) ) {
val = $el.is( ':checked' ) ? $el.val() : 'false';
} else {
val = $el.val();
}
$controlled.each( function() {
var possible = $( this ).attr( 'data-controller-value' ),
vals = [];
if ( -1 !== possible.indexOf( ',' ) ) {
vals = possible.split( ',' );
} else {
vals.push( possible );
}
if ( -1 !== vals.indexOf( val ) ) {
$( this ).slideDown( 200 );
} else {
$( this ).slideUp( 200 );
}
} );
} );
$el.trigger( 'change' );
} );
};
/**
* Bind a single datepicker element
*
* @param obj $el jQuery selector for the input to bind the datepicker to
* @return void
* @since 3.0.0
* @version 3.10.0
*/
this.bind_datepicker = function( $el ) {
var format = $el.attr( 'data-format' ) || 'mm/dd/yy',
maxDate = $el.attr( 'data-max-date' ) || null,
minDate = $el.attr( 'data-min-date' ) || null;
$el.datepicker( {
dateFormat: format,
maxDate: maxDate,
minDate: minDate,
} );
}
/**
* Bind all LifterLMS datepickers
*
* @param obj $datepickers jQuery selector for the elements to bind
* @return void
* @since 3.0.0
* @version 3.11.0
*/
this.bind_datepickers = function( $datepickers ) {
var self = this;
$datepickers = $datepickers || $( '.llms-datepicker' );
$datepickers.each( function() {
self.bind_datepicker( $( this ) );
} );
};
/**
* Bind llms-editable metabox fields and related dom interactions
*
* @since 3.10.0
* @since 3.28.0 Unknown.
* @since 5.3.0 Bind editables when editable buttons are present in addition to anchors.
*
* @return void
*/
this.bind_editables = function() {
var self = this;
function make_editable( $field ) {
var $label = $field.find( 'label' ).clone(),
name = $field.attr( 'data-llms-editable' ),
type = $field.attr( 'data-llms-editable-type' ),
required = $field.attr( 'data-llms-editable-required' ) || 'no',
val = $field.attr( 'data-llms-editable-value' ),
$input;
required = ( 'yes' === required ) ? ' required="required"' : '';
if ( 'select' === type ) {
var options = JSON.parse( $field.attr( 'data-llms-editable-options' ) ),
selected;
$input = $( '<select name="' + name + '"' + required + ' />' );
for ( var key in options ) {
selected = val === key ? ' selected="selected"' : '';
$input.append( '<option value="' + key + '"' + selected + '>' + options[ key ] + '</option>' );
}
} else if ( 'datetime' === type ) {
$input = $( '<div class="llms-datetime-field" />' );
val = JSON.parse( val );
var format = $field.attr( 'data-llms-editable-date-format' ) || '',
min_date = $field.attr( 'data-llms-editable-date-min' ) || '',
max_date = $field.attr( 'data-llms-editable-date-max' ) || '';
$picker = $( '<input class="llms-date-input llms-datepicker" data-format="' + format + '" data-max-date="' + max_date + '" data-min-date="' + min_date + '" name="' + name + '[date]" type="text" value="' + val.date + '">' );
self.bind_datepicker( $picker );
$input.append( $picker );
$input.append( '<em>@</em>' );
$input.append( '<input class="llms-time-input" max="23" min="0" name="' + name + '[hour]" type="number" value="' + val.hour + '">' );
$input.append( '<em>:</em>' );
$input.append( '<input class="llms-time-input" max="59" min="0" name="' + name + '[minute]" type="number" value="' + val.minute + '">' );
} else {
$input = $( '<input name="' + name + '" type="' + type + '" value="' + val + '"' + required + '>' );
}
$field.empty().append( $label ).append( $input );
if ( 'select' === type ) {
setTimeout( function() {
$input.trigger( 'change' );
}, 100 );
}
};
$( 'a.llms-editable, button.llms-editable' ).on( 'click', function( e ) {
e.preventDefault();
var $btn = $( this ),
$fields;
if ( $btn.attr( 'data-fields' ) ) {
$fields = $( $btn.attr( 'data-fields' ) );
} else {
$fields = $btn.closest( '.llms-metabox-section' ).find( '[data-llms-editable]' );
}
$btn.remove();
$fields.each( function() {
make_editable( $( this ) );
} );
} );
};
/**
* Bind Engagement post type JS
*
* @return void
* @since 3.1.0
* @version 3.1.0
*/
this.bind_llms_engagement = function() {
var self = this;
// when the engagement type changes we need to do some things to the UI
$( '#_llms_engagement_type' ).on( 'change', function() {
$( '#_llms_engagement' ).trigger( 'llms-engagement-type-change', $( this ).val() );
} );
// custom trigger when called when the engagement type changes
$( '#_llms_engagement' ).on( 'llms-engagement-type-change', function( e, engagement_type ) {
var $select = $( this );
switch ( engagement_type ) {
/**
* core engagements related to a CPT
*/
case 'achievement':
case 'certificate':
case 'email':
var cpt = 'llms_' + engagement_type;
$select.val( null ).attr( 'data-post-type', cpt ).trigger( 'change' );
self.post_select( $select );
break;
/**
* Allow other plugins and developers to hook into the engagement type change action
*/
default:
$select.trigger( 'llms-engagement-type-change-external', engagement_type );
}
} );
};
/**
* Actions for memberships
*
* @since 3.0.0
* @since 3.30.0 Made autoenroll table sortable, added AJAX save for adding new courses.
* @version 3.30.0
*
* @return void
*/
this.bind_llms_membership = function() {
var $table = $( '.llms-mb-list._llms_content_table' );
/**
* Hide/Show empty message header row depending on the number of rows in the tbody
*
* @since 3.30.0
* @version 3.30.0
*
* @return void
*/
function toggle_header_row() {
var $rows = $table.find( 'tbody tr' );
if ( 1 === $rows.length ) {
$rows.first().show();
} else {
$rows.first().hide();
}
}
/**
* Retrieve an array of course IDs in the table.
*
* @since 3.30.0
* @version 3.30.0
*
* @return array
*/
function get_course_ids() {
var courses = [];
$table.find( 'tbody tr a[href="#llms-course-remove"]' ).each( function() {
courses.push( $( this ).attr( 'data-id' ) );
} );
return courses;
}
// On init, toggle the header row visibility.
toggle_header_row();
// remove auto-enroll course
$table.on( 'click', 'a[href="#llms-course-remove"]', function( e ) {
e.preventDefault();
var $el = $( this ),
$row = $el.closest( 'tr' ),
$container = $el.closest( '.llms-mb-list' );
LLMS.Spinner.start( $container );
window.LLMS.Ajax.call( {
data: {
action: 'membership_remove_auto_enroll_course',
course_id: $el.attr( 'data-id' ),
},
beforeSend: function() {
$container.find( 'p.error' ).remove();
},
success: function( r ) {
if ( r.success ) {
$row.fadeOut( 200 );
setTimeout( function() {
$row.remove();
toggle_header_row();
}, 400 );
} else {
$container.prepend( '<p class="error">' + r.message + '</p>' );
}
LLMS.Spinner.stop( $container );
},
} );
} );
// bulk enroll all members into a course
$table.on( 'click', 'a[href="#llms-course-bulk-enroll"]', function( e ) {
e.preventDefault();
var $el = $( this ),
$row = $el.closest( 'tr' ),
$container = $el.closest( '.llms-mb-list' );
if ( ! window.confirm( LLMS.l10n.translate( 'Click okay to enroll all active members into the selected course. Enrollment will take place in the background and you may leave your site after confirmation. This action cannot be undone!' ) ) ) {
return;
}
LLMS.Spinner.start( $container );
window.LLMS.Ajax.call( {
data: {
action: 'bulk_enroll_membership_into_course',
course_id: $el.attr( 'data-id' ),
},
beforeSend: function() {
$container.find( 'p.error' ).remove();
},
success: function( r ) {
if ( r.success ) {
$el.replaceWith( '<strong style="float:right;">' + r.data.message + ' </strong>' );
} else {
$container.prepend( '<p class="error">' + r.message + '</p>' );
}
LLMS.Spinner.stop( $container );
},
} );
} );
// Add an item to the autoenroll table on select.
$( '#_llms_auto_enroll' ).on( 'change', function() {
var id = $( this ).val(),
title = $( this ).find( 'option[value="' + $( this ).val() + '"]' ).text();
// If there's no ID
if ( ! id ) {
return;
// Prevent Dupes.
} else if ( -1 !== get_course_ids().indexOf( id ) ) {
alert( LLMS.l10n.replace( '"%s" is already in the course list.', { '%s': title } ) )
// reset the select field.
$( this ).val( '' ).trigger( 'change' );
return;
}
var $table = $( '.llms-mb-list._llms_content_table' );
$tr = $( '<tr />' );
$tr.append( '<td><span class="dashicons dashicons-menu llms-drag-handle ui-sortable-handle"></span></td>' );
$tr.append( '<td><a href="' + window.llms.admin_url + 'post.php?action=edit&post=' + id + '">' + title + '</a></td>' );
$tr.append( '<td><a class="llms-button-danger small" data-id="' + id + '" href="#llms-course-remove" style="float:right;">' + LLMS.l10n.translate( 'Remove course' ) + '</a><a class="llms-button-secondary small" data-id="' + id + '" href="#llms-course-bulk-enroll" style="float:right;margin-right:5px;">' + LLMS.l10n.translate( 'Enroll All Members' ) + '</a></td>' );
// append the element to the table.
$table.find( 'table tbody' ).append( $tr );
// reset the select field.
$( this ).val( '' ).trigger( 'change' );
// Show the header row.
toggle_header_row();
// trigger a save event.
$table.trigger( 'llms-save-autoenroll-courses' );
} );
// Make autoenrollment table sortable.
$table.find( 'table tbody' ).sortable( {
handle: '.llms-drag-handle',
// Save order on stop.
stop: function( event, ui ) {
ui.item.closest( '.llms-mb-list' ).trigger( 'llms-save-autoenroll-courses' );
},
} );
// Save courses & course order.
$table.on( 'llms-save-autoenroll-courses', function() {
var $container = $( this );
LLMS.Spinner.start( $container );
window.LLMS.Ajax.call( {
data: {
action: 'llms_save_membership_autoenroll_courses',
courses: get_course_ids(),
},
error: function( jqxhr, code, error_msg ) {
alert( error_msg );
},
complete: function() {
LLMS.Spinner.stop( $container );
},
} );
} );
};
/**
* Actions for ORDERS
*
* @return void
* @since 3.0.0
* @version 3.28.0
*/
this.bind_llms_order = function() {
$( 'button[name="llms-refund-toggle"]' ).on( 'click', function() {
var $btn = $( this ),
$row = $btn.closest( 'tr' ),
txn_id = $row.attr( 'data-transaction-id' ),
refundable_amount = $btn.attr( 'data-refundable' ),
gateway_supports = ( '1' === $btn.attr( 'data-gateway-supports' ) ) ? true : false,
gateway_title = $btn.attr( 'data-gateway' ),
$new_row = $( '#llms-txn-refund-model .llms-txn-refund-form' ).clone(),
$gateway_btn = $new_row.find( '.gateway-btn' );
// configure and add the form
if ( 'remove' !== $btn.attr( 'data-action' ) ) {
$btn.text( LLMS.l10n.translate( 'Cancel' ) );
$btn.attr( 'data-action', 'remove' );
$new_row.find( 'input' ).removeAttr( 'disabled' );
$new_row.find( 'input[name="llms_refund_amount"]' ).attr( 'max', refundable_amount );
$new_row.find( 'input[name="llms_refund_txn_id"]' ).val( txn_id );
if ( gateway_supports ) {
$gateway_btn.find( '.llms-gateway-title' ).text( gateway_title );
$gateway_btn.show();
}
$row.after( $new_row );
} else {
$btn.text( LLMS.l10n.translate( 'Refund' ) );
$btn.attr( 'data-action', '' );
$row.next( 'tr' ).remove();
}
} );
$( 'button[name="llms-manual-txn-toggle"]' ).on( 'click', function() {
var $btn = $( this ),
$row = $btn.closest( 'tr' ),
$new_row = $( '#llms-manual-txn-model .llms-manual-txn-form' ).clone();
// configure and add the form
if ( 'remove' !== $btn.attr( 'data-action' ) ) {
$btn.text( LLMS.l10n.translate( 'Cancel' ) );
$btn.attr( 'data-action', 'remove' );
$new_row.find( 'input' ).removeAttr( 'disabled' );
$row.after( $new_row );
} else {
$btn.text( LLMS.l10n.translate( 'Record a Manual Payment' ) );
$btn.attr( 'data-action', '' );
$row.next( 'tr' ).remove();
}
} );
// cache the original value when focusing on a payment gateway select
// used below so the original field related data can be restored when switching back to the originally selected gateway
$( '.llms-metabox' ).one( 'focus', '.llms-metabox-field[data-llms-editable="payment_gateway"] select', function() {
if ( ! $( this ).attr( 'data-original-value' ) ) {
$( this ).attr( 'data-original-value', $( this ).val() );
}
} );
// when selecting a new payment gateway get field data and update the dom to only display the fields
// supported/needed by the newly selected gateway
$( '.llms-metabox' ).on( 'change', '.llms-metabox-field[data-llms-editable="payment_gateway"] select', function() {
var $select = $( this ),
gateway = $select.val(),
data = JSON.parse( $select.closest( '.llms-metabox-field' ).attr( 'data-gateway-fields' ) ),
gateway_data = data[ gateway ];
for ( var field in gateway_data ) {
var $field = $( 'input[name="' + gateway_data[ field ].name + '"]' ),
$wrap = $field.closest( '.llms-metabox-field' );
// if the field is enabled show it the field and, if we're switching back to the originally selected
// gateway, reload the value from the dom
if ( gateway_data[ field ].enabled ) {
$wrap.show();
$field.attr( 'required', 'required' );
$field.removeAttr( 'disabled' );
if ( gateway === $select.attr( 'data-original-value' ) ) {
$field.val( $wrap.attr( 'data-llms-editable-value' ) );
}
// otherwise hide the field
// this will ensure it gets updated in the database
} else {
// always clear the value when switching
// ensures that outdated data is removed from the DB
$field.attr( 'value', '' );
$field.removeAttr( 'required' );
// $field.attr( 'disabled', 'disabled' );
$wrap.hide();
}
}
} );
};
/**
* Re-initializes TinyMCE Editors found within metaboxes
*
* @since 4.19.0
* @since 4.21.2 Improve early return dependency check.
* @since 7.0.1 Add `undefined` condition on early return check.
*
* @link https://github.com/gocodebox/lifterlms/issues/1553
* @link https://github.com/gocodebox/lifterlms/pull/1618
* @link https://github.com/gocodebox/lifterlms/issues/2298
*
* @return {void}
*/
this.bind_mce_fixes = function() {
// We need `wp.data` to proceed.
if ( undefined === wp.data || [ null, undefined ].includes( wp.data.select( 'core/edit-post' ) ) ) {
return;
}
LLMS.wait_for(
function() {
return undefined !== wp.data.select( 'core/edit-post' ).getMetaBoxesPerLocation( 'normal' );
},
function() {
var shouldRun = false;
find = [ 'lifterlms-product', 'lifterlms-membership', 'lifterlms-course-options' ];
metaboxes = wp.data.select( 'core/edit-post' ).getMetaBoxesPerLocation( 'normal' );
// Determine if we should run the fixer.
for ( var key in metaboxes ) {
if ( -1 !== find.indexOf( metaboxes[ key ].id ) ) {
shouldRun = true;
break;
}
}
if ( ! shouldRun ) {
return;
}
// Fix them.
var toFix = {};
/**
* Determines if the TinyMCE instance should be fixed.
*
* @since 4.19.0
*
* @param {string} key Editor Key. This is the HTML id attribute of the textarea powering the editor instance.
* @return {Boolean} Returns `true` if the editor should be fixed.
*/
function llmsShouldFixTinyMCEEditor( key ) {
return ( 'excerpt' === key || -1 !== key.indexOf( 'llms' ) || -1 !== key.indexOf( 'lifterlms' ) )
};
// Loop through all the loaded editors.
for ( var key in tinyMCE.EditorManager.editors ) {
// Mark LifterLMS editors to be fixed & de-init the editor.
if ( llmsShouldFixTinyMCEEditor( key ) ) {
toFix[ key ] = tinyMCE.EditorManager.get( key );
tinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, key );
}
}
// If we remove and re-init immediately it doesn't work, so we'll wait a bit and then re-init them all.
setTimeout( function() {
for ( var key in toFix ) {
tinyMCE.EditorManager.init( toFix[ key ].settings || tinyMCE.EditorManager.settings );
}
}, 500 );
}
);
};
/**
* Binds custom llms merge code buttons
*
* @return void
* @since 3.1.0
* @version 3.9.2
*/
this.bind_merge_code_buttons = function( $wrappers ) {
$wrappers = $wrappers || $( '.llms-merge-code-wrapper' );
$wrappers.find( '.llms-merge-code-button' ).on( 'click', function() {
$( this ).next( '.llms-merge-codes' ).toggleClass( 'active' );
} );
$wrappers.find( '.llms-merge-codes li' ).on( 'click', function() {
var $el = $( this ),
$parent = $el.closest( '.llms-merge-codes' ),
target = $parent.attr( 'data-target' ),
code = $el.attr( 'data-code' );
// dealing with a tinymce instance
if ( -1 === target.indexOf( '#' ) ) {
var editor = window.tinymce.editors[ target ];
if ( editor ) {
editor.insertContent( code );
} // fallback in case we can't access the editor directly
else {
alert( LLMS.l10n.translate( 'Copy this code and paste it into the desired area' ) + ': ' + code );
}
}
// dealing with a DOM id
else {
$( target ).val( $( target ).val() + code );
}
$parent.removeClass( 'active' );
} );
};
/**
* Bind metabox tabs
*
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.bind_tabs = function() {
$( '.llms-nav-tab-wrapper .tabs li' ).on( 'click', function() {
var $btn = $( this ),
$metabox = $btn.closest( '.llms-mb-container' ),
tab_id = $btn.attr( 'data-tab' );
$btn.siblings().removeClass( 'llms-active' );
$metabox.find( '.tab-content' ).removeClass( 'llms-active' );
$btn.addClass( 'llms-active' );
$( '#' + tab_id ).addClass( 'llms-active' );
} );
};
/**
* Enable WP Post Table searches for applicable select2 boxes
*
* @since 3.0.0
* @since 3.21.0 Unknown.
* @since 6.0.0 Show element at 100% width if not displaying a view button.
* @since 7.1.1 Fixed `home_url` for view button.
*
* @return void
*/
this.post_select = function( $el ) {
var multi = 'multiple' === $el.attr( 'multiple' ),
noViewBtn = $el.attr( 'data-no-view-button' );
$el.llmsPostsSelect2( {
width: multi || noViewBtn ? '100%' : '65%',
} );
if ( multi || noViewBtn ) {
return;
}
// add a "View" button to see what the selected page looks like
var msg = LLMS.l10n.translate( 'View' ),
$btn = $( '<a class="llms-button-secondary small" style="margin-left:5px;" target="_blank" href="#">' + msg + ' <i class="fa fa-external-link" aria-hidden="true"></i></a>' );
$el.next( '.select2' ).after( $btn );
$el.on( 'change', function() {
var id = $( this ).val();
if ( id ) {
$btn.attr( 'href', window.llms.home_url + '/?p=' + id ).show();
} else {
$btn.hide();
}
} ).trigger( 'change' );
};
/**
* Bind dom events for .llms-tables
*
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.bind_tables = function() {
$( '.llms-table button[name="llms-expand-table"]' ).on( 'click', function() {
var $btn = $( this ),
$table = $btn.closest( '.llms-table' )
// switch the text on the button if alt text is found
if ( $btn.attr( 'data-text' ) ) {
var text = $btn.text();
$btn.text( $btn.attr( 'data-text' ) );
$btn.attr( 'data-text', text );
}
// switch classes on all expandable elements
$table.find( '.expandable' ).each( function() {
if ( $( this ).hasClass( 'closed' ) ) {
$( this ).addClass( 'opened' ).removeClass( 'closed' );
} else {
$( this ).addClass( 'closed' ).removeClass( 'opened' );
}
} );
} );
};
// go
this.init();
};
// initialize the object
window.llms.metaboxes = new Metaboxes();
} )( jQuery );