|
<?php |
|
|
|
namespace ProcessWire; |
|
|
|
// WE GOT AN AJAX REQUEST |
|
if ($config->ajax) { |
|
|
|
// check CSRF |
|
if (!$session->CSRF->hasValidToken()) { |
|
// form submission is NOT valid |
|
throw new WireException('CSRF check failed!'); |
|
} |
|
|
|
$id = 0; |
|
$options = []; |
|
// get previously removed locations (keeping things in sync) |
|
$removedLocations = $session->get('removedLocations'); |
|
//--------- |
|
if ((int) $input->post->location_remove_id) { |
|
// REMOVING LOCATION |
|
$mode = 'remove'; |
|
$notice = "Removed"; |
|
$id = (int) $input->post->location_remove_id; |
|
} else if ((int) $input->post->location_add_id) { |
|
// ADDING LOCATION |
|
$mode = 'add'; |
|
$notice = 'Added'; |
|
$id = (int) $input->post->location_add_id; |
|
} |
|
|
|
// check if we have the page, else error |
|
$locationTitle = $pages->getRaw("id={$id}", 'title'); |
|
// location not found |
|
if (empty($locationTitle)) { |
|
// handle error |
|
$noticeType = "error"; |
|
$notice = "Location could not be " . strtolower($notice); |
|
} |
|
// location found |
|
else { |
|
// add removed to session |
|
if ($mode === 'remove') { |
|
$removedLocations[] = $id; |
|
// reset session |
|
$session->set('removedLocations', $removedLocations); |
|
} |
|
|
|
// build options |
|
$options['skip_pages_ids'] = $removedLocations; |
|
|
|
//---------------- |
|
$noticeType = "success"; |
|
$notice .= " {$locationTitle}"; |
|
} |
|
|
|
//----------- |
|
// build updated content |
|
$out = buildLocationCards($page, $options); |
|
// @note - here we always return one input <only!></only!> |
|
$out .= "<input type='hidden' id='location_notice' name='location_notice' data-notice-type='{$noticeType}' value='{$notice}'>"; |
|
echo $out; |
|
// halt proceedings |
|
$this->halt(); |
|
} |
|
|
|
|
|
function buildCard(Page $page) { |
|
|
|
$firstImage = $page->images->first(); |
|
$imageThumbURL = $firstImage->height(260)->url; |
|
//----------------- |
|
// component @credits: https://tailwindcomponents.com/component/simple-card |
|
$out = |
|
"<div class='max-w-xs rounded overflow-hidden shadow-lg my-2'>" . |
|
"<img class='w-full' src='{$imageThumbURL}' alt='{$firstImage->description}'>" . |
|
"<div class='px-6 py-4'>" . |
|
"<div class='font-bold text-xl mb-2'>{$page->title}</div>" . |
|
// ripTags(wordLimiter($page->body)) . |
|
wordLimiter($page->body, 60) . |
|
"</div>" . |
|
"<div class='px-6 py-4'>" . |
|
getActionButtons($page) . |
|
"</div>" . |
|
"</div>"; |
|
|
|
return $out; |
|
} |
|
|
|
|
|
/** |
|
* Wordlimiter cuts a textarea only after complete words not between |
|
* used seo function and in some templates |
|
* @credit: @soma: https://processwire.com/talk/topic/3429-how-to-set-text-linecharacter-limits-in-templates/?do=findComment&comment=33779 |
|
*/ |
|
function wordLimiter($str = '', $limit = 120, $endstr = '...') { |
|
|
|
if ($str == '') return ''; |
|
|
|
if (strlen($str) <= $limit) return $str; |
|
|
|
$out = substr($str, 0, $limit); |
|
$pos = strrpos($out, " "); |
|
if ($pos > 0) { |
|
$out = substr($out, 0, $pos); |
|
} |
|
$out .= $endstr; |
|
return $out; |
|
} |
|
|
|
/** |
|
* Alternative with regex for striptags function |
|
* used for seo function and in some templates |
|
* @credit: @soma: https://processwire.com/talk/topic/3429-how-to-set-text-linecharacter-limits-in-templates/?do=findComment&comment=33779 |
|
*/ |
|
function ripTags($string) { |
|
|
|
// ----- remove HTML TAGs ----- |
|
$string = preg_replace('/<[^>]*>/', ' ', $string); |
|
|
|
// ----- remove control characters ----- |
|
$string = str_replace("\r", '', $string); // --- replace with empty space |
|
$string = str_replace("\n", ' ', $string); // --- replace with space |
|
$string = str_replace("\t", ' ', $string); // --- replace with space |
|
|
|
// ----- remove multiple spaces ----- |
|
$string = trim(preg_replace('/ {2,}/', ' ', $string)); |
|
|
|
return $string; |
|
} |
|
|
|
function getActionButtons(Page $page) { |
|
// @note: in this example we SWAP the whole div#locations! |
|
$out = |
|
// ADD LOCATION |
|
"<a hx-post='./' hx-target='#locations' hx-vals='{\"location_add_id\": \"{$page->id}\"}' hx-include='._post_token' hx-indicator='#locations_spinner_indicator' class='cursor-pointer inline-block bg-grey-lighter rounded-lg px-3 py-1 text-sm font-semibold text-grey-darker mr-2 hover:bg-green-500 hover:text-white'>Add</a>" . |
|
// REMOVE LOCATION |
|
"<a hx-post='./' hx-target='#locations' hx-vals='{\"location_remove_id\": \"{$page->id}\"}' hx-include='._post_token' hx-indicator='#locations_spinner_indicator' class='cursor-pointer inline-block bg-grey-lighter rounded-lg px-3 py-1 text-sm font-semibold text-grey-darker mr-2 hover:bg-red-500 hover:text-white'>Remove</a>"; |
|
|
|
return $out; |
|
} |
|
|
|
function buildLocationCards(Page $parentPage, $options = []) { |
|
|
|
$defaultOptions = ['skip_pages_ids' => []]; |
|
|
|
if (!empty($options)) { |
|
$options = array_merge($defaultOptions, $options); |
|
} else { |
|
$options = $defaultOptions; |
|
} |
|
|
|
$locations = $parentPage->children; |
|
$allLocationsRemoved = !empty($options['skip_pages_ids']) && (count($options['skip_pages_ids']) === $locations->count); |
|
//-------------- |
|
// @note: in this example, we SWAP whole div#locations! |
|
$out = "<div id='locations' class='flex flex-wrap gap-9 mb-10'>"; |
|
// locations instruction |
|
$out .= |
|
"<h3 class='text-4xl w-full'>Select Locations" . |
|
"<span id='locations_spinner_indicator' class='htmx-indicator text-green-200 fa fa-fw fa-spin fa-spinner'></span>" . |
|
"</h3>"; |
|
// if we have some locations |
|
if (!$allLocationsRemoved) { |
|
foreach ($locations as $location) { |
|
// if skipping some locations/locationren |
|
if (in_array($location->id, $options['skip_pages_ids'])) { |
|
continue; |
|
}; |
|
//----------------- |
|
// vertical card - GRID |
|
// $content .= "<div id='location_{$location->id}' class='col-span-full md:col-span-4 lg:col-span-3'>" . buildCard($location) . "</div>"; |
|
// vertical card - FLEX |
|
$out .= "<div id='location_{$location->id}' class=''>" . buildCard($location) . "</div>"; |
|
} |
|
} else { |
|
$out .= "<p class='text-2xl text-white bg-red-600 mt-7 py-3 px-5'>All Locations have been removed!</p>"; |
|
} |
|
|
|
// close flex |
|
$out .= "</div>"; |
|
|
|
// add notices handler |
|
$out .= buildNoticesHandler(); |
|
// add CSRF |
|
$out .= wire('session')->CSRF->renderInput(); |
|
return $out; |
|
} |
|
// @credit: https://tailwindcomponents.com/component/alphine-js-toast-notification |
|
function buildNoticesHandler() { |
|
$out = " |
|
<div x-data='HtmxChallenges.noticesHandler()' @notice.window='add(\$event.detail)'> |
|
<div class='fixed right-0 top-0 m-5 w-1/2 xl:w-1/5 lg:w-1/4 md:w-2/5 sm:w-1/2'> |
|
<template x-for='notice of notices' :key='notice.id'> |
|
<div x-show='visible.includes(notice)' x-transition:enter='transition ease-in duration-200' |
|
x-transition:enter-start='transform opacity-0 translate-y-2' |
|
x-transition:enter-end='transform opacity-100' x-transition:leave='transition ease-out duration-500' |
|
x-transition:leave-start='transform translate-x-0 opacity-100' |
|
x-transition:leave-end='transform translate-x-full opacity-0' @click='remove(notice.id)' |
|
class='py-2 px-3 shadow-md mb-2 border-r-4 grid grid-cols-4' :class='{ |
|
\"bg-green-500 border-green-700\": notice.type === `success`, |
|
\"bg-blue-400 border-blue-700\": notice.type === `info`, |
|
\"bg-yellow-400 border-yellow-700\": notice.type === `warning`, |
|
\"bg-red-500 border-red-700\": notice.type === `error`, |
|
}' style='pointer-events:all'> |
|
<div class='col-start-1 col-span-3'> |
|
<div class='text-white text-right'><span x-text='notice.text'></span></div> |
|
</div> |
|
<div class='col-start-4 col-span-1' x-html='getIcon(notice)'></div> |
|
</div> |
|
</template> |
|
</div> |
|
</div>"; |
|
|
|
return $out; |
|
} |
|
|
|
|
|
|
|
//------------------- |
|
// USUAL NON-AJAX CONTENT |
|
|
|
// Primary content is the page's body copy |
|
// $content = $page->body; |
|
$content = ""; |
|
|
|
|
|
// build options for page cards, e.g. for those to remove |
|
$options = []; |
|
// init removed locations for session if one not present already |
|
if (empty($session->get('removedLocations'))) { |
|
$session->set('removedLocations', []); |
|
} else { |
|
// current removed locations |
|
$removedLocations = $session->get('removedLocations'); |
|
// if we have removed locations, set them in options |
|
if (!empty($removedLocations)) { |
|
$options['skip_pages_ids'] = $removedLocations; |
|
} |
|
} |
|
|
|
// --------------- |
|
// build the cards |
|
// @note: off to _main.php |
|
$content .= buildLocationCards($page, $options); |