summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--management/editor/elements.go4
-rw-r--r--management/editor/repeaters.go388
-rw-r--r--management/manager/manager.go2
-rw-r--r--system/admin/handlers.go3
-rw-r--r--system/admin/static/dashboard/css/admin.css9
-rw-r--r--system/api/external.go3
-rw-r--r--system/db/config.go3
8 files changed, 408 insertions, 8 deletions
diff --git a/README.md b/README.md
index 10825e9..f877bb5 100644
--- a/README.md
+++ b/README.md
@@ -180,10 +180,10 @@ $ ponzu --dev --fork=github.com/nilslice/ponzu new /path/to/new/project
### Logo
-The Go gopher was designed by Renee Frech. (http://reneefrench.blogspot.com)
+The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com)
The design is licensed under the Creative Commons 3.0 Attributions license.
Read this article for more details: http://blog.golang.org/gopher
The Go gopher vector illustraition by Hugo Arganda [@argandas](https://twitter.com/argandas) (http://about.me/argandas)
-"Gotoro", the sushi chef, is a modification of Hugo Arganda's illustration by Steve Manuel (https://github.com/nilslice). \ No newline at end of file
+"Gotoro", the sushi chef, is a modification of Hugo Arganda's illustration by Steve Manuel (https://github.com/nilslice).
diff --git a/management/editor/elements.go b/management/editor/elements.go
index 9b35c65..873e81c 100644
--- a/management/editor/elements.go
+++ b/management/editor/elements.go
@@ -15,7 +15,7 @@ import (
// }
//
// func (p *Person) MarshalEditor() ([]byte, error) {
-// view, err := Form(p,
+// view, err := editor.Form(p,
// editor.Field{
// View: editor.Input("Name", p, map[string]string{
// "label": "Name",
@@ -325,7 +325,7 @@ func Checkbox(fieldName string, p interface{}, attrs, options map[string]string)
}
}
- // create a *element manually using the maodified tagNameFromStructFieldMulti
+ // create a *element manually using the modified tagNameFromStructFieldMulti
// func since this is for a multi-value name
input := &element{
TagName: "input",
diff --git a/management/editor/repeaters.go b/management/editor/repeaters.go
new file mode 100644
index 0000000..37fb982
--- /dev/null
+++ b/management/editor/repeaters.go
@@ -0,0 +1,388 @@
+package editor
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+// InputRepeater returns the []byte of an <input> HTML element with a label.
+// It also includes repeat controllers (+ / -) so the element can be
+// dynamically multiplied or reduced.
+// IMPORTANT:
+// The `fieldName` argument will cause a panic if it is not exactly the string
+// form of the struct field that this editor input is representing
+// type Person struct {
+// Names []string `json:"names"`
+// }
+//
+// func (p *Person) MarshalEditor() ([]byte, error) {
+// view, err := editor.Form(p,
+// editor.Field{
+// View: editor.InputRepeater("Names", p, map[string]string{
+// "label": "Names",
+// "type": "text",
+// "placeholder": "Enter a Name here",
+// }),
+// }
+// )
+// }
+func InputRepeater(fieldName string, p interface{}, attrs map[string]string) []byte {
+ // find the field values in p to determine pre-filled inputs
+ fieldVals := valueFromStructField(fieldName, p)
+ vals := strings.Split(fieldVals, "__ponzu")
+
+ scope := tagNameFromStructField(fieldName, p)
+ html := bytes.Buffer{}
+
+ html.WriteString(`<span class="__ponzu-repeat ` + scope + `">`)
+ for i, val := range vals {
+ el := &element{
+ TagName: "input",
+ Attrs: attrs,
+ Name: tagNameFromStructFieldMulti(fieldName, i, p),
+ data: val,
+ viewBuf: &bytes.Buffer{},
+ }
+
+ // only add the label to the first input in repeated list
+ if i == 0 {
+ el.label = attrs["label"]
+ }
+
+ html.Write(domElementSelfClose(el))
+ }
+ html.WriteString(`</span>`)
+
+ return append(html.Bytes(), RepeatController(fieldName, p, "input", ".input-field")...)
+}
+
+// SelectRepeater returns the []byte of a <select> HTML element plus internal <options> with a label.
+// It also includes repeat controllers (+ / -) so the element can be
+// dynamically multiplied or reduced.
+// IMPORTANT:
+// The `fieldName` argument will cause a panic if it is not exactly the string
+// form of the struct field that this editor input is representing
+func SelectRepeater(fieldName string, p interface{}, attrs, options map[string]string) []byte {
+ // options are the value attr and the display value, i.e.
+ // <option value="{map key}">{map value}</option>
+ scope := tagNameFromStructField(fieldName, p)
+ html := bytes.Buffer{}
+ html.WriteString(`<span class="__ponzu-repeat ` + scope + `">`)
+
+ // find the field values in p to determine if an option is pre-selected
+ fieldVals := valueFromStructField(fieldName, p)
+ vals := strings.Split(fieldVals, "__ponzu")
+
+ attrs["class"] = "browser-default"
+
+ // loop through vals and create selects and options for each, adding to html
+ if len(vals) > 0 {
+ for i, val := range vals {
+ sel := &element{
+ TagName: "select",
+ Attrs: attrs,
+ Name: tagNameFromStructFieldMulti(fieldName, i, p),
+ viewBuf: &bytes.Buffer{},
+ }
+
+ // only add the label to the first select in repeated list
+ if i == 0 {
+ sel.label = attrs["label"]
+ }
+
+ // create options for select element
+ var opts []*element
+
+ // provide a call to action for the select element
+ cta := &element{
+ TagName: "option",
+ Attrs: map[string]string{"disabled": "true", "selected": "true"},
+ data: "Select an option...",
+ viewBuf: &bytes.Buffer{},
+ }
+
+ // provide a selection reset (will store empty string in db)
+ reset := &element{
+ TagName: "option",
+ Attrs: map[string]string{"value": ""},
+ data: "None",
+ viewBuf: &bytes.Buffer{},
+ }
+
+ opts = append(opts, cta, reset)
+
+ for k, v := range options {
+ optAttrs := map[string]string{"value": k}
+ if k == val {
+ optAttrs["selected"] = "true"
+ }
+ opt := &element{
+ TagName: "option",
+ Attrs: optAttrs,
+ data: v,
+ viewBuf: &bytes.Buffer{},
+ }
+
+ opts = append(opts, opt)
+ }
+
+ html.Write(domElementWithChildrenSelect(sel, opts))
+ }
+ }
+
+ html.WriteString(`</span>`)
+ return append(html.Bytes(), RepeatController(fieldName, p, "select", ".input-field")...)
+}
+
+// FileRepeater returns the []byte of a <input type="file"> HTML element with a label.
+// It also includes repeat controllers (+ / -) so the element can be
+// dynamically multiplied or reduced.
+// IMPORTANT:
+// The `fieldName` argument will cause a panic if it is not exactly the string
+// form of the struct field that this editor input is representing
+func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []byte {
+ // find the field values in p to determine if an option is pre-selected
+ fieldVals := valueFromStructField(fieldName, p)
+ vals := strings.Split(fieldVals, "__ponzu")
+
+ addLabelFirst := func(i int, label string) string {
+ if i == 0 {
+ return `<label class="active">` + label + `</label>`
+ }
+
+ return ""
+ }
+
+ tmpl :=
+ `<div class="file-input %[5]s %[4]s input-field col s12">
+ %[2]s
+ <div class="file-field input-field">
+ <div class="btn">
+ <span>Upload</span>
+ <input class="upload %[4]s" type="file" />
+ </div>
+ <div class="file-path-wrapper">
+ <input class="file-path validate" placeholder="Add %[5]s" type="text" />
+ </div>
+ </div>
+ <div class="preview"><div class="img-clip"></div></div>
+ <input class="store %[4]s" type="hidden" name="%[1]s" value="%[3]s" />
+ </div>`
+ // 1=nameidx, 2=addLabelFirst, 3=val, 4=className, 5=fieldName
+ script :=
+ `<script>
+ $(function() {
+ var $file = $('.file-input.%[2]s'),
+ upload = $file.find('input.upload'),
+ store = $file.find('input.store'),
+ preview = $file.find('.preview'),
+ clip = preview.find('.img-clip'),
+ reset = document.createElement('div'),
+ img = document.createElement('img'),
+ uploadSrc = store.val();
+ preview.hide();
+
+ // when %[2]s input changes (file is selected), remove
+ // the 'name' and 'value' attrs from the hidden store input.
+ // add the 'name' attr to %[2]s input
+ upload.on('change', function(e) {
+ resetImage();
+ });
+
+ if (uploadSrc.length > 0) {
+ $(img).attr('src', store.val());
+ clip.append(img);
+ preview.show();
+
+ $(reset).addClass('reset %[2]s btn waves-effect waves-light grey');
+ $(reset).html('<i class="material-icons tiny">clear<i>');
+ $(reset).on('click', function(e) {
+ e.preventDefault();
+ var preview = $(this).parent().closest('.preview');
+ preview.animate({"opacity": 0.1}, 200, function() {
+ preview.slideUp(250, function() {
+ resetImage();
+ });
+ })
+
+ });
+ clip.append(reset);
+ }
+
+ function resetImage() {
+ store.val('');
+ store.attr('name', '');
+ upload.attr('name', '%[1]s');
+ clip.empty();
+ }
+ });
+ </script>`
+ // 1=nameidx, 2=className
+
+ name := tagNameFromStructField(fieldName, p)
+
+ html := bytes.Buffer{}
+ html.WriteString(`<span class="__ponzu-repeat ` + name + `">`)
+ for i, val := range vals {
+ className := fmt.Sprintf("%s-%d", name, i)
+ nameidx := tagNameFromStructFieldMulti(fieldName, i, p)
+ html.WriteString(fmt.Sprintf(tmpl, nameidx, addLabelFirst(i, attrs["label"]), val, className, fieldName))
+ html.WriteString(fmt.Sprintf(script, nameidx, className))
+ }
+ html.WriteString(`</span>`)
+
+ return append(html.Bytes(), RepeatController(fieldName, p, "input.upload", "div.file-input."+fieldName)...)
+}
+
+// RepeatController generates the javascript to control any repeatable form
+// element in an editor based on its type, field name and HTML tag name
+func RepeatController(fieldName string, p interface{}, inputSelector, cloneSelector string) []byte {
+ scope := tagNameFromStructField(fieldName, p)
+ script := `
+ <script>
+ $(function() {
+ // define the scope of the repeater
+ var scope = $('.__ponzu-repeat.` + scope + `');
+
+ var getChildren = function() {
+ return scope.find('` + cloneSelector + `')
+ }
+
+ var resetFieldNames = function() {
+ // loop through children, set its name to the fieldName.i where
+ // i is the current index number of children array
+ var children = getChildren();
+
+ for (var i = 0; i < children.length; i++) {
+ var preset = false;
+ var $el = children.eq(i);
+ var name = '` + scope + `.'+String(i);
+
+ $el.find('` + inputSelector + `').attr('name', name);
+
+ // ensure no other input-like elements besides ` + inputSelector + `
+ // get the new name by setting it to an empty string
+ $el.find('input, select, textarea').each(function(i, elem) {
+ var $elem = $(elem);
+
+ // if the elem is not ` + inputSelector + ` and has no value
+ // set the name to an empty string
+ if (!$elem.is('` + inputSelector + `')) {
+ if ($elem.val() === '' || $elem.is('.file-path')) {
+ $elem.attr('name', '');
+ } else {
+ $elem.attr('name', name);
+ preset = true;
+ }
+ }
+ });
+
+ // if there is a preset value, remove the name attr from the
+ // ` + inputSelector + ` element so it doesn't overwrite db
+ if (preset) {
+ $el.find('` + inputSelector + `').attr('name', '');
+ }
+
+ // reset controllers
+ $el.find('.controls').remove();
+ }
+
+ applyRepeatControllers();
+ }
+
+ var addRepeater = function(e) {
+ e.preventDefault();
+
+ var add = e.target;
+
+ // find and clone the repeatable input-like element
+ var source = $(add).parent().closest('` + cloneSelector + `');
+ var clone = source.clone();
+
+ // if clone has label, remove it
+ clone.find('label').remove();
+
+ // remove the pre-filled value from clone
+ clone.find('` + inputSelector + `').val('');
+ clone.find('input').val('');
+
+ // remove controls from clone if already present
+ clone.find('.controls').remove();
+
+ // remove input preview on clone if copied from source
+ clone.find('.preview').remove();
+
+ // add clone to scope and reset field name attributes
+ scope.append(clone);
+
+ resetFieldNames();
+ }
+
+ var delRepeater = function(e) {
+ e.preventDefault();
+
+ // do nothing if the input is the only child
+ var children = getChildren();
+ if (children.length === 1) {
+ return;
+ }
+
+ var del = e.target;
+
+ // pass label onto next input-like element if del 0 index
+ var wrapper = $(del).parent().closest('` + cloneSelector + `');
+ if (wrapper.find('` + inputSelector + `').attr('name') === '` + scope + `.0') {
+ wrapper.next().append(wrapper.find('label'))
+ }
+
+ wrapper.remove();
+
+ resetFieldNames();
+ }
+
+ var createControls = function() {
+ // create + / - controls for each input-like child element of scope
+ var add = $('<button>+</button>');
+ add.addClass('repeater-add');
+ add.addClass('btn-flat waves-effect waves-green');
+
+ var del = $('<button>-</button>');
+ del.addClass('repeater-del');
+ del.addClass('btn-flat waves-effect waves-red');
+
+ var controls = $('<span></span>');
+ controls.addClass('controls');
+ controls.addClass('right');
+
+ // bind listeners to child's controls
+ add.on('click', addRepeater);
+ del.on('click', delRepeater);
+
+ controls.append(add);
+ controls.append(del);
+
+ return controls;
+ }
+
+ var applyRepeatControllers = function() {
+ // add controls to each child
+ var children = getChildren()
+ for (var i = 0; i < children.length; i++) {
+ var el = children[i];
+
+ $(el).find('` + inputSelector + `').parent().find('.controls').remove();
+
+ var controls = createControls();
+ $(el).append(controls);
+ }
+ }
+
+ resetFieldNames();
+ });
+
+ </script>
+ `
+
+ return []byte(script)
+}
diff --git a/management/manager/manager.go b/management/manager/manager.go
index c54918f..594c258 100644
--- a/management/manager/manager.go
+++ b/management/manager/manager.go
@@ -136,7 +136,7 @@ func Manage(e editor.Editable, typeName string) ([]byte, error) {
ID: i.ItemID(),
UUID: i.UniqueID(),
Kind: typeName,
- Slug: s.ItemSlug(), // TODO: just added this and its implementation -- need to rebuild & test
+ Slug: s.ItemSlug(),
Editor: template.HTML(v),
}
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
index e1487c5..ff30040 100644
--- a/system/admin/handlers.go
+++ b/system/admin/handlers.go
@@ -1403,10 +1403,11 @@ func editHandler(res http.ResponseWriter, req *http.Request) {
if req.PostForm.Get(key) == "" {
req.PostForm.Set(key, v[0])
- discardKeys = append(discardKeys, k)
} else {
req.PostForm.Add(key, v[0])
}
+
+ discardKeys = append(discardKeys, k)
}
}
diff --git a/system/admin/static/dashboard/css/admin.css b/system/admin/static/dashboard/css/admin.css
index 8d2fb89..9cba7d0 100644
--- a/system/admin/static/dashboard/css/admin.css
+++ b/system/admin/static/dashboard/css/admin.css
@@ -185,6 +185,11 @@ li:hover .quick-delete-post, li:hover .delete-user {
padding: 20px;
}
+.controls button {
+ padding: 0px 10px 5px 10px;
+ position: relative;
+ top: -10px;
+}
/* OVERRIDE Bootstrap + Materialize conflicts */
.iso-texteditor.input-field label {
@@ -216,4 +221,8 @@ li:hover .quick-delete-post, li:hover .delete-user {
.approve-details {
text-align: right;
padding: 10px 0 !important;
+}
+
+select {
+ border: 1px solid #e2e2e2;
} \ No newline at end of file
diff --git a/system/api/external.go b/system/api/external.go
index 86e4a99..302b7c9 100644
--- a/system/api/external.go
+++ b/system/api/external.go
@@ -85,10 +85,11 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) {
if req.PostForm.Get(key) == "" {
req.PostForm.Set(key, v[0])
- discardKeys = append(discardKeys, k)
} else {
req.PostForm.Add(key, v[0])
}
+
+ discardKeys = append(discardKeys, k)
}
}
diff --git a/system/db/config.go b/system/db/config.go
index 0e49307..ce76021 100644
--- a/system/db/config.go
+++ b/system/db/config.go
@@ -34,10 +34,11 @@ func SetConfig(data url.Values) error {
if data.Get(key) == "" {
data.Set(key, v[0])
- discardKeys = append(discardKeys, k)
} else {
data.Add(key, v[0])
}
+
+ discardKeys = append(discardKeys, k)
}
}