Loading config/schema/fullcalendar_block.schema.yml 0 → 100644 +39 −0 Original line number Diff line number Diff line block.settings.fullcalendar_block: type: block_settings label: 'FullCalendar block' mapping: event_source: type: string label: 'Event source URL' initial_view: type: string label: 'Initial View' header_start: type: string label: 'Start of the header toolbar' header_center: type: string label: 'Center of the header toolbar' header_end: type: string label: 'End of the header toolbar' open_dialog: type: 'fullcalendar_block_dialog_type.[%parent.open_dialog]' # label: 'Open in a new tab/dialog' dialog_width: type: integer label: 'Dialog width' advanced: # Translatable text so that you can easily have a different configuration # per site. # @see config_translation_config_schema_info_alter() type: text label: 'Advanced settings.' # The dialog types. fullcalendar_block_dialog_type.0: type: integer label: 'Open in a new tab' fullcalendar_block_dialog_type.1: type: integer label: 'Open in a dialog' fullcalendar_block.libraries.yml +5 −1 Original line number Diff line number Diff line fullcalendar: libraries.fullcalendar: remote: https://fullcalendar.io/ version: '5.10.2' license: Loading @@ -11,6 +11,9 @@ fullcalendar: js: /libraries/drupal-fullcalendar/main.min.js: { minified: true } /libraries/drupal-fullcalendar/locales-all.min.js: { minified: true } fullcalendar: js: js/fullcalendar_block.js: {} dependencies: - core/drupal Loading @@ -18,3 +21,4 @@ fullcalendar: - core/once - core/drupalSettings - core/drupal.dialog.ajax - fullcalendar_block/libraries.fullcalendar fullcalendar_block.module +66 −2 Original line number Diff line number Diff line <?php /** * @file * Contains Fullcalendar block module functions. */ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Site\Settings; /** * Implements hook_theme(). */ function fullcalendar_block_theme($existing, $type, $theme, $path): array { function fullcalendar_block_theme($existing, $type, $theme, $path) { return [ 'fullcalendar_block' => [ 'variables' => [ Loading @@ -12,3 +20,59 @@ function fullcalendar_block_theme($existing, $type, $theme, $path): array { ], ]; } /** * Prepares variables for fullcalendar_block templates. * * Default template: fullcalendar-block.html.twig. * * @param array $variables * An associative array containing: * - block_index: The block index. */ function template_preprocess_fullcalendar_block(array &$variables) { $variables['attributes']['class'][] = 'fullcalendar-block'; $variables['attributes']['data-calendar-block-index'] = $variables['block_index']; } /** * Implements hook_library_info_alter(). */ function fullcalendar_block_library_info_alter(&$libraries, $extension) { if ($extension === 'fullcalendar_block') { /* * Provide an easy way for developers to switch to non-minified assets * during dev by specifying the following in the settings.php: * @code * $settings['fullcalendar_block.dev_mode'] = TRUE; * @endcode */ $dev_mode = Settings::get('fullcalendar_block.dev_mode', FALSE); if (!$dev_mode) { return; } // External libraries to use unminimized versions for. $elements = ['libraries.fullcalendar']; foreach ($elements as $element) { foreach (['css.theme', 'js'] as $component) { $component_path = explode('.', $component); $updated_libraries = []; foreach ((array) NestedArray::getValue($libraries[$element], $component_path, $key_exists) as $asset_key => $asset_config) { if (empty($asset_config['no_min']) && ($new_asset_key = preg_replace('/\.min\.(css|js)$/', '.$1', $asset_key)) !== $asset_key) { // Strip out the "/min/" from the path if it exists. $new_asset_key = str_replace('/min/', '/', $new_asset_key); if (is_file(DRUPAL_ROOT . $new_asset_key)) { // The supposed file exists. Use it. $asset_key = $new_asset_key; $asset_config['minified'] = FALSE; } } $updated_libraries[$asset_key] = $asset_config; } if ($key_exists) { NestedArray::setValue($libraries[$element], $component_path, $updated_libraries); } } } } } fullcalendar_block.post_update.php 0 → 100644 +34 −0 Original line number Diff line number Diff line <?php /** * @file * Post update hooks for the Fullcalendar Block module. */ use Drupal\block\BlockInterface; use Drupal\Core\Config\Entity\ConfigEntityUpdater; /** * Update the Fullcalendar block configuration schema. */ function fullcalendar_block_post_update_update_block_data(&$sandbox = NULL) { /** @var \Drupal\Core\Config\Entity\ConfigEntityUpdater $config_entity_updater */ $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class); $callback = function (BlockInterface $block) { if ($block->getPluginId() === 'fullcalendar_block') { $settings = $block->get('settings'); if ($settings && !empty($settings['advanced'])) { $settings['advanced'] = trim(str_replace(["\r\n", "\r"], "\n", $settings['advanced'])); $block->set('settings', $settings); } // Resave the fullcalendar block instance. $block->save(); } // Return false since we're updating the block ourselves. return FALSE; }; $config_entity_updater->update($sandbox, 'block', $callback); } js/fullcalendar_block.js +140 −26 Original line number Diff line number Diff line Loading @@ -6,15 +6,22 @@ // Codes run both on normal page loads and when data is loaded by AJAX (or BigPipe!) // @See https://www.drupal.org/docs/drupal-apis/javascript-api/javascript-api-overview (function ($, Drupal, once) { /** * @namespace */ Drupal.fullcalendar_block = {}; Drupal.fullcalendar_block.instances = {}; Drupal.behaviors.buildCalendarBlock = { attach: function (context, settings) { var dialogWidth = [400]; var dialogType = ['modal']; var dialogOpen = [1]; const dialogWidth = [400]; const dialogType = ['modal']; const dialogOpen = [1]; // Redirect to the URL function gotoURL(url) { let eventURL = new URL(url, location.origin); const eventURL = new URL(url, location.origin); if (eventURL.origin !== "null") { window.open(url, '_blank').focus(); } Loading @@ -23,14 +30,19 @@ // Event click callback function eventClick(info) { info.jsEvent.preventDefault(); let blockIndex = parseInt(this.el.getAttribute("calendar-block-index")); if (info.event.url && dialogOpen[blockIndex] === "1") { let ajaxSettings = { if (!info.event.url) { return; } const blockIndex = this.el.getAttribute('data-calendar-block-index'); if (dialogOpen[blockIndex] === 1) { // Open in a dialog. const ajaxSettings = { url: info.event.url, dialogType: dialogType[blockIndex], dialog: { width: dialogWidth[blockIndex] }, dialog: {width: dialogWidth[blockIndex]} }; let openDialog = Drupal.ajax(ajaxSettings).execute(); const openDialog = Drupal.ajax(ajaxSettings).execute(); openDialog.done(function() { // The modal dialog open successfully. Loading @@ -40,36 +52,138 @@ gotoURL(info.event.url); }); } else if (info.event.url) { else { gotoURL(info.event.url); } } // Function to build calendar objects. function buildCalendars() { $('.fullcalendar-block') function buildCalendars(context, settings) { $('.fullcalendar-block', context) .each(function() { let blockIndex = parseInt(this.getAttribute("calendar-block-index")); let blockSettings = drupalSettings.fullCalendarBlock[blockIndex]; var calendarOptions = JSON.parse(blockSettings.calendar_options); const blockIndex = this.getAttribute('data-calendar-block-index'); this.setAttribute('data-calendar-block-initialized', true); const blockSettings = settings.fullCalendarBlock && settings.fullCalendarBlock[blockIndex]; if (!blockSettings) { console.warn('Could not fetch fullcalendar_block settings', blockIndex, this); return; } const calendarOptions = typeof blockSettings.calendar_options === 'string' ? JSON.parse(blockSettings.calendar_options) : $.extend(true, {}, blockSettings.calendar_options); if (typeof calendarOptions.events === 'string' && Drupal.url.isLocal(calendarOptions.events)) { // Check if there are any GET parameters to send to the internal // Drupal path. This should provide better integration with Views exposed filters. let queryString = window.location.search || ''; if (queryString !== '') { // Remove the question mark and any Drupal path component, // existing fullcalendar search filters if any. const startParam = calendarOptions.startParam || 'start'; let initialDate; const specialParams = [ // Internal Drupal paths. 'q', 'render', // Remove parameters that will be sent by fullcalendar. startParam, calendarOptions.endParam || 'end', calendarOptions.timeZoneParam || 'timeZone', ]; if (typeof URLSearchParams !== 'undefined') { const query = new URLSearchParams(queryString); specialParams.forEach(function(specialParam) { if (specialParam === startParam) { initialDate = query.get(startParam); } query.delete(specialParam); }); queryString = query.toString(); } else { // Fallback to strip out query strings. // https://stackoverflow.com/a/1634841 const params = queryString.slice(1).split(/[&;]/g); // Reverse iteration as may be destructive. specialParams.forEach(function(specialParam) { const prefix = encodeURIComponent(specialParam); let i; for (i = params.length; i-- > 0;) { // Idiom for string.startsWith. if (params[i] === prefix || (params[i].lastIndexOf(prefix + '=', 0) !== -1)) { initialDate = params[i].split('=')[1]; if (initialDate) { initialDate = decodeURIComponent(initialDate); } params.splice(i, 1); } } }); queryString = params.join('&'); } // Provide a default initial date if it's specified in the query string. if (!calendarOptions.initialDate && initialDate) { const date = new Date(initialDate); if (date instanceof Date && !isNaN(date)) { // Valid date string. calendarOptions.initialDate = initialDate; } } if (queryString !== '') { // If there is a '?' in events URL, & should be used to add // parameters. queryString = (/\?/.test(calendarOptions.events) ? '&' : '?') + queryString; // Add the current query string to the query. // Useful for taking in exposed filters. calendarOptions.events = calendarOptions.events + queryString; } } } dialogOpen[blockIndex] = blockSettings.dialog_open; dialogWidth[blockIndex] = blockSettings.dialog_width; dialogOpen[blockIndex] = parseInt(blockSettings.dialog_open); dialogWidth[blockIndex] = parseInt(blockSettings.dialog_width); dialogType[blockIndex] = 'modal'; // Bind the event click handler. calendarOptions.eventClick = eventClick; var calendar = new FullCalendar.Calendar(this, calendarOptions); const calendar = new FullCalendar.Calendar(this, calendarOptions); calendar.render(); // Store a reference to the calendar block instance. Drupal.fullcalendar_block.instances[blockIndex] = { element: this, index: blockIndex, calendar: calendar, calendarOptions: calendarOptions, blockSettings, }; // Trigger an event to let other modules know when a calendar // has been built. $(context).trigger('fullcalendar_block.build', [Drupal.fullcalendar_block.instances[blockIndex]]); }); } once('buildCalendarBlock', 'html', context).forEach( function () { // Build all calendars. buildCalendars(); buildCalendars(context, settings); }) }, detach: function (context, settings, trigger) { if (trigger === 'unload') { $('.fullcalendar-block[data-calendar-block-initialized]', context) .each(function() { // Unload the calendar block instance. const blockIndex = this.getAttribute('data-calendar-block-index'); if (Drupal.fullcalendar_block.instances[blockIndex]) { if (Drupal.fullcalendar_block.instances[blockIndex].calendar) { Drupal.fullcalendar_block.instances[blockIndex].calendar.destroy(); } delete Drupal.fullcalendar_block.instances[blockIndex]; delete settings.fullcalendar_block[blockIndex]; } }); } } } })(jQuery, Drupal, once); Loading
config/schema/fullcalendar_block.schema.yml 0 → 100644 +39 −0 Original line number Diff line number Diff line block.settings.fullcalendar_block: type: block_settings label: 'FullCalendar block' mapping: event_source: type: string label: 'Event source URL' initial_view: type: string label: 'Initial View' header_start: type: string label: 'Start of the header toolbar' header_center: type: string label: 'Center of the header toolbar' header_end: type: string label: 'End of the header toolbar' open_dialog: type: 'fullcalendar_block_dialog_type.[%parent.open_dialog]' # label: 'Open in a new tab/dialog' dialog_width: type: integer label: 'Dialog width' advanced: # Translatable text so that you can easily have a different configuration # per site. # @see config_translation_config_schema_info_alter() type: text label: 'Advanced settings.' # The dialog types. fullcalendar_block_dialog_type.0: type: integer label: 'Open in a new tab' fullcalendar_block_dialog_type.1: type: integer label: 'Open in a dialog'
fullcalendar_block.libraries.yml +5 −1 Original line number Diff line number Diff line fullcalendar: libraries.fullcalendar: remote: https://fullcalendar.io/ version: '5.10.2' license: Loading @@ -11,6 +11,9 @@ fullcalendar: js: /libraries/drupal-fullcalendar/main.min.js: { minified: true } /libraries/drupal-fullcalendar/locales-all.min.js: { minified: true } fullcalendar: js: js/fullcalendar_block.js: {} dependencies: - core/drupal Loading @@ -18,3 +21,4 @@ fullcalendar: - core/once - core/drupalSettings - core/drupal.dialog.ajax - fullcalendar_block/libraries.fullcalendar
fullcalendar_block.module +66 −2 Original line number Diff line number Diff line <?php /** * @file * Contains Fullcalendar block module functions. */ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Site\Settings; /** * Implements hook_theme(). */ function fullcalendar_block_theme($existing, $type, $theme, $path): array { function fullcalendar_block_theme($existing, $type, $theme, $path) { return [ 'fullcalendar_block' => [ 'variables' => [ Loading @@ -12,3 +20,59 @@ function fullcalendar_block_theme($existing, $type, $theme, $path): array { ], ]; } /** * Prepares variables for fullcalendar_block templates. * * Default template: fullcalendar-block.html.twig. * * @param array $variables * An associative array containing: * - block_index: The block index. */ function template_preprocess_fullcalendar_block(array &$variables) { $variables['attributes']['class'][] = 'fullcalendar-block'; $variables['attributes']['data-calendar-block-index'] = $variables['block_index']; } /** * Implements hook_library_info_alter(). */ function fullcalendar_block_library_info_alter(&$libraries, $extension) { if ($extension === 'fullcalendar_block') { /* * Provide an easy way for developers to switch to non-minified assets * during dev by specifying the following in the settings.php: * @code * $settings['fullcalendar_block.dev_mode'] = TRUE; * @endcode */ $dev_mode = Settings::get('fullcalendar_block.dev_mode', FALSE); if (!$dev_mode) { return; } // External libraries to use unminimized versions for. $elements = ['libraries.fullcalendar']; foreach ($elements as $element) { foreach (['css.theme', 'js'] as $component) { $component_path = explode('.', $component); $updated_libraries = []; foreach ((array) NestedArray::getValue($libraries[$element], $component_path, $key_exists) as $asset_key => $asset_config) { if (empty($asset_config['no_min']) && ($new_asset_key = preg_replace('/\.min\.(css|js)$/', '.$1', $asset_key)) !== $asset_key) { // Strip out the "/min/" from the path if it exists. $new_asset_key = str_replace('/min/', '/', $new_asset_key); if (is_file(DRUPAL_ROOT . $new_asset_key)) { // The supposed file exists. Use it. $asset_key = $new_asset_key; $asset_config['minified'] = FALSE; } } $updated_libraries[$asset_key] = $asset_config; } if ($key_exists) { NestedArray::setValue($libraries[$element], $component_path, $updated_libraries); } } } } }
fullcalendar_block.post_update.php 0 → 100644 +34 −0 Original line number Diff line number Diff line <?php /** * @file * Post update hooks for the Fullcalendar Block module. */ use Drupal\block\BlockInterface; use Drupal\Core\Config\Entity\ConfigEntityUpdater; /** * Update the Fullcalendar block configuration schema. */ function fullcalendar_block_post_update_update_block_data(&$sandbox = NULL) { /** @var \Drupal\Core\Config\Entity\ConfigEntityUpdater $config_entity_updater */ $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class); $callback = function (BlockInterface $block) { if ($block->getPluginId() === 'fullcalendar_block') { $settings = $block->get('settings'); if ($settings && !empty($settings['advanced'])) { $settings['advanced'] = trim(str_replace(["\r\n", "\r"], "\n", $settings['advanced'])); $block->set('settings', $settings); } // Resave the fullcalendar block instance. $block->save(); } // Return false since we're updating the block ourselves. return FALSE; }; $config_entity_updater->update($sandbox, 'block', $callback); }
js/fullcalendar_block.js +140 −26 Original line number Diff line number Diff line Loading @@ -6,15 +6,22 @@ // Codes run both on normal page loads and when data is loaded by AJAX (or BigPipe!) // @See https://www.drupal.org/docs/drupal-apis/javascript-api/javascript-api-overview (function ($, Drupal, once) { /** * @namespace */ Drupal.fullcalendar_block = {}; Drupal.fullcalendar_block.instances = {}; Drupal.behaviors.buildCalendarBlock = { attach: function (context, settings) { var dialogWidth = [400]; var dialogType = ['modal']; var dialogOpen = [1]; const dialogWidth = [400]; const dialogType = ['modal']; const dialogOpen = [1]; // Redirect to the URL function gotoURL(url) { let eventURL = new URL(url, location.origin); const eventURL = new URL(url, location.origin); if (eventURL.origin !== "null") { window.open(url, '_blank').focus(); } Loading @@ -23,14 +30,19 @@ // Event click callback function eventClick(info) { info.jsEvent.preventDefault(); let blockIndex = parseInt(this.el.getAttribute("calendar-block-index")); if (info.event.url && dialogOpen[blockIndex] === "1") { let ajaxSettings = { if (!info.event.url) { return; } const blockIndex = this.el.getAttribute('data-calendar-block-index'); if (dialogOpen[blockIndex] === 1) { // Open in a dialog. const ajaxSettings = { url: info.event.url, dialogType: dialogType[blockIndex], dialog: { width: dialogWidth[blockIndex] }, dialog: {width: dialogWidth[blockIndex]} }; let openDialog = Drupal.ajax(ajaxSettings).execute(); const openDialog = Drupal.ajax(ajaxSettings).execute(); openDialog.done(function() { // The modal dialog open successfully. Loading @@ -40,36 +52,138 @@ gotoURL(info.event.url); }); } else if (info.event.url) { else { gotoURL(info.event.url); } } // Function to build calendar objects. function buildCalendars() { $('.fullcalendar-block') function buildCalendars(context, settings) { $('.fullcalendar-block', context) .each(function() { let blockIndex = parseInt(this.getAttribute("calendar-block-index")); let blockSettings = drupalSettings.fullCalendarBlock[blockIndex]; var calendarOptions = JSON.parse(blockSettings.calendar_options); const blockIndex = this.getAttribute('data-calendar-block-index'); this.setAttribute('data-calendar-block-initialized', true); const blockSettings = settings.fullCalendarBlock && settings.fullCalendarBlock[blockIndex]; if (!blockSettings) { console.warn('Could not fetch fullcalendar_block settings', blockIndex, this); return; } const calendarOptions = typeof blockSettings.calendar_options === 'string' ? JSON.parse(blockSettings.calendar_options) : $.extend(true, {}, blockSettings.calendar_options); if (typeof calendarOptions.events === 'string' && Drupal.url.isLocal(calendarOptions.events)) { // Check if there are any GET parameters to send to the internal // Drupal path. This should provide better integration with Views exposed filters. let queryString = window.location.search || ''; if (queryString !== '') { // Remove the question mark and any Drupal path component, // existing fullcalendar search filters if any. const startParam = calendarOptions.startParam || 'start'; let initialDate; const specialParams = [ // Internal Drupal paths. 'q', 'render', // Remove parameters that will be sent by fullcalendar. startParam, calendarOptions.endParam || 'end', calendarOptions.timeZoneParam || 'timeZone', ]; if (typeof URLSearchParams !== 'undefined') { const query = new URLSearchParams(queryString); specialParams.forEach(function(specialParam) { if (specialParam === startParam) { initialDate = query.get(startParam); } query.delete(specialParam); }); queryString = query.toString(); } else { // Fallback to strip out query strings. // https://stackoverflow.com/a/1634841 const params = queryString.slice(1).split(/[&;]/g); // Reverse iteration as may be destructive. specialParams.forEach(function(specialParam) { const prefix = encodeURIComponent(specialParam); let i; for (i = params.length; i-- > 0;) { // Idiom for string.startsWith. if (params[i] === prefix || (params[i].lastIndexOf(prefix + '=', 0) !== -1)) { initialDate = params[i].split('=')[1]; if (initialDate) { initialDate = decodeURIComponent(initialDate); } params.splice(i, 1); } } }); queryString = params.join('&'); } // Provide a default initial date if it's specified in the query string. if (!calendarOptions.initialDate && initialDate) { const date = new Date(initialDate); if (date instanceof Date && !isNaN(date)) { // Valid date string. calendarOptions.initialDate = initialDate; } } if (queryString !== '') { // If there is a '?' in events URL, & should be used to add // parameters. queryString = (/\?/.test(calendarOptions.events) ? '&' : '?') + queryString; // Add the current query string to the query. // Useful for taking in exposed filters. calendarOptions.events = calendarOptions.events + queryString; } } } dialogOpen[blockIndex] = blockSettings.dialog_open; dialogWidth[blockIndex] = blockSettings.dialog_width; dialogOpen[blockIndex] = parseInt(blockSettings.dialog_open); dialogWidth[blockIndex] = parseInt(blockSettings.dialog_width); dialogType[blockIndex] = 'modal'; // Bind the event click handler. calendarOptions.eventClick = eventClick; var calendar = new FullCalendar.Calendar(this, calendarOptions); const calendar = new FullCalendar.Calendar(this, calendarOptions); calendar.render(); // Store a reference to the calendar block instance. Drupal.fullcalendar_block.instances[blockIndex] = { element: this, index: blockIndex, calendar: calendar, calendarOptions: calendarOptions, blockSettings, }; // Trigger an event to let other modules know when a calendar // has been built. $(context).trigger('fullcalendar_block.build', [Drupal.fullcalendar_block.instances[blockIndex]]); }); } once('buildCalendarBlock', 'html', context).forEach( function () { // Build all calendars. buildCalendars(); buildCalendars(context, settings); }) }, detach: function (context, settings, trigger) { if (trigger === 'unload') { $('.fullcalendar-block[data-calendar-block-initialized]', context) .each(function() { // Unload the calendar block instance. const blockIndex = this.getAttribute('data-calendar-block-index'); if (Drupal.fullcalendar_block.instances[blockIndex]) { if (Drupal.fullcalendar_block.instances[blockIndex].calendar) { Drupal.fullcalendar_block.instances[blockIndex].calendar.destroy(); } delete Drupal.fullcalendar_block.instances[blockIndex]; delete settings.fullcalendar_block[blockIndex]; } }); } } } })(jQuery, Drupal, once);