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 }