dynamic_image_generator-1.0.x-dev/dynamic_image_generator.module
dynamic_image_generator.module
<?php
/**
* @file
* Contains dynamic_image_generator.module.
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Get available content types for the content_type field options.
*
* @return array
* An array of content type labels keyed by machine name.
*/
function dynamic_image_generator_get_content_types() {
$options = [];
$content_types = \Drupal::entityTypeManager()->getStorage('node_type')->loadMultiple();
foreach ($content_types as $content_type) {
$options[$content_type->id()] = $content_type->label();
}
return $options;
}
/**
* Get available image and media fields for the selected content type.
*
* @param string $content_type
* The content type machine name.
*
* @return array
* An array of field labels keyed by field name.
*/
function dynamic_image_generator_get_target_fields($content_type) {
$options = [];
if (empty($content_type)) {
return $options;
}
// Get all field definitions for the content type
$field_definitions = \Drupal::service('entity_field.manager')
->getFieldDefinitions('node', $content_type);
// Filter for image and media fields
foreach ($field_definitions as $field_name => $field_definition) {
if ($field_definition->getFieldStorageDefinition()->isBaseField()) {
continue; // Skip base fields
}
$field_type = $field_definition->getType();
$field_label = $field_definition->getLabel();
// Include image fields and media reference fields
if ($field_type === 'image') {
$options[$field_name] = t('@label (Image field)', ['@label' => $field_label]);
}
elseif ($field_type === 'entity_reference' &&
$field_definition->getSetting('handler') === 'default:media') {
$options[$field_name] = t('@label (Media field)', ['@label' => $field_label]);
}
}
return $options;
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for node forms.
* Shows auto-generation options for image templates.
*/
function dynamic_image_generator_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Get the node from the form
$node = $form_state->getFormObject()->getEntity();
// Skip if this is not a node form
if (!$node || $node->getEntityTypeId() !== 'node') {
return;
}
// Get the node's content type
$content_type = $node->bundle();
// Find all active poster templates for this content type
$poster_storage = \Drupal::entityTypeManager()->getStorage('poster_entity');
$query = $poster_storage->getQuery()
->condition('content_type', $content_type)
->condition('status', 1)
->accessCheck(FALSE);
$poster_ids = $query->execute();
if (empty($poster_ids)) {
return;
}
// Load poster entities and filter for those with target fields that exist on this node
$poster_entities = $poster_storage->loadMultiple($poster_ids);
$qualifying_posters = [];
foreach ($poster_entities as $poster) {
$target_field = $poster->getTargetField();
// Only include posters that have a target field AND the field exists on this node
if (!empty($target_field) && $node->hasField($target_field)) {
$qualifying_posters[$poster->id()] = $poster;
}
}
if (empty($qualifying_posters)) {
return;
}
// Add form elements
if (!isset($form['advanced'])) {
$form['advanced'] = [
'#type' => 'vertical_tabs',
'#weight' => 99,
];
}
// Add dynamic image settings tab
$form['dynamic_image_settings'] = [
'#type' => 'details',
'#title' => t('Dynamic Image Generation'),
'#group' => 'advanced',
'#weight' => 100,
'#open' => FALSE,
'#description' => t('Configure automatic dynamic image generation for this content.'),
'#attached' => [
'library' => [
'core/drupal.dialog.ajax',
'core/drupal.ajax',
],
],
];
// Add checkbox and preview button for each qualifying poster
foreach ($qualifying_posters as $poster_id => $poster) {
$checkbox_key = 'poster_auto_generate_' . $poster_id;
$target_field = $poster->getTargetField();
// Get field label for better UX
$field_definition = $node->getFieldDefinition($target_field);
$field_label = $field_definition ? $field_definition->getLabel() : $target_field;
// Create a container for each template
$form['dynamic_image_settings']['template_' . $poster_id] = [
'#type' => 'container',
'#attributes' => [
'class' => ['dynamic-image-template-container'],
'style' => 'margin-bottom: 1.5rem; border-bottom: 1px solid #ddd;',
],
];
// Template title
$form['dynamic_image_settings']['template_' . $poster_id]['title'] = [
'#markup' => '<div class="form-item__label">' . $poster->label() . '</div>',
'#weight' => -10,
];
// Auto-generate checkbox
$form['dynamic_image_settings']['template_' . $poster_id][$checkbox_key] = [
'#type' => 'checkbox',
'#title' => t('Auto-generate Image', [
'@field' => $field_label,
]),
'#description' => t('Generate this image automatically when content is saved. The generated image will be saved to the <strong>@field</strong> field.', [
'@field' => $field_label,
]),
'#default_value' => FALSE,
'#weight' => 0,
];
// Preview button with real-time data
$preview_url = Url::fromRoute('dynamic_image_generator.preview_with_data', [
'poster_entity' => $poster_id,
'node' => $node->id() ?: 'new',
]);
/*$form['dynamic_image_settings']['template_' . $poster_id]['preview_button'] = [
'#type' => 'button',
'#value' => t('Preview with This Content'),
'#attributes' => [
'class' => ['button', 'button--small', 'node-content-preview-trigger'],
'style' => 'margin-left: 10px; background: #28a745; color: white; text-decoration: none; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;',
'data-node-id' => $node->id() ?: 'new',
'data-poster-id' => $poster_id,
'type' => 'button',
],
'#weight' => 5,
];*/
}
// Add form data extraction JavaScript for preview with node data
$form['#attached']['library'][] = 'core/drupal.ajax';
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
$form['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => '
(function($, Drupal) {
"use strict";
Drupal.behaviors.nodeContentPreview = {
attach: function(context, settings) {
$(".node-content-preview-trigger", context).once("node-content-preview").on("click", function(e) {
e.preventDefault();
e.stopPropagation();
var $button = $(this);
var nodeId = $button.attr("data-node-id");
var posterId = $button.attr("data-poster-id");
// Prevent double-clicking
if ($button.data("processing")) {
return false;
}
$button.data("processing", true);
$button.prop("disabled", true).text("Generating...");
// Show loader
var $loader = $("<div class=\"node-preview-loader\" style=\"position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;\">" +
"<div style=\"background: white; padding: 30px; border-radius: 8px; text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\">" +
"<div style=\"width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #28a745; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 15px;\"></div>" +
"<p style=\"margin: 0; font-weight: bold; color: #333;\">Generating Preview...</p>" +
"<p style=\"margin: 5px 0 0; font-size: 0.9em; color: #666;\">Using this content data</p>" +
"</div>" +
"</div>");
// Add spinner CSS if not exists
if (!$("#node-preview-spinner-css").length) {
$("<style id=\"node-preview-spinner-css\">@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>").appendTo("head");
}
$("body").append($loader);
// Build form data with current node form values
var formData = {};
// Get current form field values
$("#node-form input, #node-form textarea, #node-form select").each(function() {
var $field = $(this);
var name = $field.attr("name");
var value = $field.val();
if (name && value) {
if ($field.attr("type") === "checkbox" || $field.attr("type") === "radio") {
if ($field.is(":checked")) {
formData[name] = value;
}
} else {
formData[name] = value;
}
}
});
// Add node ID and preview mode
formData.current_node_id = nodeId;
formData.preview_mode = "node_form";
formData.trigger_type = "node_form_preview";
// Function to reset button state
function resetButton() {
$button.removeData("processing").prop("disabled", false).text("Preview with This Content");
}
// Make AJAX request to preview endpoint
var previewUrl = "/admin/dynamic-image-generator/" + posterId + "/preview-with-data/" + nodeId;
$.ajax({
url: previewUrl,
type: "POST",
data: {
form_data: JSON.stringify(formData)
},
timeout: 30000,
success: function(response) {
$loader.remove();
resetButton();
if (response && response.length > 0) {
for (var i = 0; i < response.length; i++) {
var command = response[i];
if (command.command === "openModalDialog" || command.command === "openDialog") {
var title = "Preview: " + ($button.closest(".dynamic-image-template-container").find("h4").text() || "Template");
if (!command.data || command.data.trim() === "") {
alert("Preview generated but no content received.");
return;
}
// Create simple modal
var modalId = "node-preview-modal-" + Date.now();
var $modal = $("<div id=\"" + modalId + "\" style=\"position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; align-items: center; justify-content: center;\">" +
"<div style=\"background: white; max-width: 90%; max-height: 90%; overflow: auto; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); position: relative;\">" +
"<div style=\"padding: 20px; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; background: #f8f9fa;\">" +
"<h3 style=\"margin: 0; color: #2c3e50;\">" + title + "</h3>" +
"<button id=\"close-" + modalId + "\" style=\"background: #dc3545; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 16px;\">×</button>" +
"</div>" +
"<div style=\"padding: 20px; min-width: 800px; min-height: 500px;\">" + command.data + "</div>" +
"</div>" +
"</div>");
$("body").append($modal);
// Close functionality
$("#close-" + modalId + ", #" + modalId).on("click", function(e) {
if (e.target === this) {
$modal.remove();
$("body").css("overflow", "auto");
}
});
$modal.find("> div").on("click", function(e) {
e.stopPropagation();
});
break;
}
}
} else {
alert("No preview data received. Please check the template configuration.");
}
},
error: function(xhr, status, error) {
$loader.remove();
resetButton();
console.error("Preview request failed:", error, xhr.responseText);
alert("Preview request failed: " + error);
}
});
return false;
});
}
};
})(jQuery, Drupal);',
],
'node_content_preview_script',
];
// Add submit handler
$form['actions']['submit']['#submit'][] = 'dynamic_image_generator_node_form_submit';
// Store qualifying poster info for the submit handler
$form_state->set('qualifying_posters', array_keys($qualifying_posters));
}
/**
* Submit handler for auto-generation when saving content.
*/
function dynamic_image_generator_node_form_submit($form, FormStateInterface $form_state) {
$node = $form_state->getFormObject()->getEntity();
$node_id = $node->id();
if (!$node_id) {
return;
}
// Get the qualifying posters from form state
$qualifying_poster_ids = $form_state->get('qualifying_posters') ?: [];
if (empty($qualifying_poster_ids)) {
return;
}
$poster_storage = \Drupal::entityTypeManager()->getStorage('poster_entity');
$generated_count = 0;
$failed_count = 0;
foreach ($qualifying_poster_ids as $poster_id) {
$checkbox_key = 'poster_auto_generate_' . $poster_id;
$auto_generate = $form_state->getValue($checkbox_key);
if (!$auto_generate) {
continue; // User didn't check this poster's checkbox
}
$poster = $poster_storage->load($poster_id);
if (!$poster) {
continue;
}
try {
// Check if service exists before using it
if (!\Drupal::hasService('dynamic_image_generator.dynamic_image_generator_service')) {
\Drupal::logger('dynamic_image_generator')->error('Dynamic Image Generator service not available');
$failed_count++;
continue;
}
// Check if we have the required media types before proceeding
$media_type_storage = \Drupal::entityTypeManager()->getStorage('media_type');
$available_types = $media_type_storage->loadMultiple();
if (empty($available_types)) {
\Drupal::logger('dynamic_image_generator')->error('No media types available for image generation');
$failed_count++;
continue;
}
$image_generator = \Drupal::service('dynamic_image_generator.dynamic_image_generator_service');
$result = $image_generator->generatePosterImage($poster_id, [], $node_id);
if ($result) {
$generated_count++;
\Drupal::logger('dynamic_image_generator')->notice('Auto-generated image for template @poster_id "@title" for node @node_id', [
'@poster_id' => $poster_id,
'@title' => $poster->label(),
'@node_id' => $node_id,
]);
} else {
$failed_count++;
\Drupal::logger('dynamic_image_generator')->error('Failed to auto-generate image for template @poster_id for node @node_id', [
'@poster_id' => $poster_id,
'@node_id' => $node_id,
]);
}
} catch (\Exception $e) {
$failed_count++;
\Drupal::logger('dynamic_image_generator')->error('Exception during auto-generation of template @poster_id for node @node_id: @error', [
'@poster_id' => $poster_id,
'@node_id' => $node_id,
'@error' => $e->getMessage(),
]);
}
}
// Provide user feedback
if ($generated_count > 0) {
if ($generated_count === 1) {
\Drupal::messenger()->addMessage(t('Image was automatically generated and saved successfully!'), 'status');
} else {
\Drupal::messenger()->addMessage(t('@count images were automatically generated and saved successfully!', [
'@count' => $generated_count,
]), 'status');
}
}
if ($failed_count > 0) {
\Drupal::messenger()->addError(t('@count image(s) failed to generate. Please check the logs for details.', [
'@count' => $failed_count,
]));
}
}
/**
* Implements hook_theme().
*/
function dynamic_image_generator_theme() {
return [
'poster_entity' => [
'render element' => 'elements',
],
];
}
/**
* Prepares variables for poster entity templates.
*
* Default template: poster-entity.html.twig.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the poster entity information and any
* fields attached to the entity.
* - attributes: HTML attributes for the containing element.
*/
function template_preprocess_poster_entity(array &$variables) {
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
/**
* Implements hook_form_FORM_ID_alter() for image template forms.
*/
function dynamic_image_generator_form_poster_entity_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Get the entity from the form
$entity = $form_state->getFormObject()->getEntity();
// First, let's improve how we get the selected content type
$selected_content_type = NULL;
// Check form_state first (for AJAX updates)
if ($form_state->hasValue('content_type')) {
$content_type_values = $form_state->getValue('content_type');
if (is_array($content_type_values) && isset($content_type_values[0]['value'])) {
$selected_content_type = $content_type_values[0]['value'];
}
}
// If not in form_state, try to get from entity
elseif (method_exists($entity, 'getContentType') && $entity->getContentType()) {
$selected_content_type = $entity->getContentType();
}
// Add AJAX callback to content_type field
$form['content_type']['widget']['#ajax'] = [
'callback' => 'dynamic_image_generator_content_type_ajax_callback',
'wrapper' => 'image-template-form',
'event' => 'change',
'progress' => [
'type' => 'throbber',
'message' => t('Loading...'),
],
];
// Add an ID to the form so it can be targeted by AJAX
$form['#prefix'] = '<div id="image-template-form">';
$form['#suffix'] = '</div>';
// Add a new select element for user interface
$form['target_field_select'] = [
'#type' => 'select',
'#title' => t('Target Field'),
'#description' => t('The field in the content type where auto-generation options will appear.'),
'#options' => ['' => t('- Select a field -')],
'#weight' => -3.5,
'#attributes' => ['id' => 'edit-target-field-select'],
];
// Update options based on selected content type
if ($selected_content_type) {
$target_field_options = dynamic_image_generator_get_target_fields($selected_content_type);
$form['target_field_select']['#options'] = ['' => t('- None -')] + $target_field_options;
// Update description
$form['target_field_select']['#description'] = t('Select which field in @type content will show auto-generation options to users.', [
'@type' => $selected_content_type,
]);
// Get the current target field value
$current_target_field = '';
if (method_exists($entity, 'getTargetField') && $entity->getTargetField()) {
$current_target_field = $entity->getTargetField();
}
// Set default value for the select if there's a value in the entity
if (!empty($current_target_field)) {
$form['target_field_select']['#default_value'] = $current_target_field;
}
}
// Hide the original target_field and update it with JavaScript
if (isset($form['target_field'])) {
$form['target_field']['#access'] = FALSE;
// Add JavaScript to sync the fields
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
$form['#attached']['library'][] = 'core/jquery.form';
// Add inline JavaScript to sync the selection
$form['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => 'document.addEventListener("DOMContentLoaded", function() {
(function ($) {
$("#edit-target-field-select").on("change", function() {
$("input[name=\'target_field[0][value]\']").val($(this).val());
});
// Initialize on page load
$("input[name=\'target_field[0][value]\']").val($("#edit-target-field-select").val());
})(jQuery);
});',
],
'sync_target_field_script',
];
$form['#submit'][] = 'dynamic_image_generator_target_field_submit';
}
// Add token browser container
$form['token_browser'] = [
'#type' => 'details',
'#title' => t('Available Tokens'),
'#open' => TRUE,
'#weight' => 5,
];
// If we have a selected content type, show token information
if ($selected_content_type) {
// Make sure token module is enabled
if (\Drupal::moduleHandler()->moduleExists('token')) {
// Get all available fields for the selected content type
$entity_fields = [];
$field_definitions = \Drupal::service('entity_field.manager')
->getFieldDefinitions('node', $selected_content_type);
foreach ($field_definitions as $field_name => $field_definition) {
$field_type = $field_definition->getType();
$entity_fields[] = [
'name' => $field_name,
'type' => $field_type,
'label' => $field_definition->getLabel(),
];
}
// Add token browser with comprehensive token information
$form['token_browser']['token_help'] = [
'#type' => 'markup',
'#markup' => '<h4>' . t('Available Tokens for @type', ['@type' => $selected_content_type]) . '</h4><p>' .
t('Use tokens like <code>[node:title]</code>, <code>[node:field_image]</code>, etc. in both HTML and CSS templates.') . '</p>' .
'<p>' . t('You can also use Twig syntax for conditionals, loops, etc. in both templates.') . '</p>',
];
// Group fields by type for better organization
$grouped_fields = [];
foreach ($entity_fields as $field) {
$type = $field['type'];
if (!isset($grouped_fields[$type])) {
$grouped_fields[$type] = [];
}
$grouped_fields[$type][] = $field;
}
// Display fields grouped by type
$markup = '';
foreach ($grouped_fields as $type => $fields) {
$markup .= '<h5>' . t('@type Fields', ['@type' => ucfirst($type)]) . '</h5><ul class="token-field-list">';
foreach ($fields as $field) {
$markup .= '<li><code>[node:' . $field['name'] . ']</code> - ' . $field['label'] . '</li>';
}
$markup .= '</ul>';
}
$form['token_browser']['field_list'] = [
'#type' => 'markup',
'#markup' => $markup,
];
// Use token module's token tree browser
$form['token_browser']['browser'] = [
'#type' => 'token_tree_link',
'#token_types' => ['node'],
'#global_types' => TRUE,
'#click_insert' => TRUE,
'#dialog' => TRUE,
'#show_restricted' => FALSE,
'#recursion_limit' => 3,
'#weight' => 10,
];
// Add common token usage examples
$form['token_browser']['examples'] = [
'#type' => 'markup',
'#markup' => '<h5>' . t('Common Token Examples') . '</h5>' .
'<ul>' .
'<li><code>[node:title]</code> - ' . t('The title of the content') . '</li>' .
'<li><code>[node:body]</code> - ' . t('The body content') . '</li>' .
'<li><code>[node:field_image]</code> - ' . t('An image field (if available)') . '</li>' .
'<li><code>[node:created:medium]</code> - ' . t('The creation date in medium format') . '</li>' .
'<li><code>[node:author:name]</code> - ' . t("The content author's name") . '</li>' .
'<li><code>[site:name]</code> - ' . t('The site name') . '</li>' .
'</ul>',
'#weight' => 30,
];
}
else {
$form['token_browser']['message'] = [
'#type' => 'markup',
'#markup' => '<p>' . t('Install the Token module to browse available tokens.') . '</p>',
];
}
}
else {
$form['token_browser']['message'] = [
'#type' => 'markup',
'#markup' => '<p>' . t('Select a content type to see available tokens.') . '</p>',
];
}
// Update descriptions for HTML and CSS fields to mention tokens and Twig
if (isset($form['html']['widget'][0]['value'])) {
$form['html']['widget'][0]['value']['#description'] = t('HTML content for the image. Use tokens like [node:title], [node:field_image], etc. for content. Use [image_1], [image_2], etc. to reference uploaded images. Supports Twig syntax for conditionals, loops, etc.');
}
if (isset($form['css']['widget'][0]['value'])) {
$form['css']['widget'][0]['value']['#description'] = t('CSS styles for the image. You can use tokens like [node:title] and [image_1], [image_2], etc. tokens in your CSS. Supports Twig syntax for conditionals, loops, etc.');
}
// Add validation handler
$form['#validate'][] = 'dynamic_image_generator_image_template_validate';
// Add required libraries
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
// Add preview functionality to the template form
$form['preview_section'] = [
'#type' => 'details',
'#title' => t('Template Preview & Testing'),
'#open' => FALSE,
'#weight' => 10,
];
$form['preview_section']['preview_info'] = [
'#markup' => '<p>' . t('Test your template with different data sources to see how it will look.') . '</p>' .
'<div style="background: #e7f3ff; padding: 10px; border-radius: 4px; margin-bottom: 15px; border-left: 4px solid #0066cc;">' .
'<strong>Live Preview:</strong> Uses your current HTML/CSS from the form fields above (not saved data). ' .
'Make changes to the template and preview them instantly without saving.' .
'</div>',
];
// Add preview button for testing the template
if (!$entity->isNew()) {
// Sample data preview button
$preview_url = \Drupal\Core\Url::fromRoute('dynamic_image_generator.generate_preview', [
'poster_entity' => $entity->id(),
]);
// Add node selection for real content preview
if ($selected_content_type) {
$form['preview_section']['node_selection'] = [
'#type' => 'fieldset',
'#title' => t('Test with Real Content'),
'#attributes' => [
'style' => 'margin-top: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;',
],
];
// Node selection autocomplete
$form['preview_section']['node_selection']['selected_node'] = [
'#type' => 'entity_autocomplete',
'#title' => t('Select @type Content', ['@type' => $selected_content_type]),
'#target_type' => 'node',
'#selection_settings' => [
'target_bundles' => [$selected_content_type],
'sort' => [
'field' => 'created',
'direction' => 'DESC',
],
],
'#description' => t('Start typing to search for @type content to use for preview.', ['@type' => $selected_content_type]),
'#attributes' => [
'id' => 'edit-selected-node',
],
'#ajax' => [
'callback' => 'dynamic_image_generator_node_selection_ajax_callback',
'wrapper' => 'node-preview-buttons',
'event' => 'autocompleteclose',
],
];
// Container for preview buttons
$form['preview_section']['node_selection']['preview_buttons'] = [
'#type' => 'container',
'#attributes' => ['id' => 'node-preview-buttons'],
];
// Get selected node ID from form state or default
$selected_node_id = NULL;
if ($form_state->hasValue(['preview_section', 'node_selection', 'selected_node'])) {
$selected_node_id = $form_state->getValue(['preview_section', 'node_selection', 'selected_node']);
}
if ($selected_node_id) {
// Remove the old preview button - only keep live preview
$form['preview_section']['node_selection']['preview_buttons']['instruction'] = [
'#markup' => '<p style="margin-top: 10px; color: #28a745; font-weight: bold;">' .
t('✓ Content selected. Use the Live Preview button below to test.') . '</p>',
];
} else {
$form['preview_section']['node_selection']['preview_buttons']['instruction'] = [
'#markup' => '<p style="margin-top: 10px; color: #666; font-style: italic;">' .
t('Select content above to enable live preview.') . '</p>',
];
}
// Live preview button with current form data (initially hidden)
$form['preview_section']['live_preview_container'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'live-preview-container',
'style' => 'margin-top: 20px; padding: 15px; border: 1px solid #17a2b8; border-radius: 4px; background: #e7f6fd; display: none;',
],
];
$form['preview_section']['live_preview_container']['info'] = [
'#markup' => '<p style="margin: 0 0 10px 0; color: #0c5460;"><strong>Live Template Testing:</strong> Preview your current HTML/CSS changes with the selected content. <em>Uses form field values, not saved template data.</em></p>',
];
$form['preview_section']['live_preview_container']['live_preview_button'] = [
'#type' => 'link',
'#title' => t('Live Preview with Selected Content'),
'#url' => \Drupal\Core\Url::fromRoute('dynamic_image_generator.preview_with_data', [
'poster_entity' => $entity->id(),
'node' => 'new',
]),
'#attributes' => [
'class' => ['button', 'live-preview-trigger', 'use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode([
'width' => 900,
'height' => 700,
]),
'style' => 'background: #17a2b8; color: white; border-color: #17a2b8;',
],
];
// Add JavaScript for quick node selection and live preview visibility - FIXED VERSION
$form['#attached']['library'][] = 'dynamic_image_generator/node_selection';
// Add the JavaScript as an external file to avoid encoding issues
$form['#attached']['drupalSettings']['dynamicImageGenerator'] = [
'nodeSelectionEnabled' => TRUE,
'containerId' => 'live-preview-container',
'fieldId' => 'edit-selected-node',
];
}
else {
$form['preview_section']['content_type_required'] = [
'#markup' => '<p style="margin-top: 15px; color: #e74c3c; font-style: italic;">' .
t('Select a content type above to enable preview with real content.') . '</p>',
];
}
} else {
$form['preview_section']['save_first'] = [
'#markup' => '<p><em>' . t('Save the template first to enable preview functionality.') . '</em></p>',
];
}
}
/**
* AJAX callback for content_type field.
*/
function dynamic_image_generator_content_type_ajax_callback(array &$form, FormStateInterface $form_state) {
// Simply return the entire form
return $form;
}
/**
* AJAX callback for node selection.
*/
function dynamic_image_generator_node_selection_ajax_callback(array &$form, FormStateInterface $form_state) {
return $form['preview_section']['node_selection']['preview_buttons'];
}
/**
* Validation handler for image template forms.
*/
function dynamic_image_generator_image_template_validate($form, FormStateInterface $form_state) {
// Get the content type
$content_type_values = $form_state->getValue('content_type');
$content_type = NULL;
if (is_array($content_type_values) && isset($content_type_values[0]['value'])) {
$content_type = $content_type_values[0]['value'];
}
// Get the target field - now take it from target_field_select
$target_field = $form_state->getValue('target_field_select');
// Get the target field value from the hidden field if select is empty
if (empty($target_field)) {
$target_field_values = $form_state->getValue('target_field');
if (is_array($target_field_values) && isset($target_field_values[0]['value'])) {
$target_field = $target_field_values[0]['value'];
}
// If we got a value from the hidden field, update the select field value
if (!empty($target_field)) {
$form_state->setValue('target_field_select', $target_field);
}
} else {
// Ensure the target_field value matches the select
$form_state->setValue(['target_field', 0, 'value'], $target_field);
}
// Only validate target field if it's not empty
if (!empty($target_field) && !empty($content_type)) {
// Validate that the target field is a valid option for the content type
$valid_fields = dynamic_image_generator_get_target_fields($content_type);
if (!isset($valid_fields[$target_field])) {
$form_state->setErrorByName('target_field_select', t('The selected target field "@field" is not valid for the content type "@type".', [
'@field' => $target_field,
'@type' => $content_type,
]));
}
}
}
/**
* Submit handler to ensure target field value is saved correctly.
*/
function dynamic_image_generator_target_field_submit($form, FormStateInterface $form_state) {
// Get the selected target field from the select element
$target_field = $form_state->getValue('target_field_select');
// Update the actual target_field value
if (!empty($target_field)) {
$form_state->setValue(['target_field', 0, 'value'], $target_field);
}
}
