diff --git a/RoboFile.php b/RoboFile.php
index 6a66ea8393..99fcfacb16 100644
--- a/RoboFile.php
+++ b/RoboFile.php
@@ -2,18 +2,6 @@
class RoboFile extends \Robo\Tasks {
- private $css_files = array(
- 'assets/css/src/*.styl',
- 'assets/css/src/**/*.styl'
- );
-
- private $js_files = array(
- 'assets/js/src/*.js',
- 'assets/js/src/*.jsx',
- 'assets/js/src/**/*.js',
- 'assets/js/src/**/*.jsx'
- );
-
function install() {
$this->_exec('./composer.phar install');
$this->_exec('npm install');
@@ -25,16 +13,29 @@ class RoboFile extends \Robo\Tasks {
$this->_exec('npm update');
}
- function watch() {
- $js_files = array();
- array_map(function($path) use(&$js_files) {
- $js_files = array_merge($js_files, glob($path));
- }, $this->js_files);
+ protected function rsearch($folder, $extensions = array()) {
+ $dir = new RecursiveDirectoryIterator($folder);
+ $iterator = new RecursiveIteratorIterator($dir);
- $css_files = array();
- array_map(function($path) use(&$css_files) {
- $css_files = array_merge($css_files, glob($path));
- }, $this->css_files);
+ $pattern = '/^.+\.('.join($extensions, '|').')$/i';
+
+ $files = new RegexIterator(
+ $iterator,
+ $pattern,
+ RecursiveRegexIterator::GET_MATCH
+ );
+
+ $list = array();
+ foreach($files as $file) {
+ $list[] = $file[0];
+ }
+
+ return $list;
+ }
+
+ function watch() {
+ $css_files = $this->rsearch('assets/css/src/', array('styl'));
+ $js_files = $this->rsearch('assets/js/src/', array('js', 'jsx'));
$this->taskWatch()
->monitor($js_files, function() {
diff --git a/assets/css/select2-spinner.gif b/assets/css/select2-spinner.gif
new file mode 100644
index 0000000000..5b33f7e54f
Binary files /dev/null and b/assets/css/select2-spinner.gif differ
diff --git a/assets/css/select2.png b/assets/css/select2.png
new file mode 100644
index 0000000000..1d804ffb99
Binary files /dev/null and b/assets/css/select2.png differ
diff --git a/assets/css/select2x2.png b/assets/css/select2x2.png
new file mode 100644
index 0000000000..4bdd5c961d
Binary files /dev/null and b/assets/css/select2x2.png differ
diff --git a/assets/css/src/admin.styl b/assets/css/src/admin.styl
index 46cc289c3f..efb6a8f47b 100644
--- a/assets/css/src/admin.styl
+++ b/assets/css/src/admin.styl
@@ -10,4 +10,5 @@
@require 'form_editor'
@require 'listing'
@require 'box'
-@require 'breadcrumb'
\ No newline at end of file
+@require 'breadcrumb'
+@require 'form'
\ No newline at end of file
diff --git a/assets/css/src/form.styl b/assets/css/src/form.styl
new file mode 100644
index 0000000000..e5832b1630
--- /dev/null
+++ b/assets/css/src/form.styl
@@ -0,0 +1,2 @@
+.mailpoet_form td
+ vertical-align: top
\ No newline at end of file
diff --git a/assets/js/src/_phantomjs_shim.js b/assets/js/src/_phantomjs_shim.js
deleted file mode 100644
index 15be214b00..0000000000
--- a/assets/js/src/_phantomjs_shim.js
+++ /dev/null
@@ -1,41 +0,0 @@
-if (!Function.prototype.bind) {
- var Empty = function() {};
- Function.prototype.bind = function bind(that) { // .length is 1
- var target = this;
- if (typeof target != "function") {
- throw new TypeError("Function.prototype.bind called on incompatible " + target);
- }
- var args = Array.prototype.slice.call(arguments, 1); // for normal call
- var binder = function() {
- if (this instanceof bound) {
- var result = target.apply(
- this,
- args.concat(Array.prototype.slice.call(arguments))
- );
- if (Object(result) === result) {
- return result;
- }
- return this;
- } else {
- return target.apply(
- that,
- args.concat(Array.prototype.slice.call(arguments))
- );
- }
- };
- var boundLength = Math.max(0, target.length - args.length);
- var boundArgs = [];
- for (var i = 0; i < boundLength; i++) {
- boundArgs.push("$" + i);
- }
- var bound = Function("binder", "return function(" + boundArgs.join(",") + "){return binder.apply(this,arguments)}")(binder);
-
- if (target.prototype) {
- Empty.prototype = target.prototype;
- bound.prototype = new Empty();
- // Clean up dangling references.
- Empty.prototype = null;
- }
- return bound;
- }
-};
\ No newline at end of file
diff --git a/assets/js/src/form/fields/checkbox.jsx b/assets/js/src/form/fields/checkbox.jsx
new file mode 100644
index 0000000000..f28e40b12a
--- /dev/null
+++ b/assets/js/src/form/fields/checkbox.jsx
@@ -0,0 +1,64 @@
+define([
+ 'react',
+ 'react-checkbox-group'
+],
+function(
+ React,
+ CheckboxGroup
+) {
+ var FormFieldCheckbox = React.createClass({
+ render: function() {
+ var selected_values = this.props.item[this.props.field.name] || '';
+ if(
+ selected_values !== undefined
+ && selected_values.constructor !== Array
+ ) {
+ selected_values = selected_values.split(';').map(function(value) {
+ return value.trim();
+ });
+ }
+ var count = Object.keys(this.props.field.values).length;
+
+ var options = Object.keys(this.props.field.values).map(
+ function(value, index) {
+ return (
+
+
+
+ );
+ }.bind(this)
+ );
+
+ return (
+
+ { options }
+
+ );
+ },
+ handleValueChange: function() {
+ var field = this.props.field.name;
+ var group = this.refs[field];
+ var selected_values = [];
+
+ if(group !== undefined) {
+ selected_values = group.getCheckedValues();
+ }
+
+ return this.props.onValueChange({
+ target: {
+ name: field,
+ value: selected_values.join(';')
+ }
+ });
+ }
+ });
+
+ return FormFieldCheckbox;
+});
\ No newline at end of file
diff --git a/assets/js/src/form/fields/field.jsx b/assets/js/src/form/fields/field.jsx
new file mode 100644
index 0000000000..0dfdd151c6
--- /dev/null
+++ b/assets/js/src/form/fields/field.jsx
@@ -0,0 +1,110 @@
+define([
+ 'react',
+ 'form/fields/text.jsx',
+ 'form/fields/textarea.jsx',
+ 'form/fields/select.jsx',
+ 'form/fields/radio.jsx',
+ 'form/fields/checkbox.jsx'
+],
+function(
+ React,
+ FormFieldText,
+ FormFieldTextarea,
+ FormFieldSelect,
+ FormFieldRadio,
+ FormFieldCheckbox
+) {
+ var FormField = React.createClass({
+ renderField: function(data, inline = true) {
+ var description = false;
+ if(data.field.description) {
+ description = (
+ { data.field.description }
+ );
+ }
+
+ var field = false;
+
+ if(data.field['field'] !== undefined) {
+ field = data.field.field;
+ } else{
+ switch(data.field.type) {
+ case 'text':
+ field = ();
+ break;
+
+ case 'textarea':
+ field = ();
+ break;
+
+ case 'select':
+ field = ();
+ break;
+
+ case 'radio':
+ field = ();
+ break;
+
+ case 'checkbox':
+ field = ();
+ break;
+ }
+ }
+
+ if(inline === true) {
+ return (
+
+ { field }
+ { description }
+
+ );
+ } else {
+ return (
+
+ { field }
+ { description }
+
+ );
+ }
+ },
+ render: function() {
+ var field = false;
+
+ if(this.props.field['fields'] !== undefined) {
+ field = this.props.field.fields.map(function(subfield) {
+ return this.renderField({
+ field: subfield,
+ item: this.props.item
+ });
+ }.bind(this));
+ } else {
+ field = this.renderField(this.props);
+ }
+
+ var tip = false;
+ if(this.props.field.tip) {
+ tip = (
+ { this.props.field.tip }
+ );
+ }
+
+ return (
+
+
+
+ |
+
+ { field }
+ |
+
+ );
+ }
+ });
+
+ return FormField;
+});
\ No newline at end of file
diff --git a/assets/js/src/form/fields/radio.jsx b/assets/js/src/form/fields/radio.jsx
new file mode 100644
index 0000000000..072855e407
--- /dev/null
+++ b/assets/js/src/form/fields/radio.jsx
@@ -0,0 +1,39 @@
+define([
+ 'react'
+],
+function(
+ React
+) {
+ var FormFieldRadio = React.createClass({
+ render: function() {
+ var selected_value = this.props.item[this.props.field.name];
+ var count = Object.keys(this.props.field.values).length;
+
+ var options = Object.keys(this.props.field.values).map(
+ function(value, index) {
+ return (
+
+
+
+ );
+ }.bind(this)
+ );
+
+ return (
+
+ { options }
+
+ );
+ }
+ });
+
+ return FormFieldRadio;
+});
\ No newline at end of file
diff --git a/assets/js/src/form/fields/select.jsx b/assets/js/src/form/fields/select.jsx
new file mode 100644
index 0000000000..6b4ddbe076
--- /dev/null
+++ b/assets/js/src/form/fields/select.jsx
@@ -0,0 +1,34 @@
+define([
+ 'react'
+],
+function(
+ React
+) {
+ var FormFieldSelect = React.createClass({
+ render: function() {
+ var options =
+ Object.keys(this.props.field.values).map(function(value, index) {
+ return (
+
+ );
+ }.bind(this)
+ );
+
+ return (
+
+ );
+ }
+ });
+
+ return FormFieldSelect;
+});
\ No newline at end of file
diff --git a/assets/js/src/form/fields/selection.jsx b/assets/js/src/form/fields/selection.jsx
new file mode 100644
index 0000000000..97396bfc7b
--- /dev/null
+++ b/assets/js/src/form/fields/selection.jsx
@@ -0,0 +1,119 @@
+define([
+ 'react',
+ 'jquery'
+],
+function(
+ React,
+ jQuery
+) {
+ var Selection = React.createClass({
+ getInitialState: function() {
+ return {
+ loading: false,
+ items: [],
+ selected: []
+ }
+ },
+ componentWillMount: function() {
+ this.loadCachedItems();
+ },
+ componentDidMount: function() {
+ jQuery('#'+this.props.id).select2({
+ width: '25em'
+ });
+ },
+ loadCachedItems: function() {
+ if(typeof(window['mailpoet_'+this.props.endpoint]) !== 'undefined') {
+ var items = window['mailpoet_'+this.props.endpoint];
+ this.setState({
+ items: items
+ });
+ }
+ },
+ loadItems: function() {
+ this.setState({ loading: true });
+
+ MailPoet.Ajax.post({
+ endpoint: this.props.endpoint,
+ action: 'listing',
+ data: {
+ 'offset': 0,
+ 'limit': 100,
+ 'search': '',
+ 'sort_by': 'name',
+ 'sort_order': 'asc'
+ }
+ })
+ .done(function(response) {
+ if(this.isMounted()) {
+ if(response === false) {
+ this.setState({
+ loading: false,
+ items: []
+ });
+ } else {
+ this.setState({
+ loading: false,
+ items: response.items
+ });
+ }
+ }
+ }.bind(this));
+ },
+ handleChange: function() {
+ var new_value = this.refs.selection.getDOMNode().value;
+
+ if(this.props.multiple === false) {
+ if(new_value.trim().length === 0) {
+ new_value = false;
+ }
+
+ this.setState({
+ selected: new_value
+ });
+ } else {
+ var selected_values = this.state.selected || [];
+
+ if(selected_values.indexOf(new_value) !== -1) {
+ // value already present so remove it
+ selected_values.splice(selected_values.indexOf(new_value), 1);
+ } else {
+ selected_values.push(new_value);
+ }
+
+ this.setState({
+ selected: selected_values
+ });
+ }
+ },
+ getSelected: function() {
+ return this.state.selected;
+ },
+ render: function() {
+ var options = this.state.items.map(function(item, index) {
+ return (
+
+ );
+ });
+
+ return (
+
+ );
+ }
+ });
+
+ return Selection;
+});
\ No newline at end of file
diff --git a/assets/js/src/form/fields/text.jsx b/assets/js/src/form/fields/text.jsx
new file mode 100644
index 0000000000..3306ff29ac
--- /dev/null
+++ b/assets/js/src/form/fields/text.jsx
@@ -0,0 +1,24 @@
+define([
+ 'react'
+],
+function(
+ React
+) {
+ var FormFieldText = React.createClass({
+ render: function() {
+ return (
+
+ );
+ }
+ });
+
+ return FormFieldText;
+});
\ No newline at end of file
diff --git a/assets/js/src/form/fields/textarea.jsx b/assets/js/src/form/fields/textarea.jsx
new file mode 100644
index 0000000000..d938bbae88
--- /dev/null
+++ b/assets/js/src/form/fields/textarea.jsx
@@ -0,0 +1,22 @@
+define([
+ 'react'
+],
+function(
+ React
+) {
+ var FormFieldTextarea = React.createClass({
+ render: function() {
+ return (
+
+ );
+ }
+ });
+
+ return FormFieldTextarea;
+});
\ No newline at end of file
diff --git a/assets/js/src/form/form.jsx b/assets/js/src/form/form.jsx
index ff44795b6f..c624bc0286 100644
--- a/assets/js/src/form/form.jsx
+++ b/assets/js/src/form/form.jsx
@@ -4,206 +4,15 @@ define(
'mailpoet',
'classnames',
'react-router',
- 'react-checkbox-group'
+ 'form/fields/field.jsx'
],
function(
React,
MailPoet,
classNames,
Router,
- CheckboxGroup
+ FormField
) {
-
-
- var FormFieldSelect = React.createClass({
- render: function() {
- var options =
- Object.keys(this.props.field.values).map(function(value, index) {
- return (
-
- );
- }.bind(this)
- );
-
- return (
-
- );
- }
- });
-
- var FormFieldRadio = React.createClass({
- render: function() {
- var selected_value = this.props.item[this.props.field.name];
- var count = Object.keys(this.props.field.values).length;
-
- var options = Object.keys(this.props.field.values).map(
- function(value, index) {
- return (
-
-
-
- );
- }.bind(this)
- );
-
- return (
-
- { options }
-
- );
- }
- });
-
- var FormFieldCheckbox = React.createClass({
- render: function() {
- var selected_values = this.props.item[this.props.field.name] || '';
- if(
- selected_values !== undefined
- && selected_values.constructor !== Array
- ) {
- selected_values = selected_values.split(';').map(function(value) {
- return value.trim();
- });
- }
- var count = Object.keys(this.props.field.values).length;
-
- var options = Object.keys(this.props.field.values).map(
- function(value, index) {
- return (
-
-
-
- );
- }.bind(this)
- );
-
- return (
-
- { options }
-
- );
- },
- handleValueChange: function() {
- var field = this.props.field.name;
- var group = this.refs[field];
- var selected_values = [];
-
- if(group !== undefined) {
- selected_values = group.getCheckedValues();
- }
-
- return this.props.onValueChange({
- target: {
- name: field,
- value: selected_values.join(';')
- }
- });
- }
- });
-
- var FormFieldText = React.createClass({
- render: function() {
- return (
-
- );
- }
- });
-
- var FormFieldTextarea = React.createClass({
- render: function() {
- return (
-
- );
- }
- });
-
- var FormField = React.createClass({
- render: function() {
-
- var description = false;
- if(this.props.field.description) {
- description = (
- { this.props.field.description }
- );
- }
-
- var field = false;
-
- switch(this.props.field.type) {
- case 'text':
- field = ();
- break;
-
- case 'textarea':
- field = ();
- break;
-
- case 'select':
- field = ();
- break;
-
- case 'radio':
- field = ();
- break;
-
- case 'checkbox':
- field = ();
- break;
- }
-
- return (
-
-
-
- |
-
- { field }
- { description }
- |
-
- );
- }
- });
-
var Form = React.createClass({
mixins: [
Router.Navigation
@@ -305,25 +114,42 @@ define(
});
var formClasses = classNames(
+ 'mailpoet_form',
{ 'mailpoet_form_loading': this.state.loading }
);
- var fields = this.props.fields.map(function(field, index) {
- return (
-
- );
+ var fields = this.props.fields.map(function(field, i) {
+ // if(field['fields'] !== undefined) {
+ // return field.fields.map(function(subfield, j) {
+ // return (
+ //
+ // );
+ // }.bind(this));
+ // } else {
+ return (
+
+ );
+ // }
}.bind(this));
return (