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

     1  package editor
     2  
     3  import (
     4  	"bytes"
     5  	"html"
     6  	"strings"
     7  )
     8  
     9  // Input returns the []byte of an <input> HTML element with a label.
    10  // IMPORTANT:
    11  // The `fieldName` argument will cause a panic if it is not exactly the string
    12  // form of the struct field that this editor input is representing
    13  // 	type Person struct {
    14  //		item.Item
    15  // 		editor editor.Editor
    16  //
    17  // 		Name string `json:"name"`
    18  //		//...
    19  // 	}
    20  //
    21  // 	func (p *Person) MarshalEditor() ([]byte, error) {
    22  // 		view, err := editor.Form(p,
    23  // 			editor.Field{
    24  // 				View: editor.Input("Name", p, map[string]string{
    25  // 					"label":       "Name",
    26  // 					"type":        "text",
    27  // 					"placeholder": "Enter the Name here",
    28  // 				}),
    29  // 			}
    30  // 		)
    31  // 	}
    32  func Input(fieldName string, p interface{}, attrs map[string]string) []byte {
    33  	e := NewElement("input", attrs["label"], fieldName, p, attrs)
    34  
    35  	return DOMElementSelfClose(e)
    36  }
    37  
    38  // Textarea returns the []byte of a <textarea> HTML element with a label.
    39  // IMPORTANT:
    40  // The `fieldName` argument will cause a panic if it is not exactly the string
    41  // form of the struct field that this editor input is representing
    42  func Textarea(fieldName string, p interface{}, attrs map[string]string) []byte {
    43  	// add materialize css class to make UI correct
    44  	className := "materialize-textarea"
    45  	if _, ok := attrs["class"]; ok {
    46  		class := attrs["class"]
    47  		attrs["class"] = class + " " + className
    48  	} else {
    49  		attrs["class"] = className
    50  	}
    51  
    52  	e := NewElement("textarea", attrs["label"], fieldName, p, attrs)
    53  
    54  	return DOMElement(e)
    55  }
    56  
    57  // Timestamp returns the []byte of an <input> HTML element with a label.
    58  // IMPORTANT:
    59  // The `fieldName` argument will cause a panic if it is not exactly the string
    60  // form of the struct field that this editor input is representing
    61  func Timestamp(fieldName string, p interface{}, attrs map[string]string) []byte {
    62  	var data string
    63  	val := ValueFromStructField(fieldName, p)
    64  	if val == "0" {
    65  		data = ""
    66  	} else {
    67  		data = val
    68  	}
    69  
    70  	e := &Element{
    71  		TagName: "input",
    72  		Attrs:   attrs,
    73  		Name:    TagNameFromStructField(fieldName, p),
    74  		Label:   attrs["label"],
    75  		Data:    data,
    76  		ViewBuf: &bytes.Buffer{},
    77  	}
    78  
    79  	return DOMElementSelfClose(e)
    80  }
    81  
    82  // File returns the []byte of a <input type="file"> HTML element with a label.
    83  // IMPORTANT:
    84  // The `fieldName` argument will cause a panic if it is not exactly the string
    85  // form of the struct field that this editor input is representing
    86  func File(fieldName string, p interface{}, attrs map[string]string) []byte {
    87  	name := TagNameFromStructField(fieldName, p)
    88  	value := ValueFromStructField(fieldName, p)
    89  	tmpl :=
    90  		`<div class="file-input ` + name + ` input-field col s12">
    91  			<label class="active">` + attrs["label"] + `</label>
    92  			<div class="file-field input-field">
    93  				<div class="btn">
    94  					<span>Upload</span>
    95  					<input class="upload" type="file">
    96  				</div>
    97  				<div class="file-path-wrapper">
    98  					<input class="file-path validate" placeholder="` + attrs["label"] + `" type="text">
    99  				</div>
   100  			</div>
   101  			<div class="preview"><div class="img-clip"></div></div>			
   102  			<input class="store ` + name + `" type="hidden" name="` + name + `" value="` + value + `" />
   103  		</div>`
   104  
   105  	script :=
   106  		`<script>
   107  			$(function() {
   108  				var $file = $('.file-input.` + name + `'),
   109  					upload = $file.find('input.upload'),
   110  					store = $file.find('input.store'),
   111  					preview = $file.find('.preview'),
   112  					clip = preview.find('.img-clip'),
   113  					reset = document.createElement('div'),
   114  					img = document.createElement('img'),
   115  					video = document.createElement('video'),
   116  					unknown = document.createElement('div'),
   117  					viewLink = document.createElement('a'),
   118  					viewLinkText = document.createTextNode('Download / View '),
   119  					iconLaunch = document.createElement('i'),
   120  					iconLaunchText = document.createTextNode('launch'),
   121  					uploadSrc = store.val();
   122  					video.setAttribute
   123  					preview.hide();
   124  					viewLink.setAttribute('href', '` + value + `');
   125  					viewLink.setAttribute('target', '_blank');
   126  					viewLink.appendChild(viewLinkText);
   127  					viewLink.style.display = 'block';
   128  					viewLink.style.marginRight = '10px';					
   129  					viewLink.style.textAlign = 'right';
   130  					iconLaunch.className = 'material-icons tiny';
   131  					iconLaunch.style.position = 'relative';
   132  					iconLaunch.style.top = '3px';
   133  					iconLaunch.appendChild(iconLaunchText);
   134  					viewLink.appendChild(iconLaunch);
   135  					preview.append(viewLink);
   136  
   137  				// when ` + name + ` input changes (file is selected), remove
   138  				// the 'name' and 'value' attrs from the hidden store input.
   139  				// add the 'name' attr to ` + name + ` input
   140  				upload.on('change', function(e) {
   141  					resetImage();
   142  				});
   143  
   144  				if (uploadSrc.length > 0) {
   145  					var ext = uploadSrc.substring(uploadSrc.lastIndexOf('.'));
   146  					ext = ext.toLowerCase();
   147  					switch (ext) {
   148  						case '.jpg':
   149  						case '.jpeg':
   150  						case '.webp':
   151  						case '.gif':
   152  						case '.png':
   153  							$(img).attr('src', store.val());
   154  							clip.append(img);
   155  							break;
   156  						case '.mp4':
   157  						case '.webm':
   158  							$(video)
   159  								.attr('src', store.val())
   160  								.attr('type', 'video/'+ext.substring(1))
   161  								.attr('controls', true)
   162  								.css('width', '100%');
   163  							clip.append(video);
   164  							break;
   165  						default:
   166  							$(img).attr('src', '/admin/static/dashboard/img/ponzu-file.png');
   167  							$(unknown)
   168  								.css({
   169  									position: 'absolute', 
   170  									top: '10px', 
   171  									left: '10px',
   172  									border: 'solid 1px #ddd',
   173  									padding: '7px 7px 5px 12px',
   174  									fontWeight: 'bold',
   175  									background: '#888',
   176  									color: '#fff',
   177  									textTransform: 'uppercase',
   178  									letterSpacing: '2px' 
   179  								})
   180  								.text(ext);
   181  							clip.append(img);
   182  							clip.append(unknown);
   183  							clip.css('maxWidth', '200px');
   184  					}
   185  					preview.show();
   186  
   187  					$(reset).addClass('reset ` + name + ` btn waves-effect waves-light grey');
   188  					$(reset).html('<i class="material-icons tiny">clear<i>');
   189  					$(reset).on('click', function(e) {
   190  						e.preventDefault();
   191  						preview.animate({"opacity": 0.1}, 200, function() {
   192  							preview.slideUp(250, function() {
   193  								resetImage();
   194  							});
   195  						})
   196  						
   197  					});
   198  					clip.append(reset);
   199  				}
   200  
   201  				function resetImage() {
   202  					store.val('');
   203  					store.attr('name', '');
   204  					upload.attr('name', '` + name + `');
   205  					clip.empty();
   206  				}
   207  			});	
   208  		</script>`
   209  
   210  	return []byte(tmpl + script)
   211  }
   212  
   213  // Richtext returns the []byte of a rich text editor (provided by http://summernote.org/) with a label.
   214  // IMPORTANT:
   215  // The `fieldName` argument will cause a panic if it is not exactly the string
   216  // form of the struct field that this editor input is representing
   217  func Richtext(fieldName string, p interface{}, attrs map[string]string) []byte {
   218  	// create wrapper for richtext editor, which isolates the editor's css
   219  	iso := []byte(`<div class="iso-texteditor input-field col s12"><label>` + attrs["label"] + `</label>`)
   220  	isoClose := []byte(`</div>`)
   221  
   222  	if _, ok := attrs["class"]; ok {
   223  		attrs["class"] += "richtext " + fieldName
   224  	} else {
   225  		attrs["class"] = "richtext " + fieldName
   226  	}
   227  
   228  	if _, ok := attrs["id"]; ok {
   229  		attrs["id"] += "richtext-" + fieldName
   230  	} else {
   231  		attrs["id"] = "richtext-" + fieldName
   232  	}
   233  
   234  	// create the target element for the editor to attach itself
   235  	div := &Element{
   236  		TagName: "div",
   237  		Attrs:   attrs,
   238  		Name:    "",
   239  		Label:   "",
   240  		Data:    "",
   241  		ViewBuf: &bytes.Buffer{},
   242  	}
   243  
   244  	// create a hidden input to store the value from the struct
   245  	val := ValueFromStructField(fieldName, p)
   246  	name := TagNameFromStructField(fieldName, p)
   247  	input := `<input type="hidden" name="` + name + `" class="richtext-value ` + fieldName + `" value="` + html.EscapeString(val) + `"/>`
   248  
   249  	// build the dom tree for the entire richtext component
   250  	iso = append(iso, DOMElement(div)...)
   251  	iso = append(iso, []byte(input)...)
   252  	iso = append(iso, isoClose...)
   253  
   254  	script := `
   255  	<script>
   256  		$(function() { 
   257  			var _editor = $('.richtext.` + fieldName + `');
   258  			var hidden = $('.richtext-value.` + fieldName + `');
   259  
   260  			_editor.materialnote({
   261  				height: 250,
   262  				placeholder: '` + attrs["placeholder"] + `',
   263  				toolbar: [
   264  					['style', ['style']],
   265  					['font', ['bold', 'italic', 'underline', 'clear', 'strikethrough', 'superscript', 'subscript']],
   266  					['fontsize', ['fontsize']],
   267  					['color', ['color']],
   268  					['insert', ['link', 'picture', 'video', 'hr']],					
   269  					['para', ['ul', 'ol', 'paragraph']],
   270  					['table', ['table']],
   271  					['height', ['height']],
   272  					['misc', ['codeview']]
   273  				],
   274  				// intercept file insertion, upload and insert img with new src
   275  				onImageUpload: function(files) {
   276  					var data = new FormData();
   277  					data.append("file", files[0]);
   278  					$.ajax({
   279  						data: data,
   280  						type: 'PUT',	
   281  						url: '/admin/edit/upload',
   282  						cache: false,
   283  						contentType: false,
   284  						processData: false,
   285  						success: function(resp) {
   286  							var img = document.createElement('img');
   287  							img.setAttribute('src', resp.data[0].url);
   288  							_editor.materialnote('insertNode', img);
   289  						},
   290  						error: function(xhr, status, err) {
   291  							console.log(status, err);
   292  						}
   293  					})
   294  
   295  				}
   296  			});
   297  
   298  			// inject content into editor
   299  			if (hidden.val() !== "") {
   300  				_editor.code(hidden.val());
   301  			}
   302  
   303  			// update hidden input with encoded value on different events
   304  			_editor.on('materialnote.change', function(e, content, $editable) {
   305  				hidden.val(replaceBadChars(content));			
   306  			});
   307  
   308  			_editor.on('materialnote.paste', function(e) {
   309  				hidden.val(replaceBadChars(_editor.code()));			
   310  			});
   311  
   312  			// bit of a hack to stop the editor buttons from causing a refresh when clicked 
   313  			$('.note-toolbar').find('button, i, a').on('click', function(e) { e.preventDefault(); });
   314  		});
   315  	</script>`
   316  
   317  	return append(iso, []byte(script)...)
   318  }
   319  
   320  // Select returns the []byte of a <select> HTML element plus internal <options> with a label.
   321  // IMPORTANT:
   322  // The `fieldName` argument will cause a panic if it is not exactly the string
   323  // form of the struct field that this editor input is representing
   324  func Select(fieldName string, p interface{}, attrs, options map[string]string) []byte {
   325  	// options are the value attr and the display value, i.e.
   326  	// <option value="{map key}">{map value}</option>
   327  
   328  	// find the field value in p to determine if an option is pre-selected
   329  	fieldVal := ValueFromStructField(fieldName, p)
   330  
   331  	if _, ok := attrs["class"]; ok {
   332  		attrs["class"] += " browser-default"
   333  	} else {
   334  		attrs["class"] = "browser-default"
   335  	}
   336  
   337  	sel := NewElement("select", attrs["label"], fieldName, p, attrs)
   338  	var opts []*Element
   339  
   340  	// provide a call to action for the select element
   341  	cta := &Element{
   342  		TagName: "option",
   343  		Attrs:   map[string]string{"disabled": "true", "selected": "true"},
   344  		Data:    "Select an option...",
   345  		ViewBuf: &bytes.Buffer{},
   346  	}
   347  
   348  	// provide a selection reset (will store empty string in db)
   349  	reset := &Element{
   350  		TagName: "option",
   351  		Attrs:   map[string]string{"value": ""},
   352  		Data:    "None",
   353  		ViewBuf: &bytes.Buffer{},
   354  	}
   355  
   356  	opts = append(opts, cta, reset)
   357  
   358  	for k, v := range options {
   359  		optAttrs := map[string]string{"value": k}
   360  		if k == fieldVal {
   361  			optAttrs["selected"] = "true"
   362  		}
   363  		opt := &Element{
   364  			TagName: "option",
   365  			Attrs:   optAttrs,
   366  			Data:    v,
   367  			ViewBuf: &bytes.Buffer{},
   368  		}
   369  
   370  		opts = append(opts, opt)
   371  	}
   372  
   373  	return DOMElementWithChildrenSelect(sel, opts)
   374  }
   375  
   376  // Checkbox returns the []byte of a set of <input type="checkbox"> HTML elements
   377  // wrapped in a <div> with a label.
   378  // IMPORTANT:
   379  // The `fieldName` argument will cause a panic if it is not exactly the string
   380  // form of the struct field that this editor input is representing
   381  func Checkbox(fieldName string, p interface{}, attrs, options map[string]string) []byte {
   382  	if _, ok := attrs["class"]; ok {
   383  		attrs["class"] += "input-field col s12"
   384  	} else {
   385  		attrs["class"] = "input-field col s12"
   386  	}
   387  
   388  	div := NewElement("div", attrs["label"], fieldName, p, attrs)
   389  
   390  	var opts []*Element
   391  
   392  	// get the pre-checked options if this is already an existing post
   393  	checkedVals := ValueFromStructField(fieldName, p)
   394  	checked := strings.Split(checkedVals, "__ponzu")
   395  
   396  	i := 0
   397  	for k, v := range options {
   398  		inputAttrs := map[string]string{
   399  			"type":  "checkbox",
   400  			"value": k,
   401  			"id":    strings.Join(strings.Split(v, " "), "-"),
   402  		}
   403  
   404  		// check if k is in the pre-checked values and set to checked
   405  		for _, x := range checked {
   406  			if k == x {
   407  				inputAttrs["checked"] = "checked"
   408  			}
   409  		}
   410  
   411  		// create a *element manually using the modified TagNameFromStructFieldMulti
   412  		// func since this is for a multi-value name
   413  		input := &Element{
   414  			TagName: "input",
   415  			Attrs:   inputAttrs,
   416  			Name:    TagNameFromStructFieldMulti(fieldName, i, p),
   417  			Label:   v,
   418  			Data:    "",
   419  			ViewBuf: &bytes.Buffer{},
   420  		}
   421  
   422  		opts = append(opts, input)
   423  		i++
   424  	}
   425  
   426  	return DOMElementWithChildrenCheckbox(div, opts)
   427  }
   428  
   429  // Tags returns the []byte of a tag input (in the style of Materialze 'Chips') with a label.
   430  // IMPORTANT:
   431  // The `fieldName` argument will cause a panic if it is not exactly the string
   432  // form of the struct field that this editor input is representing
   433  func Tags(fieldName string, p interface{}, attrs map[string]string) []byte {
   434  	name := TagNameFromStructField(fieldName, p)
   435  
   436  	// get the saved tags if this is already an existing post
   437  	values := ValueFromStructField(fieldName, p)
   438  	var tags []string
   439  	if strings.Contains(values, "__ponzu") {
   440  		tags = strings.Split(values, "__ponzu")
   441  	}
   442  
   443  	// case where there is only one tag stored, thus has no separator
   444  	if len(values) > 0 && !strings.Contains(values, "__ponzu") {
   445  		tags = append(tags, values)
   446  	}
   447  
   448  	html := `
   449  	<div class="col s12 __ponzu-tags ` + name + `">
   450  		<label class="active">` + attrs["label"] + ` (Type and press "Enter")</label>
   451  		<div class="chips ` + name + `"></div>
   452  	`
   453  
   454  	var initial []string
   455  	i := 0
   456  	for _, tag := range tags {
   457  		tagName := TagNameFromStructFieldMulti(fieldName, i, p)
   458  		html += `<input type="hidden" class="__ponzu-tag ` + tag + `" name=` + tagName + ` value="` + tag + `"/>`
   459  		initial = append(initial, `{tag: '`+tag+`'}`)
   460  		i++
   461  	}
   462  
   463  	script := `
   464  	<script>
   465  		$(function() {
   466  			var tags = $('.__ponzu-tags.` + name + `');
   467  			$('.chips.` + name + `').material_chip({
   468  				data: [` + strings.Join(initial, ",") + `],
   469  				secondaryPlaceholder: '+` + name + `'
   470  			});		
   471  
   472  			// handle events specific to tags
   473  			var chips = tags.find('.chips');
   474  			
   475  			chips.on('chip.add', function(e, chip) {
   476  				chips.parent().find('.empty-tag').remove();
   477  				
   478  				var input = $('<input>');
   479  				input.attr({
   480  					class: '__ponzu-tag '+chip.tag.split(' ').join('__'),
   481  					name: '` + name + `.'+String(tags.find('input[type=hidden]').length),
   482  					value: chip.tag,
   483  					type: 'hidden'
   484  				});
   485  				
   486  				tags.append(input);
   487  			});
   488  
   489  			chips.on('chip.delete', function(e, chip) {
   490  				// convert tag string to class-like selector "some tag" -> ".some.tag"
   491  				var sel = '.__ponzu-tag.' + chip.tag.split(' ').join('__');
   492  				chips.parent().find(sel).remove();
   493  
   494  				// iterate through all hidden tag inputs to re-name them with the correct ` + name + `.index
   495  				var hidden = chips.parent().find('input[type=hidden]');
   496  				
   497  				// if there are no tags, set a blank
   498  				if (hidden.length === 0) {
   499  					var input = $('<input>');
   500  					input.attr({
   501  						class: 'empty-tag',
   502  						name: '` + name + `',
   503  						type: 'hidden'
   504  					});
   505  					
   506  					tags.append(input);
   507  				}
   508  				
   509  				// re-name hidden storage elements in necessary format 
   510  				for (var i = 0; i < hidden.length; i++) {
   511  					$(hidden[i]).attr('name', '` + name + `.'+String(i));
   512  				}
   513  			});
   514  		});
   515  	</script>
   516  	`
   517  
   518  	html += `</div>`
   519  
   520  	return []byte(html + script)
   521  }