github.com/rpdict/ponzu@v0.10.1-0.20190226054626-477f29d6bf5e/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 }