many improvements on listing + cleaned up modal library

This commit is contained in:
Jonathan Labreuille
2015-08-26 19:10:09 +02:00
committed by marco
parent 739f06bad7
commit 0e2c04d97a
10 changed files with 796 additions and 753 deletions

View File

@ -1,8 +1,7 @@
define('admin', [ define('admin', [
'mailpoet',
'jquery' 'jquery'
], ],
function(MailPoet, jQuery) { function(jQuery) {
jQuery(function($) { jQuery(function($) {
// dom ready // dom ready
$(function() { $(function() {

View File

@ -1,5 +1,5 @@
define('ajax', ['mailpoet', 'jquery'], function(MailPoet, jQuery) { define('ajax', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
"use strict"; 'use strict';
MailPoet.Ajax = { MailPoet.Ajax = {
version: 0.5, version: 0.5,
options: {}, options: {},

View File

@ -0,0 +1,48 @@
define('listing.column', ['react', 'classnames'], function(React, classNames) {
/*
props:
onSort -> callback(sort_by, sort_order)
column -> {
sorted: (string) asc | desc
sortable: (bool)
name: (string) field name
label: (string) displayed label
*/
var ListingColumn = React.createClass({
handleSort: function() {
var sort_by = this.props.column.name,
sort_order = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
this.props.onSort(sort_by, sort_order);
},
render: function() {
var classes = classNames(
'manage-column',
{ 'sortable': this.props.column.sortable },
this.props.column.sorted
);
var label;
if(this.props.column.sortable === true) {
label = (
<a onClick={this.handleSort}>
<span>{ this.props.column.label }</span>
<span className="sorting-indicator"></span>
</a>
);
} else {
label = this.props.column.label;
}
return (
<th
className={ classes }
id={this.props.column.name }
scope="col">
{label}
</th>
);
}
});
return ListingColumn;
});

View File

@ -0,0 +1,51 @@
define('listing.header', ['react', 'classnames'], function(React, classNames) {
/*
props:
onSort: callback(sort_by, sort_order)
onSelectAll: callback(is_checked)
sort_by: (string) field name
columns -> (array)
column -> {
sorted: (string) asc | desc
sortable: (bool)
name: (string) field name
label: (string) displayed label
}
*/
var ListingColumn = require('listing/column.jsx');
var ListingHeader = React.createClass({
handleSelectAll: function() {
return this.props.onSelectAll(
this.refs.select_all.getDOMNode().checked
);
},
render: function() {
var columns = this.props.columns.map(function(column) {
return (
<ListingColumn
onSort={this.props.onSort}
sort_by={this.props.sort_by}
key={column.name}
column={column} />
);
}.bind(this));
return (
<tr>
<td className="manage-column column-cb check-column" id="cb">
<label className="screen-reader-text">
{ 'Select All' }
</label>
<input
type="checkbox"
ref="select_all"
onChange={this.handleSelectAll} />
</td>
{columns}
</tr>
);
}
});
return ListingHeader;
});

View File

@ -0,0 +1,143 @@
define('listing.pages', ['react', 'classnames'], function(React, classNames) {
/*
props:
onSetPage -> callback(page)
page -> (int) current page
count -> (int) total number of items
limit -> (int) limit per page
*/
var ListingPages = React.createClass({
setFirstPage: function() {
this.props.onSetPage(1);
},
setLastPage: function() {
this.props.onSetPage(this.getLastPage());
},
setPreviousPage: function() {
this.props.onSetPage(this.constrainPage(this.props.page - 1));
},
setNextPage: function() {
this.props.onSetPage(this.constrainPage(this.props.page + 1));
},
constrainPage: function(page) {
return Math.min(Math.max(1, Math.abs(~~page)), this.getLastPage());
},
handleSetPage: function() {
this.props.onSetPage(
this.constrainPage(this.refs.page.getDOMNode().value)
);
},
getLastPage: function() {
return Math.ceil(this.props.count / this.props.limit);
},
render: function() {
if(this.props.count === 0) {
return (<div></div>);
} else {
var pagination,
firstPage = (
<span aria-hidden="true" className="tablenav-pages-navspan">«</span>
),
previousPage = (
<span aria-hidden="true" className="tablenav-pages-navspan"></span>
),
nextPage = (
<span aria-hidden="true" className="tablenav-pages-navspan"></span>
),
lastPage = (
<span aria-hidden="true" className="tablenav-pages-navspan">»</span>
);
if(this.props.count > this.props.limit) {
if(this.props.page > 1) {
previousPage = (
<a href="javascript:;"
onClick={this.setPreviousPage}
className="prev-page">
<span className="screen-reader-text">Previous page</span>
<span aria-hidden="true"></span>
</a>
);
}
if(this.props.page > 2) {
firstPage = (
<a href="javascript:;"
onClick={this.setFirstPage}
className="first-page">
<span className="screen-reader-text">First page</span>
<span aria-hidden="true">«</span>
</a>
);
}
if(this.props.page < this.getLastPage()) {
nextPage = (
<a href="javascript:;"
onClick={this.setNextPage}
className="next-page">
<span className="screen-reader-text">Next page</span>
<span aria-hidden="true"></span>
</a>
);
}
if(this.props.page < this.getLastPage() - 1) {
lastPage = (
<a href="javascript:;"
onClick={this.setLastPage}
className="last-page">
<span className="screen-reader-text">Last page</span>
<span aria-hidden="true">»</span>
</a>
);
}
pagination = (
<span className="pagination-links">
{firstPage}
{previousPage}
&nbsp;
<span className="paging-input">
<label
className="screen-reader-text"
htmlFor="current-page-selector">Current Page</label>
<input
type="text"
onChange={this.handleSetPage}
aria-describedby="table-paging"
size="1"
ref="page"
value={this.props.page}
name="paged"
id="current-page-selector"
className="current-page" />
&nbsp;of&nbsp;
<span className="total-pages">
{Math.ceil(this.props.count / this.props.limit)}
</span>
</span>
&nbsp;
{nextPage}
{lastPage}
</span>
);
}
var classes = classNames(
'tablenav-pages',
{ 'one-page': (this.props.count <= this.props.limit) }
);
return (
<div className={classes}>
<span className="displaying-num">{ this.props.count } item(s)</span>
{ pagination }
</div>
);
}
}
});
return ListingPages;
});

View File

@ -0,0 +1,36 @@
define('listing.search', ['react'], function(React) {
/*
props:
onSearch -> callback(search)
search -> string
*/
var ListingSearch = React.createClass({
handleSearch: function(e) {
e.preventDefault();
this.props.onSearch(
this.refs.search.getDOMNode().value
);
},
render: function() {
return (
<form name="search" onSubmit={this.handleSearch}>
<p className="search-box">
<label htmlFor="search_input" className="screen-reader-text">
Search
</label>
<input
type="search"
ref="search"
defaultValue={this.props.search} />
<input
type="submit"
defaultValue="Search"
className="button" />
</p>
</form>
);
}
});
return ListingSearch;
});

View File

@ -1,84 +1,27 @@
define('modal', ['mailpoet', 'jquery'], function(MailPoet, jQuery) { define('modal', ['mailpoet', 'jquery'],
"use strict"; function(MailPoet, jQuery) {
/*================================================================================================== 'use strict';
/***************************************************************************
MailPoet Modal:
MailPoet Modal: version: 0.9
author: Jonathan Labreuille
company: Wysija
dependencies: jQuery
version: 0.8 Usage:
author: Jonathan Labreuille // popup mode
company: Wysija MailPoet.Modal.popup(options);
dependencies: jQuery
// panel mode
MailPoet.Modal.panel(options);
Options: // loading mode
MailPoet.Modal.loading(bool);
***************************************************************************/
Mandatory: MailPoet.Modal = {
// Modal window's title version: 0.9,
(string) title: 'Modal title'
// template
(string) template: jQuery('#handlebars_template').html() or
literal html
Optional:
// jQuery cached element object node to be displayed,
// instead of creating a new one
(object) element: jQuery(selector)
// - data object that will be passed to the template when rendering
(object) data: {},
// - data will be loaded via this url and passed to the template
// when rendering
// - if a "data" option was specified, it will be merged with the
// ajax's response data
(string) url: '/url.json'
// ajax method
(string) method: 'post' (default: 'get')
// ajax post params
(object) params: {}
// - integers are expressed in pixels
(mixed) width: '50%' | 100 | '100px'
// - integers are expressed in pixels
// - will be ignored when in "panel" mode
(mixed) height: '50%' | 100 | '100px'
// - only used for "panel" mode
// - will be ignored in "popup" mode
(string) position: 'left' | 'right'
// display overlay or not
(boolean) overlay: true | false
// element(s) to be highlighted when the overlay is "on"
(object) highlight: jQuery element
// callbacks
(function) onInit: called when the modal is displayed
(function) onSuccess: called by calling MailPoet_Guide.success()
(function) onCancel: called when closing the popup
or by calling MailPoet_Guide.cancel()
Usage:
// popup mode
MailPoet.Modal.popup(options);
// panel mode
MailPoet.Modal.panel(options);
// loading states
MailPoet.Modal.loading(true); // displays loading indicator
MailPoet.Modal.loading(false); // hides loading indicator
==================================================================================================*/
MailPoet.Modal = {
version: 0.8,
// flags // flags
initialized: false, initialized: false,
@ -90,324 +33,343 @@ define('modal', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
// default values // default values
defaults: { defaults: {
// title // title
title: null, title: null,
// type // type
type: null, type: null,
// positionning // positionning
position: 'right', position: 'right',
// data sources // data sources
data: {}, data: {},
url: null, url: null,
method: 'get', method: 'get',
params: {}, params: {},
// template // template
template: null, template: null,
body_template: null, body_template: null,
// dimensions // dimensions
width: 'auto', width: 'auto',
height: 'auto', height: 'auto',
// display overlay // display overlay
overlay: false, overlay: false,
// highlighted elements // highlighted elements
highlight: null, highlight: null,
// callbacks // callbacks
onInit: null, onInit: null,
onSuccess: null, onSuccess: null,
onCancel: null onCancel: null
}, },
renderer: 'html', renderer: 'html',
options: {}, options: {},
templates: { templates: {
overlay: '<div id="mailpoet_modal_overlay" style="display:none;"></div>', overlay: '<div id="mailpoet_modal_overlay" style="display:none;"></div>',
popup: '<div id="mailpoet_popup">'+ popup: '<div id="mailpoet_popup">'+
'<div class="mailpoet_popup_wrapper">'+ '<div class="mailpoet_popup_wrapper">'+
'<a href="javascript:;" id="mailpoet_modal_close"></a>'+ '<a href="javascript:;" id="mailpoet_modal_close"></a>'+
'<div id="mailpoet_popup_title"><h2></h2></div>'+ '<div id="mailpoet_popup_title"><h2></h2></div>'+
'<div class="mailpoet_popup_body clearfix"></div>'+ '<div class="mailpoet_popup_body clearfix"></div>'+
'</div>'+ '</div>'+
'</div>', '</div>',
loading: '<div id="mailpoet_loading" style="display:none;">'+ loading: '<div id="mailpoet_loading" style="display:none;">'+
'<div id="mailpoet_modal_loading_1" class="mailpoet_modal_loading"></div>'+ '<div id="mailpoet_modal_loading_1" class="mailpoet_modal_loading"></div>'+
'<div id="mailpoet_modal_loading_2" class="mailpoet_modal_loading"></div>'+ '<div id="mailpoet_modal_loading_2" class="mailpoet_modal_loading"></div>'+
'<div id="mailpoet_modal_loading_3" class="mailpoet_modal_loading"></div>'+ '<div id="mailpoet_modal_loading_3" class="mailpoet_modal_loading"></div>'+
'</div>', '</div>',
panel: '<div id="mailpoet_panel">'+ panel: '<div id="mailpoet_panel">'+
'<a href="javascript:;" id="mailpoet_modal_close"></a>'+ '<a href="javascript:;" id="mailpoet_modal_close"></a>'+
'<div class="mailpoet_panel_wrapper">'+ '<div class="mailpoet_panel_wrapper">'+
'<div class="mailpoet_panel_body clearfix"></div>'+ '<div class="mailpoet_panel_body clearfix"></div>'+
'</div>'+ '</div>'+
'</div>', '</div>',
subpanel: '<div class="mailpoet_panel_wrapper">'+ subpanel: '<div class="mailpoet_panel_wrapper">'+
'<div class="mailpoet_panel_body clearfix"></div>'+ '<div class="mailpoet_panel_body clearfix"></div>'+
'</div>' '</div>'
}, },
setRenderer: function() { getContentContainer: function() {
this.renderer = (typeof(Handlebars) === "undefined") ? 'html' : 'handlebars'; return jQuery('.mailpoet_'+this.options.type+'_body');
},
setRenderer: function(renderer) {
this.renderer = renderer;
return this;
}, },
compileTemplate: function(template) { compileTemplate: function(template) {
if(this.renderer === 'html') { if(this.renderer === 'html') {
return function() { return template; }; return function() { return template; };
} else { } else {
return Handlebars.compile(template); return Handlebars.compile(template);
} }
return false;
}, },
init: function(options) { init: function(options) {
if(this.initialized === true) { if(this.initialized === true) {
this.close(); this.close();
}
// merge options
this.options = jQuery.extend({}, this.defaults, options);
// set renderer
this.setRenderer(this.options.renderer);
// init overlay
this.initOverlay();
// toggle overlay
this.toggleOverlay(this.options.overlay);
if(this.options.type !== null) {
// insert modal depending on its type
if(this.options.type === 'popup') {
var modal = this.compileTemplate(
this.templates[this.options.type]
);
// create modal
jQuery('#mailpoet_modal_overlay')
.append(modal(this.options));
// set title
jQuery('#mailpoet_popup_title h2')
.html(this.options.title);
} else if(this.options.type === 'panel') {
// create panel
jQuery('#mailpoet_modal_overlay')
.after(this.templates[this.options.type]);
} }
// merge options // add proper overlay class
this.options = jQuery.extend({}, this.defaults, options); jQuery('#mailpoet_modal_overlay')
.removeClass('mailpoet_popup_overlay mailpoet_panel_overlay')
.addClass('mailpoet_'+this.options.type+'_overlay');
}
// set renderer // set "success" callback if specified
this.setRenderer(); if(options.onSuccess !== undefined) {
this.options.onSuccess = options.onSuccess;
}
// init overlay // set "cancel" callback if specified
this.initOverlay(); if(options.onCancel !== undefined) {
this.options.onCancel = options.onCancel;
}
// toggle overlay // compile template
this.toggleOverlay(this.options.overlay); this.options.body_template = this.compileTemplate(
this.options.template
);
if(this.options.type !== null) { // setup events
// insert modal depending on its type this.setupEvents();
if(this.options.type === 'popup') {
var modal = this.compileTemplate(this.templates[this.options.type]);
// create modal
jQuery('#mailpoet_modal_overlay').append(modal(this.options));
// set title
jQuery('#mailpoet_popup_title h2').html(this.options.title);
} else if(this.options.type === 'panel') {
// create panel
jQuery('#mailpoet_modal_overlay').after(this.templates[this.options.type]);
}
// add proper overlay class // set popup as initialized
jQuery('#mailpoet_modal_overlay') this.initialized = true;
.removeClass('mailpoet_popup_overlay mailpoet_panel_overlay')
.addClass('mailpoet_'+this.options.type+'_overlay');
}
// render template if specified return this;
if(this.options.template !== null) {
// set "success" callback if specified
if(options.onSuccess !== undefined) {
this.options.onSuccess = options.onSuccess;
}
// set "cancel" callback if specified
if(options.onCancel !== undefined) {
this.options.onCancel = options.onCancel;
}
// compile template
this.options.body_template = this.compileTemplate(this.options.template);
// setup events
this.setupEvents();
}
// set popup as initialized
this.initialized = true;
return this;
}, },
initOverlay: function(toggle) { initOverlay: function(toggle) {
if(jQuery('#mailpoet_modal_overlay').length === 0) { if(jQuery('#mailpoet_modal_overlay').length === 0) {
// insert overlay into the DOM // insert overlay into the DOM
jQuery('body').append(this.templates.overlay); jQuery('body').append(this.templates.overlay);
// insert loading indicator into overlay // insert loading indicator into overlay
jQuery('#mailpoet_modal_overlay').append(this.templates.loading); jQuery('#mailpoet_modal_overlay').append(this.templates.loading);
} }
return this; return this;
}, },
toggleOverlay: function(toggle) { toggleOverlay: function(toggle) {
if(toggle === true) { if(toggle === true) {
jQuery('#mailpoet_modal_overlay').removeClass('mailpoet_overlay_hidden'); jQuery('#mailpoet_modal_overlay')
.removeClass('mailpoet_overlay_hidden');
} else { } else {
jQuery('#mailpoet_modal_overlay').addClass('mailpoet_overlay_hidden'); jQuery('#mailpoet_modal_overlay')
.addClass('mailpoet_overlay_hidden');
} }
return this; return this;
}, },
setupEvents: function() { setupEvents: function() {
// close popup when user clicks on close button // close popup when user clicks on close button
jQuery('#mailpoet_modal_close').on('click', this.cancel.bind(this)); jQuery('#mailpoet_modal_close').on('click', this.cancel.bind(this));
// close popup when user clicks on overlay // close popup when user clicks on overlay
jQuery('#mailpoet_modal_overlay').on('click', function(e) { jQuery('#mailpoet_modal_overlay').on('click', function(e) {
// we need to make sure that we are actually clicking on the overlay // we need to make sure that we are actually clicking on the overlay
// because when clicking on the popup content, it will trigger the click // because when clicking on the popup content, it will trigger
// event on the overlay // the click event on the overlay
if(e.target.id === 'mailpoet_modal_overlay') { this.cancel(); } if(e.target.id === 'mailpoet_modal_overlay') { this.cancel(); }
}.bind(this)); }.bind(this));
// close popup when user presses ESC key // close popup when user presses ESC key
jQuery(document).on('keyup.mailpoet_modal', function(e) { jQuery(document).on('keyup.mailpoet_modal', function(e) {
if(this.opened === false) { return false; } if(this.opened === false) { return false; }
if(e.keyCode === 27) { this.cancel(); } if(e.keyCode === 27) { this.cancel(); }
}.bind(this)); }.bind(this));
// make sure the popup is repositioned when the window is resized // make sure the popup is repositioned when the window is resized
jQuery(window).on('resize.mailpoet_modal', function() { jQuery(window).on('resize.mailpoet_modal', function() {
this.setPosition(); this.setPosition();
}.bind(this)); }.bind(this));
return this; return this;
}, },
removeEvents: function() { removeEvents: function() {
jQuery(document).unbind('keyup.mailpoet_modal'); jQuery(document).unbind('keyup.mailpoet_modal');
jQuery(window).unbind('resize.mailpoet_modal'); jQuery(window).unbind('resize.mailpoet_modal');
jQuery('#mailpoet_modal_close').off('click'); jQuery('#mailpoet_modal_close').off('click');
if(this.options.overlay === true) { if(this.options.overlay === true) {
jQuery('#mailpoet_modal_overlay').off('click'); jQuery('#mailpoet_modal_overlay').off('click');
} }
return this; return this;
}, },
lock: function() { lock: function() {
this.locked = true; this.locked = true;
return this; return this;
}, },
unlock: function() { unlock: function() {
this.locked = false; this.locked = false;
return this; return this;
}, },
isLocked: function() { isLocked: function() {
return this.locked; return this.locked;
}, },
loadTemplate: function() { loadTemplate: function() {
if(this.subpanels.length > 0) { if(this.subpanels.length > 0) {
// hide panel // hide panel
jQuery('.mailpoet_'+this.options.type+'_wrapper').hide(); jQuery('.mailpoet_'+this.options.type+'_wrapper').hide();
// add sub panel wrapper // add sub panel wrapper
jQuery('#mailpoet_'+this.options.type).append(this.templates['subpanel']); jQuery('#mailpoet_'+this.options.type)
.append(this.templates['subpanel']);
// add sub panel content // add sub panel content
jQuery('.mailpoet_'+this.options.type+'_body').last().html(this.subpanels[(this.subpanels.length - 1)].element); jQuery('.mailpoet_'+this.options.type+'_body').last()
.html(this.subpanels[(this.subpanels.length - 1)].element);
} else if (this.options.element) { } else if (this.options.element) {
jQuery('.mailpoet_'+this.options.type+'_body').empty(); jQuery('.mailpoet_'+this.options.type+'_body').empty();
jQuery('.mailpoet_'+this.options.type+'_body').append(this.options.element); jQuery('.mailpoet_'+this.options.type+'_body')
.append(this.options.element);
} else { } else {
jQuery('.mailpoet_'+this.options.type+'_body').html( jQuery('.mailpoet_'+this.options.type+'_body')
this.options.body_template( .html(
this.options.data this.options.body_template(
) this.options.data
)
); );
} }
return this; return this;
}, },
loadUrl: function() { loadUrl: function() {
if(this.options.method === 'get') { if(this.options.method === 'get') {
// make ajax request // make ajax request
jQuery.getJSON(this.options.url, function(data) { jQuery.getJSON(this.options.url,
// merge returned data with existing data passed when calling the "open" method function(data) {
this.options.data = jQuery.extend({}, this.options.data, data); this.options.data = jQuery.extend({}, this.options.data, data);
// load template using fetched data // load template using fetched data
this.loadTemplate(); this.loadTemplate();
// show modal window // show modal window
this.showModal(); this.showModal();
}.bind(this)); }.bind(this)
);
} else if(this.options.method === 'post') { } else if(this.options.method === 'post') {
// make ajax request // make ajax request
jQuery.post(this.options.url, JSON.stringify(this.options.params), function(data) { jQuery.post(this.options.url, JSON.stringify(this.options.params),
// merge returned data with existing data passed when calling the "open" method function(data) {
this.options.data = jQuery.extend({}, this.options.data, data); this.options.data = jQuery.extend({}, this.options.data, data);
// load template using fetched data // load template using fetched data
this.loadTemplate(); this.loadTemplate();
// show modal window // show modal window
this.showModal(); this.showModal();
}.bind(this), 'json'); }.bind(this),
'json'
);
} }
return this; return this;
}, },
setDimensions: function() { setDimensions: function() {
switch(this.options.type) { switch(this.options.type) {
case 'popup': case 'popup':
// set popup dimensions // set popup dimensions
jQuery('#mailpoet_popup').css({ jQuery('#mailpoet_popup').css({
width: this.options.width, width: this.options.width,
minHeight: this.options.height minHeight: this.options.height
}); });
// set popup wrapper height // set popup wrapper height
jQuery('#mailpoet_popup_wrapper').css({ height: this.options.height}); jQuery('#mailpoet_popup_wrapper').css({
break; height: this.options.height
case 'panel': });
// set dimensions break;
if(this.options.position === 'right') { case 'panel':
jQuery('#mailpoet_panel').css({ // set dimensions
width: this.options.width, if(this.options.position === 'right') {
right: 0, jQuery('#mailpoet_panel').css({
marginRight: '-' + this.options.width, width: this.options.width,
left: 'auto' right: 0,
}); marginRight: '-' + this.options.width,
} else if(this.options.position === 'left') { left: 'auto'
jQuery('#mailpoet_panel').css({ });
width: this.options.width, } else if(this.options.position === 'left') {
left: 0, jQuery('#mailpoet_panel').css({
marginLeft: '-' + this.options.width, width: this.options.width,
right: 'auto' left: 0,
}); marginLeft: '-' + this.options.width,
} right: 'auto'
jQuery('#mailpoet_panel').css({ minHeight: 'auto' }); });
break; }
jQuery('#mailpoet_panel').css({ minHeight: 'auto' });
break;
} }
return this; return this;
}, },
setPosition: function() { setPosition: function() {
switch(this.options.type) { switch(this.options.type) {
case 'popup': case 'popup':
var screenWidth = jQuery(window).width(), var screenWidth = jQuery(window).width(),
screenHeight = jQuery(window).height(), screenHeight = jQuery(window).height(),
modalWidth = jQuery('.mailpoet_'+ this.options.type +'_wrapper').width(), modalWidth = jQuery('.mailpoet_'+ this.options.type +'_wrapper').width(),
modalHeight = jQuery('.mailpoet_'+ this.options.type +'_wrapper').height(); modalHeight = jQuery('.mailpoet_'+ this.options.type +'_wrapper').height();
var top = Math.max(48, parseInt((screenHeight / 2) - (modalHeight / 2))), var top = Math.max(48, parseInt((screenHeight / 2) - (modalHeight / 2))),
left = Math.max(0, parseInt((screenWidth / 2) - (modalWidth / 2))); left = Math.max(0, parseInt((screenWidth / 2) - (modalWidth / 2)));
// set position of popup depending on screen dimensions. // set position of popup depending on screen dimensions.
jQuery('#mailpoet_popup').css({ jQuery('#mailpoet_popup').css({
top: top, top: top,
left: left left: left
});
break;
case 'panel':
setTimeout(function() {
// set position of popup depending on screen dimensions.
if(this.options.position === 'right') {
jQuery('#mailpoet_panel').css({
marginRight: 0
}); });
break; } else if(this.options.position === 'left') {
case 'panel': jQuery('#mailpoet_panel').css({
setTimeout(function() { marginLeft: 0
// set position of popup depending on screen dimensions. });
if(this.options.position === 'right') { }
jQuery('#mailpoet_panel').css( }.bind(this), 0);
{ marginRight: 0 } break;
);
} else if(this.options.position === 'left') {
jQuery('#mailpoet_panel').css(
{ marginLeft: 0 }
);
}
}.bind(this), 0);
break;
} }
return this; return this;
}, },
showModal: function() { showModal: function() {
// set modal dimensions // set modal dimensions
this.setDimensions(); this.setDimensions();
// add a flag on the body so that we can prevent scrolling (setting overflow hidden) // add a flag on the body so that we can prevent scrolling
jQuery('body').addClass('mailpoet_modal_opened'); jQuery('body').addClass('mailpoet_modal_opened');
// show popup // show popup
@ -421,9 +383,9 @@ define('modal', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
// add class on highlighted elements // add class on highlighted elements
if(this.options.highlight !== null) { if(this.options.highlight !== null) {
if(this.options.highlight.length > 0) { if(this.options.highlight.length > 0) {
this.highlightOn(this.options.highlight); this.highlightOn(this.options.highlight);
} }
} }
// set popup as opened // set popup as opened
@ -431,20 +393,21 @@ define('modal', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
// trigger init event if specified // trigger init event if specified
if(this.options.onInit !== null) { if(this.options.onInit !== null) {
this.options.onInit(); this.options.onInit(this);
} }
return this; return this;
}, },
highlightOn: function(element) { highlightOn: function(element) {
jQuery(element).addClass('mailpoet_modal_highlight'); jQuery(element).addClass('mailpoet_modal_highlight');
return this; return this;
}, },
highlightOff: function() { highlightOff: function() {
jQuery('.mailpoet_modal_highlight').removeClass('mailpoet_modal_highlight'); jQuery('.mailpoet_modal_highlight')
.removeClass('mailpoet_modal_highlight');
return this; return this;
}, },
hideModal: function(callback) { hideModal: function(callback) {
// set modal as closed // set modal as closed
this.opened = false; this.opened = false;
@ -452,22 +415,22 @@ define('modal', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
jQuery('#mailpoet_'+this.options.type).hide(); jQuery('#mailpoet_'+this.options.type).hide();
// remove class on highlighted elements // remove class on highlighted elements
this.highlightOff(); this.highlightOff();
// remove class from body to let it be scrollable // remove class from body to let it be scrollable
jQuery('body').removeClass('mailpoet_modal_opened'); jQuery('body').removeClass('mailpoet_modal_opened');
return this; return this;
}, },
showOverlay: function(force) { showOverlay: function(force) {
jQuery('#mailpoet_modal_overlay').show(); jQuery('#mailpoet_modal_overlay').show();
return this; return this;
}, },
hideOverlay: function() { hideOverlay: function() {
jQuery('#mailpoet_modal_overlay').hide(); jQuery('#mailpoet_modal_overlay').hide();
return this; return this;
}, },
popup: function(options) { popup: function(options) {
// get options // get options
options = options || {}; options = options || {};
// set modal type // set modal type
@ -480,8 +443,8 @@ define('modal', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
this.open(); this.open();
return this; return this;
}, },
panel: function(options) { panel: function(options) {
// get options // get options
options = options || {}; options = options || {};
// reset subpanels // reset subpanels
@ -501,121 +464,122 @@ define('modal', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
this.open(); this.open();
return this; return this;
}, },
subpanel: function(options) { subpanel: function(options) {
if(this.opened === false) { if(this.opened === false) {
// if no panel is already opened, let's create one instead // if no panel is already opened, let's create one instead
this.panel(options); this.panel(options);
} else { } else {
// if a panel is already opened, add a sub panel to it // if a panel is already opened, add a sub panel to it
this.subpanels.push(options); this.subpanels.push(options);
this.loadTemplate(); this.loadTemplate();
} }
return this; return this;
}, },
loading: function(toggle) { loading: function(toggle) {
// make sure the overlay is initialized and that it's visible // make sure the overlay is initialized and that it's visible
this.initOverlay(true); this.initOverlay(true);
if(toggle === true) { if(toggle === true) {
this.showLoading(); this.showLoading();
} else { } else {
this.hideLoading(); this.hideLoading();
} }
return this; return this;
}, },
showLoading: function() { showLoading: function() {
jQuery('#mailpoet_loading').show(); jQuery('#mailpoet_loading').show();
// add loading class to overlay // add loading class to overlay
jQuery('#mailpoet_modal_overlay').addClass('mailpoet_overlay_loading'); jQuery('#mailpoet_modal_overlay')
.addClass('mailpoet_overlay_loading');
return this; return this;
}, },
hideLoading: function() { hideLoading: function() {
jQuery('#mailpoet_loading').hide(); jQuery('#mailpoet_loading').hide();
// remove loading class from overlay // remove loading class from overlay
jQuery('#mailpoet_modal_overlay').removeClass('mailpoet_overlay_loading'); jQuery('#mailpoet_modal_overlay')
.removeClass('mailpoet_overlay_loading');
return this; return this;
}, },
open: function() { open: function() {
// load template if specified // load template if specified
if(this.options.template !== null) { if(this.options.template !== null) {
// check if a url was specified to get extra data // check if a url was specified to get extra data
if(this.options.url !== null) { if(this.options.url !== null) {
this.loadUrl(); this.loadUrl();
} else { } else {
// load template // load template
this.loadTemplate(); this.loadTemplate();
// show modal window // show modal window
this.showModal(); this.showModal();
} }
} else { } else {
this.cancel(); this.cancel();
} }
return this; return this;
}, },
success: function() { success: function() {
if(this.subpanels.length > 0) { if(this.subpanels.length > 0) {
if(this.subpanels[(this.subpanels.length - 1)].onSuccess !== undefined) { if(this.subpanels[(this.subpanels.length - 1)].onSuccess !== undefined) {
this.subpanels[(this.subpanels.length - 1)].onSuccess(this.subpanels[(this.subpanels.length - 1)].data); this.subpanels[(this.subpanels.length - 1)].onSuccess(this.subpanels[(this.subpanels.length - 1)].data);
} }
} else { } else {
if(this.options.onSuccess !== null) { if(this.options.onSuccess !== null) {
this.options.onSuccess(this.options.data); this.options.onSuccess(this.options.data);
} }
} }
this.close(); this.close();
return this; return this;
}, },
cancel: function() { cancel: function() {
if(this.subpanels.length > 0) { if(this.subpanels.length > 0) {
if(this.subpanels[(this.subpanels.length - 1)].onCancel !== undefined) { if(this.subpanels[(this.subpanels.length - 1)].onCancel !== undefined) {
this.subpanels[(this.subpanels.length - 1)].onCancel(this.subpanels[(this.subpanels.length - 1)].data); this.subpanels[(this.subpanels.length - 1)].onCancel(this.subpanels[(this.subpanels.length - 1)].data);
} }
} else { } else {
if(this.options.onCancel !== null) { if(this.options.onCancel !== null) {
this.options.onCancel(this.options.data); this.options.onCancel(this.options.data);
} }
} }
this.close(); this.close();
return this; return this;
}, },
destroy: function() { destroy: function() {
this.hideOverlay(); this.hideOverlay();
// remove extra modal // remove extra modal
if(jQuery('#mailpoet_'+this.options.type).length > 0) { if(jQuery('#mailpoet_'+this.options.type).length > 0) {
jQuery('#mailpoet_'+this.options.type).remove(); jQuery('#mailpoet_'+this.options.type).remove();
} }
this.initialized = false; this.initialized = false;
return this; return this;
}, },
close: function() { close: function() {
if(this.isLocked() === true) return this; if(this.isLocked() === true) { return this; }
if(this.subpanels.length > 0) { if(this.subpanels.length > 0) {
// close subpanel
jQuery('.mailpoet_'+this.options.type+'_wrapper').last().remove();
// close subpanel // show previous panel
jQuery('.mailpoet_'+this.options.type+'_wrapper').last().remove(); jQuery('.mailpoet_'+this.options.type+'_wrapper').last().show();
// show previous panel // remove last subpanels
jQuery('.mailpoet_'+this.options.type+'_wrapper').last().show(); this.subpanels.pop();
// remove last subpanels return this;
this.subpanels.pop();
return this;
} }
// remove event handlers // remove event handlers
@ -629,11 +593,13 @@ define('modal', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
// reset options // reset options
this.options = { this.options = {
onSuccess: null, onSuccess: null,
onCancel: null onCancel: null
}; };
return this; return this;
} }
}; };
}); }
);

View File

@ -2,6 +2,11 @@ define('subscribers.listing',
['mailpoet', 'jquery', 'react/addons', 'classnames'], ['mailpoet', 'jquery', 'react/addons', 'classnames'],
function(MailPoet, jQuery, React, classNames) { function(MailPoet, jQuery, React, classNames) {
var ListingSearch = require('listing/search.jsx');
var ListingPages = require('listing/pages.jsx');
var ListingColumn = require('listing/column.jsx');
var ListingHeader = require('listing/header.jsx');
var ListingGroups = React.createClass({ var ListingGroups = React.createClass({
render: function() { render: function() {
return ( return (
@ -23,35 +28,6 @@ define('subscribers.listing',
} }
}); });
var ListingSearch = React.createClass({
handleSearch: function(e) {
e.preventDefault();
this.props.onSearch(
this.refs.search.getDOMNode().value
);
},
render: function() {
return (
<form name="search" onSubmit={this.handleSearch}>
<p className="search-box">
<label htmlFor="search_input" className="screen-reader-text">
Search
</label>
<input
type="search"
ref="search"
id="search_input"
defaultValue={this.props.search} />
<input
type="submit"
defaultValue="Search"
className="button" />
</p>
</form>
);
}
});
var ListingFilters = React.createClass({ var ListingFilters = React.createClass({
render: function() { render: function() {
return ( return (
@ -60,222 +36,110 @@ define('subscribers.listing',
} }
}); });
var ListingPages = React.createClass({ var ListSelector = React.createClass({
setFirstPage: function() { getList: function(e) {
this.props.onSetPage(1); e.preventDefault();
}, MailPoet.Modal.popup({
setLastPage: function() { title: 'Bulk action',
this.props.onSetPage(this.getLastPage()); template: '',
}, onInit: function(modal) {
setPreviousPage: function() { var target = modal.getContentContainer();
this.props.onSetPage(this.constrainPage(this.props.page - 1)); React.render(
}, <ListSelector />,
setNextPage: function() { target[0]
this.props.onSetPage(this.constrainPage(this.props.page + 1));
},
constrainPage: function(page) {
return Math.min(Math.max(1, Math.abs(~~page)), this.getLastPage());
},
handleSetPage: function() {
this.props.onSetPage(
this.constrainPage(this.refs.page.getDOMNode().value)
);
},
getLastPage: function() {
return Math.ceil(this.props.count / this.props.limit);
},
render: function() {
if(this.props.count === 0) {
return (<div></div>);
} else {
var pagination,
firstPage = (
<span aria-hidden="true" className="tablenav-pages-navspan">«</span>
),
previousPage = (
<span aria-hidden="true" className="tablenav-pages-navspan"></span>
),
nextPage = (
<span aria-hidden="true" className="tablenav-pages-navspan"></span>
),
lastPage = (
<span aria-hidden="true" className="tablenav-pages-navspan">»</span>
);
if(this.props.count > this.props.limit) {
if(this.props.page > 1) {
previousPage = (
<a href="javascript:;"
onClick={this.setPreviousPage}
className="prev-page">
<span className="screen-reader-text">Previous page</span>
<span aria-hidden="true"></span>
</a>
);
}
if(this.props.page > 2) {
firstPage = (
<a href="javascript:;"
onClick={this.setFirstPage}
className="first-page">
<span className="screen-reader-text">First page</span>
<span aria-hidden="true">«</span>
</a>
);
}
if(this.props.page < this.getLastPage()) {
nextPage = (
<a href="javascript:;"
onClick={this.setNextPage}
className="next-page">
<span className="screen-reader-text">Next page</span>
<span aria-hidden="true"></span>
</a>
);
}
if(this.props.page < this.getLastPage() - 1) {
lastPage = (
<a href="javascript:;"
onClick={this.setLastPage}
className="last-page">
<span className="screen-reader-text">Last page</span>
<span aria-hidden="true">»</span>
</a>
);
}
pagination = (
<span className="pagination-links">
{firstPage}
{previousPage}
&nbsp;
<span className="paging-input">
<label
className="screen-reader-text"
htmlFor="current-page-selector">Current Page</label>
<input
type="text"
onChange={this.handleSetPage}
aria-describedby="table-paging"
size="1"
ref="page"
value={this.props.page}
name="paged"
id="current-page-selector"
className="current-page" />
&nbsp;of&nbsp;
<span className="total-pages">
{Math.ceil(this.props.count / this.props.limit)}
</span>
</span>
&nbsp;
{nextPage}
{lastPage}
</span>
); );
} }
})
},
render: function() {
return (
<div>
<select >
<option>Select a list</option>
</select>
<a onClick={this.getList}>Test me</a>
</div>
);
}
});
var classes = classNames( var ListingBulkAction = React.createClass({
'tablenav-pages', render: function() {
{ 'one-page': (this.props.count <= this.props.limit) } return (
); <option value={this.props.action.action}>
{ this.props.action.label }
return ( </option>
<div className={classes}> );
<span className="displaying-num">{ this.props.count } item(s)</span>
{ pagination }
</div>
);
}
} }
}); });
var ListingBulkActions = React.createClass({ var ListingBulkActions = React.createClass({
render: function() { handleAction: function() {
return ( var action = jQuery(this.refs.action.getDOMNode()).val();
<div></div> console.log(action, this.props.selected);
);
}
});
var ListingColumn = React.createClass({
handleSort: function() {
var sort_by = this.props.column.name,
sort_order = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
this.props.onSort(sort_by, sort_order);
}, },
render: function() { render: function() {
var classes = classNames(
'manage-column',
{ 'sortable': this.props.column.sortable },
this.props.column.sorted
);
var label;
if(this.props.column.sortable === true) {
label = (
<a onClick={this.handleSort}>
<span>{ this.props.column.label }</span>
<span className="sorting-indicator"></span>
</a>
);
} else {
label = this.props.column.label;
}
return ( return (
<th <div className="alignleft actions bulkactions">
className={ classes } <label
id={this.props.column.name } className="screen-reader-text"
scope="col"> htmlFor="bulk-action-selector-top">
{label} Select bulk action
</th> </label>
);
}
});
var ListingHeader = React.createClass({ <select ref="action">
render: function() { <option>Bulk Actions</option>
var columns = this.props.columns.map(function(column) { {this.props.actions.map(function(action, index) {
return ( return (
<ListingColumn <ListingBulkAction
onSort={this.props.onSort} action={action}
sort_by={this.props.sort_by} key={index} />
key={column.name} );
column={column} /> }.bind(this))}
); </select>
}.bind(this)); <input
onClick={this.handleAction}
return ( type="submit"
<tr> defaultValue="Apply"
<td className="manage-column column-cb check-column" id="cb"> className="button action" />
<label htmlFor="cb-select-all-1" className="screen-reader-text"> </div>
Select All
</label>
<input type="checkbox" id="cb-select-all-1" />
</td>
{columns}
</tr>
); );
} }
}); });
var ListingItem = React.createClass({ var ListingItem = React.createClass({
handleSelect: function(e) {
var is_checked = jQuery(e.target).is(':checked');
this.props.onSelect(
parseInt(e.target.value, 10),
is_checked
);
return !e.target.checked;
},
render: function() { render: function() {
var rowClasses = classNames(
'title',
'column-title',
'has-row-actions',
'column-primary',
'page-title'
);
return ( return (
<tr> <tr>
<th className="check-column" scope="row"> <th className="check-column" scope="row">
<label htmlFor="cb-select-1" className="screen-reader-text"> <label className="screen-reader-text">
Select { this.props.item.email }</label> { 'Select ' + this.props.item.email }</label>
<input <input
type="checkbox" type="checkbox"
defaultValue={ this.props.item.id } defaultValue={ this.props.item.id }
name="item[]" id="cb-select-1" /> defaultChecked={ this.props.item.selected }
onChange={ this.handleSelect } />
</th> </th>
<td className="title column-title has-row-actions column-primary page-title"> <td className={rowClasses}>
<strong> <strong>
<a className="row-title">{ this.props.item.email }</a> <a className="row-title">{ this.props.item.email }</a>
</strong> </strong>
@ -311,9 +175,11 @@ define('subscribers.listing',
return ( return (
<tbody> <tbody>
{this.props.items.map(function(item) { {this.props.items.map(function(item) {
item.selected = (this.props.selected.indexOf(item.id) !== -1);
return ( return (
<ListingItem <ListingItem
columns={this.props.columns} columns={this.props.columns}
onSelect={this.props.onSelect}
key={item.id} key={item.id}
item={item} /> item={item} />
); );
@ -334,7 +200,8 @@ define('subscribers.listing',
limit: 10, limit: 10,
sort_by: 'email', sort_by: 'email',
sort_order: 'asc', sort_order: 'asc',
items: [] items: [],
selected: []
}; };
}, },
componentDidMount: function() { componentDidMount: function() {
@ -377,6 +244,31 @@ define('subscribers.listing',
this.getItems(); this.getItems();
}.bind(this)); }.bind(this));
}, },
handleSelect: function(id, is_checked) {
var selected = this.state.selected;
if(is_checked) {
selected = jQuery.merge(selected, [ id ]);
} else {
selected.splice(selected.indexOf(id), 1);
}
this.setState({
selected: selected
});
},
handleSelectAll: function(is_checked) {
if(is_checked === false) {
this.setState({ selected: [] });
} else {
var selected = this.state.items.map(function(item) {
return ~~item.id;
});
this.setState({
selected: selected
});
}
},
handleSetPage: function(page) { handleSetPage: function(page) {
this.setState({ page: page }, function() { this.setState({ page: page }, function() {
this.getItems(); this.getItems();
@ -408,7 +300,9 @@ define('subscribers.listing',
onSearch={this.handleSearch} onSearch={this.handleSearch}
search={this.state.search} /> search={this.state.search} />
<div className="tablenav top clearfix"> <div className="tablenav top clearfix">
<ListingBulkActions /> <ListingBulkActions
actions={this.props.actions}
selected={this.state.selected} />
<ListingFilters /> <ListingFilters />
<ListingPages <ListingPages
count={this.state.count} count={this.state.count}
@ -420,6 +314,7 @@ define('subscribers.listing',
<thead> <thead>
<ListingHeader <ListingHeader
onSort={this.handleSort} onSort={this.handleSort}
onSelectAll={this.handleSelectAll}
sort_by={this.state.sort_by} sort_by={this.state.sort_by}
sort_order={this.state.sort_order} sort_order={this.state.sort_order}
columns={this.props.columns} /> columns={this.props.columns} />
@ -427,11 +322,14 @@ define('subscribers.listing',
<ListingItems <ListingItems
columns={this.props.columns} columns={this.props.columns}
selected={this.state.selected}
onSelect={this.handleSelect}
items={items} /> items={items} />
<tfoot> <tfoot>
<ListingHeader <ListingHeader
onSort={this.handleSort} onSort={this.handleSort}
onSelectAll={this.handleSelectAll}
sort_by={this.state.sort_by} sort_by={this.state.sort_by}
sort_order={this.state.sort_order} sort_order={this.state.sort_order}
columns={this.props.columns} /> columns={this.props.columns} />
@ -439,7 +337,9 @@ define('subscribers.listing',
</table> </table>
<div className="tablenav bottom"> <div className="tablenav bottom">
<ListingBulkActions /> <ListingBulkActions
actions={this.props.actions}
selected={this.state.selected} />
<ListingPages <ListingPages
count={this.state.count} count={this.state.count}
page={this.state.page} page={this.state.page}
@ -480,11 +380,32 @@ define('subscribers.listing',
}, },
]; ];
var actions = [
{
label: 'Move to list...',
endpoint: 'subscribers',
action: 'move',
},
{
label: 'Add to list...',
endpoint: 'subscribers',
action: 'add',
},
{
label: 'Remove from list...',
endpoint: 'subscribers',
action: 'remove',
}
];
var element = jQuery('#mailpoet_subscribers_listing'); var element = jQuery('#mailpoet_subscribers_listing');
if(element.length > 0) { if(element.length > 0) {
React.render( React.render(
<Listing columns={columns} />, <Listing columns={columns} actions={actions} />,
element[0] element[0]
); );
} }

View File

@ -1,123 +0,0 @@
define('subscribers.table',
['mailpoet', 'jquery', 'react/addons', 'react-waypoint'],
function(MailPoet, jQuery, React, Waypoint) {
var InfiniteScrollExample = React.createClass({
_loadMoreItems: function() {
this.setState({ loading: true });
this.setState({ page: (this.state.page + 1) }, function() {
this.loadItems();
}.bind(this));
},
loadItems: function() {
MailPoet.Ajax.post({
endpoint: 'subscribers',
action: 'get',
data: {
offset: (this.state.page - 1) * this.state.limit,
limit: this.state.limit
},
onSuccess: function(response) {
if(this.isMounted()) {
var items = jQuery.merge(this.state.items, response);
this.setState({
items: items,
loading: false
});
}
}.bind(this)
});
},
componentDidMount: function() {
this.loadItems();
},
getInitialState: function() {
// set up list of items ...
return {
loading: false,
items: [],
page: 1,
limit: 50
};
},
_renderLoadingMessage: function() {
if (this.state.loading) {
return (
<p>
Loading {this.state.limit} more items
</p>
);
} else {
return (
<p>{this.state.items.length} items</p>
);
}
},
_renderItems: function() {
return this.state.items.map(function(subscriber, index) {
return (
<tr>
<th className="check-column" scope="row">
<label htmlFor="cb-select-1" className="screen-reader-text">
Select { subscriber.email }</label>
<input
type="checkbox"
value={ subscriber.id }
name="item[]" id="cb-select-1" />
</th>
<td className="title column-title has-row-actions column-primary page-title">
<strong>
<a className="row-title">{ subscriber.email }</a>
</strong>
</td>
<td>
{ subscriber.first_name }
</td>
<td>
{ subscriber.last_name }
</td>
<td className="date column-date">
<abbr title="">{ subscriber.created_at }</abbr>
</td>
<td className="date column-date">
<abbr title="">{ subscriber.updated_at }</abbr>
</td>
</tr>
);
});
},
_renderWaypoint: function() {
if (!this.state.loading) {
return (
<Waypoint
onEnter={this._loadMoreItems}
threshold={0.8} />
);
}
},
render: function() {
return (
<div>
{this._renderLoadingMessage()}
<div className="infinite-scroll-example">
<div className="infinite-scroll-example__scrollable-parent">
{this._renderItems()}
{this._renderWaypoint()}
</div>
</div>
</div>
);
}
});
React.render(
<InfiniteScrollExample />,
document.getElementById('example')
)
}
);

View File

@ -9,9 +9,11 @@ class Widget {
} }
function init() { function init() {
add_action('widgets_init', array($this, 'registerWidget')); if(!is_admin() && !is_login_page()) {
add_action('widgets_init', array($this, 'setupActions')); add_action('widgets_init', array($this, 'registerWidget'));
add_action('widgets_init', array($this, 'setupDependencies')); add_action('widgets_init', array($this, 'setupActions'));
add_action('widgets_init', array($this, 'setupDependencies'));
}
} }
function registerWidget() { function registerWidget() {