Add frontend logic for recipe ingredients "separator"

This commit is contained in:
Christopher C. Wells 2021-03-26 05:23:44 -07:00
parent ca27b06d6e
commit 1ff88d5075
2 changed files with 72 additions and 33 deletions

View File

@ -4,7 +4,7 @@
<x-slot name="header"> <x-slot name="header">
<h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $title }}</h1> <h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $title }}</h1>
</x-slot> </x-slot>
<form method="POST" enctype="multipart/form-data" action="{{ ($recipe->exists ? route('recipes.update', $recipe) : route('recipes.store')) }}"> <form x-data method="POST" enctype="multipart/form-data" action="{{ ($recipe->exists ? route('recipes.update', $recipe) : route('recipes.store')) }}">
@if ($recipe->exists)@method('put')@endif @if ($recipe->exists)@method('put')@endif
@csrf @csrf
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0"> <div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
@ -131,14 +131,20 @@
@empty @empty
@include('recipes.partials.ingredient-input') @include('recipes.partials.ingredient-input')
@endforelse @endforelse
<div class="entry-template hidden"> <div class="templates hidden">
@include('recipes.partials.ingredient-input') <div class="ingredient-template">
@include('recipes.partials.ingredient-input')
</div>
<div class="separator-template">
@include('recipes.partials.separator-input')
</div>
</div> </div>
<x-inputs.icon-button type="button" color="green" x-on:click="addEntryNode($el);"> <x-inputs.button type="button" color="green" x-on:click="addNodeFromTemplate($el, 'ingredient');">
<svg class="h-10 w-10 pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> Add Ingredient
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" /> </x-inputs.button>
</svg> <x-inputs.button type="button" color="blue" x-on:click="addNodeFromTemplate($el, 'separator');">
</x-inputs.icon-button> Add Separator
</x-inputs.button>
</div> </div>
<!-- Steps --> <!-- Steps -->
@ -149,18 +155,18 @@
@empty @empty
@include('recipes.partials.step-input') @include('recipes.partials.step-input')
@endforelse @endforelse
<div class="entry-template hidden"> <div class="templates hidden">
@include('recipes.partials.step-input') <div class="step-template">
@include('recipes.partials.step-input')
</div>
</div> </div>
<x-inputs.icon-button type="button" color="green" x-on:click="addEntryNode($el);"> <x-inputs.button type="button" color="green" x-on:click="addNodeFromTemplate($el, 'step');">
<svg class="h-10 w-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> Add Step
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" /> </x-inputs.button>
</svg>
</x-inputs.icon-button>
</div> </div>
<div x-data class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
<x-inputs.button x-on:click="prepareForm();" class="ml-3"> <x-inputs.button x-on:click="prepareForm($el);" class="ml-3">
{{ ($recipe->exists ? 'Save' : 'Add') }} {{ ($recipe->exists ? 'Save' : 'Add') }}
</x-inputs.button> </x-inputs.button>
</div> </div>
@ -215,9 +221,9 @@
// Recalculate weight (order) of all ingredients. // Recalculate weight (order) of all ingredients.
ingredientsSortable.on('drag:stopped', (e) => { ingredientsSortable.on('drag:stopped', (e) => {
Array.from(e.sourceContainer.children) Array.from(e.sourceContainer.children)
.filter(el => el.classList.contains('ingredient')) .filter(el => el.classList.contains('draggable'))
.forEach((el, index) => { .forEach((el, index) => {
el.querySelector('input[name="ingredients[weight][]"]').value = index; el.querySelector('input[name$="[weight][]"]').value = index;
}); });
}) })
@ -233,35 +239,39 @@
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
/** /**
* Adds a set of entry form fields from the template. * Adds a node to ingredients or steps based on a template.
* *
* @param {object} $el Entry lines parent element. * @param {object} $el Parent element.
* @param {string} type Template type -- "ingredient", "separator", or "step".
*/ */
let addEntryNode = ($el) => { let addNodeFromTemplate = ($el, type) => {
// Create clone of template entry. // Create clone of relevant template.
const template = $el.querySelector(':scope .entry-template'); const templates = $el.querySelector(`:scope .templates`);
const newEntry = template.cloneNode(true).firstElementChild; const template = templates.querySelector(`:scope .${type}-template`);
const newNode = template.cloneNode(true).firstElementChild;
// Set weight based on previous sibling. // Set weight based on previous sibling.
const lastWeight = template.previousElementSibling.querySelector('input[name="ingredients[weight][]"]'); const lastWeight = templates.previousElementSibling.querySelector('input[name$="[weight][]"]');
if (lastWeight && lastWeight.value) { if (lastWeight && lastWeight.value) {
newEntry.querySelector('input[name="ingredients[weight][]"]').value = Number.parseInt(lastWeight.value) + 1; newNode.querySelector('input[name$="[weight][]"]').value = Number.parseInt(lastWeight.value) + 1;
} }
// Insert new entry before add button. // Insert new node before templates.
$el.insertBefore(newEntry, template); $el.insertBefore(newNode, templates);
} }
/** /**
* Prepare form values for submit. * Prepare form values for submit.
*
* @param {object} $el Form element.
*/ */
let prepareForm = () => { let prepareForm = ($el) => {
// Remove any hidden templates before form submit. // Remove any hidden templates before form submit.
document.querySelectorAll(':scope .entry-template').forEach(e => e.remove()); $el.querySelectorAll(':scope .templates').forEach(e => e.remove());
// Add description values to hidden fields. // Add description values to hidden fields.
document.querySelector('input[name="description_delta"]').value = JSON.stringify(description.getContents()); $el.querySelector('input[name="description_delta"]').value = JSON.stringify(description.getContents());
document.querySelector('input[name="description"]').value = description.root.innerHTML $el.querySelector('input[name="description"]').value = description.root.innerHTML
// Remove extraneous spaces from rendered result. // Remove extraneous spaces from rendered result.
.replaceAll('<p><br></p>', ''); .replaceAll('<p><br></p>', '');
} }

View File

@ -0,0 +1,29 @@
<div class="separator draggable">
<x-inputs.input type="hidden" name="separators[key][]" :value="$key ?? null" />
<x-inputs.input type="hidden" name="separators[weight][]" :value="$weight ?? null" />
<div class="flex items-center space-x-2">
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0 w-full">
<div class="draggable-handle self-center text-gray-500 bg-gray-100 w-full md:w-auto p-2 cursor-move">
<svg class="h-6 w-6 mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd" />
</svg>
</div>
<x-inputs.label for="source" value="Separator text" class="hidden" />
<x-inputs.input name="separators[text][]"
type="text"
placeholder="Separator text (optional)"
class="block w-full"
:value="$text ?? null" />
</div>
<div class="flex-none">
<x-inputs.icon-button type="button"
color="red"
x-on:click="$event.target.parentNode.parentNode.parentNode.remove();">
<svg class="h-8 w-8 pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</x-inputs.icon-button>
</div>
</div>
</div>