installed plugin Easy Digital Downloads version 3.1.0.3

This commit is contained in:
2022-11-27 15:03:07 +00:00
committed by Gitium
parent 555673545b
commit c5dce2cec6
1200 changed files with 238970 additions and 0 deletions

View File

@ -0,0 +1,9 @@
/* global jQuery */
jQuery( document ).ready( function( $ ) {
$( '.edd-advanced-filters-button' ).on( 'click', function( e ) {
e.preventDefault();
$( this ).closest( '#edd-advanced-filters' ).toggleClass( 'open' );
} );
} );

View File

@ -0,0 +1,147 @@
/* global _ */
/**
* Internal dependencies.
*/
import { getChosenVars } from 'utils/chosen.js';
jQuery( document ).ready( function( $ ) {
// Globally apply to elements on the page.
$( '.edd-select-chosen' ).each( function() {
const el = $( this );
el.chosen( getChosenVars( el ) );
} );
$( '.edd-select-chosen .chosen-search input' ).each( function() {
// Bail if placeholder already set
if ( $( this ).attr( 'placeholder' ) ) {
return;
}
const selectElem = $( this ).parent().parent().parent().prev( 'select.edd-select-chosen' ),
placeholder = selectElem.data( 'search-placeholder' );
if ( placeholder ) {
$( this ).attr( 'placeholder', placeholder );
}
} );
// Add placeholders for Chosen input fields
$( '.chosen-choices' ).on( 'click', function() {
let placeholder = $( this ).parent().prev().data( 'search-placeholder' );
if ( typeof placeholder === 'undefined' ) {
placeholder = edd_vars.type_to_search;
}
$( this ).children( 'li' ).children( 'input' ).attr( 'placeholder', placeholder );
} );
// This fixes the Chosen box being 0px wide when the thickbox is opened
$( '#post' ).on( 'click', '.edd-thickbox', function() {
$( '.edd-select-chosen', '#choose-download' ).css( 'width', '100%' );
} );
// Variables for setting up the typing timer
// Time in ms, Slow - 521ms, Moderate - 342ms, Fast - 300ms
let userInteractionInterval = 342,
typingTimerElements = '.edd-select-chosen .chosen-search input, .edd-select-chosen .search-field input',
typingTimer;
// Replace options with search results
$( document.body ).on( 'keyup', typingTimerElements, _.debounce( function( e ) {
let element = $( this ),
val = element.val(),
container = element.closest( '.edd-select-chosen' ),
select = container.prev(),
select_type = select.data( 'search-type' ),
no_bundles = container.hasClass( 'no-bundles' ),
variations = container.hasClass( 'variations' ),
variations_only = container.hasClass( 'variations-only' ),
lastKey = e.which,
search_type = 'edd_download_search';
// String replace the chosen container IDs
container.attr( 'id' ).replace( '_chosen', '' );
// Detect if we have a defined search type, otherwise default to downloads
if ( typeof select_type !== 'undefined' ) {
// Don't trigger AJAX if this select has all options loaded
if ( 'no_ajax' === select_type ) {
return;
}
search_type = 'edd_' + select_type + '_search';
} else {
return;
}
// Don't fire if short or is a modifier key (shift, ctrl, apple command key, or arrow keys)
if (
( val.length <= 3 && 'edd_download_search' === search_type ) ||
(
lastKey === 16 ||
lastKey === 13 ||
lastKey === 91 ||
lastKey === 17 ||
lastKey === 37 ||
lastKey === 38 ||
lastKey === 39 ||
lastKey === 40
)
) {
container.children( '.spinner' ).remove();
return;
}
// Maybe append a spinner
if ( ! container.children( '.spinner' ).length ) {
container.append( '<span class="spinner is-active"></span>' );
}
$.ajax( {
type: 'GET',
dataType: 'json',
url: ajaxurl,
data: {
s: val,
action: search_type,
no_bundles: no_bundles,
variations: variations,
variations_only: variations_only,
},
beforeSend: function() {
select.closest( 'ul.chosen-results' ).empty();
},
success: function( data ) {
// Remove all options but those that are selected
$( 'option:not(:selected)', select ).remove();
// Add any option that doesn't already exist
$.each( data, function( key, item ) {
if ( ! $( 'option[value="' + item.id + '"]', select ).length ) {
select.prepend( '<option value="' + item.id + '">' + item.name + '</option>' );
}
} );
// Get the text immediately before triggering an update.
// Any sooner will cause the text to jump around.
const val = element.val();
// Update the options
select.trigger( 'chosen:updated' );
element.val( val );
},
} ).fail( function( response ) {
if ( window.console && window.console.log ) {
console.log( response );
}
} ).done( function( response ) {
container.children( '.spinner' ).remove();
} );
}, userInteractionInterval ) );
} );

View File

@ -0,0 +1,26 @@
/**
* Date picker
*
* This juggles a few CSS classes to avoid styling collisions with other
* third-party plugins.
*/
jQuery( document ).ready( function( $ ) {
const edd_datepicker = $( 'input.edd_datepicker' );
if ( edd_datepicker.length > 0 ) {
edd_datepicker
// Disable autocomplete to avoid it covering the calendar
.attr( 'autocomplete', 'off' )
// Invoke the datepickers
.datepicker( {
dateFormat: edd_vars.date_picker_format,
beforeShow: function() {
$( '#ui-datepicker-div' )
.removeClass( 'ui-datepicker' )
.addClass( 'edd-datepicker' );
},
} );
}
} );

View File

@ -0,0 +1,23 @@
jQuery( document ).ready( function ( $ ) {
$( '.edd_countries_filter' ).on( 'change', function () {
const select = $( this ),
data = {
action: 'edd_get_shop_states',
country: select.val(),
nonce: select.data( 'nonce' ),
field_name: 'edd_regions_filter',
};
$.post( ajaxurl, data, function ( response ) {
$( 'select.edd_regions_filter' ).find( 'option:gt(0)' ).remove();
if ( 'nostates' !== response ) {
$( response ).find( 'option:gt(0)' ).appendTo( 'select.edd_regions_filter' );
}
$( 'select.edd_regions_filter' ).trigger( 'chosen:updated' );
} );
return false;
} );
} );

View File

@ -0,0 +1,125 @@
/* global edd_vars */
document.addEventListener( 'alpine:init', () => {
Alpine.store( 'eddNotifications', {
isPanelOpen: false,
notificationsLoaded: false,
numberActiveNotifications: 0,
activeNotifications: [],
inactiveNotifications: [],
init: function() {
const eddNotifications = this;
/*
* The bubble starts out hidden until AlpineJS is initialized. Once it is, we remove
* the hidden class. This prevents a flash of the bubble's visibility in the event that there
* are no notifications.
*/
const notificationCountBubble = document.querySelector( '#edd-notification-button .edd-number' );
if ( notificationCountBubble ) {
notificationCountBubble.classList.remove( 'edd-hidden' );
}
document.addEventListener( 'keydown', function( e ) {
if ( e.key === 'Escape' ) {
eddNotifications.closePanel();
}
} );
const params = new URLSearchParams( window.location.search );
const triggerNotifications = params.has( 'notifications' );
if ( triggerNotifications && 'true' === params.get( 'notifications' ) ) {
eddNotifications.openPanel();
}
},
openPanel: function() {
const panelHeader = document.getElementById( 'edd-notifications-header' );
if ( this.notificationsLoaded ) {
this.isPanelOpen = true;
if ( panelHeader ) {
setTimeout( function() {
panelHeader.focus();
} );
}
return;
}
this.isPanelOpen = true;
this.apiRequest( '/notifications', 'GET' )
.then( data => {
this.activeNotifications = data.active;
this.inactiveNotifications = data.dismissed;
this.notificationsLoaded = true;
if ( panelHeader ) {
panelHeader.focus();
}
} )
.catch( error => {
console.log( 'Notification error', error );
} );
},
closePanel: function() {
if ( ! this.isPanelOpen ) {
return;
}
this.isPanelOpen = false;
const notificationButton = document.getElementById( 'edd-notification-button' );
if ( notificationButton ) {
notificationButton.focus();
}
},
apiRequest: function( endpoint, method ) {
return fetch( edd_vars.restBase + endpoint, {
method: method,
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': edd_vars.restNonce
}
} ).then( response => {
if ( ! response.ok ) {
return Promise.reject( response );
}
/*
* Returning response.text() instead of response.json() because dismissing
* a notification doesn't return a JSON response, so response.json() will break.
*/
return response.text();
//return response.json();
} ).then( data => {
return data ? JSON.parse( data ) : null;
} );
} ,
dismiss: function( event, index ) {
if ( 'undefined' === typeof this.activeNotifications[ index ] ) {
return;
}
event.target.disabled = true;
const notification = this.activeNotifications[ index ];
this.apiRequest( '/notifications/' + notification.id, 'DELETE' )
.then( response => {
this.activeNotifications.splice( index, 1 );
this.numberActiveNotifications = this.activeNotifications.length;
} )
.catch( error => {
console.log( 'Dismiss error', error );
} );
}
} );
} );

View File

@ -0,0 +1,42 @@
/* global ajaxurl */
jQuery( document ).ready( function( $ ) {
/**
* Display notices
*/
const topOfPageNotice = $( '.edd-admin-notice-top-of-page' );
if ( topOfPageNotice ) {
const topOfPageNoticeEl = topOfPageNotice.detach();
$( '#wpbody-content' ).prepend( topOfPageNoticeEl );
topOfPageNotice.delay( 1000 ).slideDown();
}
/**
* Dismiss notices
*/
$( '.edd-promo-notice' ).each( function() {
const notice = $( this );
notice.on( 'click', '.edd-promo-notice-dismiss', function( e ) {
// Only prevent default behavior for buttons, not links.
if ( ! $( this ).attr( 'href' ) ) {
e.preventDefault();
}
$.ajax( {
type: 'POST',
data: {
action: 'edd_dismiss_promo_notice',
notice_id: notice.data( 'id' ),
nonce: notice.data( 'nonce' ),
lifespan: notice.data( 'lifespan' )
},
url: ajaxurl,
success: function( response ) {
notice.slideUp();
}
} );
} );
} );
} );

View File

@ -0,0 +1,34 @@
/**
* Sortables
*
* This makes certain settings sortable, and attempts to stash the results
* in the nearest .edd-order input value.
*/
jQuery( document ).ready( function( $ ) {
const edd_sortables = $( 'ul.edd-sortable-list' );
if ( edd_sortables.length > 0 ) {
edd_sortables.sortable( {
axis: 'y',
items: 'li',
cursor: 'move',
tolerance: 'pointer',
containment: 'parent',
distance: 2,
opacity: 0.7,
scroll: true,
/**
* When sorting stops, assign the value to the previous input.
* This input should be a hidden text field
*/
stop: function() {
const keys = $.map( $( this ).children( 'li' ), function( el ) {
return $( el ).data( 'key' );
} );
$( this ).prev( 'input.edd-order' ).val( keys );
},
} );
}
} );

View File

@ -0,0 +1,7 @@
/* global jQuery */
jQuery( document ).ready( function ( $ ) {
if ( $( 'body' ).hasClass( 'taxonomy-download_category' ) || $( 'body' ).hasClass( 'taxonomy-download_tag' ) ) {
$( '.nav-tab-wrapper, .nav-tab-wrapper + br' ).detach().insertAfter( '.wp-header-end' );
}
} );

View File

@ -0,0 +1,28 @@
/**
* Attach tooltips
*
* @param {string} selector
*/
export const edd_attach_tooltips = function( selector ) {
selector.tooltip( {
content: function() {
return $( this ).prop( 'title' );
},
tooltipClass: 'edd-ui-tooltip',
position: {
my: 'center top',
at: 'center bottom+10',
collision: 'flipfit',
},
hide: {
duration: 200,
},
show: {
duration: 200,
},
} );
};
jQuery( document ).ready( function( $ ) {
edd_attach_tooltips( $( '.edd-help-tip' ) );
} );

View File

@ -0,0 +1,74 @@
jQuery( document ).ready( function( $ ) {
// AJAX user search
$( '.edd-ajax-user-search' )
// Search
.keyup( function() {
let user_search = $( this ).val(),
exclude = '';
if ( $( this ).data( 'exclude' ) ) {
exclude = $( this ).data( 'exclude' );
}
$( '.edd_user_search_wrap' ).addClass( 'loading' );
const data = {
action: 'edd_search_users',
user_name: user_search,
exclude: exclude,
};
$.ajax( {
type: 'POST',
data: data,
dataType: 'json',
url: ajaxurl,
success: function( search_response ) {
$( '.edd_user_search_wrap' ).removeClass( 'loading' );
$( '.edd_user_search_results' ).removeClass( 'hidden' );
$( '.edd_user_search_results span' ).html( '' );
if ( search_response.results ) {
$( search_response.results ).appendTo( '.edd_user_search_results span' );
}
},
} );
} )
// Hide
.blur( function() {
if ( edd_user_search_mouse_down ) {
edd_user_search_mouse_down = false;
} else {
$( this ).removeClass( 'loading' );
$( '.edd_user_search_results' ).addClass( 'hidden' );
}
} )
// Show
.focus( function() {
$( this ).keyup();
} );
$( document.body ).on( 'click.eddSelectUser', '.edd_user_search_results span a', function( e ) {
e.preventDefault();
const login = $( this ).data( 'login' );
$( '.edd-ajax-user-search' ).val( login );
$( '.edd_user_search_results' ).addClass( 'hidden' );
$( '.edd_user_search_results span' ).html( '' );
} );
$( document.body ).on( 'click.eddCancelUserSearch', '.edd_user_search_results a.edd-ajax-user-cancel', function( e ) {
e.preventDefault();
$( '.edd-ajax-user-search' ).val( '' );
$( '.edd_user_search_results' ).addClass( 'hidden' );
$( '.edd_user_search_results span' ).html( '' );
} );
// Cancel user-search.blur when picking a user
var edd_user_search_mouse_down = false;
$( '.edd_user_search_results' ).mousedown( function() {
edd_user_search_mouse_down = true;
} );
} );

View File

@ -0,0 +1,61 @@
jQuery( document ).ready( function( $ ) {
const sectionSelector = '.edd-vertical-sections.use-js';
// If the current screen doesn't have JS sections, return.
if ( 0 === $( sectionSelector ).length ) {
return;
}
// Hides the section content.
$( `${ sectionSelector } .section-content` ).hide();
const hash = window.location.hash;
if ( hash && hash.includes( 'edd_' ) ) {
// Show the section content related to the URL.
$( sectionSelector ).find( hash ).show();
// Set the aria-selected for section titles to be false
$( `${ sectionSelector } .section-title` ).attr( 'aria-selected', 'false' ).removeClass( 'section-title--is-active' );
// Set aria-selected true on the related link.
$( sectionSelector ).find( '.section-title a[href="' + hash + '"]' ).parents( '.section-title' ).attr( 'aria-selected', 'true' ).addClass( 'section-title--is-active' );
} else {
// Shows the first section's content.
$( `${ sectionSelector } .section-content:first-child` ).show();
// Makes the 'aria-selected' attribute true for the first section nav item.
$( `${ sectionSelector } .section-nav li:first-child` ).attr( 'aria-selected', 'true' ).addClass( 'section-title--is-active' );
}
// When a section nav item is clicked.
$( `${ sectionSelector } .section-nav li a` ).on( 'click',
function( j ) {
// Prevent the default browser action when a link is clicked.
j.preventDefault();
// Get the `href` attribute of the item.
const them = $( this ),
href = them.attr( 'href' ),
rents = them.parents( '.edd-vertical-sections' );
// Hide all section content.
rents.find( '.section-content' ).hide();
// Find the section content that matches the section nav item and show it.
rents.find( href ).show();
// Set the `aria-selected` attribute to false for all section nav items.
rents.find( '.section-title' ).attr( 'aria-selected', 'false' ).removeClass( 'section-title--is-active' );
// Set the `aria-selected` attribute to true for this section nav item.
them.parent().attr( 'aria-selected', 'true' ).addClass( 'section-title--is-active' );
// Maybe re-Chosen
rents.find( 'div.chosen-container' ).css( 'width', '100%' );
// Add the current "link" to the page URL
window.history.pushState( 'object or string', '', href );
}
); // click()
} );

View File

@ -0,0 +1,140 @@
/**
* Customer management screen JS
*/
var EDD_Customer = {
vars: {
customer_card_wrap_editable: $( '#edit-customer-info .editable' ),
customer_card_wrap_edit_item: $( '#edit-customer-info .edit-item' ),
user_id: $( 'input[name="customerinfo[user_id]"]' ),
},
init: function() {
this.edit_customer();
this.add_email();
this.user_search();
this.remove_user();
this.cancel_edit();
this.change_country();
this.delete_checked();
},
edit_customer: function() {
$( document.body ).on( 'click', '#edit-customer', function( e ) {
e.preventDefault();
EDD_Customer.vars.customer_card_wrap_editable.hide();
EDD_Customer.vars.customer_card_wrap_edit_item.show().css( 'display', 'block' );
} );
},
add_email: function() {
$( document.body ).on( 'click', '#add-customer-email', function( e ) {
e.preventDefault();
const button = $( this ),
wrapper = button.parent().parent().parent().parent(),
customer_id = wrapper.find( 'input[name="customer-id"]' ).val(),
email = wrapper.find( 'input[name="additional-email"]' ).val(),
primary = wrapper.find( 'input[name="make-additional-primary"]' ).is( ':checked' ),
nonce = wrapper.find( 'input[name="add_email_nonce"]' ).val(),
postData = {
edd_action: 'customer-add-email',
customer_id: customer_id,
email: email,
primary: primary,
_wpnonce: nonce,
};
wrapper.parent().find( '.notice-container' ).remove();
wrapper.find( '.spinner' ).css( 'visibility', 'visible' );
button.attr( 'disabled', true );
$.post( ajaxurl, postData, function( response ) {
setTimeout( function() {
if ( true === response.success ) {
window.location.href = response.redirect;
} else {
button.attr( 'disabled', false );
wrapper.before( '<div class="notice-container"><div class="notice notice-error inline"><p>' + response.message + '</p></div></div>' );
wrapper.find( '.spinner' ).css( 'visibility', 'hidden' );
}
}, 342 );
}, 'json' );
} );
},
user_search: function() {
// Upon selecting a user from the dropdown, we need to update the User ID
$( document.body ).on( 'click.eddSelectUser', '.edd_user_search_results a', function( e ) {
e.preventDefault();
const user_id = $( this ).data( 'userid' );
EDD_Customer.vars.user_id.val( user_id );
} );
},
remove_user: function() {
$( document.body ).on( 'click', '#disconnect-customer', function( e ) {
e.preventDefault();
if ( confirm( edd_vars.disconnect_customer ) ) {
const customer_id = $( 'input[name="customerinfo[id]"]' ).val(),
postData = {
edd_action: 'disconnect-userid',
customer_id: customer_id,
_wpnonce: $( '#edit-customer-info #_wpnonce' ).val(),
};
$.post( ajaxurl, postData, function( response ) {
// Weird
window.location.href = window.location.href;
}, 'json' );
}
} );
},
cancel_edit: function() {
$( document.body ).on( 'click', '#edd-edit-customer-cancel', function( e ) {
e.preventDefault();
EDD_Customer.vars.customer_card_wrap_edit_item.hide();
EDD_Customer.vars.customer_card_wrap_editable.show();
$( '.edd_user_search_results' ).html( '' );
} );
},
change_country: function() {
$( 'select[name="customerinfo[country]"]' ).change( function() {
const select = $( this ),
state_input = $( ':input[name="customerinfo[region]"]' ),
data = {
action: 'edd_get_shop_states',
country: select.val(),
nonce: select.data( 'nonce' ),
field_name: 'customerinfo[region]',
};
$.post( ajaxurl, data, function( response ) {
console.log( response );
if ( 'nostates' === response ) {
state_input.replaceWith( '<input type="text" name="' + data.field_name + '" value="" class="edd-edit-toggles medium-text"/>' );
} else {
state_input.replaceWith( response );
}
} );
return false;
} );
},
delete_checked: function() {
$( '#edd-customer-delete-confirm' ).change( function() {
const records_input = $( '#edd-customer-delete-records' );
const submit_button = $( '#edd-delete-customer' );
if ( $( this ).prop( 'checked' ) ) {
records_input.attr( 'disabled', false );
submit_button.attr( 'disabled', false );
} else {
records_input.attr( 'disabled', true );
records_input.prop( 'checked', false );
submit_button.attr( 'disabled', true );
}
} );
},
};
jQuery( document ).ready( function( $ ) {
EDD_Customer.init();
} );

View File

@ -0,0 +1,14 @@
jQuery( document ).ready( function( $ ) {
if ( $( '#edd_dashboard_sales' ).length ) {
$.ajax( {
type: 'GET',
data: {
action: 'edd_load_dashboard_widget',
},
url: ajaxurl,
success: function( response ) {
$( '#edd_dashboard_sales .edd-loading' ).html( response );
},
} );
}
} );

View File

@ -0,0 +1,21 @@
/**
* Internal dependencies.
*/
import { jQueryReady } from 'utils/jquery.js';
/**
* DOM ready.
*/
jQueryReady( () => {
const products = $( '#edd_products' );
if ( ! products ) {
return;
}
/**
* Show/hide conditions based on input value.
*/
products.change( function() {
$( '#edd-discount-product-conditions' ).toggle( null !== products.val() );
} );
} );

View File

@ -0,0 +1,43 @@
jQuery( document ).ready( function( $ ) {
$( 'body' ).on( 'click', '#the-list .editinline', function() {
let post_id = $( this ).closest( 'tr' ).attr( 'id' );
post_id = post_id.replace( 'post-', '' );
const $edd_inline_data = $( '#post-' + post_id );
const regprice = $edd_inline_data.find( '.column-price .downloadprice-' + post_id ).val();
// If variable priced product disable editing, otherwise allow price changes
if ( regprice !== $( '#post-' + post_id + '.column-price .downloadprice-' + post_id ).val() ) {
$( '.regprice', '#edd-download-data' ).val( regprice ).attr( 'disabled', false );
} else {
$( '.regprice', '#edd-download-data' ).val( edd_vars.quick_edit_warning ).attr( 'disabled', 'disabled' );
}
} );
// Bulk edit save
$( document.body ).on( 'click', '#bulk_edit', function() {
// define the bulk edit row
const $bulk_row = $( '#bulk-edit' );
// get the selected post ids that are being edited
const $post_ids = new Array();
$bulk_row.find( '#bulk-titles' ).children().each( function() {
$post_ids.push( $( this ).attr( 'id' ).replace( /^(ttle)/i, '' ) );
} );
// get the stock and price values to save for all the product ID's
const $price = $( '#edd-download-data input[name="_edd_regprice"]' ).val();
const data = {
action: 'edd_save_bulk_edit',
edd_bulk_nonce: $post_ids,
post_ids: $post_ids,
price: $price,
};
// save the data
$.post( ajaxurl, data );
} );
} );

View File

@ -0,0 +1,409 @@
/**
* Internal dependencies.
*/
import { getChosenVars } from 'utils/chosen.js';
import { edd_attach_tooltips } from 'admin/components/tooltips';
import './bulk-edit.js';
/**
* Download Configuration Metabox
*/
var EDD_Download_Configuration = {
init: function() {
this.add();
this.move();
this.remove();
this.type();
this.prices();
this.files();
this.updatePrices();
this.showAdvanced();
},
clone_repeatable: function( row ) {
// Retrieve the highest current key
let key = 1;
let highest = 1;
row.parent().find( '.edd_repeatable_row' ).each( function() {
const current = $( this ).data( 'key' );
if ( parseInt( current ) > highest ) {
highest = current;
}
} );
key = highest += 1;
const clone = row.clone();
clone.removeClass( 'edd_add_blank' );
clone.attr( 'data-key', key );
clone.find( 'input, select, textarea' ).val( '' ).each( function() {
let elem = $( this ),
name = elem.attr( 'name' ),
id = elem.attr( 'id' );
if ( name ) {
name = name.replace( /\[(\d+)\]/, '[' + parseInt( key ) + ']' );
elem.attr( 'name', name );
}
elem.attr( 'data-key', key );
if ( typeof id !== 'undefined' ) {
id = id.replace( /(\d+)/, parseInt( key ) );
elem.attr( 'id', id );
}
} );
/** manually update any select box values */
clone.find( 'select' ).each( function() {
$( this ).val( row.find( 'select[name="' + $( this ).attr( 'name' ) + '"]' ).val() );
} );
/** manually uncheck any checkboxes */
clone.find( 'input[type="checkbox"]' ).each( function() {
// Make sure checkboxes are unchecked when cloned
const checked = $( this ).is( ':checked' );
if ( checked ) {
$( this ).prop( 'checked', false );
}
// reset the value attribute to 1 in order to properly save the new checked state
$( this ).val( 1 );
} );
clone.find( 'span.edd_price_id' ).each( function() {
$( this ).text( parseInt( key ) );
} );
clone.find( 'input.edd_repeatable_index' ).each( function() {
$( this ).val( parseInt( $( this ).data( 'key' ) ) );
} );
clone.find( 'span.edd_file_id' ).each( function() {
$( this ).text( parseInt( key ) );
} );
clone.find( '.edd_repeatable_default_input' ).each( function() {
$( this ).val( parseInt( key ) ).removeAttr( 'checked' );
} );
clone.find( '.edd_repeatable_condition_field' ).each( function() {
$( this ).find( 'option:eq(0)' ).prop( 'selected', 'selected' );
} );
clone.find( 'label' ).each( function () {
var labelFor = $( this ).attr( 'for' );
if ( labelFor ) {
$( this ).attr( 'for', labelFor.replace( /(\d+)/, parseInt( key ) ) );
}
} );
// Remove Chosen elements
clone.find( '.search-choice' ).remove();
clone.find( '.chosen-container' ).remove();
edd_attach_tooltips( clone.find( '.edd-help-tip' ) );
return clone;
},
add: function() {
$( document.body ).on( 'click', '.edd_add_repeatable', function( e ) {
e.preventDefault();
const button = $( this ),
row = button.closest( '.edd_repeatable_table' ).find( '.edd_repeatable_row' ).last(),
clone = EDD_Download_Configuration.clone_repeatable( row );
clone.insertAfter( row ).find( 'input, textarea, select' ).filter( ':visible' ).eq( 0 ).focus();
// Setup chosen fields again if they exist
clone.find( '.edd-select-chosen' ).each( function() {
const el = $( this );
el.chosen( getChosenVars( el ) );
} );
clone.find( '.edd-select-chosen' ).css( 'width', '100%' );
clone.find( '.edd-select-chosen .chosen-search input' ).attr( 'placeholder', edd_vars.search_placeholder );
} );
},
move: function() {
$( '.edd_repeatable_table .edd-repeatables-wrap' ).sortable( {
axis: 'y',
handle: '.edd-draghandle-anchor',
items: '.edd_repeatable_row',
cursor: 'move',
tolerance: 'pointer',
containment: 'parent',
distance: 2,
opacity: 0.7,
scroll: true,
update: function() {
let count = 0;
$( this ).find( '.edd_repeatable_row' ).each( function() {
$( this ).find( 'input.edd_repeatable_index' ).each( function() {
$( this ).val( count );
} );
count++;
} );
},
start: function( e, ui ) {
ui.placeholder.height( ui.item.height() - 2 );
},
} );
},
remove: function() {
$( document.body ).on( 'click', '.edd-remove-row, .edd_remove_repeatable', function( e ) {
e.preventDefault();
let row = $( this ).parents( '.edd_repeatable_row' ),
count = row.parent().find( '.edd_repeatable_row' ).length,
type = $( this ).data( 'type' ),
repeatable = 'div.edd_repeatable_' + type + 's',
focusElement,
focusable,
firstFocusable;
// Set focus on next element if removing the first row. Otherwise set focus on previous element.
if ( $( this ).is( '.ui-sortable .edd_repeatable_row:first-child .edd-remove-row, .ui-sortable .edd_repeatable_row:first-child .edd_remove_repeatable' ) ) {
focusElement = row.next( '.edd_repeatable_row' );
} else {
focusElement = row.prev( '.edd_repeatable_row' );
}
focusable = focusElement.find( 'select, input, textarea, button' ).filter( ':visible' );
firstFocusable = focusable.eq( 0 );
if ( type === 'price' ) {
const price_row_id = row.data( 'key' );
/** remove from price condition */
$( '.edd_repeatable_condition_field option[value="' + price_row_id + '"]' ).remove();
}
if ( count > 1 ) {
$( 'input, select', row ).val( '' );
row.fadeOut( 'fast' ).remove();
firstFocusable.focus();
} else {
switch ( type ) {
case 'price' :
alert( edd_vars.one_price_min );
break;
case 'file' :
$( 'input, select', row ).val( '' );
break;
default:
alert( edd_vars.one_field_min );
break;
}
}
/* re-index after deleting */
$( repeatable ).each( function( rowIndex ) {
$( this ).find( 'input, select' ).each( function() {
let name = $( this ).attr( 'name' );
name = name.replace( /\[(\d+)\]/, '[' + rowIndex + ']' );
$( this ).attr( 'name', name ).attr( 'id', name );
} );
} );
} );
},
type: function() {
$( document.body ).on( 'change', '#_edd_product_type', function( e ) {
const edd_products = $( '#edd_products' ),
edd_download_files = $( '#edd_download_files' ),
edd_download_limit_wrap = $( '#edd_download_limit_wrap' );
if ( 'bundle' === $( this ).val() ) {
edd_products.show();
edd_download_files.hide();
edd_download_limit_wrap.hide();
} else {
edd_products.hide();
edd_download_files.show();
edd_download_limit_wrap.show();
}
} );
},
prices: function() {
$( document.body ).on( 'change', '#edd_variable_pricing', function( e ) {
const checked = $( this ).is( ':checked' ),
single = $( '#edd_regular_price_field' ),
variable = $( '#edd_variable_price_fields, .edd_repeatable_table .pricing' ),
bundleRow = $( '.edd-bundled-product-row, .edd-repeatable-row-standard-fields' );
if ( checked ) {
single.hide();
variable.show();
bundleRow.addClass( 'has-variable-pricing' );
} else {
single.show();
variable.hide();
bundleRow.removeClass( 'has-variable-pricing' );
}
} );
},
files: function() {
var file_frame;
window.formfield = '';
$( document.body ).on( 'click', '.edd_upload_file_button', function( e ) {
e.preventDefault();
const button = $( this );
window.formfield = button.closest( '.edd_repeatable_upload_wrapper' );
// If the media frame already exists, reopen it.
if ( file_frame ) {
file_frame.open();
return;
}
// Create the media frame.
file_frame = wp.media.frames.file_frame = wp.media( {
title: button.data( 'uploader-title' ),
frame: 'post',
state: 'insert',
button: { text: button.data( 'uploader-button-text' ) },
multiple: $( this ).data( 'multiple' ) === '0' ? false : true, // Set to true to allow multiple files to be selected
} );
file_frame.on( 'menu:render:default', function( view ) {
// Store our views in an object.
const views = {};
// Unset default menu items
view.unset( 'library-separator' );
view.unset( 'gallery' );
view.unset( 'featured-image' );
view.unset( 'embed' );
// Initialize the views in our view object.
view.set( views );
} );
// When an image is selected, run a callback.
file_frame.on( 'insert', function() {
const selection = file_frame.state().get( 'selection' );
selection.each( function( attachment, index ) {
attachment = attachment.toJSON();
let selectedSize = 'image' === attachment.type ? $( '.attachment-display-settings .size option:selected' ).val() : false,
selectedURL = attachment.url,
selectedName = attachment.title.length > 0 ? attachment.title : attachment.filename;
if ( selectedSize && typeof attachment.sizes[ selectedSize ] !== 'undefined' ) {
selectedURL = attachment.sizes[ selectedSize ].url;
}
if ( 'image' === attachment.type ) {
if ( selectedSize && typeof attachment.sizes[ selectedSize ] !== 'undefined' ) {
selectedName = selectedName + '-' + attachment.sizes[ selectedSize ].width + 'x' + attachment.sizes[ selectedSize ].height;
} else {
selectedName = selectedName + '-' + attachment.width + 'x' + attachment.height;
}
}
if ( 0 === index ) {
// place first attachment in field
window.formfield.find( '.edd_repeatable_attachment_id_field' ).val( attachment.id );
window.formfield.find( '.edd_repeatable_thumbnail_size_field' ).val( selectedSize );
window.formfield.find( '.edd_repeatable_upload_field' ).val( selectedURL );
window.formfield.find( '.edd_repeatable_name_field' ).val( selectedName );
} else {
// Create a new row for all additional attachments
const row = window.formfield,
clone = EDD_Download_Configuration.clone_repeatable( row );
clone.find( '.edd_repeatable_attachment_id_field' ).val( attachment.id );
clone.find( '.edd_repeatable_thumbnail_size_field' ).val( selectedSize );
clone.find( '.edd_repeatable_upload_field' ).val( selectedURL );
clone.find( '.edd_repeatable_name_field' ).val( selectedName );
clone.insertAfter( row );
}
} );
} );
// Finally, open the modal
file_frame.open();
} );
// @todo Break this out and remove jQuery.
$( '.edd_repeatable_upload_field' )
.on( 'focus', function() {
const input = $( this );
input.data( 'originalFile', input.val() );
} )
.on( 'change', function() {
const input = $( this );
const originalFile = input.data( 'originalFile' );
if ( originalFile !== input.val() ) {
input
.closest( '.edd-repeatable-row-standard-fields' )
.find( '.edd_repeatable_attachment_id_field' )
.val( 0 );
}
} );
var file_frame;
window.formfield = '';
},
updatePrices: function() {
$( '#edd_price_fields' ).on( 'keyup', '.edd_variable_prices_name', function() {
const key = $( this ).parents( '.edd_repeatable_row' ).data( 'key' ),
name = $( this ).val(),
field_option = $( '.edd_repeatable_condition_field option[value=' + key + ']' );
if ( field_option.length > 0 ) {
field_option.text( name );
} else {
$( '.edd_repeatable_condition_field' ).append(
$( '<option></option>' )
.attr( 'value', key )
.text( name )
);
}
} );
},
showAdvanced: function() {
// Toggle display of entire custom settings section for a price option
$( document.body ).on( 'click', '.toggle-custom-price-option-section', function( e ) {
e.preventDefault();
const toggle = $( this ),
show = toggle.html() === edd_vars.show_advanced_settings ?
true :
false;
if ( show ) {
toggle.html( edd_vars.hide_advanced_settings );
} else {
toggle.html( edd_vars.show_advanced_settings );
}
const header = toggle.parents( '.edd-repeatable-row-header' );
header.siblings( '.edd-custom-price-option-sections-wrap' ).slideToggle();
let first_input;
if ( show ) {
first_input = $( ':input:not(input[type=button],input[type=submit],button):visible:first', header.siblings( '.edd-custom-price-option-sections-wrap' ) );
} else {
first_input = $( ':input:not(input[type=button],input[type=submit],button):visible:first', header.siblings( '.edd-repeatable-row-standard-fields' ) );
}
first_input.focus();
} );
}
};
jQuery( document ).ready( function( $ ) {
EDD_Download_Configuration.init();
} );

View File

@ -0,0 +1,16 @@
/**
* Internal dependencies.
*/
import './components/date-picker';
import './components/chosen';
import './components/tooltips';
import './components/vertical-sections';
import './components/sortable-list';
import './components/user-search';
import './components/advanced-filters';
import './components/taxonomies';
import './components/location';
import './components/promos';
import './components/notifications';
// Note: This is not common across all admin pages and at some point this code will be moved to a new file that only loads on the orders table page.
import './orders/list-table';

View File

@ -0,0 +1,134 @@
/**
* Notes
*/
const EDD_Notes = {
init: function() {
this.enter_key();
this.add_note();
this.remove_note();
},
enter_key: function() {
$( document.body ).on( 'keydown', '#edd-note', function( e ) {
if ( e.keyCode === 13 && ( e.metaKey || e.ctrlKey ) ) {
e.preventDefault();
$( '#edd-add-note' ).click();
}
} );
},
/**
* Ajax handler for adding new notes
*
* @since 3.0
*/
add_note: function() {
$( '#edd-add-note' ).on( 'click', function( e ) {
e.preventDefault();
const edd_button = $( this ),
edd_note = $( '#edd-note' ),
edd_notes = $( '.edd-notes' ),
edd_no_notes = $( '.edd-no-notes' ),
edd_spinner = $( '.edd-add-note .spinner' ),
edd_note_nonce = $( '#edd_note_nonce' );
const postData = {
action: 'edd_add_note',
nonce: edd_note_nonce.val(),
object_id: edd_button.data( 'object-id' ),
object_type: edd_button.data( 'object-type' ),
note: edd_note.val(),
};
if ( postData.note ) {
edd_button.prop( 'disabled', true );
edd_spinner.css( 'visibility', 'visible' );
$.ajax( {
type: 'POST',
data: postData,
url: ajaxurl,
success: function( response ) {
let res = wpAjax.parseAjaxResponse( response );
res = res.responses[ 0 ];
edd_notes.append( res.data );
edd_no_notes.hide();
edd_button.prop( 'disabled', false );
edd_spinner.css( 'visibility', 'hidden' );
edd_note.val( '' );
},
} ).fail( function( data ) {
if ( window.console && window.console.log ) {
console.log( data );
}
edd_button.prop( 'disabled', false );
edd_spinner.css( 'visibility', 'hidden' );
} );
} else {
const border_color = edd_note.css( 'border-color' );
edd_note.css( 'border-color', 'red' );
setTimeout( function() {
edd_note.css( 'border-color', border_color );
}, userInteractionInterval );
}
} );
},
/**
* Ajax handler for deleting existing notes
*
* @since 3.0
*/
remove_note: function() {
$( document.body ).on( 'click', '.edd-delete-note', function( e ) {
e.preventDefault();
const edd_link = $( this ),
edd_notes = $( '.edd-note' ),
edd_note = edd_link.parents( '.edd-note' ),
edd_no_notes = $( '.edd-no-notes' ),
edd_note_nonce = $( '#edd_note_nonce' );
if ( confirm( edd_vars.delete_note ) ) {
const postData = {
action: 'edd_delete_note',
nonce: edd_note_nonce.val(),
note_id: edd_link.data( 'note-id' ),
};
edd_note.addClass( 'deleting' );
$.ajax( {
type: 'POST',
data: postData,
url: ajaxurl,
success: function( response ) {
if ( '1' === response ) {
edd_note.remove();
}
if ( edd_notes.length === 1 ) {
edd_no_notes.show();
}
return false;
},
} ).fail( function( data ) {
if ( window.console && window.console.log ) {
console.log( data );
}
edd_note.removeClass( 'deleting' );
} );
return true;
}
} );
},
};
jQuery( document ).ready( function( $ ) {
EDD_Notes.init();
} );

View File

@ -0,0 +1,28 @@
/**
* Deletes the debug log file and disables logging.
*/
; ( function ( document, $ ) {
'use strict';
$( '#edd-disable-debug-log' ).on( 'click', function ( e ) {
e.preventDefault();
$( this ).attr( 'disabled', true );
var notice = $( '#edd-debug-log-notice' );
$.ajax( {
type: "GET",
data: {
action: 'edd_disable_debugging',
nonce: $( '#edd_debug_log_delete' ).val(),
},
url: ajaxurl,
success: function ( response ) {
notice.empty().append( response.data );
setTimeout( function () {
notice.slideUp();
}, 3000 );
}
} ).fail( function ( response ) {
notice.empty().append( response.responseJSON.data );
} );
} );
} )( document, jQuery );

View File

@ -0,0 +1,130 @@
/**
* Internal dependencies
*/
import OrderOverview from './order-overview';
import './order-details';
import { jQueryReady } from 'utils/jquery.js';
jQueryReady( () => {
// Order Overview.
if ( window.eddAdminOrderOverview ) {
OrderOverview.render();
/**
* Add validation to Add/Edit Order form.
*
* @since 3.0
*/
( () => {
const overview = OrderOverview.options.state;
const orderItems = overview.get( 'items' );
const noItemErrorEl = document.getElementById( 'edd-add-order-no-items-error' );
const noCustomerErrorEl = document.getElementById( 'edd-add-order-customer-error' );
const assignCustomerEl = document.getElementById( 'customer_id' );
const newCustomerEmailEl = document.getElementById( 'edd_new_customer_email' );
[
'edd-add-order-form',
'edd-edit-order-form',
].forEach( ( form ) => {
const formEl = document.getElementById( form );
if ( ! formEl ) {
return;
}
formEl.addEventListener( 'submit', submitForm );
} );
/**
* Submits an Order form.
*
* @since 3.0
*
* @param {Object} event Submit event.
*/
function submitForm( event ) {
let hasError = false;
// Ensure `OrderItem`s.
if ( noItemErrorEl ) {
if ( 0 === orderItems.length ) {
noItemErrorEl.style.display = 'block';
hasError = true;
} else {
noItemErrorEl.style.display = 'none';
}
}
// Ensure Customer.
if ( noCustomerErrorEl ) {
if ( '0' === assignCustomerEl.value && '' === newCustomerEmailEl.value ) {
noCustomerErrorEl.style.display = 'block';
hasError = true;
} else {
noCustomerErrorEl.style.display = 'none';
}
if ( true === hasError ) {
event.preventDefault();
}
}
}
/**
* Remove `OrderItem` notice when an `OrderItem` is added.
*
* @since 3.0
*/
orderItems.on( 'add', function() {
noItemErrorEl.style.display = 'none';
} );
/**
* Remove Customer notice when a Customer is changed.
*
* Uses a jQuery binding for Chosen support.
*
* @since 3.0
*
* @param {Object} event Change event.
*/
$( assignCustomerEl ).on( 'change', ( event ) => {
const val = event.target.value;
if ( '0' !== val ) {
noCustomerErrorEl.style.display = 'none';
}
} )
if ( newCustomerEmailEl ) {
/**
* Remove Customer notice when a Customer is set.
*
* @since 3.0
*
* @param {Object} event Input event.
*/
newCustomerEmailEl.addEventListener( 'input', ( event ) => {
const val = event.target.value;
if ( '' !== val ) {
noCustomerErrorEl.style.display = 'none';
}
} );
}
} )();
}
// Move `.update-nag` items below the top header.
// `#update-nag` is legacy styling, which core still supports.
//
// `.notice` items are properly moved, but WordPress core
// does not move `.update-nag`.
if ( 0 !== $( '.edit-post-editor-regions__header' ).length ) {
$( 'div.update-nag, div#update-nag' ).insertAfter( $( '.edit-post-editor-regions__header' ) );
}
} );

View File

@ -0,0 +1,17 @@
/* global $, ajaxurl */
/**
* Internal dependencies
*/
import { jQueryReady } from 'utils/jquery.js';
jQueryReady( () => {
$( '.download_page_edd-payment-history .row-actions .delete a' ).on( 'click', function() {
if( confirm( edd_vars.delete_payment ) ) {
return true;
}
return false;
});
} );

View File

@ -0,0 +1,256 @@
/* global $, ajaxurl, _ */
/**
* Internal dependencies
*/
import OrderOverview from './../order-overview';
import { getChosenVars } from 'utils/chosen.js';
import { jQueryReady } from 'utils/jquery.js';
// Store customer search results to help prefill address data.
let CUSTOMER_SEARCH_RESULTS = {
addresses: {
'0': {
address: '',
address2: '',
city: '',
region: '',
postal_code: '',
country: '',
},
},
};
jQueryReady( () => {
/**
* Adjusts Overview tax configuration when the Customer's address changes.
*
* @since 3.0
*/
( () => {
const { state: overviewState } = OrderOverview.options;
// No tax, do nothing.
if ( false === overviewState.get( 'hasTax' ) ) {
return;
}
// Editing, do nothing.
if ( false === overviewState.get( 'isAdding' ) ) {
return;
}
const countryInput = document.getElementById(
'edd_order_address_country'
);
const regionInput = document.getElementById(
'edd_order_address_region'
);
if ( ! ( countryInput && regionInput ) ) {
return;
}
/**
* Retrieves a tax rate based on the currently selected Address.
*
* @since 3.0
*/
function getTaxRate() {
const country = $( '#edd_order_address_country' ).val();
const region = $( '#edd_order_address_region' ).val();
const nonce = document.getElementById( 'edd_get_tax_rate_nonce' )
.value;
wp.ajax.send( 'edd_get_tax_rate', {
data: {
nonce,
country,
region,
},
/**
* Updates the Overview's tax configuration on successful retrieval.
*
* @since 3.0
*
* @param {Object} response AJAX response.
*/
success( response ) {
let { tax_rate: rate } = response;
// Make a percentage.
rate = rate * 100;
overviewState.set( 'hasTax', {
...overviewState.get( 'hasTax' ),
country,
region,
rate,
} );
},
/*
* Updates the Overview's tax configuration on failed retrieval.
*
* @since 3.0
*/
error() {
overviewState.set( 'hasTax', 'none' );
},
} );
}
// Update rate on Address change.
//
// Wait for Region field to be replaced when Country changes.
// Wait for typing when Regino field changes.
// jQuery listeners for Chosen compatibility.
$( '#edd_order_address_country' ).on( 'change', _.debounce( getTaxRate, 250 ) );
$( '#edd-order-address' ).on( 'change', '#edd_order_address_region', getTaxRate );
$( '#edd-order-address' ).on( 'keyup', '#edd_order_address_region', _.debounce( getTaxRate, 250 ) );
} )();
$( '.edd-payment-change-customer-input' ).on( 'change', function() {
const $this = $( this ),
data = {
action: 'edd_customer_addresses',
customer_id: $this.val(),
nonce: $( '#edd_add_order_nonce' ).val(),
};
$.post( ajaxurl, data, function( response ) {
const { success, data } = response;
if ( ! success ) {
$( '.customer-address-select-wrap' ).hide();
return;
}
// Store response for later use.
CUSTOMER_SEARCH_RESULTS = {
...CUSTOMER_SEARCH_RESULTS,
...data,
addresses: {
...CUSTOMER_SEARCH_RESULTS.addresses,
...data.addresses,
},
};
if ( data.html ) {
$( '.customer-address-select-wrap' ).show();
$( '.customer-address-select-wrap .edd-form-group__control' ).html( data.html );
} else {
$( '.customer-address-select-wrap' ).hide();
}
}, 'json' );
return false;
} );
/**
* Retrieves a list of states based on a Country HTML <select>.
*
* @since 3.0
*
* @param {HTMLElement} countryEl Element containing country information.
* @param {string} fieldName the name of the field to use in response.
* @return {$.promise} Region data response.
*/
function getStates( countryEl, fieldName, fieldId ) {
const data = {
action: 'edd_get_shop_states',
country: countryEl.val(),
nonce: countryEl.data( 'nonce' ),
field_name: fieldName,
field_id: fieldId,
};
return $.post( ajaxurl, data );
}
/**
* Replaces the Region area with the appropriate field type.
*
* @todo This is hacky and blindly picks elements from the DOM.
*
* @since 3.0
*
* @param {string} regions Regions response.
*/
function replaceRegionField( regions ) {
const state_wrapper = $( '#edd_order_address_region' );
$( '#edd_order_address_region_chosen' ).remove();
if ( 'nostates' === regions ) {
state_wrapper
.replaceWith( '<input type="text" name="edd_order_address[region]" id="edd_order_address_region" value="" class="wide-fat" style="max-width: none; width: 100%;" />' );
} else {
state_wrapper
.replaceWith( regions );
$( '#edd_order_address_region' ).chosen( getChosenVars( $( '#edd_order_address_region' ) ) );
}
}
/**
* Handles replacing a Region field when a Country field changes.
*
* @since 3.0
*/
function updateRegionFieldOnChange() {
getStates(
$( this ),
'edd_order_address[region]',
'edd_order_address_region'
)
.done( replaceRegionField );
}
$( document.body ).on( 'change', '.customer-address-select-wrap .add-order-customer-address-select', function() {
const $this = $( this ),
val = $this.val(),
address = CUSTOMER_SEARCH_RESULTS.addresses[ val ];
$( '#edd-add-order-form input[name="edd_order_address[address]"]' ).val( address.address );
$( '#edd-add-order-form input[name="edd_order_address[address2]"]' ).val( address.address2 );
$( '#edd-add-order-form input[name="edd_order_address[postal_code]"]' ).val( address.postal_code );
$( '#edd-add-order-form input[name="edd_order_address[city]"]' ).val( address.city );
$( '#edd-add-order-form input[name="edd_order_address[address_id]"]' ).val( val );
// Remove global `change` event handling to prevent loop.
$( '#edd_order_address_country' ).off( 'change', updateRegionFieldOnChange );
// Set Country.
$( '#edd_order_address_country' )
.val( address.country )
.trigger( 'change' )
.trigger( 'chosen:updated' );
// Set Region.
getStates(
$( '#edd_order_address_country' ),
'edd_order_address[region]',
'edd_order_address_region'
)
.done( replaceRegionField )
.done( ( response ) => {
$( '#edd_order_address_region' )
.val( address.region )
.trigger( 'change' )
.trigger( 'chosen:updated' );
} );
// Add back global `change` event handling.
$( '#edd_order_address_country' ).on( 'change', updateRegionFieldOnChange );
return false;
} );
// Country change.
$( '#edd_order_address_country' ).on( 'change', updateRegionFieldOnChange );
} );

View File

@ -0,0 +1,69 @@
/* global $ */
/**
* Internal dependencies
*/
import { jQueryReady } from 'utils/jquery.js';
jQueryReady( () => {
// Change Customer.
$( '.edd-payment-change-customer-input' ).on( 'change', function() {
const $this = $( this ),
data = {
action: 'edd_customer_details',
customer_id: $this.val(),
nonce: $( '#edd_customer_details_nonce' ).val(),
};
if ( '' === data.customer_id ) {
return;
}
$( '.customer-details' ).css( 'display', 'none' );
$( '#customer-avatar' ).html( '<span class="spinner is-active"></span>' );
$.post( ajaxurl, data, function( response ) {
const { success, data } = response;
if ( success ) {
$( '.customer-details' ).css( 'display', 'flex' );
$( '.customer-details-wrap' ).css( 'display', 'flex' );
$( '#customer-avatar' ).html( data.avatar );
$( '.customer-name' ).html( data.name );
$( '.customer-since span' ).html( data.date_created_i18n );
$( '.customer-record a' ).prop( 'href', data._links.self );
} else {
$( '.customer-details-wrap' ).css( 'display', 'none' );
}
}, 'json' );
} );
$( '.edd-payment-change-customer-input' ).trigger( 'change' );
// New Customer.
$( '#edd-customer-details' ).on( 'click', '.edd-payment-new-customer, .edd-payment-new-customer-cancel', function( e ) {
e.preventDefault();
var new_customer = $( this ).hasClass( 'edd-payment-new-customer' ),
cancel = $( this ).hasClass( 'edd-payment-new-customer-cancel' );
if ( new_customer ) {
$( '.order-customer-info' ).hide();
$( '.new-customer' ).show();
} else if ( cancel ) {
$( '.order-customer-info' ).show();
$( '.new-customer' ).hide();
}
var new_customer = $( '#edd-new-customer' );
if ( $( '.new-customer' ).is( ':visible' ) ) {
new_customer.val( 1 );
} else {
new_customer.val( 0 );
}
} );
} );

View File

@ -0,0 +1,3 @@
import './address.js';
import './customer.js';
import './receipt.js';

View File

@ -0,0 +1,32 @@
/* global $, ajaxurl */
/**
* Internal dependencies
*/
import { jQueryReady } from 'utils/jquery.js';
jQueryReady( () => {
const emails_wrap = $( '.edd-order-resend-receipt-addresses' );
$( document.body ).on( 'click', '#edd-select-receipt-email', function( e ) {
e.preventDefault();
emails_wrap.slideDown();
} );
$( document.body ).on( 'change', '.edd-order-resend-receipt-email', function() {
const selected = $('input:radio.edd-order-resend-receipt-email:checked').val();
$( '#edd-select-receipt-email').data( 'email', selected );
} );
$( document.body).on( 'click', '#edd-select-receipt-email', function () {
if ( confirm( edd_vars.resend_receipt ) ) {
const href = $( this ).prop( 'href' ) + '&email=' + $( this ).data( 'email' );
window.location = href;
}
} );
$( document.body ).on( 'click', '#edd-resend-receipt', function() {
return confirm( edd_vars.resend_receipt );
} );
} );

View File

@ -0,0 +1,279 @@
import { NumberFormat } from '@easy-digital-downloads/currency';
const number = new NumberFormat();
/* global eddAdminOrderOverview */
// Loads the modal when the refund button is clicked.
$(document.body).on('click', '.edd-refund-order', function (e) {
e.preventDefault();
var link = $(this),
postData = {
action : 'edd_generate_refund_form',
order_id: $('input[name="edd_payment_id"]').val(),
};
$.ajax({
type : 'POST',
data : postData,
url : ajaxurl,
success: function success(data) {
let modal_content = '';
if (data.success) {
modal_content = data.html;
} else {
modal_content = data.message;
}
$('#edd-refund-order-dialog').dialog({
position: { my: 'top center', at: 'center center-25%' },
width : '75%',
modal : true,
resizable: false,
draggable: false,
classes: {
'ui-dialog': 'edd-dialog',
},
closeText: eddAdminOrderOverview.i18n.closeText,
open: function( event, ui ) {
$(this).html( modal_content );
},
close: function( event, ui ) {
$( this ).html( '' );
if ( $( this ).hasClass( 'did-refund' ) ) {
location.reload();
}
}
});
return false;
}
}).fail(function (data) {
$('#edd-refund-order-dialog').dialog({
position: { my: 'top center', at: 'center center-25%' },
width : '75%',
modal : true,
resizable: false,
draggable: false
}).html(data.message);
return false;
});
});
$( document.body ).on( 'click', '.ui-widget-overlay', function ( e ) {
$( '#edd-refund-order-dialog' ).dialog( 'close' );
} );
/**
* Listen for the bulk actions checkbox, since WP doesn't trigger a change on sub-items.
*/
$( document.body ).on( 'change', '#edd-refund-order-dialog #cb-select-all-1', function () {
const itemCheckboxes = $( '.edd-order-item-refund-checkbox' );
const isChecked = $( this ).prop( 'checked' );
itemCheckboxes.each( function() {
$( this ).prop( 'checked', isChecked ).trigger( 'change' );
} );
} );
/**
* Listen for individual checkbox changes.
* When it does, trigger a quantity change.
*/
$( document.body ).on( 'change', '.edd-order-item-refund-checkbox', function () {
const parent = $( this ).parent().parent();
const quantityField = parent.find( '.edd-order-item-refund-quantity' );
if ( quantityField.length ) {
if ( $( this ).prop( 'checked' ) ) {
// Triggering a change on the quantity field handles enabling the inputs.
quantityField.trigger( 'change' );
} else {
// Disable inputs and recalculate total.
parent.find( '.edd-order-item-refund-input' ).prop( 'disabled', true );
recalculateRefundTotal();
}
}
} );
/**
* Handles quantity changes, which includes items in the refund.
*/
$( document.body ).on( 'change', '#edd-refund-order-dialog .edd-order-item-refund-input', function () {
let parent = $( this ).closest( '.refunditem' ),
quantityField = parent.find( '.edd-order-item-refund-quantity' ),
quantity = parseInt( quantityField.val() );
if ( quantity > 0 ) {
parent.addClass( 'refunded' );
} else {
parent.removeClass( 'refunded' );
}
// Only auto calculate subtotal / tax if we've adjusted the quantity.
if ( $( this ).hasClass( 'edd-order-item-refund-quantity' ) ) {
// Enable/disable amount fields.
parent.find( '.edd-order-item-refund-input:not(.edd-order-item-refund-quantity)' ).prop( 'disabled', quantity === 0 );
if ( quantity > 0 ) {
quantityField.prop( 'disabled', false );
}
let subtotalField = parent.find( '.edd-order-item-refund-subtotal' ),
taxField = parent.find( '.edd-order-item-refund-tax' ),
originalSubtotal = number.unformat( subtotalField.data( 'original' ) ),
originalTax = taxField.length ? number.unformat( taxField.data( 'original' ) ) : 0.00,
originalQuantity = parseInt( quantityField.data( 'max' ) ),
calculatedSubtotal = ( originalSubtotal / originalQuantity ) * quantity,
calculatedTax = taxField.length ? ( originalTax / originalQuantity ) * quantity : 0.00;
// Make sure totals don't go over maximums.
if ( calculatedSubtotal > parseFloat( subtotalField.data( 'max' ) ) ) {
calculatedSubtotal = subtotalField.data( 'max' );
}
if ( taxField.length && calculatedTax > parseFloat( taxField.data( 'max' ) ) ) {
calculatedTax = taxField.data( 'max' );
}
// Guess the subtotal and tax for the selected quantity.
subtotalField.val( number.format( calculatedSubtotal ) );
if ( taxField.length ) {
taxField.val( number.format( calculatedTax ) );
}
}
recalculateRefundTotal();
} );
/**
* Calculates all the final refund values.
*/
function recalculateRefundTotal() {
let newSubtotal = 0,
newTax = 0,
newTotal = 0,
canRefund = false,
allInputBoxes = $( '#edd-refund-order-dialog .edd-order-item-refund-input' ),
allReadOnly = $( '#edd-refund-order-dialog .edd-order-item-refund-input.readonly' );
// Set a readonly while we recalculate, to avoid race conditions in the browser.
allInputBoxes.prop( 'readonly', true );
// Loop over all order items.
$( '#edd-refund-order-dialog .edd-order-item-refund-quantity' ).each( function() {
const thisItemQuantity = parseInt( $( this ).val() );
if ( ! thisItemQuantity ) {
return;
}
const thisItemParent = $( this ).closest( '.refunditem' );
const thisItemSelected = thisItemParent.find( '.edd-order-item-refund-checkbox' ).prop( 'checked' );
if ( ! thisItemSelected ) {
thisItemParent.removeClass( 'refunded' );
return;
}
// Values for this item.
let thisItemTax = 0.00;
let thisItemSubtotal = number.unformat( thisItemParent.find( '.edd-order-item-refund-subtotal' ).val() );
if ( thisItemParent.find( '.edd-order-item-refund-tax' ).length ) {
thisItemTax = number.unformat( thisItemParent.find( '.edd-order-item-refund-tax' ).val() );
}
let thisItemTotal = thisItemSubtotal + thisItemTax;
thisItemParent.find( '.column-total span' ).text( number.format( thisItemTotal ) );
// Negate amounts if working with credit.
if ( thisItemParent.data( 'credit' ) ) {
thisItemSubtotal = thisItemSubtotal * -1;
thisItemTax = thisItemTax * -1;
thisItemTotal = thisItemTotal * -1;
}
// Only include order items in the subtotal.
if ( thisItemParent.data( 'orderItem' ) ) {
newSubtotal += thisItemSubtotal;
}
newTax += thisItemTax;
newTotal += thisItemTotal;
} );
if ( parseFloat( newTotal ) > 0 ) {
canRefund = true;
}
$( '#edd-refund-submit-subtotal-amount' ).text( number.format( newSubtotal ) );
$( '#edd-refund-submit-tax-amount' ).text( number.format( newTax ) );
$( '#edd-refund-submit-total-amount' ).text( number.format( newTotal ) );
$( '#edd-submit-refund-submit' ).attr( 'disabled', ! canRefund );
// Remove the readonly.
allInputBoxes.prop( 'readonly', false );
allReadOnly.prop( 'readonly', true );
}
/**
* Process the refund form after the button is clicked.
*/
$(document.body).on( 'click', '#edd-submit-refund-submit', function(e) {
e.preventDefault();
$('.edd-submit-refund-message').removeClass('success').removeClass('fail');
$( this ).removeClass( 'button-primary' ).attr( 'disabled', true ).addClass( 'updating-message' );
$('#edd-submit-refund-status').hide();
const refundForm = $( '#edd-submit-refund-form' );
const refundData = refundForm.serialize();
var postData = {
action: 'edd_process_refund_form',
data: refundData,
order_id: $('input[name="edd_payment_id"]').val()
};
$.ajax({
type : 'POST',
data : postData,
url : ajaxurl,
success: function success(response) {
const message_target = $('.edd-submit-refund-message'),
url_target = $('.edd-submit-refund-url');
if ( response.success ) {
message_target.text(response.data.message).addClass('success');
url_target.attr( 'href', response.data.refund_url ).show();
$( '#edd-submit-refund-status' ).show();
url_target.focus();
$( '#edd-refund-order-dialog' ).addClass( 'did-refund' );
} else {
message_target.html(response.data).addClass('fail');
url_target.hide();
$('#edd-submit-refund-status').show();
$( '#edd-submit-refund-submit' ).attr( 'disabled', false ).removeClass( 'updating-message' ).addClass( 'button-primary' );
}
}
} ).fail( function ( data ) {
const message_target = $('.edd-submit-refund-message'),
url_target = $('.edd-submit-refund-url'),
json = data.responseJSON;
message_target.text( json.data ).addClass( 'fail' );
url_target.hide();
$( '#edd-submit-refund-status' ).show();
$( '#edd-submit-refund-submit' ).attr( 'disabled', false ).removeClass( 'updating-message' ).addClass( 'button-primary' );
return false;
});
});
// Initialize WP toggle behavior for the modal.
$( document.body ).on( 'click', '.refund-items .toggle-row', function () {
$( this ).closest( 'tr' ).toggleClass( 'is-expanded' );
} );

View File

@ -0,0 +1,101 @@
/* global Backbone */
/**
* Internal dependencies
*/
import { OrderAdjustment } from './../models/order-adjustment.js';
import { OrderAdjustmentDiscount } from './../models/order-adjustment-discount.js';
/**
* Collection of `OrderAdjustment`s.
*
* @since 3.0
*
* @class Adjustments
* @augments Backbone.Collection
*/
export const OrderAdjustments = Backbone.Collection.extend( {
/**
* @since 3.0
*/
comparator: 'type',
/**
* Initializes the `OrderAdjustments` collection.
*
* @since 3.0
*
* @constructs OrderAdjustments
* @augments Backbone.Collection
*/
initialize() {
this.getByType = this.getByType.bind( this );
},
/**
* Determines which Model to use and instantiates it.
*
* @since 3.0
*
* @param {Object} attributes Model attributes.
* @param {Object} options Model options.
*/
model( attributes, options ) {
let model;
switch ( attributes.type ) {
case 'discount':
model = new OrderAdjustmentDiscount( attributes, options );
break;
default:
model = new OrderAdjustment( attributes, options );
}
return model;
},
/**
* Defines the model's attribute that defines it's ID.
*
* Uses the `OrderAdjustment`'s Type ID.
*
* @since 3.0
*
* @param {Object} attributes Model attributes.
* @return {number}
*/
modelId( attributes ) {
return `${ attributes.type }-${ attributes.typeId }-${ attributes.description }`;
},
/**
* Determines if `OrderAdjustments` contains a specific `OrderAdjustment`.
*
* @since 3.0
*
* @param {OrderAdjustment} model Model to look for.
* @return {bool} True if the Collection contains the Model.
*/
has( model ) {
return (
undefined !==
this.findWhere( {
typeId: model.get( 'typeId' ),
} )
);
},
/**
* Returns a list of `OrderAdjustment`s by type.
*
* @since 3.0
*
* @param {string} type Type of adjustment to retrieve. `fee`, `credit`, or `discount`.
* @return {Array} List of type-specific adjustments.
*/
getByType( type ) {
return this.where( {
type,
} );
},
} );

View File

@ -0,0 +1,137 @@
/* global Backbone, $, _ */
/**
* External dependencies
*/
import uuid from 'uuid-random';
/**
* Internal dependencies
*/
import { OrderAdjustments } from './../collections/order-adjustments.js';
import { OrderAdjustmentDiscount } from './../models/order-adjustment-discount.js';
import { OrderItem } from './../models/order-item.js';
/**
* Collection of `OrderItem`s.
*
* @since 3.0
*
* @class OrderItems
* @augments Backbone.Collection
*/
export const OrderItems = Backbone.Collection.extend( {
/**
* @since 3.0
*
* @type {OrderItem}
*/
model: OrderItem,
/**
* Ensures `OrderItems` has access to the current state through a similar
* interface as Views. BackBone.Collection does not automatically set
* passed options as a property.
*
* @since 3.0
*
* @param {null|Array} models List of Models.
* @param {Object} options Collection options.
*/
preinitialize( models, options ) {
this.options = options;
},
/**
* Determines if `OrderItems` contains a specific `OrderItem`.
*
* Uses the `OrderItem`s Product ID and Price ID to create a unique
* value to check against.
*
* @since 3.0
*
* @param {OrderItem} model Model to look for.
* @return {bool} True if the Collection contains the Model.
*/
has( model ) {
const duplicates = this.filter( ( item ) => {
const itemId =
item.get( 'productId' ) + '_' + item.get( 'priceId' );
const modelId =
model.get( 'productId' ) + '_' + model.get( 'priceId' );
return itemId === modelId;
} );
return duplicates.length > 0;
},
/**
* Updates the amounts for all current `OrderItem`s.
*
* @since 3.0
*
* @return {$.promise} A jQuery promise representing zero or more requests.
*/
updateAmounts() {
const { options } = this;
const { state } = options;
const items = state.get( 'items' );
const discounts = new Backbone.Collection(
state.get( 'adjustments' ).getByType( 'discount' )
);
const args = {
country: state.getTaxCountry(),
region: state.getTaxRegion(),
products: items.map( ( item ) => ( {
id: item.get( 'productId' ),
quantity: item.get( 'quantity' ),
options: {
price_id: item.get( 'priceId' ),
}
} ) ),
discountIds: discounts.pluck( 'typeId' ),
};
// Keep track of all jQuery Promises.
const promises = [];
// Find each `OrderItem`'s amounts.
items.models.forEach( ( item ) => {
const getItemAmounts = item.getAmounts( args );
getItemAmounts
// Update `OrderItem`-level Adjustments.
.done( ( { adjustments } ) => {
// Map returned Discounts to `OrderAdjustmentDiscount`.
const orderItemDiscounts = adjustments.map( ( adjustment ) => {
return new OrderAdjustmentDiscount( {
...adjustment,
id: uuid(),
objectId: item.get( 'id' ),
} );
} );
// Gather existing `fee` and `credit` `OrderItem`-level Adjustments.
const orderItemAdjustments = item.get( 'adjustments' ).filter( ( adjustment ) => {
return [ 'fee', 'credit' ].includes( adjustment.type );
} );
// Reset `OrderAdjustments` collection with new data.
item.set( 'adjustments', new OrderAdjustments( [
...orderItemDiscounts,
...orderItemAdjustments,
] ) );
} )
// Update individual `OrderItem`s and `OrderAdjustment`s with new amounts.
.done( ( response ) => item.setAmounts( response ) );
// Track jQuery Promise.
promises.push( getItemAmounts );
} );
return $.when.apply( $, promises );
},
} );

View File

@ -0,0 +1,21 @@
/* global Backbone */
/**
* Internal dependencies
*/
import { OrderRefund } from './../models/order-refund.js';
/**
* Collection of `OrderRefund`s.
*
* @since 3.0
*
* @class OrderRefunds
* @augments Backbone.Collection
*/
export const OrderRefunds = Backbone.Collection.extend( {
/**
* @since 3.0
*/
model: OrderRefund,
} );

View File

@ -0,0 +1,106 @@
/**
* Internal dependencies
*/
import { Currency, NumberFormat } from '@easy-digital-downloads/currency';
import { Overview } from './views/overview.js';
import { OrderItems } from './collections/order-items.js';
import { OrderItem } from './models/order-item.js';
import { OrderAdjustments } from './collections/order-adjustments.js';
import { OrderRefunds } from './collections/order-refunds.js';
import { State } from './models/state.js';
// Temporarily include old Refund flow.
import './_refund.js';
let overview;
( () => {
if ( ! window.eddAdminOrderOverview ) {
return;
}
const {
isAdding,
isRefund,
hasTax,
hasQuantity,
hasDiscounts,
order,
items,
adjustments,
refunds,
} = window.eddAdminOrderOverview;
const currencyFormatter = new Currency( {
currency: order.currency,
currencySymbol: order.currencySymbol,
} );
// Create and hydrate state.
const state = new State( {
isAdding: '1' === isAdding,
isRefund: '1' === isRefund,
hasTax: '0' === hasTax ? false : hasTax,
hasQuantity: '1' === hasQuantity,
hasDiscounts: '1' === hasDiscounts,
formatters: {
currency: currencyFormatter,
// Backbone doesn't merge nested defaults.
number: new NumberFormat(),
},
order,
} );
// Create collections and add to state.
state.set( {
items: new OrderItems( null, {
state,
} ),
adjustments: new OrderAdjustments( null, {
state,
} ),
refunds: new OrderRefunds( null, {
state,
} ),
} );
// Create Overview.
overview = new Overview( {
state,
} );
// Hydrate collections.
// Hydrate `OrderItem`s.
//
// Models are created manually before being added to the collection to
// ensure attributes maintain schema with deep model attributes.
items.forEach( ( item ) => {
const orderItemAdjustments = new OrderAdjustments( item.adjustments );
const orderItem = new OrderItem( {
...item,
adjustments: orderItemAdjustments,
state,
} );
state.get( 'items' ).add( orderItem );
} );
// Hyrdate `Order`-level `Adjustments`.
adjustments.forEach( ( adjustment ) => {
state.get( 'adjustments' ).add( {
state,
...adjustment,
} )
} );
// Hydrate `OrderRefund`s.
refunds.forEach( ( refund ) => {
state.get( 'refunds' ).add( {
state,
...refund,
} );
} );
} ) ();
export default overview;

View File

@ -0,0 +1,76 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderAdjustment } from './order-adjustment.js';
/**
* OrderAdjustmentDiscount
*
* @since 3.0
*
* @class OrderAdjustmentDiscount
* @augments Backbone.Model
*/
export const OrderAdjustmentDiscount = OrderAdjustment.extend( {
/**
* @since 3.0
*
* @typedef {Object} OrderAdjustmentDiscount
*/
defaults: {
...OrderAdjustment.prototype.defaults,
type: 'discount',
},
/**
* @since 3.0
*/
idAttribute: 'typeId',
/**
* Returns the `OrderAdjustmentDiscount`'s amount based on the current values
* of all `OrderItems` discounts.
*
* @since 3.0
*
* @return {number} `OrderAdjustmentDiscount` amount.
*/
getAmount() {
let amount = 0;
const state = this.get( 'state' );
// Return stored amount if viewing an existing Order.
if ( false === state.get( 'isAdding' ) ) {
return OrderAdjustment.prototype.getAmount.apply( this, arguments );
}
const { models: items } = state.get( 'items' );
const { number } = state.get( 'formatters' );
items.forEach( ( item ) => {
const discount = item.get( 'adjustments' ).findWhere( {
typeId: this.get( 'typeId' ),
} );
if ( undefined !== discount ) {
amount += number.unformat(
number.format( discount.get( 'subtotal' ) )
);
}
} );
return amount;
},
/**
* Returns the `OrderAdjustment` total.
*
* @since 3.0
*/
getTotal() {
return this.getAmount();
},
} );

View File

@ -0,0 +1,101 @@
/* global Backbone */
/**
* OrderAdjustment
*
* @since 3.0
*
* @class OrderAdjustment
* @augments Backbone.Model
*/
export const OrderAdjustment = Backbone.Model.extend( {
/**
* @since 3.0
*
* @typedef {Object} OrderAdjustment
*/
defaults: {
id: 0,
objectId: 0,
objectType: '',
typeId: 0,
type: '',
description: '',
subtotal: 0,
tax: 0,
total: 0,
dateCreated: '',
dateModified: '',
uuid: '',
},
/**
* Returns the `OrderAdjustment` amount.
*
* Separate from subtotal or total calculation so `OrderAdjustmentDiscount`
* can be calculated independently.
*
* @see OrderAdjustmentDiscount.prototype.getAmount()
*
* @since 3.0
*/
getAmount() {
return this.get( 'subtotal' );
},
/**
* Retrieves the `OrderAdjustment` tax.
*
* @since 3.0.0
*
* @return {number} Total amount.
*/
getTax() {
return this.get( 'tax' );
},
/**
* Returns the `OrderAdjustment` total.
*
* @since 3.0
*/
getTotal() {
// Fees always have tax added exclusively.
// @link https://github.com/easydigitaldownloads/easy-digital-downloads/issues/2445#issuecomment-53215087
// @link https://github.com/easydigitaldownloads/easy-digital-downloads/blob/f97f4f6f5454921a2014dc1fa8f4caa5f550108c/includes/cart/class-edd-cart.php#L1306-L1311
return this.get( 'subtotal' ) + this.get( 'tax' );
},
/**
* Recalculates the tax amount based on the current tax rate.
*
* @since 3.0.0
*/
updateTax() {
const state = this.get( 'state' );
const hasTax = state.get( 'hasTax' );
if (
'none' === hasTax ||
'' === hasTax.country ||
'' === hasTax.rate
) {
return;
}
const { number } = state.get( 'formatters' );
const taxRate = hasTax.rate / 100;
const adjustments = state.get( 'adjustments' ).getByType( 'fee' );
adjustments.forEach( ( adjustment ) => {
if ( false === adjustment.get( 'isTaxable' ) ) {
return;
}
const taxableAmount = adjustment.getAmount();
const taxAmount = number.unformat( taxableAmount * taxRate );
adjustment.set( 'tax', taxAmount );
} );
}
} );

View File

@ -0,0 +1,250 @@
/* global Backbone, _, $ */
/**
* Internal dependencies
*/
import { OrderAdjustments } from './../collections/order-adjustments.js';
/**
* OrderItem
*
* @since 3.0
*
* @class OrderItem
* @augments Backbone.Model
*/
export const OrderItem = Backbone.Model.extend( {
/**
* @since 3.0
*
* @typedef {Object} OrderItem
*/
defaults: {
id: 0,
orderId: 0,
productId: 0,
productName: '',
priceId: null,
cartIndex: 0,
type: 'download',
status: '',
statusLabel: '',
quantity: 1,
amount: 0,
subtotal: 0,
discount: 0,
tax: 0,
total: 0,
dateCreated: '',
dateModified: '',
uuid: '',
// Track manually set amounts.
amountManual: 0,
taxManual: 0,
subtotalManual: 0,
// Track if the amounts have been adjusted manually on addition.
_isAdjustingManually: false,
// Track `OrderItem`-level adjustments.
//
// The handling of Adjustments in the API is currently somewhat
// fragmented with certain extensions creating Adjustments at the
// `Order` level, some at a duplicate `OrderItem` level, and some both.
adjustments: new OrderAdjustments(),
},
/**
* Returns the `OrderItem` subtotal amount.
*
* @since 3.0.0
*
* @param {bool} includeTax If taxes should be included when retrieving the subtotal.
* This is needed in some scenarios with inclusive taxes.
* @return {number} Subtotal amount.
*/
getSubtotal( includeTax = false ) {
const state = this.get( 'state' );
const subtotal = this.get( 'subtotal' );
// Use stored value if the record has already been created.
if ( false === state.get( 'isAdding' ) ) {
return subtotal;
}
// Calculate subtotal.
if ( true === state.hasInclusiveTax() && false === includeTax ) {
return subtotal - this.getTax();
}
return subtotal;
},
/**
* Returns the Discount amount.
*
* If an Order is being added the amount is calculated based
* on the total of `OrderItem`-level Adjustments that are
* currently applied.
*
* If an Order has already been added use the amount stored
* directly in the database.
*
* @since 3.0
*
* @return {number} Discount amount.
*/
getDiscountAmount() {
let amount = 0;
const discounts = this.get( 'adjustments' ).getByType( 'discount' );
if ( 0 === discounts.length ) {
return this.get( 'discount' );
}
discounts.forEach( ( discount ) => {
amount += +discount.get( 'subtotal' );
} );
return amount;
},
/**
* Retrieves the rounded Tax for the order item.
*
* Rounded to match storefront checkout.
*
* @since 3.0.0
*
* @return {number} Total amount.
*/
getTax() {
const state = this.get( 'state' );
const tax = this.get( 'tax' );
// Use stored value if the record has already been created.
if ( false === state.get( 'isAdding' ) ) {
return tax;
}
// Calculate tax.
const { number } = state.get( 'formatters' );
return number.unformat( number.format( tax ) );
},
/**
* Retrieves the Total for the order item.
*
* @since 3.0.0
*
* @return {number} Total amount.
*/
getTotal() {
const state = this.get( 'state' );
// Use stored value if the record has already been created.
if ( false === state.get( 'isAdding' ) ) {
return this.get( 'total' );
}
// Calculate total.
if ( true === state.hasInclusiveTax() ) {
return this.get( 'subtotal' ) - this.getDiscountAmount();
}
return ( this.get( 'subtotal' ) - this.getDiscountAmount() ) + this.getTax();
},
/**
* Retrieves amounts for the `OrderItem` based on other `OrderItem`s and `OrderAdjustment`s.
*
* @since 3.0
*
* @param {Object} args Arguments to pass as data in the XHR request.
* @param {string} args.country Country code to determine tax rate.
* @param {string} args.region Region to determine tax rate.
* @param {Array} args.products List of current products added to the order.
* @param {Array} args.discountIds List of `OrderAdjustmentDiscount`s to calculate amounts against.
* @return {$.promise} A jQuery promise that represents the request.
*/
getAmounts( {
country = '',
region = '',
products = [],
discountIds = [],
} ) {
const {
nonces: { edd_admin_order_get_item_amounts: nonce },
} = window.eddAdminOrderOverview;
const { productId, priceId, quantity, amount, tax, subtotal } = _.clone(
this.attributes
);
return wp.ajax.send( 'edd-admin-order-get-item-amounts', {
data: {
nonce,
productId,
priceId,
quantity,
amount,
tax,
subtotal,
country,
region,
products: _.uniq( [
...products,
{
id: productId,
quantity,
options: {
price_id: priceId,
},
},
], function( { id, options: { price_id } } ) {
return `${ id }_${ price_id }`
} ),
discounts: _.uniq( discountIds ),
},
} );
},
/**
* Bulk sets amounts.
*
* Only adjusts the Discount amount if adjusting manually.
*
* @since 3.0
*
* @param {Object} amounts Amounts to set.
* @param {number} amounts.amount `OrderItem` unit price.
* @param {number} amounts.discount `OrderItem` discount amount.
* @param {number} amounts.tax `OrderItem` tax amount.
* @param {number} amounts.subtotal `OrderItem` subtotal amount.
* @param {number} amounts.total `OrderItem` total amount.
*/
setAmounts( {
amount = 0,
discount = 0,
tax = 0,
subtotal = 0,
total = 0,
} ) {
if ( true === this.get( '_isAdjustingManually' ) ) {
this.set( {
discount,
} );
} else {
this.set( {
amount,
discount,
tax,
subtotal,
total,
} );
}
},
} );

View File

@ -0,0 +1,24 @@
/* global Backbone */
/**
* OrderRefund
*
* @since 3.0
*
* @class OrderRefund
* @augments Backbone.Model
*/
export const OrderRefund = Backbone.Model.extend( {
/**
* @since 3.0
*
* @typedef {Object} OrderAdjustment
*/
defaults: {
id: 0,
number: '',
total: 0,
dateCreated: '',
dateCreatedi18n: '',
},
} );

View File

@ -0,0 +1,233 @@
/* global Backbone, _ */
/**
* Internal dependencies
*/
import { Currency, NumberFormat } from '@easy-digital-downloads/currency';
/**
* State
*
* Leverages `Backbone.Model` and subsequently `Backbone.Events`
* to easily track changes to top level state changes.
*
* @since 3.0
*
* @class State
* @augments Backbone.Model
*/
export const State = Backbone.Model.extend(
/** Lends State.prototype */ {
/**
* @since 3.0
*
* @typedef {Object} State
*/
defaults: {
isAdding: false,
isFetching: false,
hasQuantity: false,
hasTax: false,
items: [],
adjustments: [],
refunds: [],
formatters: {
currency: new Currency(),
number: new NumberFormat(),
},
},
/**
* Returns the current tax rate's country code.
*
* @since 3.0
*
* @return {string} Tax rate country code.
*/
getTaxCountry() {
return false !== this.get( 'hasTax' )
? this.get( 'hasTax' ).country
: '';
},
/**
* Returns the current tax rate's region.
*
* @since 3.0
*
* @return {string} Tax rate region.
*/
getTaxRegion() {
return false !== this.get( 'hasTax' )
? this.get( 'hasTax' ).region
: '';
},
/**
* Retrieves the Order subtotal.
*
* @since 3.0
*
* @param {bool} includeTax If taxes should be included when retrieving the subtotal.
* This is needed in some scenarios with inclusive taxes.
* @return {number} Order subtotal.
*/
getSubtotal( includeTax = false ) {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).subtotal;
}
const { models: items } = this.get( 'items' );
return items.reduce(
( amount, item ) => {
return amount += +item.getSubtotal( includeTax );
},
0
);
},
/**
* Retrieves the Order discount.
*
* @since 3.0
*
* @return {number} Order discount.
*/
getDiscount() {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).discount;
}
const adjustments = this.get( 'adjustments' ).getByType( 'discount' );
return adjustments.reduce(
( amount, adjustment ) => {
return amount += +adjustment.getAmount();
},
0
);
},
/**
* Retrieves the Order tax.
*
* @since 3.0
*
* @return {number} Order tax.
*/
getTax() {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).tax;
}
const items = this.get( 'items' ).models;
const feesTax = this.getFeesTax();
return items.reduce(
( amount, item ) => {
return amount += +item.getTax();
},
feesTax
);
},
/**
* Retrieves the Order tax amount for fees.
*
* @since 3.0
*
* @return {number} Order tax amount for fees.
*/
getFeesTax() {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).tax;
}
const adjustments = this.get( 'adjustments' ).getByType( 'fee' );
return adjustments.reduce(
( amount, item ) => {
return amount += +item.getTax();
},
0
);
},
/**
* Retrieves the Order total.
*
* @since 3.0
*
* @return {number} Order total.
*/
getTotal() {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).total;
}
// Calculate all adjustments that affect the total.
const { models: adjustments } = this.get( 'adjustments' );
const includeTaxInSubtotal = true;
const adjustedSubtotal = adjustments.reduce(
( amount, adjustment ) => {
if (
[ 'discount', 'credit' ].includes(
adjustment.get( 'type' )
)
) {
return amount -= +adjustment.getAmount();
} else {
return amount += +adjustment.get( 'subtotal' );
}
},
this.getSubtotal( includeTaxInSubtotal )
);
if ( true === this.hasInclusiveTax() ) {
// Fees always have tax added exclusively.
// @link https://github.com/easydigitaldownloads/easy-digital-downloads/issues/2445#issuecomment-53215087
// @link https://github.com/easydigitaldownloads/easy-digital-downloads/blob/f97f4f6f5454921a2014dc1fa8f4caa5f550108c/includes/cart/class-edd-cart.php#L1306-L1311
return adjustedSubtotal + this.getFeesTax();
}
return adjustedSubtotal + this.getTax();
},
/**
* Determines if the state has a new, valid, tax rate.
*
* @since 3.0
*
* @return {bool} True if the rate has changed.
*/
hasNewTaxRate() {
const hasTax = this.get( 'hasTax' );
if ( false === hasTax ) {
return false;
}
const prevHasTax = this.previous( 'hasTax' );
return ! _.isEqual( hasTax, prevHasTax );
},
/**
* Determines if the state has prices entered inclusive of tax.
*
* @since 3.0
*
* @returns {bool} True if prices are entered inclusive of tax.
*/
hasInclusiveTax() {
return this.get( 'hasTax' ).inclusive;
}
}
);

View File

@ -0,0 +1,91 @@
/**
* Internal dependencies
*/
import { edd_attach_tooltips as setupTooltips } from 'admin/components/tooltips';
import { FormAddOrderItem } from './form-add-order-item.js';
import { FormAddOrderDiscount } from './form-add-order-discount.js';
import { FormAddOrderAdjustment } from './form-add-order-adjustment.js';
/**
* Actions
*
* @since 3.0
*
* @class Actions
* @augments wp.Backbone.View
*/
export const Actions = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
el: '#edd-order-overview-actions',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-actions' ),
/**
* @since 3.0
*/
events: {
'click #add-item': 'onAddOrderItem',
'click #add-discount': 'onAddOrderDiscount',
'click #add-adjustment': 'onAddOrderAdjustment',
},
/**
* Ensures tooltips can be used after render.
*
* @since 3.0
*
* @return {Object}
*/
render() {
wp.Backbone.View.prototype.render.apply( this, arguments );
// Setup Tooltips after render.
setupTooltips( $( '.edd-help-tip' ) );
return this;
},
/**
* Renders the "Add Item" flow.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onAddOrderItem( e ) {
e.preventDefault();
new FormAddOrderItem( this.options ).openDialog().render();
},
/**
* Renders the "Add Discount" flow.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onAddOrderDiscount( e ) {
e.preventDefault();
new FormAddOrderDiscount( this.options ).openDialog().render();
},
/**
* Renders the "Add Adjustment" flow.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onAddOrderAdjustment( e ) {
e.preventDefault();
new FormAddOrderAdjustment( this.options ).openDialog().render();
},
} );

View File

@ -0,0 +1,255 @@
/* global _, $ */
/**
* WordPress dependencies
*/
import { focus } from '@wordpress/dom';
/**
* Internal dependencies
*/
import { getChosenVars } from 'utils/chosen.js';
// Set noconflict when using Lodash (@wordpress packages) and Underscores.
// @todo Find a better place to set this up. Webpack?
window.lodash = _.noConflict();
/**
* Base
*
* Supplies additional functionality and helpers beyond
* what is provided by `wp.Backbone.View`.
*
* - Maintains focus and caret positioning on rendering.
* - Extends events via `addEvents()`.
*
* @since 3.0
*
* @class Base
* @augments wp.Backbone.View
*/
export const Base = wp.Backbone.View.extend( {
/**
* Defines base events to help maintain focus and caret position.
*
* @since 3.0
*/
events: {
'keydown input': 'handleTabBehavior',
'keydown textarea': 'handleTabBehavior',
'focus input': 'onFocus',
'focus textarea': 'onFocus',
'focus select': 'onFocus',
'change input': 'onChange',
'change textarea': 'onChange',
'change select': 'onChange',
},
/**
* Sets up additional properties.
*
* @since 3.0
*/
preinitialize() {
this.focusedEl = null;
this.focusedElCaretPos = 0;
wp.Backbone.View.prototype.preinitialize.apply( this, arguments );
},
/**
* Merges additional events with existing events.
*
* @since 3.0
*
* @param {Object} events Hash of events to add.
*/
addEvents( events ) {
this.delegateEvents( {
...this.events,
...events,
} );
},
/**
* Moves the focus when dealing with tabbing.
*
* @since 3.0
*
* @param {Object} e Keydown event.
*/
handleTabBehavior( e ) {
const { keyCode, shiftKey, target } = e;
// 9 = TAB
if ( 9 !== keyCode ) {
return;
}
const tabbables = focus.tabbable.find( this.el );
if ( ! tabbables.length ) {
return;
}
const firstTabbable = tabbables[ 0 ];
const lastTabbable = tabbables[ tabbables.length - 1 ];
let toFocus;
if ( shiftKey && target === firstTabbable ) {
toFocus = lastTabbable;
} else if ( ! shiftKey && target === lastTabbable ) {
toFocus = firstTabbable;
} else if ( shiftKey ) {
toFocus = focus.tabbable.findPrevious( target );
} else {
toFocus = focus.tabbable.findNext( target );
}
if ( 'undefined' !== typeof toFocus ) {
this.focusedEl = toFocus;
this.focusedElCartetPos = toFocus.value.length;
} else {
this.focusedEl = null;
this.focusedElCartetPos = 0;
}
},
/**
* Tracks the current element when focusing.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onFocus( e ) {
this.focusedEl = e.target;
},
/**
* Tracks the current cursor position when editing.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChange( e ) {
const { target, keyCode } = e;
// 9 = TAB
if ( undefined !== typeof keyCode && 9 === keyCode ) {
return;
}
try {
if ( target.selectionStart ) {
this.focusedElCaretPos = target.selectionStart;
}
} catch ( error ) {
this.focusedElCaretPos = target.value.length;
}
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
return this.model
? {
...this.model.toJSON(),
state: this.model.get( 'state' ).toJSON(),
}
: {};
},
/**
* Adds additional handling after initial render.
*
* @since 3.0
*/
render() {
wp.Backbone.View.prototype.render.apply( this, arguments );
this.initializeSelects();
this.setFocus();
return this;
},
/**
* Reinitializes special <select> fields.
*
* @since 3.0
*/
initializeSelects() {
const selects = this.el.querySelectorAll( '.edd-select-chosen' );
// Reinialize Chosen.js
_.each( selects, ( el ) => {
$( el ).chosen( {
...getChosenVars( $( el ) ),
width: '100%',
} );
} );
},
/**
* Sets the focus and caret position.
*
* @since 3.0
*/
setFocus() {
const { el, focusedEl, focusedElCaretPos } = this;
// Do nothing extra if nothing is focused.
if ( null === focusedEl || 'undefined' === typeof focusedEl ) {
return;
}
// Convert full element in to a usable selector.
// We can't search for the actual HTMLElement because
// the DOM has since changed.
let selector = null;
if ( '' !== focusedEl.id ) {
selector = `#${ focusedEl.id }`;
} else if ( '' !== focusedEl.name ) {
selector = `[name="${ focusedEl.name }"]`;
} else if ( focusedEl.classList.length > 0 ) {
selector = `.${ [ ...focusedEl.classList ].join( '.' ) }`;
}
// Do nothing if we can't generate a selector.
if ( null === selector ) {
return;
}
// Focus element.
const elToFocus = el.querySelector( selector );
if ( ! elToFocus ) {
return;
}
elToFocus.focus();
// Attempt to set the caret position.
try {
if ( elToFocus.setSelectionRange ) {
elToFocus.setSelectionRange(
focusedElCaretPos,
focusedElCaretPos
);
}
} catch ( error ) {}
},
} );

View File

@ -0,0 +1,113 @@
/**
* Internal dependencies
*/
import { Base } from './base.js';
import { Dialog } from './dialog.js';
/**
* "Copy Download Link" view
*
* @since 3.0
*
* @class FormAddOrderItem
* @augments Dialog
*/
export const CopyDownloadLink = Dialog.extend( {
/**
* @since 3.0
*/
el: '#edd-admin-order-copy-download-link-dialog',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-copy-download-link' ),
/**
* "Copy Download Link" view.
*
* @since 3.0
*
* @constructs CopyDownloadLink
* @augments Base
*/
initialize() {
Dialog.prototype.initialize.apply( this, arguments );
this.link = false;
this.addEvents( {
'click #close': 'closeDialog',
} );
this.fetchLink.call( this );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { link } = this;
return {
link,
};
},
/**
* Renders the view.
*
* @since 3.0
*/
render() {
Base.prototype.render.apply( this, arguments );
const { el, link } = this;
// Select the contents if a link is available.
if ( false !== link && '' !== link ) {
el.querySelector( '#link' ).select();
}
},
/**
* Fetches the Download's file URLs.
*
* @since 3.0
*/
fetchLink() {
const { orderId, productId, priceId } = this.options;
// Retrieve and set link.
//
// We can't use wp.ajax.send because the `edd_ajax_generate_file_download_link()`
// does not send back JSON responses.
$.ajax( {
type: 'POST',
url: ajaxurl,
data: {
action: 'edd_get_file_download_link',
payment_id: orderId,
download_id: productId,
price_id: priceId,
},
} )
.done( ( link ) => {
link = link.trim();
if ( [ '-1', '-2', '-3', '-4', '' ].includes( link ) ) {
this.link = '';
} else {
this.link = link.trim();
}
} )
.done( () => this.render() );
},
} );

View File

@ -0,0 +1,79 @@
/* global eddAdminOrderOverview */
/**
* Internal dependencies
*/
import { Base } from './base.js';
/**
* "Dialog" view
*
* @since 3.0
*
* @class Dialog
* @augments Base
*/
export const Dialog = Base.extend( {
/**
* "Dialog" view.
*
* @since 3.0
*
* @constructs Dialog
* @augments wp.Backbone.View
*/
initialize() {
this.$el.dialog( {
position: {
my: 'top center',
at: 'center center-25%',
},
classes: {
'ui-dialog': 'edd-dialog',
},
closeText: eddAdminOrderOverview.i18n.closeText,
width: '350px',
modal: true,
resizable: false,
draggable: false,
autoOpen: false,
create: function() {
$( this ).css( 'maxWidth', '90vw' );
},
} );
},
/**
* Opens the jQuery UI Dialog containing this view.
*
* @since 3.0
*
* @return {Dialog} Current view.
*/
openDialog() {
this.$el.dialog( 'open' );
return this;
},
/**
* Closes the jQuery UI Dialog containing this view.
*
* @since 3.0
*
* @param {Object=} e Event that triggered the close.
* @return {Dialog} Current view.
*/
closeDialog( e ) {
if ( e && e.preventDefault ) {
e.preventDefault();
}
this.$el.dialog( 'close' );
// Prevent events from stacking.
this.undelegateEvents();
return this;
},
} );

View File

@ -0,0 +1,289 @@
/**
* External dependencies
*/
import uuid from 'uuid-random';
/**
* Internal dependencies
*/
import { Base } from './base.js';
import { Dialog } from './dialog.js';
import { OrderAdjustment } from './../models/order-adjustment.js';
/**
* FormAddOrderAdjustment
*
* @since 3.0
*
* @class FormAddOrderAdjustment
* @augments Dialog
*/
export const FormAddOrderAdjustment = Dialog.extend( {
/**
* @since 3.0
*/
el: '#edd-admin-order-add-adjustment-dialog',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-form-add-order-adjustment' ),
/**
* "Add Adjustment" view.
*
* @since 3.0
*
* @constructs FormAddOrderAdjustment
* @augments Dialog
*/
initialize() {
Dialog.prototype.initialize.apply( this, arguments );
// Delegate additional events.
this.addEvents( {
'change #object_type': 'onChangeObjectType',
'change [name="type"]': 'onChangeType',
'keyup #amount': 'onChangeAmount',
'change #no-tax': 'onHasTaxToggle',
'click #set-address': 'onSetAddress',
'keyup #description': 'onChangeDescription',
'submit form': 'onAdd',
} );
const { state } = this.options;
// Create a model `OrderAdjustment` to be added.
this.model = new OrderAdjustment( {
id: uuid(),
objectId: uuid(),
typeId: uuid(),
objectType: 'order',
type: 'fee',
amountManual: '',
isTaxed: true,
state,
} );
// Listen for events.
this.listenTo( this.model, 'change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add', this.closeDialog );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
return {
...Base.prototype.prepare.apply( this, arguments ),
// Pass existing OrderItems so we can apply a fee at OrderItem level.
orderItems: state.get( 'items' ).models.map( ( item ) => ( {
id: item.get( 'id' ),
productName: item.get( 'productName' ),
} ) ),
};
},
/**
* Updates the `OrderAdjustment` when the Object Type changes.
*
* @since 3.0
*
* @param {Object} e Change event
*/
onChangeObjectType( e ) {
const {
target: { options, selectedIndex },
} = e;
const selected = options[ selectedIndex ];
const objectType = selected.value;
let objectId = this.model.get( 'objectId' );
// Apply to a specific `OrderItem`.
if ( 'order_item' === objectType ) {
objectId = selected.dataset.orderItemId;
this.model.set( {
objectId,
objectType,
} );
// Apply to the whole order.
} else {
this.model.set( {
objectType,
objectId,
} );
}
},
/**
* Updates the `OrderAdjustment` when the Type changes.
*
* @since 3.0
*
* @param {Object} e Change event
*/
onChangeType( e ) {
const type = e.target.value;
this.model.set( 'type', type );
if ( 'credit' === type ) {
this.model.set( 'objectId', 0 );
this.model.set( 'objectType', 'order' );
}
},
/**
* Updates the `OrderAdjustment` when the Amount changes.
*
* @since 3.0
*
* @param {Object} e Change event
*/
onChangeAmount( e ) {
const { target } = e;
e.preventDefault();
const { state } = this.options;
const { number } = state.get( 'formatters' );
const amountManual = target.value;
const amountNumber = number.unformat( amountManual );
let taxNumber = 0;
const hasTax = state.get( 'hasTax' );
if (
true === this.model.get( 'isTaxed' ) &&
'fee' === this.model.get( 'type' ) &&
'none' !== hasTax &&
'' !== hasTax.country &&
'' !== hasTax.rate
) {
taxNumber = amountNumber * ( hasTax.rate / 100 );
}
this.model.set( {
amountManual,
subtotal: amountNumber,
total: amountNumber,
tax: number.unformat( number.format( taxNumber ) ),
} );
},
/**
* Toggles if the fee should be taxed.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onHasTaxToggle( e ) {
e.preventDefault();
const checked = e.target.checked;
const args = {
isTaxed: checked,
}
// Reset tax amount if it should not be taxed.
if ( false === checked ) {
args.tax = 0;
}
this.model.set( args );
},
/**
* Closes dialog and opens "Order Details - Address" section.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onSetAddress( e ) {
e.preventDefault();
this.closeDialog();
const button = $( '[href="#edd_general_address"]' );
if ( ! button ) {
return;
}
button.trigger( 'click' );
$( '#edd_order_address_country' ).trigger( 'focus' );
},
/**
* Updates the `OrderAdjustment` when the Description changes.
*
* @since 3.0
*
* @param {Object} e Change event
*/
onChangeDescription( e ) {
this.model.set( 'description', e.target.value );
},
/**
* Adds an `OrderAdjustment` to `OrderAdjustments`.
*
* @since 3.0
*
* @param {Object} e Submit event.
*/
onAdd( e ) {
e.preventDefault();
const { model, options } = this;
const { state } = options;
const adjustments = state.get( 'adjustments' );
const items = state.get( 'items' );
// Add at `OrderItem` level if necessary.
if ( 'order_item' === model.get( 'objectType' ) ) {
const orderItem = items.findWhere( {
id: model.get( 'objectId' ),
} );
orderItem.get( 'adjustments' ).add( model );
// Adding to the Collection doesn't bubble up a change event.
orderItem.trigger( 'change' );
model.set( 'objectType', 'order_item' );
} else {
// Add to `Order` level.
model.set( 'objectType', 'order' );
}
adjustments.add( model );
// Stop listening to the model in this view.
this.stopListening( model );
},
} );

View File

@ -0,0 +1,155 @@
/* global _ */
/**
* External dependencies
*/
import uuid from 'uuid-random';
/**
* Internal dependencies
*/
import { Dialog } from './dialog.js';
import { Base } from './base.js';
import { OrderAdjustmentDiscount } from './../models/order-adjustment-discount.js';
/**
* "Add Discount" view
*
* @since 3.0
*
* @class FormAddOrderDiscount
* @augments wp.Backbone.View
*/
export const FormAddOrderDiscount = Dialog.extend( {
/**
* @since 3.0
*/
el: '#edd-admin-order-add-discount-dialog',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-form-add-order-discount' ),
/**
* @since 3.0
*/
events: {
'submit form': 'onAdd',
'change #discount': 'onChangeDiscount',
},
/**
* "Add Discount" view.
*
* @since 3.0
*
* @constructs FormAddOrderAdjustment
* @augments wp.Backbone.View
*/
initialize() {
Dialog.prototype.initialize.apply( this, arguments );
const { state } = this.options;
// Create a fresh `OrderAdjustmentDiscount` to be added.
this.model = new OrderAdjustmentDiscount( {
id: uuid(),
typeId: uuid(),
state,
} );
// Listen for events.
this.listenTo( this.model, 'change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add', this.closeDialog );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
const _isDuplicate = state.get( 'adjustments' ).has( model );
return {
...Base.prototype.prepare.apply( this, arguments ),
_isDuplicate,
};
},
/**
* Updates the `OrderDiscounts` when the Discount changes.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChangeDiscount( e ) {
const { target: { selectedIndex, options } } = e;
const { model } = this;
e.preventDefault();
const discount = options[ selectedIndex ];
const adjustment = discount.dataset;
if ( '' === discount.value ) {
return model.set( OrderAdjustmentDiscount.prototype.defaults );
}
model.set( {
typeId: parseInt( discount.value ),
description: adjustment.code,
} );
},
/**
* Adds an `OrderAdjustmentDiscount` to `OrderAdjustments`.
*
* @since 3.0
*
* @param {Object} e Submit event.
*/
onAdd( e ) {
e.preventDefault();
const { model, options } = this;
const { state } = options;
state.set( 'isFetching', true );
const items = state.get( 'items' );
const adjustments = state.get( 'adjustments' );
// Add to collection but do not alert.
adjustments.add( model, {
silent: true,
} );
// Update all amounts with new item and alert when done.
items
.updateAmounts()
.done( () => {
// Stop listening to the model in this view.
this.stopListening( model );
// Alert of succesful addition.
adjustments.trigger( 'add', model );
// Clear fetching.
state.set( 'isFetching', false ) ;
} );
},
} );

View File

@ -0,0 +1,364 @@
/**
* External dependencies
*/
import uuid from 'uuid-random';
/**
* Internal dependencies
*/
import { Base } from './base.js';
import { Dialog } from './dialog.js';
import { OrderItem } from './../models/order-item.js';
/**
* "Add Item" view
*
* @since 3.0
*
* @class FormAddOrderItem
* @augments Dialog
*/
export const FormAddOrderItem = Dialog.extend( {
/**
* @since 3.0
*/
el: '#edd-admin-order-add-item-dialog',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-form-add-order-item' ),
/**
* "Add Item" view.
*
* @since 3.0
*
* @constructs FormAddOrderItem
* @augments Base
*/
initialize() {
Dialog.prototype.initialize.apply( this, arguments );
// Delegate additional events.
this.addEvents( {
'change #download': 'onChangeDownload',
'change #quantity': 'onChangeQuantity',
'change #auto-calculate': 'onAutoCalculateToggle',
'keyup #amount': 'onChangeAmount',
'keyup #tax': 'onChangeTax',
'keyup #subtotal': 'onChangeSubtotal',
'click #set-address': 'onSetAddress',
'submit form': 'onAdd',
} );
const { state } = this.options;
const id = uuid();
// Create a fresh `OrderItem` to be added.
this.model = new OrderItem( {
id,
orderId: id,
state,
error: false,
} );
// Listen for events.
this.listenTo( this.model, 'change', this.render );
this.listenTo( state, 'change:isFetching', this.render );
this.listenTo( state.get( 'items' ), 'add', this.closeDialog );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
const { number } = state.get( 'formatters' );
const quantity = model.get( 'quantity' );
let amount = number.format( model.get( 'amount' ) * quantity );
let tax = number.format( model.get( 'tax' ) * quantity );
let subtotal = number.format( model.get( 'subtotal' ) * quantity );
if ( true === model.get( '_isAdjustingManually' ) ) {
amount = model.get( 'amountManual' );
tax = model.get( 'taxManual' );
subtotal = model.get( 'subtotalManual' );
}
const isDuplicate = false === state.get( 'isFetching' ) && true === state.get( 'items' ).has( model );
const isAdjustingManually = model.get( '_isAdjustingManually' );
const error = model.get( 'error' );
const defaults = Base.prototype.prepare.apply( this, arguments );
return {
...defaults,
amountManual: amount,
taxManual: tax,
subtotalManual: subtotal,
state: {
...defaults.state,
isAdjustingManually,
isDuplicate,
error,
}
};
},
/**
* Updates the OrderItem when the Download changes.
*
* @since 3.0
*
* @param {Object} e Change event for Download selector.
*/
onChangeDownload( e ) {
const {
target: { options: selectOptions, selectedIndex },
} = e;
const { model, options } = this;
const { state } = options;
const { number } = state.get( 'formatters' );
// Find the selected Download.
const selected = selectOptions[ selectedIndex ];
// Set ID and Price ID.
let productId = selected.value;
let priceId = null;
const parts = productId.split( '_' );
productId = parseInt( parts[ 0 ] );
if ( parts[ 1 ] ) {
priceId = parseInt( parts[ 1 ] );
}
state.set( 'isFetching', true );
// Update basic attributes.
model.set( {
productId,
priceId,
productName: selected.text,
error: false,
} );
// Update amount attributes.
model
.getAmounts( {
country: state.getTaxCountry(),
region: state.getTaxRegion(),
products: state.get( 'items' ).map( ( item ) => ( {
id: item.get( 'productId' ),
quantity: item.get( 'quantity' ),
options: {
price_id: item.get( 'priceId' ),
}
} ) ),
discountIds: state.get( 'adjustments' ).pluck( 'typeId' ),
} )
.fail( ( { message: error } ) => {
// Clear fetching.
state.set( 'isFetching', false );
// Set error and reset model.
model.set( {
error,
productId: 0,
priceId: null,
productName: '',
} );
} )
.then( ( response ) => {
const { amount, tax, subtotal, total } = response;
model.set( {
amount,
tax,
subtotal,
total,
amountManual: number.format( amount ),
taxManual: number.format( tax ),
subtotalManual: number.format( subtotal ),
} );
// Clear fetching.
state.set( 'isFetching', false );
} );
},
/**
* Updates the `OrderItem`'s when the Quantity changes.
*
* @since 3.0
* @todo Validate.
*
* @param {Object} e Change event.
*/
onChangeQuantity( e ) {
this.model.set( 'quantity', parseInt( e.target.value ) );
},
/**
* Updates the `OrderItem`'s when the manually managed Amount changes.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChangeAmount( e ) {
this.model.set( 'amountManual', e.target.value );
},
/**
* Updates the `OrderItem`'s when the manually managed Tax changes.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChangeTax( e ) {
this.model.set( 'taxManual', e.target.value );
},
/**
* Updates the `OrderItem`'s when the manually managed Subtotal changes.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChangeSubtotal( e ) {
this.model.set( 'subtotalManual', e.target.value );
},
/**
* Toggles manual amount adjustments.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onAutoCalculateToggle( e ) {
e.preventDefault();
this.model.set( {
_isAdjustingManually: ! e.target.checked,
} );
},
/**
* Closes dialog and opens "Order Details - Address" section.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onSetAddress( e ) {
e.preventDefault();
this.closeDialog();
const button = $( '[href="#edd_general_address"]' );
if ( ! button ) {
return;
}
button.trigger( 'click' );
$( '#edd_order_address_country' ).trigger( 'focus' );
},
/**
* Adds an `OrderItem` to `OrderItems`.
*
* @since 3.0
*
* @param {Object} e Submit event.
*/
onAdd( e ) {
e.preventDefault();
const { model, options } = this;
const { state } = options;
const { number } = state.get( 'formatters' );
state.set( 'isFetching', true );
// Use manual amounts if adjusting manually.
if ( true === model.get( '_isAdjustingManually' ) ) {
model.set( {
amount: number.unformat( model.get( 'amountManual' ) ),
tax: number.unformat( model.get( 'taxManual' ) ),
subtotal: number.unformat( model.get( 'subtotalManual' ) ),
} );
// Duplicate base amounts by the quantity set.
} else {
const quantity = model.get( 'quantity' );
model.set( {
tax: model.get( 'tax' ) * quantity,
subtotal: model.get( 'subtotal' ) * quantity,
} );
}
const items = state.get( 'items' );
// Add to collection but do not alert.
items.add( model, {
silent: true,
} );
// Update all amounts with new item and alert when done.
items
.updateAmounts()
.fail( ( { message: error } ) => {
// Remove added model on failure.
// It is is added previously to calculate Discounts
// as if adding would be successful.
items.remove( model, {
silent: true,
} );
// Clear fetching.
state.set( 'isFetching', false );
// Set error.
model.set( 'error', error );
} )
.done( () => {
// Stop listening to the model in this view.
this.stopListening( model );
// Alert of succesful addition.
items.trigger( 'add', model );
// Clear fetching.
state.set( 'isFetching', false );
} );
},
} );

View File

@ -0,0 +1,50 @@
/**
* Internal dependencies
*/
import { Base } from './base.js';
/**
* NoOrderItems
*
* @since 3.0
*
* @class NoOrderItems
* @augments wp.Backbone.View
*/
export const NoOrderItems = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tr',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-no-items' ),
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = this.options;
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 4 : 3;
return {
...Base.prototype.prepare.apply( this, arguments ),
config: {
colspan,
},
};
},
} );

View File

@ -0,0 +1,118 @@
/* global _ */
/**
* Internal dependencies
*/
import { Base } from './base.js';
/**
* OrderAdjustment
*
* @since 3.0
*
* @class OrderAdjustment
* @augments wp.Backbone.View
*/
export const OrderAdjustment = Base.extend( {
/**
* @since 3.0
*/
tagName: 'tr',
/**
* @since 3.0
*/
className: 'is-expanded',
/**
* @since 3.0
*/
events: {
'click .delete': 'onDelete',
},
initialize() {
Base.prototype.initialize.apply( this );
// Set template depending on type.
switch ( this.model.get( 'type' ) ) {
case 'credit':
case 'fee':
this.template = wp.template( 'edd-admin-order-adjustment' );
break;
default:
this.template = wp.template(
'edd-admin-order-adjustment-discount'
);
}
// Listen for events.
this.listenTo( this.model, 'change', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = this.options;
const { currency, number } = state.get( 'formatters' );
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
let orderItem;
if ( 'order_item' === model.get( 'objectType' ) ) {
orderItem = _.first( state.get( 'items' ).filter( ( item ) => {
return undefined !== item.get( 'adjustments' ).findWhere( {
objectId: item.get( 'id' ),
} );
} ) );
}
const subtotal = model.getAmount();
const total = model.getTotal();
return {
...Base.prototype.prepare.apply( this, arguments ),
config: {
colspan,
},
total,
subtotal,
orderItem: orderItem ? orderItem.toJSON() : false,
totalCurrency: currency.format( total ),
subtotalCurrency: currency.format( subtotal )
};
},
/**
* Removes the current Adjustment from Adjustments.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onDelete( e ) {
e.preventDefault();
const { state } = this.options;
// Remove `OrderAdjustment`.
state.get( 'adjustments' ).remove( this.model );
// Update `OrderItem` amounts.
state.get( 'items' ).updateAmounts();
},
} );

View File

@ -0,0 +1,101 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderAdjustment } from './order-adjustment.js';
/**
* OrderAdjustments
*
* @since 3.0
*
* @class OrderAdjustments
* @augments wp.Backbone.View
*/
export const OrderAdjustments = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__adjustments',
/**
* @since 3.0
*/
initialize() {
const { state } = this.options;
const items = state.get( 'items' );
const adjustments = state.get( 'adjustments' );
// Listen for events.
this.listenTo( state, 'change:hasTax', this.render );
this.listenTo( items, 'change', this.render );
this.listenTo( adjustments, 'add', this.render );
this.listenTo( adjustments, 'remove', this.remove );
},
/**
* Renders initial view.
*
* @since 3.0
*/
render() {
const adjustments = this.getAdjustments();
this.views.remove();
_.each( adjustments, ( adjustment ) => this.add( adjustment ) );
},
/**
* Adds an `OrderAdjustment` subview.
*
* @since 3.0
*
* @param {OrderAdjustment} model OrderAdjustment to add to view.
*/
add( model ) {
this.views.add(
new OrderAdjustment( {
...this.options,
model,
} )
);
},
/**
* Removes an `OrderAdjustment` subview.
*
* @since 3.0
*
* @param {OrderAdjustment} model OrderAdjustment to remove from view.
*/
remove( model ) {
let subview = null;
const views = this.views.get();
if ( ! views ) {
return;
}
// Find the Subview containing the model.
views.forEach( ( view ) => {
const { model: viewModel } = view;
if ( viewModel.id === model.id ) {
subview = view;
}
} );
// Remove Subview if found.
if ( null !== subview ) {
subview.remove();
}
},
} );

View File

@ -0,0 +1,27 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderAdjustments } from './order-adjustments.js';
/**
* OrderCredits
*
* @since 3.0
*
* @class OrderCredits
* @augments wp.Backbone.View
*/
export const OrderCredits = OrderAdjustments.extend( {
/**
* Returns Credit adjustments.
*
* @since 3.0.0
*/
getAdjustments() {
const { state } = this.options;
return state.get( 'adjustments' ).getByType( 'credit' );
},
} );

View File

@ -0,0 +1,31 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderAdjustments } from './order-adjustments.js';
/**
* OrderDiscountsFees
*
* @since 3.0
*
* @class OrderDiscountsFees
* @augments wp.Backbone.View
*/
export const OrderDiscountsFees = OrderAdjustments.extend( {
/**
* Returns Discount and Fee adjustments.
*
* @since 3.0.0
*/
getAdjustments() {
const { state } = this.options;
return state.get( 'adjustments' ).filter(
( adjustment ) => {
return [ 'discount', 'fee' ].includes( adjustment.get( 'type' ) );
}
);
},
} );

View File

@ -0,0 +1,127 @@
/**
* Internal dependencies
*/
import { Base } from './base.js';
import { CopyDownloadLink } from './copy-download-link.js';
/**
* OrderItem
*
* @since 3.0
*
* @class OrderItem
* @augments wp.Backbone.View
*/
export const OrderItem = Base.extend( {
/**
* @since 3.0
*/
tagName: 'tr',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-item' ),
/**
* @since 3.0
*/
events: {
'click .delete': 'onDelete',
'click .copy-download-link': 'onCopyDownloadLink',
},
/**
* "Order Item" view.
*
* @since 3.0
*
* @constructs OrderItem
* @augments Base
*/
initialize() {
// Listen for events.
this.listenTo( this.model, 'change', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
const { currency } = state.get( 'formatters' );
const subtotal = model.getSubtotal();
const discountAmount = model.getDiscountAmount();
const isAdjustingManually = model.get( '_isAdjustingManually' );
const tax = model.getTax();
return {
...Base.prototype.prepare.apply( this, arguments ),
discount: discountAmount,
amountCurrency: currency.format( model.get( 'amount' ) ),
subtotal,
subtotalCurrency: currency.format( subtotal ),
tax,
taxCurrency: currency.format( tax ),
total: model.getTotal(),
config: {
isAdjustingManually,
},
adjustments: model.get( 'adjustments' ).toJSON(),
};
},
/**
* Removes the current Item from Items.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onDelete( e ) {
e.preventDefault();
const { model, options } = this;
const { state } = options;
// Remove OrderItem.
state.get( 'items' ).remove( model );
// Update remaining OrderItem amounts.
state.get( 'items' ).updateAmounts();
},
/**
* Opens a Dialog that fetches Download File URLs.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onCopyDownloadLink( e ) {
e.preventDefault();
const { options, model } = this;
new CopyDownloadLink( {
orderId: model.get( 'orderId' ),
productId: model.get( 'productId' ),
priceId: model.get( 'priceId' ),
} )
.openDialog()
.render();
},
} );

View File

@ -0,0 +1,120 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderItem } from './order-item.js';
import { NoOrderItems } from './no-order-items.js';
/**
* OrderItems
*
* @since 3.0
*
* @class OrderItems
* @augments wp.Backbone.View
*/
export const OrderItems = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__items',
/**
* "Order Items" view.
*
* @since 3.0
*
* @constructs OrderItem
* @augments Base
*/
initialize() {
const { state } = this.options;
const items = state.get( 'items' );
const adjustments = state.get( 'adjustments' );
// Listen for events.
this.listenTo( items, 'add', this.render );
this.listenTo( items, 'remove', this.remove );
},
/**
* Renders initial view.
*
* @since 3.0
*/
render() {
const { state } = this.options;
const items = state.get( 'items' );
this.views.remove();
// Nothing available.
if ( 0 === items.length ) {
this.views.set(
new NoOrderItems( {
...this.options,
} )
);
// Render each item.
} else {
_.each( items.models, ( model ) => this.add( model ) );
}
},
/**
* Adds an `OrderItem` subview.
*
* @since 3.0
*
* @param {OrderItem} model OrderItem
*/
add( model ) {
this.views.add(
new OrderItem( {
...this.options,
model,
} )
);
},
/**
* Removes an `OrderItem` subview.
*
* @since 3.0
*
* @param {OrderItem} model OrderItem
*/
remove( model ) {
let subview = null;
// Find the subview containing the model.
this.views.get().forEach( ( view ) => {
const { model: viewModel } = view;
if ( viewModel.get( 'id' ) === model.id ) {
subview = view;
}
} );
// Remove subview if found.
if ( null !== subview ) {
subview.remove();
}
// Last item was removed, show "No items".
if ( 0 === this.views.get().length ) {
this.views.set(
new NoOrderItems( {
...this.options,
} )
);
}
},
} );

View File

@ -0,0 +1,57 @@
/* global wp */
/**
* OrderRefund
*
* @since 3.0
*
* @class OrderRefund
* @augments wp.Backbone.View
*/
export const OrderRefund = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-refund' ),
/**
* @since 3.0
*/
tagName: 'tr',
/**
* @since 3.0
*/
className: 'is-expanded',
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
const { currency } = state.get( 'formatters' );
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
return {
config: {
colspan,
},
id: model.get( 'id' ),
number: model.get( 'number' ),
dateCreated: model.get( 'dateCreatedi18n' ),
totalCurrency: currency.format( model.get( 'total' ) ),
};
},
} );

View File

@ -0,0 +1,48 @@
/**
* Internal dependencies
*/
import { OrderRefund } from './order-refund.js';
/**
* Order refunds
*
* @since 3.0
*
* @class OrderRefunds
* @augments wp.Backbone.View
*/
export const OrderRefunds = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__refunds',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-refunds' ),
/**
* Renders initial view.
*
* @since 3.0
*/
render() {
const { state } = this.options;
const { models: refunds } = state.get( 'refunds' );
_.each( refunds, ( model ) => (
this.views.add(
new OrderRefund( {
...this.options,
model,
} )
)
) );
},
} );

View File

@ -0,0 +1,68 @@
/**
* Order subtotal
*
* @since 3.0
*
* @class Subtotal
* @augments wp.Backbone.View
*/
export const OrderSubtotal = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__subtotal',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-subtotal' ),
/**
* Order subtotal view.
*
* @since 3.0
*
* @constructs OrderSubtotal
* @augments wp.Backbone.View
*/
initialize() {
const { state } = this.options;
// Listen for events.
this.listenTo( state.get( 'items' ), 'add remove change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add remove', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { state } = this.options;
const { currency, number } = state.get( 'formatters' );
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
const subtotal = state.getSubtotal();
return {
state: state.toJSON(),
config: {
colspan,
},
subtotal,
subtotalCurrency: currency.format( subtotal ),
};
},
} );

View File

@ -0,0 +1,130 @@
/**
* Order tax
*
* @since 3.0
*
* @class OrderTax
* @augments wp.Backbone.View
*/
export const OrderTax = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__tax',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-tax' ),
/**
* @since 3.0
*/
events: {
'click #notice-tax-change .notice-dismiss': 'onDismissTaxRateChange',
'click #notice-tax-change .update-amounts': 'onUpdateAmounts',
},
/**
* Order total view.
*
* @since 3.0
*
* @constructs OrderTotal
* @augments wp.Backbone.View
*/
initialize() {
const { state } = this.options;
// Listen for events.
this.listenTo( state, 'change:hasTax', this.render );
this.listenTo( state.get( 'items' ), 'add remove change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add remove', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { state } = this.options;
const { currency, number } = state.get( 'formatters' );
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
const tax = state.getTax();
const hasNewTaxRate = state.hasNewTaxRate();
const taxableItems = [
...state.get( 'items' ).models,
...state.get( 'adjustments' ).getByType( 'fee' ),
];
return {
state: {
...state.toJSON(),
hasNewTaxRate,
},
config: {
colspan,
},
tax,
taxCurrency: currency.format( tax ),
hasTaxableItems: taxableItems.length > 0,
};
},
/**
* Dismisses Tax Rate change notice.
*
* @since 3.0
*/
onDismissTaxRateChange() {
const { state } = this.options;
// Reset amount
state.set( 'hasTax', state.get( 'hasTax' ) );
// Manually trigger change because new and previous attributes
// are the same so Backbone will not.
state.trigger( 'change:hasTax' );
},
/**
* Updates amounts for existing Order Items.
*
* @since 3.0
*/
onUpdateAmounts( e ) {
e.preventDefault();
const { state } = this.options;
// Manually recalculate taxed fees.
state.get( 'adjustments' ).getByType( 'fee' ).forEach(
( fee ) => {
fee.updateTax();
}
);
// Request updated tax amounts for orders from the server.
state.get( 'items' )
.updateAmounts()
.done( () => {
this.onDismissTaxRateChange();
} );
},
} );

View File

@ -0,0 +1,81 @@
/**
* Order total
*
* @since 3.0
*
* @class OrderTotal
* @augments wp.Backbone.View
*/
export const OrderTotal = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__total',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-total' ),
/**
* Order tax view.
*
* @since 3.0
*
* @constructs OrderTax
* @augments wp.Backbone.View
*/
initialize() {
const { state } = this.options;
// Listen for events.
this.listenTo( state, 'change:hasTax', this.render );
this.listenTo( state.get( 'items' ), 'add remove change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add remove', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { state } = this.options;
const { currency, number } = state.get( 'formatters' );
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
const total = state.getTotal();
const discount = state.getDiscount();
const hasManualAdjustment = undefined !== state.get( 'items' ).findWhere( {
_isAdjustingManually: true,
} );
return {
state: {
...state.toJSON(),
hasManualAdjustment,
},
config: {
colspan,
},
total,
discount,
discountCurrency: currency.format( discount ),
totalCurrency: currency.format( total ),
};
},
} );

View File

@ -0,0 +1,66 @@
/** global wp */
/**
* Internal dependencies
*/
import { Summary } from './summary.js';
import { Actions } from './actions.js';
/**
* Overview
*
* @since 3.0
*
* @class Overview
* @augments wp.Backbone.View
*/
export const Overview = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
el: '#edd-order-overview',
/**
* @since 3.0
*/
events: {
'click .toggle-row': 'onToggleRow',
},
/**
* Renders the view.
*
* @since 3.0
*
* @return {Overview} Current view.
*/
render() {
// Add "Summary".
//
// Contains `OrderItems`, `OrderAdjustments`, and `Totals` subviews.
this.views.add( new Summary( this.options ) );
// "Actions".
if ( document.getElementById( 'edd-order-overview-actions' ) ) {
this.views.add( new Actions( this.options ) );
}
return this;
},
/**
* Toggles a row's other columns.
*
* Core does not support the dynamically added items.
*
* @since 3.0
*
* @see https://github.com/WordPress/WordPress/blob/001ffe81fbec4438a9f594f330e18103d21fbcd7/wp-admin/js/common.js#L908
*
* @param {Object} e Click event.
*/
onToggleRow( e ) {
e.preventDefault();
$( e.target ).closest( 'tr' ).toggleClass( 'is-expanded' );
},
} );

View File

@ -0,0 +1,46 @@
/** global wp */
/**
* Internal dependencies
*/
import { OrderItems } from './order-items.js';
import { OrderSubtotal } from './order-subtotal.js';
import { OrderDiscountsFees } from './order-discounts-fees.js';
import { OrderTax } from './order-tax.js';
import { OrderCredits } from './order-credits.js';
import { OrderTotal } from './order-total.js';
import { OrderRefunds } from './order-refunds.js';
/**
* Overview summary
*
* @since 3.0
*
* @class Summary
* @augments wp.Backbone.view
*/
export const Summary = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
el: '#edd-order-overview-summary',
/**
* Renders the view.
*
* @since 3.0
*
* @return {Summary} Current view.
*/
render() {
this.views.add( new OrderItems( this.options ) );
this.views.add( new OrderSubtotal( this.options ) );
this.views.add( new OrderDiscountsFees( this.options ) );
this.views.add( new OrderTax( this.options ) );
this.views.add( new OrderCredits( this.options ) );
this.views.add( new OrderTotal( this.options ) );
this.views.add( new OrderRefunds( this.options ) );
return this;
},
} );

View File

@ -0,0 +1,2 @@
// Empty file for backwards compatibility.
// @see assets/js/admin/orders/index.js

View File

@ -0,0 +1,31 @@
/* global eddAdminReportsCharts */
/**
* Internal dependencies.
*/
import moment from 'moment';
import { render as lineChartRender } from './line.js';
import { render as pieChartRender } from './pie.js';
import { isPieChart } from './utils.js';
// Access existing global `edd` variable, or create a new object.
window.edd = window.edd || {};
/**
* Render a chart based on config.
*
* This function is attached to the `edd` property attached to the `window`.
*
* @param {Object} config Chart config.
*/
window.edd.renderChart = ( config ) => {
const isPie = isPieChart( config );
Chart.defaults.global.pointHitDetectionRadius = 5;
if ( isPieChart( config ) ) {
pieChartRender( config );
} else {
lineChartRender( config );
}
};

View File

@ -0,0 +1,94 @@
/* global Chart */
/**
* Internal dependencies.
*/
import { NumberFormat } from '@easy-digital-downloads/currency';
import moment, { utc } from 'moment';
import momentTimezone from 'moment-timezone';
import { getLabelWithTypeCondition, toolTipBaseConfig } from './utils';
/**
* Render a line chart.
*
* @param {Object} config Global chart config.
* @return {Chart}
*/
export const render = ( config ) => {
const { target } = config;
const {
dates: {
utc_offset: utcOffset,
hour_by_hour: hourByHour,
day_by_day: dayByDay,
},
} = config;
const number = new NumberFormat();
const lineConfig = {
...config,
options: {
...config.options,
maintainAspectRatio: false,
tooltips: tooltipConfig( config ),
scales: {
...config.options.scales,
yAxes: [
{
...config.options.scales.yAxes[0],
ticks: {
callback: ( value, index, values ) => {
return number.format( value );
},
suggestedMin: 0,
beginAtZero: true,
},
},
],
xAxes: [
{
...config.options.scales.xAxes[0],
ticks: {
...config.options.scales.xAxes[0].ticks,
maxTicksLimit:12,
autoSkip: true,
callback( value, index, ticks ) {
return moment.tz( ticks[index].value, config.dates.timezone ).format( config.dates.time_format );
},
},
},
],
},
},
};
// Render
return new Chart( document.getElementById( target ), lineConfig );
};
/**
* Get custom tooltip config for line charts.
*
* @param {Object} config Global chart config.
* @return {Object}
*/
export const tooltipConfig = ( config ) => ( {
...toolTipBaseConfig,
callbacks: {
/**
* Generate a label.
*
* @param {Object} t
* @param {Object} d
*/
label: function( t, d ) {
const { options: { datasets } } = config;
const datasetConfig = datasets[ Object.keys( datasets )[ t.datasetIndex ] ];
const label = getLabelWithTypeCondition( t.yLabel, datasetConfig );
return `${ d.datasets[ t.datasetIndex ].label }: ${ label }`;
},
},
} );

View File

@ -0,0 +1,56 @@
/* global Chart */
/**
* Internal dependencies.
*/
import { toolTipBaseConfig, getLabelWithTypeCondition } from './utils';
/**
* Render a line chart.
*
* @param {Object} config Global chart config.
* @return {Chart}
*/
export const render = ( config ) => {
const { target } = config;
// Config tooltips.
config.options.tooltips = tooltipConfig( config );
// Render
return new Chart( document.getElementById( target ), config );
};
/**
* Get custom tooltip config for pie charts.
*
* @param {Object} config Global chart config.
* @return {Object}
*/
export const tooltipConfig = ( config ) => ( {
...toolTipBaseConfig,
callbacks: {
/**
* Generate a label.
*
* @param {Object} t
* @param {Object} d
*/
label: function( t, d ) {
const { options: { datasets } } = config;
const datasetConfig = datasets[ Object.keys( datasets )[ t.datasetIndex ] ];
const dataset = d.datasets[ t.datasetIndex ];
const total = dataset.data.reduce( function( previousValue, currentValue, currentIndex, array ) {
return previousValue + currentValue;
} );
const currentValue = dataset.data[ t.index ];
const label = getLabelWithTypeCondition( currentValue, datasetConfig );
const precentage = Math.floor( ( ( currentValue / total ) * 100 ) + 0.5 );
return `${ d.labels[ t.index ] }: ${ label } (${ precentage }%)`;
},
},
} );

View File

@ -0,0 +1,134 @@
/**
* Internal dependencies
*/
import { Currency } from '@easy-digital-downloads/currency';
/**
* Determine if a pie graph.
*
* @todo maybe pass from data?
*
* @param {Object} config Global chart config.
* @return {Bool}
*/
export const isPieChart = ( config ) => {
const { type } = config;
return type === 'pie' || type === 'doughnut';
};
/**
* Determine if a chart's dataset has a special conditional type.
*
* Currently just checks for currency.
*
* @param {string} label Current label.
* @param {Object} config Global chart config.
*/
export const getLabelWithTypeCondition = ( label, datasetConfig ) => {
let newLabel = label;
const { type } = datasetConfig;
if ( 'currency' === type ) {
const currency = new Currency();
newLabel = currency.format( label, false );
}
return newLabel;
};
/**
* Shared tooltip configuration.
*/
export const toolTipBaseConfig = {
enabled: false,
mode: 'index',
position: 'nearest',
/**
* Output a a custom tooltip.
*
* @param {Object} tooltip Tooltip data.
*/
custom: function( tooltip ) {
// Tooltip element.
let tooltipEl = document.getElementById( 'edd-chartjs-tooltip' );
if ( ! tooltipEl ) {
tooltipEl = document.createElement( 'div' );
tooltipEl.id = 'edd-chartjs-tooltip';
tooltipEl.innerHTML = '<table></table>';
this._chart.canvas.parentNode.appendChild( tooltipEl );
}
// Hide if no tooltip.
if ( tooltip.opacity === 0 ) {
tooltipEl.style.opacity = 0;
return;
}
// Set caret position.
tooltipEl.classList.remove( 'above', 'below', 'no-transform' );
if ( tooltip.yAlign ) {
tooltipEl.classList.add( tooltip.yAlign );
} else {
tooltipEl.classList.add( 'no-transform' );
}
function getBody( bodyItem ) {
return bodyItem.lines;
}
// Set Text
if ( tooltip.body ) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map( getBody );
let innerHtml = '<thead>';
innerHtml += '</thead><tbody>';
bodyLines.forEach( function( body, i ) {
const colors = tooltip.labelColors[ i ];
const { borderColor, backgroundColor } = colors;
// Super dirty check to use the legend's color.
let fill = borderColor;
if ( fill === 'rgb(230, 230, 230)' || fill === '#fff' ) {
fill = backgroundColor;
}
const style = [
`background: ${ fill }`,
`border-color: ${ fill }`,
'border-width: 2px',
];
const span = '<span class="edd-chartjs-tooltip-key" style="' + style.join( ';' ) + '"></span>';
innerHtml += '<tr><td>' + span + body + '</td></tr>';
} );
innerHtml += '</tbody>';
const tableRoot = tooltipEl.querySelector( 'table' );
tableRoot.innerHTML = innerHtml;
}
const positionY = this._chart.canvas.offsetTop;
const positionX = this._chart.canvas.offsetLeft;
// Display, position, and set styles for font
tooltipEl.style.opacity = 1;
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
tooltipEl.style.top = positionY + tooltip.caretY + 'px';
tooltipEl.style.fontFamily = tooltip._bodyFontFamily;
tooltipEl.style.fontSize = tooltip.bodyFontSize + 'px';
tooltipEl.style.fontStyle = tooltip._bodyFontStyle;
tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
},
};

View File

@ -0,0 +1,26 @@
export var eddLabelFormatter = function( label, series ) {
return '<div style="font-size:12px; text-align:center; padding:2px">' + label + '</div>';
};
export var eddLegendFormatterSales = function( label, series ) {
const slug = label.toLowerCase().replace( /\s/g, '-' ),
color = '<div class="edd-legend-color" style="background-color: ' + series.color + '"></div>',
value = '<div class="edd-pie-legend-item">' + label + ': ' + Math.round( series.percent ) + '% (' + eddFormatNumber( series.data[ 0 ][ 1 ] ) + ')</div>',
item = '<div id="' + series.edd_vars.id + slug + '" class="edd-legend-item-wrapper">' + color + value + '</div>';
jQuery( '#edd-pie-legend-' + series.edd_vars.id ).append( item );
return item;
};
export var eddLegendFormatterEarnings = function( label, series ) {
const slug = label.toLowerCase().replace( /\s/g, '-' ),
color = '<div class="edd-legend-color" style="background-color: ' + series.color + '"></div>',
value = '<div class="edd-pie-legend-item">' + label + ': ' + Math.round( series.percent ) + '% (' + eddFormatCurrency( series.data[ 0 ][ 1 ] ) + ')</div>',
item = '<div id="' + series.edd_vars.id + slug + '" class="edd-legend-item-wrapper">' + color + value + '</div>';
jQuery( '#edd-pie-legend-' + series.edd_vars.id ).append( item );
return item;
};

View File

@ -0,0 +1,85 @@
/* global pagenow, postboxes */
/**
* Internal dependencies.
*/
import { eddLabelFormatter, eddLegendFormatterSales, eddLegendFormatterEarnings } from './formatting.js';
import './charts';
// Enable reports meta box toggle states.
if ( typeof postboxes !== 'undefined' && /edd-reports/.test( pagenow ) ) {
postboxes.add_postbox_toggles( pagenow );
}
/**
* Reports / Exports screen JS
*/
const EDD_Reports = {
init: function() {
this.meta_boxes();
this.date_options();
this.customers_export();
},
meta_boxes: function() {
$( '.edd-reports-wrapper .postbox .handlediv' ).remove();
$( '.edd-reports-wrapper .postbox' ).removeClass( 'closed' );
// Use a timeout to ensure this happens after core binding
setTimeout( function() {
$( '.edd-reports-wrapper .postbox .hndle' ).unbind( 'click.postboxes' );
}, 1 );
},
date_options: function() {
// Show hide extended date options
$( 'select.edd-graphs-date-options' ).on( 'change', function( event ) {
const select = $( this ),
date_range_options = select.parent().siblings( '.edd-date-range-options' );
if ( 'other' === select.val() ) {
date_range_options.removeClass( 'screen-reader-text' );
} else {
date_range_options.addClass( 'screen-reader-text' );
}
} );
},
customers_export: function() {
// Show / hide Download option when exporting customers
$( '#edd_customer_export_download' ).change( function() {
const $this = $( this ),
download_id = $( 'option:selected', $this ).val(),
customer_export_option = $( '#edd_customer_export_option' );
if ( '0' === $this.val() ) {
customer_export_option.show();
} else {
customer_export_option.hide();
}
// On Download Select, Check if Variable Prices Exist
if ( parseInt( download_id ) !== 0 ) {
const data = {
action: 'edd_check_for_download_price_variations',
download_id: download_id,
all_prices: true,
};
var price_options_select = $( '.edd_price_options_select' );
$.post( ajaxurl, data, function( response ) {
price_options_select.remove();
$( '#edd_customer_export_download_chosen' ).after( response );
} );
} else {
price_options_select.remove();
}
} );
},
};
jQuery( document ).ready( function( $ ) {
EDD_Reports.init();
} );

View File

@ -0,0 +1,76 @@
/* global eddEmailTagsInserter, tb_remove, tb_position, send_to_editor, _, window, document */
/**
* Internal dependencies.
*/
import { searchItems } from './utils.js';
/**
* Make tags clickable and send them to the email content (wp_editor()).
*/
function setupEmailTags() {
// Find all of the buttons.
const insertButtons = document.querySelectorAll( '.edd-email-tags-list-button' );
if ( ! insertButtons ) {
return;
}
/**
* Listen for clicks on tag buttons.
*
* @param {object} node Button node.
*/
_.each( insertButtons, function( node ) {
/**
* Listen for clicks on tag buttons.
*/
node.addEventListener( 'click', function() {
// Close Thickbox.
tb_remove();
window.send_to_editor( node.dataset.to_insert );
} );
} );
}
/**
* Filter tags.
*/
function filterEmailTags() {
const filterInput = document.querySelector( '.edd-email-tags-filter-search' );
const tagItems = document.querySelectorAll( '.edd-email-tags-list-item' );
if ( ! filterInput ) {
return;
}
filterInput.addEventListener( 'keyup', function( event ) {
const searchTerm = event.target.value;
const foundTags = searchItems( eddEmailTagsInserter.items, searchTerm );
_.each( tagItems, function( node ) {
const found = _.findWhere( foundTags, { tag: node.dataset.tag } );
node.style.display = ! found ? 'none' : 'block';
} );
} );
}
/**
* DOM ready.
*/
document.addEventListener( 'DOMContentLoaded', function() {
// Resize Thickbox when media button is clicked.
const mediaButton = document.querySelector( '.edd-email-tags-inserter' );
if ( ! mediaButton ) {
return;
}
mediaButton.addEventListener( 'click', tb_position );
// Clickable tags.
setupEmailTags();
// Filterable tags.
filterEmailTags();
} );

View File

@ -0,0 +1,40 @@
/* global _ */
/**
* Filters an item list given a search term.
*
* @param {Array} items Item list
* @param {string} searchTerm Search term.
*
* @return {Array} Filtered item list.
*/
export const searchItems = function( items, searchTerm ) {
const normalizedSearchTerm = normalizeTerm( searchTerm );
const matchSearch = function( string ) {
return normalizeTerm( string ).indexOf( normalizedSearchTerm ) !== -1;
};
return _.filter( items, function( item ) {
return matchSearch( item.title ) || _.some( item.keywords, matchSearch );
} );
};
/**
* Converts the search term into a normalized term.
*
* @param {string} term The search term to normalize.
*
* @return {string} The normalized search term.
*/
export const normalizeTerm = function( term ) {
// Lowercase.
// Input: "MEDIA"
term = term.toLowerCase();
// Strip leading and trailing whitespace.
// Input: " media "
term = term.trim();
return term;
};

View File

@ -0,0 +1,75 @@
/* global EDDExtensionManager, ajaxurl */
; ( function ( document, $ ) {
'use strict';
$( '.edd-extension-manager__action' ).on( 'click', function ( e ) {
e.preventDefault();
var $btn = $( this ),
action = $btn.attr( 'data-action' ),
plugin = $btn.attr( 'data-plugin' ),
type = $btn.attr( 'data-type' ),
ajaxAction = '';
if ( $btn.attr( 'disabled' ) ) {
return;
}
switch ( action ) {
case 'activate':
ajaxAction = 'edd_activate_extension';
$btn.text( EDDExtensionManager.activating );
break;
case 'install':
ajaxAction = 'edd_install_extension';
$btn.text( EDDExtensionManager.installing );
break;
default:
return;
}
$btn.removeClass( 'button-primary' ).attr( 'disabled', true ).addClass( 'updating-message' );
var data = {
action: ajaxAction,
nonce: EDDExtensionManager.extension_manager_nonce,
plugin: plugin,
type: type,
pass: $btn.attr( 'data-pass' ),
id: $btn.attr( 'data-id' ),
product: $btn.attr( 'data-product' ),
};
$.post( ajaxurl, data )
.done( function ( res ) {
console.log( res );
var thisStep = $btn.closest( '.edd-extension-manager__step' );
if ( res.success ) {
var nextStep = thisStep.next();
if ( nextStep.length ) {
thisStep.fadeOut();
nextStep.prepend( '<div class="notice inline-notice notice-success"><p>' + res.data.message + '</p></div>' );
nextStep.fadeIn();
}
} else {
thisStep.fadeOut();
var message = res.data.message;
/**
* The install class returns an array of error messages, and res.data.message will be undefined.
* In that case, we'll use the standard failure messages.
*/
if ( ! message ) {
if ( 'plugin' !== type ) {
message = EDDExtensionManager.extension_install_failed;
} else {
message = EDDExtensionManager.plugin_install_failed;
}
}
thisStep.after( '<div class="notice inline-notice notice-warning"><p>' + message + '</p></div>' );
}
} );
} );
} )( document, jQuery );

View File

@ -0,0 +1,130 @@
jQuery( document ).ready( function ( $ ) {
/**
* Connect to PayPal
*/
$( '#edd-paypal-commerce-connect' ).on( 'click', function ( e ) {
e.preventDefault();
// Clear errors.
var errorContainer = $( '#edd-paypal-commerce-errors' );
errorContainer.empty().removeClass( 'notice notice-error' );
var button = document.getElementById( 'edd-paypal-commerce-connect' );
button.classList.add( 'updating-message' );
button.disabled = true;
$.post( ajaxurl, {
action: 'edd_paypal_commerce_connect',
_ajax_nonce: $( this ).data( 'nonce' )
}, function( response ) {
if ( ! response.success ) {
console.log( 'Connection failure', response.data );
button.classList.remove( 'updating-message' );
button.disabled = false;
// Set errors.
errorContainer.html( '<p>' + response.data + '</p>' ).addClass( 'notice notice-error' );
return;
}
var paypalLinkEl = document.getElementById( 'edd-paypal-commerce-link' );
paypalLinkEl.href = response.data.signupLink + '&displayMode=minibrowser';
paypalLinkEl.click();
} );
} );
/**
* Checks the PayPal connection & webhook status.
*/
function eddPayPalGetAccountStatus() {
var accountInfoEl = document.getElementById( 'edd-paypal-commerce-connect-wrap' );
if ( accountInfoEl ) {
$.post( ajaxurl, {
action: 'edd_paypal_commerce_get_account_info',
_ajax_nonce: accountInfoEl.getAttribute( 'data-nonce' )
}, function( response ) {
var newHtml = '<p>' + eddPayPalConnectVars.defaultError + '</p>';
if ( response.success ) {
newHtml = response.data.account_status;
if ( response.data.actions && response.data.actions.length ) {
newHtml += '<p class="edd-paypal-connect-actions">' + response.data.actions.join( ' ' ) + '</p>';
}
} else if ( response.data && response.data.message ) {
newHtml = response.data.message;
}
accountInfoEl.innerHTML = newHtml;
// Remove old status messages.
accountInfoEl.classList.remove( 'notice-success', 'notice-warning', 'notice-error' );
// Add new one.
var newClass = response.success && response.data.status ? 'notice-' + response.data.status : 'notice-error';
accountInfoEl.classList.add( newClass );
} );
}
}
eddPayPalGetAccountStatus();
/**
* Create webhook
*/
$( document ).on( 'click', '.edd-paypal-connect-action', function ( e ) {
e.preventDefault();
var button = $( this );
button.prop( 'disabled', true );
button.addClass( 'updating-message' );
var errorWrap = $( '#edd-paypal-commerce-connect-wrap' ).find( '.edd-paypal-actions-error-wrap' );
if ( errorWrap.length ) {
errorWrap.remove();
}
$.post( ajaxurl, {
action: button.data( 'action' ),
_ajax_nonce: button.data( 'nonce' )
}, function( response ) {
button.prop( 'disabled', false );
button.removeClass( 'updating-message' );
if ( response.success ) {
button.addClass( 'updated-message' );
// Refresh account status.
eddPayPalGetAccountStatus();
} else {
button.parent().after( '<p class="edd-paypal-actions-error-wrap">' + response.data + '</p>' );
}
} );
} );
} );
window.eddPayPalOnboardingCallback = function eddPayPalOnboardingCallback( authCode, shareId ) {
var connectButton = document.getElementById( 'edd-paypal-commerce-connect' );
var errorContainer = document.getElementById( 'edd-paypal-commerce-errors' );
jQuery.post( ajaxurl, {
action: 'edd_paypal_commerce_get_access_token',
auth_code: authCode,
share_id: shareId,
_ajax_nonce: connectButton.getAttribute( 'data-nonce' )
}, function( response ) {
connectButton.classList.remove( 'updating-message' );
if ( ! response.success ) {
connectButton.disabled = false;
errorContainer.innerHTML = '<p>' + response.data + '</p>';
errorContainer.classList.add( 'notice notice-error' );
return;
}
connectButton.classList.add( 'updated-message' );
window.location.reload();
} );
}

View File

@ -0,0 +1,168 @@
import { recaptureRemoteInstall } from './recapture';
import './gateways/paypal';
/**
* Settings screen JS
*/
const EDD_Settings = {
init: function() {
this.general();
this.misc();
this.gateways();
this.emails();
},
general: function() {
const edd_color_picker = $( '.edd-color-picker' );
if ( edd_color_picker.length ) {
edd_color_picker.wpColorPicker();
}
// Settings Upload field JS
if ( typeof wp === 'undefined' || '1' !== edd_vars.new_media_ui ) {
// Old Thickbox uploader
const edd_settings_upload_button = $( '.edd_settings_upload_button' );
if ( edd_settings_upload_button.length > 0 ) {
window.formfield = '';
$( document.body ).on( 'click', edd_settings_upload_button, function( e ) {
e.preventDefault();
window.formfield = $( this ).parent().prev();
window.tbframe_interval = setInterval( function() {
jQuery( '#TB_iframeContent' ).contents().find( '.savesend .button' ).val( edd_vars.use_this_file ).end().find( '#insert-gallery, .wp-post-thumbnail' ).hide();
}, 2000 );
tb_show( edd_vars.add_new_download, 'media-upload.php?TB_iframe=true' );
} );
window.edd_send_to_editor = window.send_to_editor;
window.send_to_editor = function( html ) {
if ( window.formfield ) {
imgurl = $( 'a', '<div>' + html + '</div>' ).attr( 'href' );
window.formfield.val( imgurl );
window.clearInterval( window.tbframe_interval );
tb_remove();
} else {
window.edd_send_to_editor( html );
}
window.send_to_editor = window.edd_send_to_editor;
window.formfield = '';
window.imagefield = false;
};
}
} else {
// WP 3.5+ uploader
var file_frame;
window.formfield = '';
$( document.body ).on( 'click', '.edd_settings_upload_button', function( e ) {
e.preventDefault();
const button = $( this );
window.formfield = $( this ).parent().prev();
// If the media frame already exists, reopen it.
if ( file_frame ) {
//file_frame.uploader.uploader.param( 'post_id', set_to_post_id );
file_frame.open();
return;
}
// Create the media frame.
file_frame = wp.media.frames.file_frame = wp.media( {
title: button.data( 'uploader_title' ),
library: { type: 'image' },
button: { text: button.data( 'uploader_button_text' ) },
multiple: false,
} );
file_frame.on( 'menu:render:default', function( view ) {
// Store our views in an object.
const views = {};
// Unset default menu items
view.unset( 'library-separator' );
view.unset( 'gallery' );
view.unset( 'featured-image' );
view.unset( 'embed' );
// Initialize the views in our view object.
view.set( views );
} );
// When an image is selected, run a callback.
file_frame.on( 'select', function() {
const selection = file_frame.state().get( 'selection' );
selection.each( function( attachment, index ) {
attachment = attachment.toJSON();
window.formfield.val( attachment.url );
} );
} );
// Finally, open the modal
file_frame.open();
} );
// WP 3.5+ uploader
var file_frame;
window.formfield = '';
}
},
misc: function() {
const downloadMethod = $( 'select[name="edd_settings[download_method]"]' ),
symlink = downloadMethod.parent().parent().next();
// Hide Symlink option if Download Method is set to Direct
if ( downloadMethod.val() === 'direct' ) {
symlink.css( 'opacity', '0.4' );
symlink.find( 'input' ).prop( 'checked', false ).prop( 'disabled', true );
}
// Toggle download method option
downloadMethod.on( 'change', function() {
if ( $( this ).val() === 'direct' ) {
symlink.css( 'opacity', '0.4' );
symlink.find( 'input' ).prop( 'checked', false ).prop( 'disabled', true );
} else {
symlink.find( 'input' ).prop( 'disabled', false );
symlink.css( 'opacity', '1' );
}
} );
},
gateways: function() {
$( '#edd-payment-gateways input[type="checkbox"]' ).on( 'change', function() {
const gateway = $( this ),
gateway_key = gateway.data( 'gateway-key' ),
default_gateway = $( '#edd_settings\\[default_gateway\\]' ),
option = default_gateway.find( 'option[value="' + gateway_key + '"]' );
// Toggle enable/disable based
option.prop( 'disabled', function( i, v ) {
return ! v;
} );
// Maybe deselect
if ( option.prop( 'selected' ) ) {
option.prop( 'selected', false );
}
default_gateway.trigger( 'chosen:updated' );
} );
},
emails: function() {
$('#edd-recapture-connect').on('click', function(e) {
e.preventDefault();
$(this).html( edd_vars.wait + ' <span class="edd-loading"></span>' );
document.body.style.cursor = 'wait';
recaptureRemoteInstall();
});
}
};
jQuery( document ).ready( function( $ ) {
EDD_Settings.init();
} );

View File

@ -0,0 +1,18 @@
export const recaptureRemoteInstall = () => {
var data = {
'action': 'edd_recapture_remote_install',
};
jQuery.post( ajaxurl, data, function( response ) {
if( ! response.success ) {
if( confirm( response.data.error ) ) {
location.reload();
return;
}
}
window.location.href = 'https://recapture.io/register';
});
}

View File

@ -0,0 +1,24 @@
/* global Backbone */
/**
* Internal dependencies.
*/
import TaxRate from './../models/tax-rate.js';
/**
* A collection of multiple tax rates.
*/
const TaxRates = Backbone.Collection.extend( {
// Map the model.
model: TaxRate,
/**
* Set initial state.
*/
initialize: function() {
this.showAll = false;
this.selected = [];
},
} );
export default TaxRates;

View File

@ -0,0 +1,47 @@
/* global _, eddTaxRates */
/**
* Internal dependencies.
*/
import TaxRate from './models/tax-rate.js';
import TaxRates from './collections/tax-rates.js';
import Manager from './views/manager.js';
import { jQueryReady } from 'utils/jquery.js';
/**
* DOM ready.
*/
jQueryReady( () => {
// Show notice if taxes are not enabled.
const noticeEl = document.getElementById( 'edd-tax-disabled-notice' );
if ( noticeEl ) {
noticeEl.classList.add( 'notice' );
noticeEl.classList.add( 'notice-warning' );
}
// Start manager with a blank collection.
const manager = new Manager( {
collection: new TaxRates(),
} );
const rates = [];
// Normalize rate data.
_.each( eddTaxRates.rates, ( rate ) => rates.push( {
id: rate.id,
country: rate.name,
region: rate.description,
global: 'country' === rate.scope,
amount: rate.amount,
status: rate.status,
} ) );
// Add initial rates.
manager.collection.set( rates, {
silent: true,
} );
// Render manager.
manager.render();
} );

View File

@ -0,0 +1,34 @@
/* global Backbone */
/**
* Model a tax rate.
*/
const TaxRate = Backbone.Model.extend( {
defaults: {
id: '',
country: '',
region: '',
global: true,
amount: 0,
status: 'active',
unsaved: false,
selected: false,
},
/**
* Format a rate amount (adds a %)
*
* @todo This should support dynamic decimal types.
*/
formattedAmount: function() {
let amount = 0;
if ( this.get( 'amount' ) ) {
amount = parseFloat( this.get( 'amount' ) ).toFixed( 2 );
}
return `${ amount }%`;
},
} );
export default TaxRate;

View File

@ -0,0 +1,55 @@
/* global wp, _ */
/**
* Apply bulk actions.
*/
const BulkActions = wp.Backbone.View.extend( {
// See https://codex.wordpress.org/Javascript_Reference/wp.template
template: wp.template( 'edd-admin-tax-rates-table-bulk-actions' ),
// Watch events.
events: {
'click .edd-admin-tax-rates-table-filter': 'filter',
'change .edd-admin-tax-rates-table-hide input': 'showHide',
},
/**
* Bulk actions for selected items.
*
* Currently only supports changing the status.
*
* @param {Object} event Event.
*/
filter: function( event ) {
event.preventDefault();
// @hack - need to access the DOM directly here because the dropdown is not tied to the button event.
const status = document.getElementById( 'edd-admin-tax-rates-table-bulk-actions' );
_.each( this.collection.selected, ( cid ) => {
const model = this.collection.get( {
cid: cid,
} );
model.set( 'status', status.value );
} );
this.collection.trigger( 'filtered' );
},
/**
* Toggle show active/inactive rates.
*
* @param {Object} event Event.
*/
showHide: function( event ) {
this.collection.showAll = event.target.checked;
// @hack -- shouldn't access this table directly.
document.getElementById( 'edd_tax_rates' ).classList.toggle( 'has-inactive', this.collection.showAll );
this.collection.trigger( 'filtered' );
},
} );
export default BulkActions;

View File

@ -0,0 +1,65 @@
/* global wp */
/**
* Internal dependencies.
*/
import Table from './table.js';
import BulkActions from './bulk-actions.js';
/**
* Manage tax rates.
*/
const Manager = wp.Backbone.View.extend( {
// Append to this element.
el: '#edd-admin-tax-rates',
/**
* Set bind changes to collection.
*/
initialize: function() {
this.listenTo( this.collection, 'add change', this.makeDirty );
// Clear unload confirmation when submitting parent form.
document.querySelector( '.edd-settings-form #submit' ).addEventListener( 'click', this.makeClean );
},
/**
* Output the manager.
*/
render: function() {
this.views.add( new BulkActions( {
collection: this.collection,
} ) );
this.views.add( new Table( {
collection: this.collection,
} ) );
},
/**
* Collection has changed so warn the user before exiting.
*/
makeDirty: function() {
window.onbeforeunload = this.confirmUnload;
},
/**
* When submitting the main form remove the dirty check.
*/
makeClean: function() {
window.onbeforeunload = null;
},
/**
* Confirm page unload.
*
* @param {Object} event Close event.
*/
confirmUnload: function( event ) {
event.preventDefault();
return '';
},
} );
export default Manager;

View File

@ -0,0 +1,38 @@
/* global wp, _ */
/**
* Internal dependencies.
*/
import { getChosenVars } from 'utils/chosen.js';
const RegionField = wp.Backbone.View.extend( {
/**
* Bind passed arguments.
*
* @param {Object} options Extra options passed.
*/
initialize: function( options ) {
_.extend( this, options );
},
/**
* Create a list of options.
*/
render: function() {
if ( this.global ) {
return;
}
if ( 'nostates' === this.states ) {
this.setElement( '<input type="text" id="tax_rate_region" />' );
} else {
this.$el.html( this.states );
this.$el.find( 'select' ).each( function() {
const el = $( this );
el.chosen( getChosenVars( el ) );
} );
}
},
} );
export default RegionField;

View File

@ -0,0 +1,236 @@
/* global wp */
/**
* Internal dependencies.
*/
import TaxRate from './../models/tax-rate.js';
import RegionField from './../views/region-field.js';
import { getChosenVars } from 'utils/chosen.js';
/**
* Add a new rate "form".
*/
const TableAdd = wp.Backbone.View.extend( {
// Use <tfoot>
tagName: 'tfoot',
// Set class.
className: 'add-new',
// See https://codex.wordpress.org/Javascript_Reference/wp.template
template: wp.template( 'edd-admin-tax-rates-table-add' ),
// Watch events.
events: {
'click button': 'addTaxRate',
'keypress': 'maybeAddTaxRate',
'change #tax_rate_country': 'setCountry',
// Can be select or input.
'keyup #tax_rate_region': 'setRegion',
'change #tax_rate_region': 'setRegion',
'change input[type="checkbox"]': 'setGlobal',
// Can be click increase or keyboard.
'keyup #tax_rate_amount': 'setAmount',
'change #tax_rate_amount': 'setAmount',
},
/**
* Set initial state and bind changes to model.
*/
initialize: function() {
this.model = new TaxRate( {
global: true,
unsaved: true,
} );
this.listenTo( this.model, 'change:country', this.updateRegion );
this.listenTo( this.model, 'change:global', this.updateRegion );
},
/**
* Render. Only overwritten so we can reinit chosen once cleared.
*/
render: function() {
wp.Backbone.View.prototype.render.apply( this, arguments );
this.$el.find( 'select' ).each( function() {
const el = $( this );
el.chosen( getChosenVars( el ) );
} );
return this;
},
/**
* Show a list of states or an input field.
*/
updateRegion: function() {
const self = this;
const data = {
action: 'edd_get_shop_states',
country: this.model.get( 'country' ),
nonce: eddTaxRates.nonce,
field_name: 'tax_rate_region',
};
$.post( ajaxurl, data, function( response ) {
self.views.set( '#tax_rate_region_wrapper', new RegionField( {
states: response,
global: self.model.get( 'global' ),
} ) );
} );
},
/**
* Set a country value.
*
* @param {Object} event Event.
*/
setCountry: function( event ) {
let country = event.target.options[ event.target.selectedIndex ].value;
let regionGlobalCheckbox = document.getElementById( "tax_rate_region_global" );
if ( 'all' === country ) {
country = '*';
regionGlobalCheckbox.checked = true;
this.model.set( 'region', '' );
this.model.set( 'global', true );
regionGlobalCheckbox.readOnly = true;
regionGlobalCheckbox.disabled = true;
} else {
regionGlobalCheckbox.disabled = false;
regionGlobalCheckbox.readOnly = false;
}
this.model.set( 'country', country );
},
/**
* Set a region value.
*
* @param {Object} event Event.
*/
setRegion: function( event ) {
let value = false;
if ( event.target.value ) {
value = event.target.value;
} else {
value = event.target.options[ event.target.selectedIndex ].value;
}
this.model.set( 'region', value );
},
/**
* Set a global scope.
*
* @param {Object} event Event.
*/
setGlobal: function( event ) {
let isChecked = event.target.checked;
this.model.set( 'global', isChecked );
if ( true === isChecked ) {
this.model.set( 'region', '' );
}
},
/**
* Set an amount value.
*
* @param {Object} event Event.
*/
setAmount: function( event ) {
this.model.set( 'amount', event.target.value );
},
/**
* Monitors keyepress for "Enter" key.
*
* We cannot use the `submit` event because we cannot nest <form>
* elements inside the settings API.
*
* @param {Object} event Keypress event.
*/
maybeAddTaxRate: function( event ) {
if ( 13 !== event.keyCode ) {
return;
}
this.addTaxRate( event );
},
/**
* Add a single rate when the "form" is submitted.
*
* @param {Object} event Event.
*/
addTaxRate: function( event ) {
event.preventDefault();
const { i18n } = eddTaxRates;
if ( ! this.model.get( 'country' ) ) {
alert( i18n.emptyCountry );
return;
}
let addingRegion = this.model.get( 'region' );
let addingCountry = this.model.get( 'country' );
let addingGlobal = '' === this.model.get( 'region' );
// For the purposes of this query, the * is really an empty query.
if ( '*' === addingCountry ) {
addingCountry = '';
addingRegion = '';
addingGlobal = false;
}
const existingCountryWide = this.collection.where( {
region: addingRegion,
country: addingCountry,
global: addingGlobal,
status: 'active',
} );
if ( existingCountryWide.length > 0 ) {
const countryString = '' === addingCountry
? '*'
: addingCountry;
const regionString = '' === addingRegion
? ''
: ': ' + addingRegion;
const taxRateString = countryString + regionString;
alert( i18n.duplicateRate.replace( '%s', `"${ taxRateString }"` ) );
return;
}
if ( this.model.get( 'amount' ) <= 0 ) {
alert( i18n.emptyTax );
return;
}
// Merge cid as ID to make this a unique model.
this.collection.add( _.extend(
this.model.attributes,
{
id: this.model.cid,
}
) );
this.render();
this.initialize();
},
} );
export default TableAdd;

View File

@ -0,0 +1,33 @@
/* global wp, _ */
/**
* Output a table header and footer.
*/
const TableMeta = wp.Backbone.View.extend( {
// See https://codex.wordpress.org/Javascript_Reference/wp.template
template: wp.template( 'edd-admin-tax-rates-table-meta' ),
// Watch events.
events: {
'change [type="checkbox"]': 'selectAll',
},
/**
* Select all items in the collection.
*
* @param {Object} event Event.
*/
selectAll: function( event ) {
const checked = event.target.checked;
_.each( this.collection.models, ( model ) => {
// Check individual models.
model.set( 'selected', checked );
// Add to global selection.
this.collection.selected.push( model.cid );
} );
},
} );
export default TableMeta;

View File

@ -0,0 +1,17 @@
/* global wp */
/**
* Empty tax rates table.
*/
const TableRowEmpty = wp.Backbone.View.extend( {
// Insert as a <tr>
tagName: 'tr',
// Set class.
className: 'edd-tax-rate-row edd-tax-rate-row--is-empty',
// See https://codex.wordpress.org/Javascript_Reference/wp.template
template: wp.template( 'edd-admin-tax-rates-table-row-empty' ),
} );
export default TableRowEmpty;

View File

@ -0,0 +1,119 @@
/* global wp, _ */
/**
* A row inside a table of rates.
*/
const TableRow = wp.Backbone.View.extend( {
// Insert as a <tr>
tagName: 'tr',
// Set class.
className: function() {
return 'edd-tax-rate-row edd-tax-rate-row--' + this.model.get( 'status' );
},
// See https://codex.wordpress.org/Javascript_Reference/wp.template
template: wp.template( 'edd-admin-tax-rates-table-row' ),
// Watch events.
events: {
'click .remove': 'removeRow',
'click .activate': 'activateRow',
'click .deactivate': 'deactivateRow',
'change [type="checkbox"]': 'selectRow',
},
/**
* Bind model to view.
*/
initialize: function() {
this.listenTo( this.model, 'change', this.render );
},
/**
* Render
*/
render: function() {
this.$el.html( this.template( {
...this.model.toJSON(),
formattedAmount: this.model.formattedAmount(),
} ) );
// Ensure the wrapper class has the new name.
this.$el.attr( 'class', _.result( this, 'className' ) );
},
/**
* Remove a rate (can only be done if it has not been saved to the database).
*
* Don't use this.model.destroy() to avoid sending a DELETE request.
*
* @param {Object} event Event.
*/
removeRow: function( event ) {
event.preventDefault();
this.collection.remove( this.model );
},
/**
* Activate a rate.
*
* @param {Object} event Event.
*/
activateRow: function( event ) {
event.preventDefault();
const { i18n } = eddTaxRates;
const existingCountryWide = this.collection.where( {
region: this.model.get( 'region' ),
country: this.model.get( 'country' ),
global: '' === this.model.get( 'region' ),
status: 'active',
} );
if ( existingCountryWide.length > 0 ) {
const regionString = '' === this.model.get( 'region' )
? ''
: ': ' + this.model.get( 'region' );
const taxRateString = this.model.get( 'country' ) + regionString;
alert( i18n.duplicateRate.replace( '%s', `"${ taxRateString }"` ) );
return;
}
this.model.set( 'status', 'active' );
},
/**
* Deactivate a rate.
*
* @param {Object} event Event.
*/
deactivateRow: function( event ) {
event.preventDefault();
this.model.set( 'status', 'inactive' );
},
/**
* Select or deselect for bulk actions.
*
* @param {Object} event Event.
*/
selectRow: function( event ) {
const checked = event.target.checked;
if ( ! checked ) {
this.collection.selected = _.reject( this.collection.selected, ( cid ) => {
return cid === this.model.cid;
} );
} else {
this.collection.selected.push( this.model.cid );
}
},
} );
export default TableRow;

View File

@ -0,0 +1,65 @@
/* global wp, _ */
/**
* Internal dependencies.
*/
import TableRowEmpty from './table-row-empty.js';
import TableRow from './table-row.js';
/**
* A bunch of rows inside a table of rates.
*/
const TableRows = wp.Backbone.View.extend( {
// Insert as a <tbody>
tagName: 'tbody',
/**
* Bind events to collection.
*/
initialize: function() {
this.listenTo( this.collection, 'add', this.render );
this.listenTo( this.collection, 'remove', this.render );
this.listenTo( this.collection, 'filtered change', this.filtered );
},
/**
* Render a collection of rows.
*/
render: function() {
// Clear to handle sorting.
this.views.remove();
// Show empty placeholder.
if ( 0 === this.collection.models.length ) {
return this.views.add( new TableRowEmpty() );
}
// Add items.
_.each( this.collection.models, ( rate ) => {
this.views.add( new TableRow( {
collection: this.collection,
model: rate,
} ) );
} );
},
/**
* Show an empty state if all items are deactivated.
*/
filtered: function() {
const disabledRates = this.collection.where( {
status: 'inactive',
} );
// Check if all rows are invisible, and show the "No Items" row if so
if ( disabledRates.length === this.collection.models.length && ! this.collection.showAll ) {
this.views.add( new TableRowEmpty() );
// Possibly re-render the view
} else {
this.render();
}
},
} );
export default TableRows;

View File

@ -0,0 +1,50 @@
/* global wp */
/**
* Internal dependencies.
*/
import TableMeta from './table-meta.js';
import TableRows from './table-rows.js';
import TableAdd from './table-add.js';
/**
* Manage the tax rate rows in a table.
*/
const Table = wp.Backbone.View.extend( {
// Render as a <table> tag.
tagName: 'table',
// Set class.
className: 'wp-list-table widefat fixed tax-rates',
// Set ID.
id: 'edd_tax_rates',
/**
* Output a table with a header, body, and footer.
*/
render: function() {
this.views.add( new TableMeta( {
tagName: 'thead',
collection: this.collection,
} ) );
this.views.add( new TableRows( {
collection: this.collection,
} ) );
this.views.add( new TableAdd( {
collection: this.collection,
} ) );
this.views.add( new TableMeta( {
tagName: 'tfoot',
collection: this.collection,
} ) );
// Trigger the `filtered` action to show/hide rows accordingly
this.collection.trigger( 'filtered' );
},
} );
export default Table;

View File

@ -0,0 +1,90 @@
/**
* Export screen JS
*/
const EDD_Export = {
init: function() {
this.submit();
},
submit: function() {
const self = this;
$( document.body ).on( 'submit', '.edd-export-form', function( e ) {
e.preventDefault();
const form = $( this ),
submitButton = form.find( 'button[type="submit"]' ).first();
if ( submitButton.hasClass( 'button-disabled' ) || submitButton.is( ':disabled' ) ) {
return;
}
const data = form.serialize();
if ( submitButton.hasClass( 'button-primary' ) ) {
submitButton.removeClass( 'button-primary' ).addClass( 'button-secondary' );
}
submitButton.attr( 'disabled', true ).addClass( 'updating-message' );
form.find( '.notice-wrap' ).remove();
form.append( '<div class="notice-wrap"><div class="edd-progress"><div></div></div></div>' );
// start the process
self.process_step( 1, data, self );
} );
},
process_step: function( step, data, self ) {
$.ajax( {
type: 'POST',
url: ajaxurl,
data: {
form: data,
action: 'edd_do_ajax_export',
step: step,
},
dataType: 'json',
success: function( response ) {
if ( 'done' === response.step || response.error || response.success ) {
// We need to get the actual in progress form, not all forms on the page
const export_form = $( '.edd-export-form' ).find( '.edd-progress' ).parent().parent();
const notice_wrap = export_form.find( '.notice-wrap' );
export_form.find( 'button' ).attr( 'disabled', false ).removeClass( 'updating-message' ).addClass( 'updated-message' );
export_form.find( 'button .spinner' ).hide().css( 'visibility', 'visible' );
if ( response.error ) {
const error_message = response.message;
notice_wrap.html( '<div class="updated error"><p>' + error_message + '</p></div>' );
} else if ( response.success ) {
const success_message = response.message;
notice_wrap.html( '<div id="edd-batch-success" class="updated notice"><p>' + success_message + '</p></div>' );
if ( response.data ) {
$.each( response.data, function ( key, value ) {
$( '.edd_' + key ).html( value );
} );
}
} else {
notice_wrap.remove();
window.location = response.url;
}
} else {
$( '.edd-progress div' ).animate( {
width: response.percentage + '%',
}, 50, function() {
// Animation complete.
} );
self.process_step( parseInt( response.step ), data, self );
}
},
} ).fail( function( response ) {
if ( window.console && window.console.log ) {
console.log( response );
}
} );
},
};
jQuery( document ).ready( function( $ ) {
EDD_Export.init();
} );

View File

@ -0,0 +1,174 @@
/**
* Import screen JS
*/
var EDD_Import = {
init: function() {
this.submit();
},
submit: function() {
const self = this;
$( '.edd-import-form' ).ajaxForm( {
beforeSubmit: self.before_submit,
success: self.success,
complete: self.complete,
dataType: 'json',
error: self.error,
} );
},
before_submit: function( arr, form, options ) {
form.find( '.notice-wrap' ).remove();
form.append( '<div class="notice-wrap"><div class="edd-progress"><div></div></div></div>' );
//check whether client browser fully supports all File API
if ( window.File && window.FileReader && window.FileList && window.Blob ) {
// HTML5 File API is supported by browser
} else {
const import_form = $( '.edd-import-form' ).find( '.edd-progress' ).parent().parent();
const notice_wrap = import_form.find( '.notice-wrap' );
import_form.find( '.button:disabled' ).attr( 'disabled', false );
//Error for older unsupported browsers that doesn't support HTML5 File API
notice_wrap.html( '<div class="update error"><p>' + edd_vars.unsupported_browser + '</p></div>' );
return false;
}
},
success: function( responseText, statusText, xhr, form ) {},
complete: function( xhr ) {
const self = $( this ),
response = jQuery.parseJSON( xhr.responseText );
if ( response.success ) {
const form = $( '.edd-import-form .notice-wrap' ).parent();
form.find( '.edd-import-file-wrap,.notice-wrap' ).remove();
form.find( '.edd-import-options' ).slideDown();
// Show column mapping
let select = form.find( 'select.edd-import-csv-column' ),
row = select.parents( 'tr' ).first(),
options = '',
columns = response.data.columns.sort( function( a, b ) {
if ( a < b ) {
return -1;
}
if ( a > b ) {
return 1;
}
return 0;
} );
$.each( columns, function( key, value ) {
options += '<option value="' + value + '">' + value + '</option>';
} );
select.append( options );
select.on( 'change', function() {
const key = $( this ).val();
if ( ! key ) {
$( this ).parent().next().html( '' );
} else if ( false !== response.data.first_row[ key ] ) {
$( this ).parent().next().html( response.data.first_row[ key ] );
} else {
$( this ).parent().next().html( '' );
}
} );
$.each( select, function() {
$( this ).val( $( this ).attr( 'data-field' ) ).change();
} );
$( document.body ).on( 'click', '.edd-import-proceed', function( e ) {
e.preventDefault();
form.find( '.edd-import-proceed.button-primary' ).addClass( 'updating-message' );
form.append( '<div class="notice-wrap"><div class="edd-progress"><div></div></div></div>' );
response.data.mapping = form.serialize();
EDD_Import.process_step( 1, response.data, self );
} );
} else {
EDD_Import.error( xhr );
}
},
error: function( xhr ) {
// Something went wrong. This will display error on form
const response = jQuery.parseJSON( xhr.responseText );
const import_form = $( '.edd-import-form' ).find( '.edd-progress' ).parent().parent();
const notice_wrap = import_form.find( '.notice-wrap' );
import_form.find( '.button:disabled' ).attr( 'disabled', false );
if ( response.data.error ) {
notice_wrap.html( '<div class="update error"><p>' + response.data.error + '</p></div>' );
} else {
notice_wrap.remove();
}
},
process_step: function( step, import_data, self ) {
$.ajax( {
type: 'POST',
url: ajaxurl,
data: {
form: import_data.form,
nonce: import_data.nonce,
class: import_data.class,
upload: import_data.upload,
mapping: import_data.mapping,
action: 'edd_do_ajax_import',
step: step,
},
dataType: 'json',
success: function( response ) {
if ( 'done' === response.data.step || response.data.error ) {
// We need to get the actual in progress form, not all forms on the page
const import_form = $( '.edd-import-form' ).find( '.edd-progress' ).parent().parent();
const notice_wrap = import_form.find( '.notice-wrap' );
import_form.find( '.button:disabled' ).attr( 'disabled', false );
if ( response.data.error ) {
notice_wrap.html( '<div class="update error"><p>' + response.data.error + '</p></div>' );
} else {
import_form.find( '.edd-import-options' ).hide();
$( 'html, body' ).animate( {
scrollTop: import_form.parent().offset().top,
}, 500 );
notice_wrap.html( '<div class="updated"><p>' + response.data.message + '</p></div>' );
}
} else {
$( '.edd-progress div' ).animate( {
width: response.data.percentage + '%',
}, 50, function() {
// Animation complete.
} );
EDD_Import.process_step( parseInt( response.data.step ), import_data, self );
}
},
} ).fail( function( response ) {
if ( window.console && window.console.log ) {
console.log( response );
}
} );
},
};
jQuery( document ).ready( function( $ ) {
EDD_Import.init();
} );

View File

@ -0,0 +1,121 @@
/**
* Tools screen JS
*/
const EDD_Tools = {
init: function() {
this.revoke_api_key();
this.regenerate_api_key();
this.create_api_key();
this.recount_stats();
},
revoke_api_key: function() {
$( document.body ).on( 'click', '.edd-revoke-api-key', function( e ) {
return confirm( edd_vars.revoke_api_key );
} );
},
regenerate_api_key: function() {
$( document.body ).on( 'click', '.edd-regenerate-api-key', function( e ) {
return confirm( edd_vars.regenerate_api_key );
} );
},
create_api_key: function() {
$( document.body ).on( 'submit', '#api-key-generate-form', function( e ) {
const input = $( 'input[type="text"][name="user_id"]' );
input.css( 'border-color', '#ddd' );
const user_id = input.val();
if ( user_id.length < 1 || user_id === 0 ) {
input.css( 'border-color', '#ff0000' );
return false;
}
} );
},
recount_stats: function() {
$( document.body ).on( 'change', '#recount-stats-type', function() {
const export_form = $( '#edd-tools-recount-form' ),
selected_type = $( 'option:selected', this ).data( 'type' ),
submit_button = $( '#recount-stats-submit' ),
products = $( '#tools-product-dropdown' );
// Reset the form
export_form.find( '.notice-wrap' ).remove();
submit_button.attr( 'disabled', false ).removeClass( 'updated-message' );
products.hide();
$( '.edd-recount-stats-descriptions span' ).hide();
if ( 'recount-download' === selected_type ) {
products.show();
products.find( '.edd-select-chosen' ).css( 'width', 'auto' );
} else if ( 'reset-stats' === selected_type ) {
export_form.append( '<div class="notice-wrap"></div>' );
const notice_wrap = export_form.find( '.notice-wrap' );
notice_wrap.html( '<div class="notice notice-warning"><p><input type="checkbox" id="confirm-reset" name="confirm_reset_store" value="1" /> <label for="confirm-reset">' + edd_vars.reset_stats_warn + '</label></p></div>' );
$( '#recount-stats-submit' ).attr( 'disabled', true );
} else {
products.hide();
products.val( 0 );
}
$( '#' + selected_type ).show();
} );
$( document.body ).on( 'change', '#confirm-reset', function() {
const checked = $( this ).is( ':checked' );
if ( checked ) {
$( '#recount-stats-submit' ).attr( 'disabled', false );
} else {
$( '#recount-stats-submit' ).attr( 'disabled', true );
}
} );
$( '#edd-tools-recount-form' ).submit( function( e ) {
e.preventDefault();
const selection = $( '#recount-stats-type' ).val(),
export_form = $( this ),
selected_type = $( 'option:selected', this ).data( 'type' );
if ( 'reset-stats' === selected_type ) {
const is_confirmed = $( '#confirm-reset' ).is( ':checked' );
if ( is_confirmed ) {
return true;
}
has_errors = true;
}
export_form.find( '.notice-wrap' ).remove();
export_form.append( '<div class="notice-wrap"></div>' );
var notice_wrap = export_form.find( '.notice-wrap' ),
has_errors = false;
if ( null === selection || 0 === selection ) {
// Needs to pick a method edd_vars.batch_export_no_class
notice_wrap.html( '<div class="updated error"><p>' + edd_vars.batch_export_no_class + '</p></div>' );
has_errors = true;
}
if ( 'recount-download' === selected_type ) {
const selected_download = $( 'select[name="download_id"]' ).val();
if ( selected_download === 0 ) {
// Needs to pick download edd_vars.batch_export_no_reqs
notice_wrap.html( '<div class="updated error"><p>' + edd_vars.batch_export_no_reqs + '</p></div>' );
has_errors = true;
}
}
if ( has_errors ) {
export_form.find( 'button:disabled' ).attr( 'disabled', false ).removeClass( 'updated-message' );
return false;
}
} );
},
};
jQuery( document ).ready( function( $ ) {
EDD_Tools.init();
} );

View File

@ -0,0 +1 @@
import './v3';

View File

@ -0,0 +1,211 @@
const EDD_v3_Upgrades = {
inProgress: false,
init: function() {
// Listen for toggle on the checkbox.
$( '.edd-v3-migration-confirmation' ).on( 'change', function( e ) {
const wrapperForm = $( this ).closest( '.edd-v3-migration' );
const formSubmit = wrapperForm.find( 'button' );
if ( e.target.checked ) {
formSubmit.removeClass( 'disabled' ).prop( 'disabled', false );
} else {
formSubmit.addClass( 'disabled' ).prop( 'disabled', true );
}
} );
$( '.edd-v3-migration' ).on( 'submit', function( e ) {
e.preventDefault();
if ( EDD_v3_Upgrades.inProgress ) {
return;
}
EDD_v3_Upgrades.inProgress = true;
const migrationForm = $( this );
const upgradeKeyField = migrationForm.find( 'input[name="upgrade_key"]' );
let upgradeKey = false;
if ( upgradeKeyField.length && upgradeKeyField.val() ) {
upgradeKey = upgradeKeyField.val();
}
// Disable submit button.
migrationForm.find( 'button' )
.removeClass( 'button-primary' )
.addClass( 'button-secondary disabled updating-message' )
.prop( 'disabled', true );
// Disable checkbox.
migrationForm.find( 'input' ).prop( 'disabled', true );
// If this is the main migration, reveal the steps & mark the first non-complete item as in progress.
if ( 'edd-v3-migration' === migrationForm.attr( 'id' ) ) {
$( '#edd-migration-progress' ).removeClass( 'edd-hidden' );
const firstNonCompleteUpgrade = $( '#edd-migration-progress li:not(.edd-upgrade-complete)' );
if ( firstNonCompleteUpgrade.length && ! upgradeKey ) {
upgradeKey = firstNonCompleteUpgrade.data( 'upgrade' );
}
}
EDD_v3_Upgrades.processStep( upgradeKey, 1, migrationForm.find( 'input[name="_wpnonce"]' ).val() );
} )
},
processStep: function( upgrade_key, step, nonce ) {
let data = {
action: 'edd_process_v3_upgrade',
_ajax_nonce: nonce,
upgrade_key: upgrade_key,
step: step
}
EDD_v3_Upgrades.clearErrors();
if ( upgrade_key ) {
EDD_v3_Upgrades.markUpgradeInProgress( upgrade_key );
}
$.ajax( {
type: 'POST',
data: data,
url: ajaxurl,
success: function( response ) {
if ( ! response.success ) {
EDD_v3_Upgrades.showError( upgrade_key, response.data );
return;
}
if ( response.data.upgrade_completed ) {
EDD_v3_Upgrades.markUpgradeComplete( response.data.upgrade_processed );
// If we just completed legacy data removal then we're all done!
if ( 'v30_legacy_data_removed' === response.data.upgrade_processed ) {
EDD_v3_Upgrades.legacyDataRemovalComplete();
return;
}
} else if( response.data.percentage ) {
// Update percentage for the upgrade we just processed.
EDD_v3_Upgrades.updateUpgradePercentage( response.data.upgrade_processed, response.data.percentage );
}
if ( response.data.next_upgrade && 'v30_legacy_data_removed' === response.data.next_upgrade && 'v30_legacy_data_removed' !== response.data.upgrade_processed ) {
EDD_v3_Upgrades.inProgress = false;
// Legacy data removal is next, which we do not start automatically.
EDD_v3_Upgrades.showLegacyDataRemoval();
} else if ( response.data.next_upgrade ) {
// Start the next upgrade (or continuation of current) automatically.
EDD_v3_Upgrades.processStep( response.data.next_upgrade, response.data.next_step, response.data.nonce );
} else {
EDD_v3_Upgrades.inProgress = false;
EDD_v3_Upgrades.stopAllSpinners();
}
}
} ).fail( ( data ) => {
// @todo
} )
},
clearErrors: function() {
$( '.edd-v3-migration-error' ).addClass( 'edd-hidden' ).html( '' );
},
showError: function( upgradeKey, message ) {
let container = $( '#edd-v3-migration' );
if ( 'v30_legacy_data_removed' === upgradeKey ) {
container = $( '#edd-v3-remove-legacy-data' );
}
const errorWrapper = container.find( '.edd-v3-migration-error' );
errorWrapper.html( '<p>' + message + '</p>' ).removeClass( 'edd-hidden' );
// Stop processing and allow form resubmission.
EDD_v3_Upgrades.inProgress = false;
container.find( 'input' ).prop( 'disabled', false );
container.find( 'button' )
.prop( 'disabled', false )
.addClass( 'button-primary' )
.removeClass( 'button-secondary disabled updating-message' );
},
markUpgradeInProgress: function( upgradeKey ) {
const upgradeRow = $( '#edd-v3-migration-' + upgradeKey );
if ( ! upgradeRow.length ) {
return;
}
const statusIcon = upgradeRow.find( '.dashicons' );
if ( statusIcon.length ) {
statusIcon.removeClass( 'dashicons-minus' ).addClass( 'dashicons-update' );
}
upgradeRow.find( '.edd-migration-percentage' ).removeClass( 'edd-hidden' );
},
updateUpgradePercentage: function( upgradeKey, newPercentage ) {
const upgradeRow = $( '#edd-v3-migration-' + upgradeKey );
if ( ! upgradeRow.length ) {
return;
}
upgradeRow.find( '.edd-migration-percentage-value' ).text( newPercentage );
},
markUpgradeComplete: function( upgradeKey ) {
const upgradeRow = $( '#edd-v3-migration-' + upgradeKey );
if ( ! upgradeRow.length ) {
return;
}
upgradeRow.addClass( 'edd-upgrade-complete' );
const statusIcon = upgradeRow.find( '.dashicons' );
if ( statusIcon.length ) {
statusIcon.removeClass( 'dashicons-minus dashicons-update' ).addClass( 'dashicons-yes' );
}
const statusLabel = upgradeRow.find( '.edd-migration-status .screen-reader-text' );
if ( statusLabel.length ) {
statusLabel.text( edd_admin_upgrade_vars.migration_complete );
}
// Update percentage to 100%;
upgradeRow.find( '.edd-migration-percentage-value' ).text( 100 );
},
showLegacyDataRemoval: function() {
// Un-spin the main submit button.
$( '#edd-v3-migration-button' ).removeClass( 'updating-message' );
// Show the "migration complete" message.
$( '#edd-v3-migration-complete' ).removeClass( 'edd-hidden' );
const dataRemovalWrapper = $( '#edd-v3-remove-legacy-data' );
if ( ! dataRemovalWrapper.length ) {
return;
}
dataRemovalWrapper.removeClass( 'edd-hidden' );
},
legacyDataRemovalComplete: function() {
const wrapper = $( '#edd-v3-remove-legacy-data' );
if ( ! wrapper.length ) {
return;
}
wrapper.find( 'form' ).addClass( 'edd-hidden' );
wrapper.find( '#edd-v3-legacy-data-removal-complete' ).removeClass( 'edd-hidden' );
},
stopAllSpinners: function() {
}
}
jQuery( document ).ready( function( $ ) {
EDD_v3_Upgrades.init();
} );