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 ( +