github.com/bosssauce/ponzu@v0.11.1-0.20200102001432-9bc41b703131/management/editor/repeaters.go (about)

     1  package editor
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"strings"
     8  )
     9  
    10  // InputRepeater returns the []byte of an <input> HTML element with a label.
    11  // It also includes repeat controllers (+ / -) so the element can be
    12  // dynamically multiplied or reduced.
    13  // IMPORTANT:
    14  // The `fieldName` argument will cause a panic if it is not exactly the string
    15  // form of the struct field that this editor input is representing
    16  // 	type Person struct {
    17  //		item.Item
    18  //		editor editor.Editor
    19  //
    20  // 		Names []string `json:"names"`
    21  //		//...
    22  // 	}
    23  //
    24  // 	func (p *Person) MarshalEditor() ([]byte, error) {
    25  // 		view, err := editor.Form(p,
    26  // 			editor.Field{
    27  // 				View: editor.InputRepeater("Names", p, map[string]string{
    28  // 					"label":       "Names",
    29  // 					"type":        "text",
    30  // 					"placeholder": "Enter a Name here",
    31  // 				}),
    32  // 			}
    33  // 		)
    34  // 	}
    35  func InputRepeater(fieldName string, p interface{}, attrs map[string]string) []byte {
    36  	// find the field values in p to determine pre-filled inputs
    37  	fieldVals := ValueFromStructField(fieldName, p)
    38  	vals := strings.Split(fieldVals, "__ponzu")
    39  
    40  	scope := TagNameFromStructField(fieldName, p)
    41  	html := bytes.Buffer{}
    42  
    43  	_, err := html.WriteString(`<span class="__ponzu-repeat ` + scope + `">`)
    44  	if err != nil {
    45  		log.Println("Error writing HTML string to InputRepeater buffer")
    46  		return nil
    47  	}
    48  
    49  	for i, val := range vals {
    50  		el := &Element{
    51  			TagName: "input",
    52  			Attrs:   attrs,
    53  			Name:    TagNameFromStructFieldMulti(fieldName, i, p),
    54  			Data:    val,
    55  			ViewBuf: &bytes.Buffer{},
    56  		}
    57  
    58  		// only add the label to the first input in repeated list
    59  		if i == 0 {
    60  			el.Label = attrs["label"]
    61  		}
    62  
    63  		_, err := html.Write(DOMElementSelfClose(el))
    64  		if err != nil {
    65  			log.Println("Error writing DOMElementSelfClose to InputRepeater buffer")
    66  			return nil
    67  		}
    68  	}
    69  	_, err = html.WriteString(`</span>`)
    70  	if err != nil {
    71  		log.Println("Error writing HTML string to InputRepeater buffer")
    72  		return nil
    73  	}
    74  
    75  	return append(html.Bytes(), RepeatController(fieldName, p, "input", ".input-field")...)
    76  }
    77  
    78  // SelectRepeater returns the []byte of a <select> HTML element plus internal <options> with a label.
    79  // It also includes repeat controllers (+ / -) so the element can be
    80  // dynamically multiplied or reduced.
    81  // IMPORTANT:
    82  // The `fieldName` argument will cause a panic if it is not exactly the string
    83  // form of the struct field that this editor input is representing
    84  func SelectRepeater(fieldName string, p interface{}, attrs, options map[string]string) []byte {
    85  	// options are the value attr and the display value, i.e.
    86  	// <option value="{map key}">{map value}</option>
    87  	scope := TagNameFromStructField(fieldName, p)
    88  	html := bytes.Buffer{}
    89  	_, err := html.WriteString(`<span class="__ponzu-repeat ` + scope + `">`)
    90  	if err != nil {
    91  		log.Println("Error writing HTML string to SelectRepeater buffer")
    92  		return nil
    93  	}
    94  
    95  	// find the field values in p to determine if an option is pre-selected
    96  	fieldVals := ValueFromStructField(fieldName, p)
    97  	vals := strings.Split(fieldVals, "__ponzu")
    98  
    99  	if _, ok := attrs["class"]; ok {
   100  		attrs["class"] += " browser-default"
   101  	} else {
   102  		attrs["class"] = "browser-default"
   103  	}
   104  
   105  	// loop through vals and create selects and options for each, adding to html
   106  	if len(vals) > 0 {
   107  		for i, val := range vals {
   108  			sel := &Element{
   109  				TagName: "select",
   110  				Attrs:   attrs,
   111  				Name:    TagNameFromStructFieldMulti(fieldName, i, p),
   112  				ViewBuf: &bytes.Buffer{},
   113  			}
   114  
   115  			// only add the label to the first select in repeated list
   116  			if i == 0 {
   117  				sel.Label = attrs["label"]
   118  			}
   119  
   120  			// create options for select element
   121  			var opts []*Element
   122  
   123  			// provide a call to action for the select element
   124  			cta := &Element{
   125  				TagName: "option",
   126  				Attrs:   map[string]string{"disabled": "true", "selected": "true"},
   127  				Data:    "Select an option...",
   128  				ViewBuf: &bytes.Buffer{},
   129  			}
   130  
   131  			// provide a selection reset (will store empty string in db)
   132  			reset := &Element{
   133  				TagName: "option",
   134  				Attrs:   map[string]string{"value": ""},
   135  				Data:    "None",
   136  				ViewBuf: &bytes.Buffer{},
   137  			}
   138  
   139  			opts = append(opts, cta, reset)
   140  
   141  			for k, v := range options {
   142  				optAttrs := map[string]string{"value": k}
   143  				if k == val {
   144  					optAttrs["selected"] = "true"
   145  				}
   146  				opt := &Element{
   147  					TagName: "option",
   148  					Attrs:   optAttrs,
   149  					Data:    v,
   150  					ViewBuf: &bytes.Buffer{},
   151  				}
   152  
   153  				opts = append(opts, opt)
   154  			}
   155  
   156  			_, err := html.Write(DOMElementWithChildrenSelect(sel, opts))
   157  			if err != nil {
   158  				log.Println("Error writing DOMElementWithChildrenSelect to SelectRepeater buffer")
   159  				return nil
   160  			}
   161  		}
   162  	}
   163  
   164  	_, err = html.WriteString(`</span>`)
   165  	if err != nil {
   166  		log.Println("Error writing HTML string to SelectRepeater buffer")
   167  		return nil
   168  	}
   169  
   170  	return append(html.Bytes(), RepeatController(fieldName, p, "select", ".input-field")...)
   171  }
   172  
   173  // FileRepeater returns the []byte of a <input type="file"> HTML element with a label.
   174  // It also includes repeat controllers (+ / -) so the element can be
   175  // dynamically multiplied or reduced.
   176  // IMPORTANT:
   177  // The `fieldName` argument will cause a panic if it is not exactly the string
   178  // form of the struct field that this editor input is representing
   179  func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []byte {
   180  	// find the field values in p to determine if an option is pre-selected
   181  	fieldVals := ValueFromStructField(fieldName, p)
   182  	vals := strings.Split(fieldVals, "__ponzu")
   183  
   184  	addLabelFirst := func(i int, label string) string {
   185  		if i == 0 {
   186  			return `<label class="active">` + label + `</label>`
   187  		}
   188  
   189  		return ""
   190  	}
   191  
   192  	tmpl :=
   193  		`<div class="file-input %[5]s %[4]s input-field col s12">
   194  			%[2]s
   195  			<div class="file-field input-field">
   196  				<div class="btn">
   197  					<span>Upload</span>
   198  					<input class="upload %[4]s" type="file" />
   199  				</div>
   200  				<div class="file-path-wrapper">
   201  					<input class="file-path validate" placeholder="Add %[5]s" type="text" />
   202  				</div>
   203  			</div>
   204  			<div class="preview"><div class="img-clip"></div></div>			
   205  			<input class="store %[4]s" type="hidden" name="%[1]s" value="%[3]s" />
   206  		</div>`
   207  		// 1=nameidx, 2=addLabelFirst, 3=val, 4=className, 5=fieldName
   208  	script :=
   209  		`<script>
   210  			$(function() {
   211  				var $file = $('.file-input.%[2]s'),
   212  					upload = $file.find('input.upload'),
   213  					store = $file.find('input.store'),
   214  					preview = $file.find('.preview'),
   215  					clip = preview.find('.img-clip'),
   216  					reset = document.createElement('div'),
   217  					img = document.createElement('img'),
   218  					video = document.createElement('video'),
   219  					unknown = document.createElement('div'),
   220  					viewLink = document.createElement('a'),
   221  					viewLinkText = document.createTextNode('Download / View '),
   222  					iconLaunch = document.createElement('i'),
   223  					iconLaunchText = document.createTextNode('launch'),
   224  					uploadSrc = store.val();
   225  					video.setAttribute
   226  					preview.hide();
   227  					viewLink.setAttribute('href', '%[3]s');
   228  					viewLink.setAttribute('target', '_blank');
   229  					viewLink.appendChild(viewLinkText);
   230  					viewLink.style.display = 'block';
   231  					viewLink.style.marginRight = '10px';					
   232  					viewLink.style.textAlign = 'right';
   233  					iconLaunch.className = 'material-icons tiny';
   234  					iconLaunch.style.position = 'relative';
   235  					iconLaunch.style.top = '3px';
   236  					iconLaunch.appendChild(iconLaunchText);
   237  					viewLink.appendChild(iconLaunch);
   238  					preview.append(viewLink);
   239  				
   240  				// when %[2]s input changes (file is selected), remove
   241  				// the 'name' and 'value' attrs from the hidden store input.
   242  				// add the 'name' attr to %[2]s input
   243  				upload.on('change', function(e) {
   244  					resetImage();
   245  				});
   246  
   247  				if (uploadSrc.length > 0) {
   248  					var ext = uploadSrc.substring(uploadSrc.lastIndexOf('.'));
   249  					ext = ext.toLowerCase();
   250  					switch (ext) {
   251  						case '.jpg':
   252  						case '.jpeg':
   253  						case '.webp':
   254  						case '.gif':
   255  						case '.png':
   256  							$(img).attr('src', store.val());
   257  							clip.append(img);
   258  							break;
   259  						case '.mp4':
   260  						case '.webm':
   261  							$(video)
   262  								.attr('src', store.val())
   263  								.attr('type', 'video/'+ext.substring(1))
   264  								.attr('controls', true)
   265  								.css('width', '100%%');
   266  							clip.append(video);
   267  							break;
   268  						default:
   269  							$(img).attr('src', '/admin/static/dashboard/img/ponzu-file.png');
   270  							$(unknown)
   271  								.css({
   272  									position: 'absolute', 
   273  									top: '10px', 
   274  									left: '10px',
   275  									border: 'solid 1px #ddd',
   276  									padding: '7px 7px 5px 12px',
   277  									fontWeight: 'bold',
   278  									background: '#888',
   279  									color: '#fff',
   280  									textTransform: 'uppercase',
   281  									letterSpacing: '2px' 
   282  								})
   283  								.text(ext);
   284  							clip.append(img);
   285  							clip.append(unknown);
   286  							clip.css('maxWidth', '200px');
   287  					}
   288  					preview.show();
   289  
   290  					$(reset).addClass('reset %[2]s btn waves-effect waves-light grey');
   291  					$(reset).html('<i class="material-icons tiny">clear<i>');
   292  					$(reset).on('click', function(e) {
   293  						e.preventDefault();
   294  						preview.animate({"opacity": 0.1}, 200, function() {
   295  							preview.slideUp(250, function() {
   296  								resetImage();
   297  							});
   298  						})
   299  						
   300  					});
   301  					clip.append(reset);
   302  				}
   303  
   304  				function resetImage() {
   305  					store.val('');
   306  					store.attr('name', '');
   307  					upload.attr('name', '%[1]s');
   308  					clip.empty();
   309  				}
   310  			});	
   311  		</script>`
   312  		// 1=nameidx, 2=className
   313  
   314  	name := TagNameFromStructField(fieldName, p)
   315  
   316  	html := bytes.Buffer{}
   317  	_, err := html.WriteString(`<span class="__ponzu-repeat ` + name + `">`)
   318  	if err != nil {
   319  		log.Println("Error writing HTML string to FileRepeater buffer")
   320  		return nil
   321  	}
   322  
   323  	for i, val := range vals {
   324  		className := fmt.Sprintf("%s-%d", name, i)
   325  		nameidx := TagNameFromStructFieldMulti(fieldName, i, p)
   326  
   327  		_, err := html.WriteString(fmt.Sprintf(tmpl, nameidx, addLabelFirst(i, attrs["label"]), val, className, fieldName))
   328  		if err != nil {
   329  			log.Println("Error writing HTML string to FileRepeater buffer")
   330  			return nil
   331  		}
   332  
   333  		_, err = html.WriteString(fmt.Sprintf(script, nameidx, className, val))
   334  		if err != nil {
   335  			log.Println("Error writing HTML string to FileRepeater buffer")
   336  			return nil
   337  		}
   338  	}
   339  	_, err = html.WriteString(`</span>`)
   340  	if err != nil {
   341  		log.Println("Error writing HTML string to FileRepeater buffer")
   342  		return nil
   343  	}
   344  
   345  	return append(html.Bytes(), RepeatController(fieldName, p, "input.upload", "div.file-input."+fieldName)...)
   346  }
   347  
   348  // RepeatController generates the javascript to control any repeatable form
   349  // element in an editor based on its type, field name and HTML tag name
   350  func RepeatController(fieldName string, p interface{}, inputSelector, cloneSelector string) []byte {
   351  	scope := TagNameFromStructField(fieldName, p)
   352  	script := `
   353      <script>
   354          $(function() {
   355              // define the scope of the repeater
   356              var scope = $('.__ponzu-repeat.` + scope + `');
   357  
   358              var getChildren = function() {
   359                  return scope.find('` + cloneSelector + `')
   360              }
   361  
   362              var resetFieldNames = function() {
   363                  // loop through children, set its name to the fieldName.i where
   364                  // i is the current index number of children array
   365                  var children = getChildren();
   366  
   367                  for (var i = 0; i < children.length; i++) {
   368  					var preset = false;					
   369                      var $el = children.eq(i);
   370  					var name = '` + scope + `.'+String(i);
   371  
   372                      $el.find('` + inputSelector + `').attr('name', name);
   373  
   374  					// ensure no other input-like elements besides ` + inputSelector + `
   375  					// get the new name by setting it to an empty string
   376  					$el.find('input, select, textarea').each(function(i, elem) {
   377  						var $elem = $(elem);
   378  						
   379  						// if the elem is not ` + inputSelector + ` and has no value 
   380  						// set the name to an empty string
   381  						if (!$elem.is('` + inputSelector + `')) {
   382  							if ($elem.val() === '' || $elem.is('.file-path')) {
   383  								$elem.attr('name', '');
   384  							} else {
   385  								$elem.attr('name', name);
   386  								preset = true;
   387  							}						
   388  						}
   389  					});      
   390  
   391  					// if there is a preset value, remove the name attr from the
   392  					// ` + inputSelector + ` element so it doesn't overwrite db
   393  					if (preset) {
   394  						$el.find('` + inputSelector + `').attr('name', '');														
   395  					}          
   396  
   397                      // reset controllers
   398                      $el.find('.controls').remove();
   399                  }
   400  
   401                  applyRepeatControllers();
   402              }
   403  
   404              var addRepeater = function(e) {
   405                  e.preventDefault();
   406                  
   407                  var add = e.target;
   408  
   409                  // find and clone the repeatable input-like element
   410                  var source = $(add).parent().closest('` + cloneSelector + `');
   411                  var clone = source.clone();
   412  
   413                  // if clone has label, remove it
   414                  clone.find('label').remove();
   415                  
   416                  // remove the pre-filled value from clone
   417                  clone.find('` + inputSelector + `').val('');
   418  				clone.find('input').val('');
   419  
   420                  // remove controls from clone if already present
   421                  clone.find('.controls').remove();
   422  
   423  				// remove input preview on clone if copied from source
   424  				clone.find('.preview').remove();
   425  
   426                  // add clone to scope and reset field name attributes
   427                  scope.append(clone);
   428  
   429                  resetFieldNames();
   430              }
   431  
   432              var delRepeater = function(e) {
   433                  e.preventDefault();
   434  
   435                  // do nothing if the input is the only child
   436                  var children = getChildren();
   437                  if (children.length === 1) {
   438                      return;
   439                  }
   440  
   441                  var del = e.target;
   442                  
   443                  // pass label onto next input-like element if del 0 index
   444                  var wrapper = $(del).parent().closest('` + cloneSelector + `');
   445                  if (wrapper.find('` + inputSelector + `').attr('name') === '` + scope + `.0') {
   446                      wrapper.next().append(wrapper.find('label'))
   447                  }
   448                  
   449                  wrapper.remove();
   450  
   451                  resetFieldNames();
   452              }
   453  
   454              var createControls = function() {
   455                  // create + / - controls for each input-like child element of scope
   456                  var add = $('<button>+</button>');
   457                  add.addClass('repeater-add');
   458                  add.addClass('btn-flat waves-effect waves-green');
   459  
   460                  var del = $('<button>-</button>');
   461                  del.addClass('repeater-del');
   462                  del.addClass('btn-flat waves-effect waves-red');                
   463  
   464                  var controls = $('<span></span>');
   465                  controls.addClass('controls');
   466                  controls.addClass('right');
   467  
   468                  // bind listeners to child's controls
   469                  add.on('click', addRepeater);
   470                  del.on('click', delRepeater);
   471  
   472                  controls.append(add);
   473                  controls.append(del);
   474  
   475                  return controls;
   476              }
   477  
   478              var applyRepeatControllers = function() {
   479                  // add controls to each child
   480                  var children = getChildren()
   481                  for (var i = 0; i < children.length; i++) {
   482                      var el = children[i];
   483                      
   484                      $(el).find('` + inputSelector + `').parent().find('.controls').remove();
   485                      
   486                      var controls = createControls();                                        
   487                      $(el).append(controls);
   488                  }
   489              }
   490  
   491  			resetFieldNames();
   492          });
   493  
   494      </script>
   495      `
   496  
   497  	return []byte(script)
   498  }