Sorry for the delay in posting my solution, but here goes, and please chip in if anything I’ve done can be improved upon or corrected if wrong…
First we create a template for the calendar based on a copy of /concrete/blocks/calendar/view.php, but we replace the two data calls (one calls all the events and the other calls the selected event) as follows:
[snip]
events: '<?= trim($view->action('get_entry_list', Core::make('token')->generate('list_view'))).'/'.$entityID?>',
nextDayThreshold: '00:00:00',
eventDataTransform: function(event) {
if(event.allDay) {
event.end = moment(event.end).add(1, 'days')
}
return event;
},
eventRender: function(event, element) {
<?php if ($controller->supportsLightbox()) { ?>
element.attr('href', '<?= trim($view->action('output_modal_content', Core::make('token')->generate('modal_view'))) ?>/' + event.id + '/event').magnificPopup({
[snip]
Next we create an endpoint for our events call by extending the Express Entry List Controller thus:
<?php
namespace Application\Block\ExpressEntryList;
use Concrete\Core\Entity\Express\Entity;
use Concrete\Core\Database\Connection\Connection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Concrete\Block\ExpressEntryList\Controller as ExpressEntryListController;
class Controller extends ExpressEntryListController
{
// ajax handler to return a JSON encoded array of express EVENT entity entries
// $bID: the block ID of the calling block (not used in this case)
// $exEntryID: the required express entity ID (int)
public function action_get_entry_list($token = false, $bID = false, $exEntityID = null)
{
// validate the supplied token (generated by the server) to ensure it is valid/current
if (\Core::make('token')->validate('list_view', $token) && !empty($exEntityID)) {
// fetch the entity object from the submitted entity ID
$entity = $this->entityManager->find(Entity::class, $exEntityID);
// fetch the entity's handle, then construct the name of its search index data table
$dbTable = camelcase($entity->getHandle()).'ExpressSearchIndexAttributes';
// extract all entries from the search index data table that have been approved (ak_publish = Y) and which are not more than two months old
$db = $this->app->make(Connection::class);
$sql = "SELECT exEntryID, ak_title, ak_date, ak_days, ak_category FROM ".$dbTable." WHERE ak_publish LIKE '%Y%' AND ak_date > date_sub(now(), interval 2 month) ORDER BY ak_date";
$query = $db->fetchAllAssociative($sql);
$entries = [];
$i = 0;
foreach ($query as $row) {
// form the exact start and end date-times (times are not shown at the front-end)
$start = date('Y-m-d 09:00:00', strtotime($row['ak_date']));
if ($row['ak_days'] < 2) {
$end = date('Y-m-d 17:00:00', strtotime($row['ak_date']));
} else {
$end = date('Y-m-d 17:00:00', (strtotime($start)+(($row['ak_days']-1)*86400)));
}
// determine the calendar's event bar colouring according to the event category/type
if (str_starts_with(trim($row['ak_category']), 1)) {
$bgColour = 'rgb(151, 151, 151)'; // grey
$txColour = 'rgb(255, 255, 255)'; // white
} elseif (str_starts_with(trim($row['ak_category']), 2)) {
$bgColour = 'rgb(52, 138, 177)'; // blue
$txColour = 'rgb(255, 255, 255)'; // white
} elseif (str_starts_with(trim($row['ak_category']), 3)) {
$bgColour = 'rgb(42, 155, 49)'; // green
$txColour = 'rgb(255, 255, 255)'; // white
} elseif (str_starts_with(trim($row['ak_category']), 4)) {
$bgColour = 'rgb(186, 165, 0)'; // mustard
$txColour = 'rgb(255, 255, 255)'; // white
} elseif (str_starts_with(trim($row['ak_category']), 5)) {
$bgColour = 'rgb(220, 130, 19)'; // orange
$txColour = 'rgb(255, 255, 255)'; // white
} elseif (str_starts_with(trim($row['ak_category']), 6)) {
$bgColour = 'rgb(224, 57, 46)'; // red
$txColour = 'rgb(255, 255, 255)'; // white
} else {
$bgColour = 'rgb(0, 0, 0)'; // black
$txColour = 'rgb(255, 255, 255)'; // white
}
$entries[$i]['id'] = $row['exEntryID'];
$entries[$i]['title'] = $row['ak_title'];
$entries[$i]['start'] = $start;
$entries[$i]['end'] = $end;
$entries[$i]['backgroundColor'] = $bgColour;
$entries[$i]['borderColor'] = $bgColour;
$entries[$i]['textColor'] = $txColour;
$i++;
}
return new JsonResponse($entries);
}
exit;
}
}
Obviously the above code includes many things that are relevant to my Express object that won’t be to yours, but you should be able to use the above as a guide to roll your own solution.
Next we need to create an endpoint for our rendered event call by extending the Express Entry Detail Controller. I have three different types of data so I’ll just show the event data here:
<?php
namespace Application\Block\ExpressEntryDetail;
use Concrete\Block\ExpressEntryDetail\Controller as ExpressEntryDetailController;
use Concrete\Core\Support\Facade\Facade;
use Doctrine\ORM\EntityManager;
class Controller extends ExpressEntryDetailController
{
// ajax handler to print the contents of an entity entry as modal-ready html
// $bID: the block ID of the calling block (int)
// $exEntryID: the required express entry ID (int)
// $type: 'event', 'club' or 'act'
public function action_output_modal_content($token = false, $bID = false, $exEntryID = null, $type = null)
{
// validate the supplied token (generated by the server) to ensure it is valid/current
if (\Core::make('token')->validate('modal_view', $token) && !empty($exEntryID) && !empty($type)) {
// call the relevant modal content method
if ($type == 'event') {
echo $this->getEventEntryModalContent($exEntryID, $bID);
} else if ($type == 'club') {
echo $this->getClubEntryModalContent($exEntryID, $bID);
} else if ($type == 'act') {
echo $this->getActEntryModalContent($exEntryID, $bID);
}
}
exit;
}
// fetch an express EVENT entry's data as modal-ready html
private function getEventEntryModalContent($exEntryID = null, $bID = false)
{
// the app and entity manager are not available when calling this
// method from a third-party block, so we need to manually create them
if (!isset($this->app) || !isset($this->entityManager)) {
$this->app = Facade::getFacadeApplication();
$this->entityManager = $this->app->make(EntityManager::class);
}
// fetch the entry and entity objects from the supplied ID
$entry = $this->entityManager->find('Concrete\Core\Entity\Express\Entry', $exEntryID);
if (is_object($entry)) {
$entity = $entry->getEntity();
// iterate through each entity attribute
// See /html/concrete/src/Express/Export/EntryList/CsvWriter.php for
// examples of how to deal with multi-column values and associations
$attributes = [];
$ak = $entity->getAttributeKeyCategory();
if (is_object($ak)) {
foreach ($entity->getAttributes() as $att) {
$name = $att->getAttributeKeyDisplayName();
$handle = $att->getAttributeKeyHandle();
$value_ob = $entry->getAttributeValueObject($handle);
if ($value_ob) {
$value = $value_ob->getPlainTextValue();
// note the file ID of the uploaded image if applicable
$index = '';
if ($handle == 'image') {
$index = $value_ob->getSearchIndexValue();
}
$attributes[$handle] = ['name' => $name, 'value' => $value ?? '', 'index' => $index];
}
}
}
//echo json_encode($attributes);
// transfer the known/expected attribute values to an array
$fID = 0;
$div = [];
$div['url'] = '';
$div['date'] = '';
$div['days'] = '';
$div['title'] = '';
$div['image'] = '';
$div['status'] = '';
$div['location'] = '';
$div['features'] = '';
$div['description'] = '';
foreach ($attributes as $handle => $a) {
$div[$handle] = $a['value'];
// note the file ID of the uploaded image if applicable
if ($handle == 'image') { $fID = $a['index']; }
}
// process and re-order the attribute values
$return = '';
// TITLE
if (!empty($div['title'])) {
$return .= '<h3>'.$this->sanitise($div['title']).'</h3>';
}
// IMAGE
if (!empty($div['image'])) {
// use 'small' thumbnail as its width is 740px (710px required)
if (!empty($fID)) {
$f = \File::getByID($fID);
if (is_object($f)) {
$type = \Concrete\Core\File\Image\Thumbnail\Type\Type::getByHandle('small');
$src = $f->getThumbnailURL($type->getBaseVersion());
$return .= '<div class="cal-pic"><img src="'.$src.'" class="img-fluid" alt="'.$this->sanitise($attributes['title']['value']).'"></img></div>';
}
}
}
return '<div class="cal-modal">'.$return.'</div>';
}
}
}
As you may have realised, I have only shown the processing of the event title and image, as my attributes are not going to be the same as your attributes, but this should be enough to demonstrate the process of calling the event data, processing it, constructing the html output, and returning it when called, to the pop-up calendar event modal.
Feel free to comment if you can see any issues with any of the above, as I do not profess to be any sort of expert, and this is my first foray into the world of concrete cms Express objects.
Note that as the two extended controllers are not otherwise instantiated on the page, we have to add both an Express Details block instance and an Express List block instance on the same page. We can select the relevant Entity for the Express Details block without it adding any content on the page (leave ‘entry’ as ‘from another page’), but we need to leave the Express List block unassigned else it will display content. Not selecting an ID will throw the odd ‘missing ID’ error in the logs when you edit the page, but there’s no way round this and it’s only a warning in the logs that you will be expecting.