From f375c625236f573b04c925b8f413ec0184edc734 Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:21:38 -0700 Subject: [PATCH 01/11] Fix `Update Preview` and `Add Another Item` Reorder Issue --- .../Field/FieldWidget/AZRankingWidget.php | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index 0272153b30..58f44242bc 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -118,14 +118,27 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $widget_state = static::getWidgetState($field_parents, $field_name, $form_state); $status = (isset($widget_state['open_status'][$delta])) ? $widget_state['open_status'][$delta] : FALSE; - // We may have had a deleted row. This shouldn't be necessary to check, but - // The experimental paragraphs widget extracts values before the submit - // handler. - if (isset($widget_state['original_deltas'][$delta]) && ($widget_state['original_deltas'][$delta] !== $delta)) { - $delta = $widget_state['original_deltas'][$delta]; + // Remap item data when rebuilding to keep previews aligned with fields. + if ($form_state->isRebuilding() && !$item->isEmpty()) { + $trigger = $form_state->getTriggeringElement(); + $is_remove = str_contains($trigger['#name'] ?? '', '_remove_button'); + if ($is_remove) { + // For Remove, use the original delta remapping (reassign $delta). + if (isset($widget_state['original_deltas'][$delta]) && ($widget_state['original_deltas'][$delta] !== $delta)) { + $delta = $widget_state['original_deltas'][$delta]; + } + } + else { + // For Update Preview / Add Another Item, use reverse delta mapping. + $reverse_deltas = !empty($widget_state['original_deltas']) + ? array_flip($widget_state['original_deltas']) : []; + if (isset($reverse_deltas[$delta])) { + $item = $items[$reverse_deltas[$delta]]; + } + } } - // New field values shouldn't be considered collapsed. + // New field values shouldn't be collapsed. if ($item->isEmpty()) { $status = TRUE; } From 60af3ea504a6c767233f3a5fc9d3590a2ed582a7 Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:58:09 -0700 Subject: [PATCH 02/11] PHPStan Fix --- .../az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index 58f44242bc..95f984da74 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -109,9 +109,9 @@ public function form(FieldItemListInterface $items, array &$form, FormStateInter */ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - /** @var \Drupal\az_ranking\Plugin\Field\FieldType\AZRankingItem $item */ $item = $items[$delta]; + /** @var \Drupal\az_ranking\Plugin\Field\FieldType\AZRankingItem $item */ // Get current collapse status. $field_name = $this->fieldDefinition->getName(); $field_parents = $element['#field_parents']; From b958227ea1b96e0494161dd5f28b51b6ae58f5fa Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Fri, 13 Feb 2026 08:14:59 -0700 Subject: [PATCH 03/11] PHPStan fix attempt 2 --- .../src/Plugin/Field/FieldWidget/AZRankingWidget.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index 95f984da74..8de9faf214 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -111,7 +111,6 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $item = $items[$delta]; - /** @var \Drupal\az_ranking\Plugin\Field\FieldType\AZRankingItem $item */ // Get current collapse status. $field_name = $this->fieldDefinition->getName(); $field_parents = $element['#field_parents']; @@ -138,6 +137,8 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen } } + /** @var \Drupal\az_ranking\Plugin\Field\FieldType\AZRankingItem $item */ + // New field values shouldn't be collapsed. if ($item->isEmpty()) { $status = TRUE; From 5dcbd82d1f09efb7c58f133ed46e7ce71e15976c Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Fri, 13 Feb 2026 08:26:24 -0700 Subject: [PATCH 04/11] PHPStan fix attempt 3 --- .../az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index 8de9faf214..e6d4a088c6 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -109,6 +109,7 @@ public function form(FieldItemListInterface $items, array &$form, FormStateInter */ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + /** @var \Drupal\az_ranking\Plugin\Field\FieldType\AZRankingItem $item */ $item = $items[$delta]; // Get current collapse status. From 8aec913a728169a4528ba89f74266e4e5077ee1e Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:54:40 -0700 Subject: [PATCH 05/11] Use #after_build callback to build previews The #after_build callback builds previews from Form API-populated values (which are always in the correct order after re-ordering). Our method before was messy and unreliable: trying to use widget_state and deltas to guess where the preview should go. --- .../Field/FieldWidget/AZRankingWidget.php | 492 ++++++++---------- 1 file changed, 230 insertions(+), 262 deletions(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index e6d4a088c6..93217210b4 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -118,79 +118,18 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $widget_state = static::getWidgetState($field_parents, $field_name, $form_state); $status = (isset($widget_state['open_status'][$delta])) ? $widget_state['open_status'][$delta] : FALSE; - // Remap item data when rebuilding to keep previews aligned with fields. - if ($form_state->isRebuilding() && !$item->isEmpty()) { - $trigger = $form_state->getTriggeringElement(); - $is_remove = str_contains($trigger['#name'] ?? '', '_remove_button'); - if ($is_remove) { - // For Remove, use the original delta remapping (reassign $delta). - if (isset($widget_state['original_deltas'][$delta]) && ($widget_state['original_deltas'][$delta] !== $delta)) { - $delta = $widget_state['original_deltas'][$delta]; - } - } - else { - // For Update Preview / Add Another Item, use reverse delta mapping. - $reverse_deltas = !empty($widget_state['original_deltas']) - ? array_flip($widget_state['original_deltas']) : []; - if (isset($reverse_deltas[$delta])) { - $item = $items[$reverse_deltas[$delta]]; - } - } - } - - /** @var \Drupal\az_ranking\Plugin\Field\FieldType\AZRankingItem $item */ - // New field values shouldn't be collapsed. if ($item->isEmpty()) { $status = TRUE; } - // Determine current ranking style for preview. - $ranking_classes = 'ranking card'; + // Gather parent paragraph config for preview building in #after_build. $parent = $item->getEntity(); - - // Get settings from parent paragraph. - if ($parent instanceof ParagraphInterface) { - // Get the behavior settings for the parent. - $parent_config = $parent->getAllBehaviorSettings(); - - // See if the parent behavior defines some ranking-specific settings. - if (!empty($parent_config['az_rankings_paragraph_behavior'])) { - $ranking_defaults = $parent_config['az_rankings_paragraph_behavior']; - $ranking_classes = $ranking_defaults['ranking_hover_style'] ?? 'ranking card'; - } - } - - // Add overflow-hidden class. - $ranking_classes .= ' overflow-hidden'; - - // Handle hover effect and background classes like the formatter does. - $ranking_hover_effect = FALSE; + $ranking_parent_config = []; if ($parent instanceof ParagraphInterface) { $parent_config = $parent->getAllBehaviorSettings(); if (!empty($parent_config['az_rankings_paragraph_behavior'])) { - $ranking_hover_effect = $parent_config['az_rankings_paragraph_behavior']['ranking_hover_effect'] ?? FALSE; - } - } - - // Hover effect takes precedence over non-hover-effect backgrounds. - if ($ranking_hover_effect) { - // Try to read hover-specific value from the item. - $hover_class = ''; - if (!empty($item->options['hover_class'])) { - $hover_class = $item->options['hover_class']; - } - // Fallback to persisted background class if no hover-specific value. - if (empty($hover_class) && !empty($item->options['class'])) { - $hover_class = $item->options['class']; - } - if (!empty($hover_class) && $item->options['ranking_type'] !== 'image_only') { - $ranking_classes .= ' from-hover-effect ' . $hover_class; - } - } - else { - if (!empty($item->options['class']) && $item->options['ranking_type'] !== 'image_only') { - $ranking_classes .= ' non-hover-effect ' . $item->options['class']; + $ranking_parent_config = $parent_config['az_rankings_paragraph_behavior']; } } @@ -203,7 +142,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#attributes' => ['class' => ['az-ranking-widget']], ]; - // When closed, show a preview of the ranking. + // When closed, add a preview wrapper. if (!$status) { $element['preview_wrapper'] = [ '#type' => 'container', @@ -215,10 +154,15 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#weight' => -10, ]; - // Build the preview using the helper method. - $element['preview_wrapper']['preview'] = $this->buildRankingPreview($item, $ranking_classes); + // Empty preview placeholder — afterBuildRebuildPreview will populate. + $element['preview_wrapper']['preview'] = [ + '#theme' => 'az_ranking', + ]; } + // Store parent config on the element for the #after_build callback. + $element['#ranking_parent_config'] = $ranking_parent_config; + // Create a globally unique ID that includes // parent entity info and field parents. $parent_entity = $item->getEntity(); @@ -468,6 +412,225 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $element['#delta'] = $delta; $element['#field_name'] = $field_name; + // Rebuild the preview in #after_build so it uses the form API-populated + // field values (which reflect drag-and-drop reorder) instead of $items. + $element['#after_build'][] = [static::class, 'afterBuildRebuildPreview']; + + return $element; + } + + /** + * #after_build callback: build preview from Form API-populated values. + * + * This callback rebuilds previews from the same #value sources + * as the form fields, ensuring the preview stays in sync after + * drag-and-drop reorder. + */ + public static function afterBuildRebuildPreview(array $element, FormStateInterface $form_state) { + // Only rebuild if there is a preview to update. + if (!isset($element['preview_wrapper']['preview'])) { + return $element; + } + + $parent_config = $element['#ranking_parent_config'] ?? []; + $details = $element['details'] ?? []; + + // Read values from this element's form fields. At this point, + // the Form API has set #value from user input (keyed by original delta). + $heading = $details['ranking_heading']['#value'] + ?? $details['ranking_heading']['#default_value'] + ?? ''; + $description = $details['ranking_description']['#value'] + ?? $details['ranking_description']['#default_value'] + ?? ''; + $source = $details['ranking_source']['#value'] + ?? $details['ranking_source']['#default_value'] + ?? ''; + $link_title = $details['link_title']['#value'] + ?? $details['link_title']['#default_value'] + ?? ''; + $link_uri = $details['link_uri']['#value'] + ?? $details['link_uri']['#default_value'] + ?? ''; + $ranking_font_color = $details['ranking_font_color']['#value'] + ?? $details['ranking_font_color']['#default_value'] + ?? 'ranking-text-midnight'; + $ranking_link_style = $details['ranking_link_style']['#value'] + ?? $details['ranking_link_style']['#default_value'] + ?? 'w-100 btn btn-red mt-2'; + $background_class = $details['options']['#value'] + ?? $details['options']['#default_value'] + ?? 'text-bg-chili'; + $hover_class = $details['options_hover_effect']['#value'] + ?? $details['options_hover_effect']['#default_value'] + ?? 'text-bg-chili'; + $ranking_type = $details['ranking_type']['#value'] + ?? $details['ranking_type']['#default_value'] + ?? 'standard'; + // Media needs special handling: $items gets reordered by weight during + // form processing, so #default_value (from $items[$delta]) is in the wrong + // order. Read from user input instead (same order as text field #values). + $media_id = NULL; + $delta = $element['#delta']; + $field_name = $element['#field_name']; + $field_parents = $element['#field_parents']; + $user_input = $form_state->getUserInput(); + $input_path = array_merge($field_parents, [$field_name, $delta, 'details', 'media']); + $media_input = NestedArray::getValue($user_input, $input_path); + if (is_array($media_input)) { + // Media library widget stores selection in various formats. + $media_id = $media_input['selection'][0]['target_id'] + ?? $media_input['media_library_selection'] + ?? NULL; + if (empty($media_id) && !empty($media_input['target_id'])) { + $media_id = $media_input['target_id']; + } + } + elseif (is_numeric($media_input)) { + $media_id = $media_input; + } + // Fallback for initial load (no user input yet). + if ($media_id === NULL && !$form_state->isRebuilding()) { + $media_id = $details['media']['#default_value'] ?? NULL; + } + + // Parent paragraph behavior settings. + $ranking_hover_effect = !empty($parent_config['ranking_hover_effect']); + $ranking_clickable = !empty($parent_config['ranking_clickable']); + $ranking_header_style = $parent_config['ranking_header_style'] ?? NULL; + $ranking_alignment = $parent_config['ranking_alignment'] ?? 'text-left'; + + // Build base ranking classes (same logic as was in formElement). + $ranking_classes = $parent_config['ranking_hover_style'] ?? 'ranking card'; + $ranking_classes .= ' overflow-hidden'; + + // Apply alignment for non-image-only types. + if ($ranking_type !== 'image_only') { + $ranking_classes .= ' ' . $ranking_alignment; + } + + // Handle hover effect and background classes. + $effective_bg = $background_class; + if ($ranking_hover_effect) { + $effective_bg = !empty($hover_class) ? $hover_class : $background_class; + if (!empty($effective_bg) && $ranking_type !== 'image_only') { + $ranking_classes .= ' from-hover-effect ' . $effective_bg; + } + } + else { + if (!empty($background_class) && $ranking_type !== 'image_only') { + $ranking_classes .= ' non-hover-effect ' . $background_class; + } + } + + // Clickable ranking styles. + if ($ranking_clickable) { + $ranking_classes .= ' shadow'; + if (!empty($ranking_hover_effect) && $ranking_type !== 'image_only') { + $ranking_classes .= ' ranking-bold-hover'; + } + } + else { + $ranking_hover_effect = FALSE; + } + + // Link color override. + if (str_contains($ranking_link_style, 'link')) { + if (str_contains($effective_bg, 'bg-oasis') || + str_contains($effective_bg, 'bg-sky')) { + $ranking_link_style .= ' text-midnight'; + } + } + + // Determine text color override based on background. + $text_color_override = ''; + $check_bg = $ranking_hover_effect ? $effective_bg : $background_class; + if (!empty($check_bg)) { + $bg_text_map = [ + 'bg-sky' => 'text-midnight', + 'bg-cool-gray' => $ranking_hover_effect ? 'text-azurite' : 'text-azurite', + 'bg-warm-gray' => 'text-midnight', + 'bg-white' => 'text-midnight', + 'bg-oasis' => 'text-midnight', + ]; + foreach ($bg_text_map as $bg_key => $text_class) { + if (str_contains($check_bg, $bg_key)) { + $text_color_override = $text_class; + break; + } + } + } + + // Determine source classes based on background. + $ranking_source_classes = ''; + if (!str_contains($effective_bg, 'bg-transparent')) { + $ranking_source_classes = 'mt-auto'; + } + else { + // Transparent: apply font color to ranking classes. + $ranking_font_color = ' ' . $ranking_font_color; + $ranking_classes .= ' ' . trim($ranking_font_color); + } + + // Build media render array. + $media_render_array = NULL; + if (!empty($media_id)) { + $media_entity = \Drupal::entityTypeManager()->getStorage('media')->load($media_id); + if ($media_entity) { + /** @var \Drupal\az_ranking\Helper\AZRankingImageHelper $image_helper */ + $image_helper = \Drupal::service('az_ranking.image'); + $media_render_array = $image_helper->generateImageRenderArray($media_entity); + } + } + + // Build link render array and URL. + $link_render_array = NULL; + $link_url = NULL; + if (!empty($link_uri)) { + if (str_starts_with($link_uri, '/' . PublicStream::basePath())) { + $link_url = Url::fromUri(urldecode('base:' . $link_uri)); + } + else { + $link_url = \Drupal::service('path.validator')->getUrlIfValid($link_uri); + } + if ($link_url) { + $link_classes = explode(' ', $ranking_link_style); + if ($ranking_clickable) { + $link_classes[] = 'stretched-link'; + } + $link_render_array = [ + '#type' => 'link', + '#title' => $link_title ?: $source, + '#url' => $link_url, + '#attributes' => ['class' => $link_classes], + ]; + } + } + + // Build the complete preview. + $element['preview_wrapper']['preview'] = [ + '#theme' => 'az_ranking', + '#media' => $media_render_array, + '#ranking_heading' => $heading, + '#ranking_description' => $description, + '#ranking_source' => $source, + '#ranking_header_style' => $ranking_header_style, + '#ranking_alignment' => $ranking_alignment, + '#ranking_hover_effect' => $ranking_hover_effect, + '#ranking_clickable' => $ranking_clickable, + '#ranking_font_color' => $ranking_font_color, + '#text_color_override' => $text_color_override, + '#ranking_link_style' => $ranking_link_style, + '#ranking_source_classes' => $ranking_source_classes, + '#link' => $link_render_array, + '#link_url' => $link_url, + '#link_title' => $link_title, + '#attributes' => [ + 'class' => $ranking_classes . ' widget-preview-ranking', + 'style' => 'transform: scale(0.8); transform-origin: center;', + ], + ]; + return $element; } @@ -771,201 +934,6 @@ public function massageFormValues(array $values, array $form, FormStateInterface return $values; } - /** - * Build the preview render array for a ranking item. - * - * @param \Drupal\az_ranking\Plugin\Field\FieldType\AZRankingItem $item - * The ranking item. - * @param string $ranking_classes - * The ranking CSS classes. - * - * @return array - * The preview render array. - */ - protected function buildRankingPreview($item, $ranking_classes) { - $parent = $item->getEntity(); - - // Get ranking settings from parent paragraph. - $ranking_hover_effect = FALSE; - $ranking_clickable = FALSE; - $ranking_header_style = NULL; - $ranking_alignment = NULL; - if ($parent instanceof ParagraphInterface) { - $parent_config = $parent->getAllBehaviorSettings(); - if (!empty($parent_config['az_rankings_paragraph_behavior'])) { - $ranking_defaults = $parent_config['az_rankings_paragraph_behavior']; - $ranking_hover_effect = $ranking_defaults['ranking_hover_effect'] ?? FALSE; - $ranking_clickable = $ranking_defaults['ranking_clickable'] ?? FALSE; - $ranking_header_style = $ranking_defaults['ranking_header_style'] ?? NULL; - $ranking_alignment = $ranking_defaults['ranking_alignment'] ?? 'text-left'; - } - } - // Apply paragraph settings found in AZRankingDefaultFormatter. - if ($item->options['ranking_type'] !== 'image_only') { - $ranking_classes .= ' ' . $ranking_alignment; - } - - // Apply clickable ranking styles (like formatter does). - $link_title = $item->link_title ?? ''; - $ranking_link_style = $item->ranking_link_style ?? 'w-100 btn btn-red mt-2'; - - if ($ranking_clickable) { - // Add shadow when ranking is clickable. - $ranking_classes .= ' shadow'; - if (!empty($ranking_hover_effect) && $item->options['ranking_type'] !== 'image_only') { - $ranking_classes .= ' ranking-bold-hover'; - } - } - else { - // Ranking is not clickable. - $ranking_hover_effect = FALSE; - } - - // Link color override. - if (str_contains($ranking_link_style, 'link')) { - if (str_contains($item->options['class'], 'bg-oasis') || - str_contains($item->options['class'], 'bg-sky')) { - $ranking_link_style .= ' text-midnight'; - } - } - // Determine font color and text color override. - $ranking_font_color = $item->ranking_font_color ?? 'ranking-text-midnight'; - $text_color_override = ''; - - // Determine source classes based on background color (like formatter does). - $ranking_source_classes = ''; - $background_class = ''; - - // Get the appropriate background class depending on hover effect. - if ($ranking_hover_effect) { - if (!empty($item->options['hover_class'])) { - $background_class = $item->options['hover_class']; - } - // Fallback to the persisted background class. - if (empty($background_class) && !empty($item->options['class'])) { - $background_class = $item->options['class']; - } - } - else { - $background_class = $item->options['class'] ?? ''; - } - - // Apply mt-auto if NOT transparent background. - if (!str_contains($background_class, 'bg-transparent')) { - $ranking_source_classes = 'mt-auto'; - } - else { - // transparent: apply font color to ranking _font_color and _classes. - $ranking_font_color = ' ' . $item->ranking_font_color; - $ranking_classes .= ' ' . $item->ranking_font_color; - } - - // Set text_color_override based on background color (like formatter does). - if (!$ranking_hover_effect) { - if (!empty($item->options['class'])) { - switch (TRUE) { - case str_contains($item->options['class'], 'bg-sky'): - $text_color_override = 'text-midnight'; - break; - - case str_contains($item->options['class'], 'bg-cool-gray'): - $text_color_override = 'text-azurite'; - break; - - case str_contains($item->options['class'], 'bg-warm-gray'): - $text_color_override = 'text-midnight'; - break; - - case str_contains($item->options['class'], 'bg-white'): - $text_color_override = 'text-midnight'; - break; - - case str_contains($item->options['class'], 'bg-oasis'): - $text_color_override = 'text-midnight'; - break; - } - } - } - else { - // Override hover class. - if (!empty($item->options['hover_class'])) { - switch (TRUE) { - case str_contains($item->options['hover_class'], 'bg-sky'): - $text_color_override = 'text-midnight'; - break; - - case str_contains($item->options['hover_class'], 'bg-cool-gray'): - $text_color_override = 'text-azurite'; - break; - - case str_contains($item->options['hover_class'], 'bg-oasis'): - $text_color_override = 'text-midnight'; - break; - } - } - } - - // Build media render array. - $media_render_array = NULL; - $media_id = $item->media ?? NULL; - if (!empty($media_id)) { - if ($media = $this->entityTypeManager->getStorage('media')->load($media_id)) { - $media_render_array = $this->rankingImageHelper->generateImageRenderArray($media); - } - } - - // Build link render array and URL. - $link_render_array = NULL; - $link_url = NULL; - if ($item->link_uri) { - if (!empty($item->link_uri) && str_starts_with($item->link_uri, '/' . PublicStream::basePath())) { - $link_url = Url::fromUri(urldecode('base:' . $item->link_uri)); - } - else { - $link_url = $this->pathValidator->getUrlIfValid($item->link_uri ?? ''); - } - - if ($link_url) { - $link_classes = explode(' ', $ranking_link_style); - - // Add stretched-link class if ranking is clickable. - if (!empty($ranking_clickable)) { - $link_classes[] = 'stretched-link'; - } - - $link_render_array = [ - '#type' => 'link', - '#title' => $link_title ?: ($item->ranking_source ?? ''), - '#url' => $link_url, - '#attributes' => ['class' => $link_classes], - ]; - } - } - - return [ - '#theme' => 'az_ranking', - '#media' => $media_render_array, - '#ranking_heading' => $item->ranking_heading ?? '', - '#ranking_description' => $item->ranking_description ?? '', - '#ranking_source' => $item->ranking_source ?? '', - '#ranking_header_style' => $ranking_header_style, - '#ranking_alignment' => $ranking_alignment, - '#ranking_hover_effect' => $ranking_hover_effect, - '#ranking_clickable' => $ranking_clickable, - '#ranking_font_color' => $ranking_font_color, - '#text_color_override' => $text_color_override, - '#ranking_link_style' => $ranking_link_style, - '#ranking_source_classes' => $ranking_source_classes, - '#link' => $link_render_array, - '#link_url' => $link_url, - '#link_title' => $link_title, - '#attributes' => [ - 'class' => $ranking_classes . ' widget-preview-ranking', - 'style' => 'transform: scale(0.8); transform-origin: center;', - ], - ]; - } - /** * Add az_ranking_context query parameter to media edit links. */ From 8a98c6fb99480dc23a3bf612090e6bdc33ee78d4 Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Wed, 18 Feb 2026 02:06:44 -0700 Subject: [PATCH 06/11] Pass PHPStan and CodeSniffer --- .../Plugin/Field/FieldWidget/AZRankingWidget.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index 93217210b4..02def9b963 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -420,11 +420,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen } /** - * #after_build callback: build preview from Form API-populated values. - * - * This callback rebuilds previews from the same #value sources - * as the form fields, ensuring the preview stays in sync after - * drag-and-drop reorder. + * This after-build callback rebuilds the preview from the same + * Form API-populated #value as the form fields, ensuring the + * preview stays in sync after drag-and-drop reorder. */ public static function afterBuildRebuildPreview(array $element, FormStateInterface $form_state) { // Only rebuild if there is a preview to update. @@ -435,8 +433,8 @@ public static function afterBuildRebuildPreview(array $element, FormStateInterfa $parent_config = $element['#ranking_parent_config'] ?? []; $details = $element['details'] ?? []; - // Read values from this element's form fields. At this point, - // the Form API has set #value from user input (keyed by original delta). + // Read values from this element's form fields. At this point, + // the Form API has set #value from user input (keyed by original delta). $heading = $details['ranking_heading']['#value'] ?? $details['ranking_heading']['#default_value'] ?? ''; @@ -577,7 +575,7 @@ public static function afterBuildRebuildPreview(array $element, FormStateInterfa if (!empty($media_id)) { $media_entity = \Drupal::entityTypeManager()->getStorage('media')->load($media_id); if ($media_entity) { - /** @var \Drupal\az_ranking\Helper\AZRankingImageHelper $image_helper */ + /** @var \Drupal\az_ranking\AZRankingImageHelper $image_helper */ $image_helper = \Drupal::service('az_ranking.image'); $media_render_array = $image_helper->generateImageRenderArray($media_entity); } From 439c1f959ccb5c18157a6b311ee9f9d857ff6f83 Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Wed, 18 Feb 2026 02:12:19 -0700 Subject: [PATCH 07/11] Pass PHPCodeSniffer --- .../src/Plugin/Field/FieldWidget/AZRankingWidget.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index 02def9b963..d4aadeeabf 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -420,9 +420,11 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen } /** - * This after-build callback rebuilds the preview from the same - * Form API-populated #value as the form fields, ensuring the - * preview stays in sync after drag-and-drop reorder. + * After-build callback to rebuild preview from form field values. + * + * Rebuilds the preview from the same Form API-populated #value as the + * form fields, ensuring the preview stays in sync after drag-and-drop + * reorder. */ public static function afterBuildRebuildPreview(array $element, FormStateInterface $form_state) { // Only rebuild if there is a preview to update. From cf7c7230c8403017e569e2d51f7dc5ffe951fb20 Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:36:41 -0700 Subject: [PATCH 08/11] use the base background class to match AZRankingDefaultFormatter behavior Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index d4aadeeabf..2539b261fa 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -563,7 +563,7 @@ public static function afterBuildRebuildPreview(array $element, FormStateInterfa // Determine source classes based on background. $ranking_source_classes = ''; - if (!str_contains($effective_bg, 'bg-transparent')) { + if (!str_contains($background_class, 'bg-transparent')) { $ranking_source_classes = 'mt-auto'; } else { From 0bdb941655590355044a9e140981325f322b1419 Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:38:18 -0700 Subject: [PATCH 09/11] Remove unused ternary operator Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index 2539b261fa..e5c6adabbe 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -548,7 +548,7 @@ public static function afterBuildRebuildPreview(array $element, FormStateInterfa if (!empty($check_bg)) { $bg_text_map = [ 'bg-sky' => 'text-midnight', - 'bg-cool-gray' => $ranking_hover_effect ? 'text-azurite' : 'text-azurite', + 'bg-cool-gray' => 'text-azurite', 'bg-warm-gray' => 'text-midnight', 'bg-white' => 'text-midnight', 'bg-oasis' => 'text-midnight', From 1f7529a7aa9098c6a86482e1e8a4aeea97769db4 Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:51:51 -0700 Subject: [PATCH 10/11] Replace static service container calls with dependency injection --- .../src/Plugin/Field/FieldWidget/AZRankingWidget.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index e5c6adabbe..34f53938a3 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -414,7 +414,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // Rebuild the preview in #after_build so it uses the form API-populated // field values (which reflect drag-and-drop reorder) instead of $items. - $element['#after_build'][] = [static::class, 'afterBuildRebuildPreview']; + $element['#after_build'][] = [$this, 'afterBuildRebuildPreview']; return $element; } @@ -426,7 +426,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen * form fields, ensuring the preview stays in sync after drag-and-drop * reorder. */ - public static function afterBuildRebuildPreview(array $element, FormStateInterface $form_state) { + public function afterBuildRebuildPreview(array $element, FormStateInterface $form_state) { // Only rebuild if there is a preview to update. if (!isset($element['preview_wrapper']['preview'])) { return $element; @@ -575,11 +575,9 @@ public static function afterBuildRebuildPreview(array $element, FormStateInterfa // Build media render array. $media_render_array = NULL; if (!empty($media_id)) { - $media_entity = \Drupal::entityTypeManager()->getStorage('media')->load($media_id); + $media_entity = $this->entityTypeManager->getStorage('media')->load($media_id); if ($media_entity) { - /** @var \Drupal\az_ranking\AZRankingImageHelper $image_helper */ - $image_helper = \Drupal::service('az_ranking.image'); - $media_render_array = $image_helper->generateImageRenderArray($media_entity); + $media_render_array = $this->rankingImageHelper->generateImageRenderArray($media_entity); } } @@ -591,7 +589,7 @@ public static function afterBuildRebuildPreview(array $element, FormStateInterfa $link_url = Url::fromUri(urldecode('base:' . $link_uri)); } else { - $link_url = \Drupal::service('path.validator')->getUrlIfValid($link_uri); + $link_url = $this->pathValidator->getUrlIfValid($link_uri); } if ($link_url) { $link_classes = explode(' ', $ranking_link_style); From 5de911110c201e0e6d6fe4f708ddda3a69558436 Mon Sep 17 00:00:00 2001 From: Kevin Lu <79181817+kevdevlu@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:15:32 -0700 Subject: [PATCH 11/11] Refactor afterBuildRebuildPreview per code review feedback - Use $media_input === NULL instead of !$form_state->isRebuilding() to detect initial load, covering validation errors and other non-rebuilding states where user input is present. - Build CSS classes as an array using explode/array_filter/array_merge instead of string concatenation to avoid duplicate spaces. --- .../src/Plugin/Field/FieldWidget/AZRankingWidget.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php index 34f53938a3..f58720da19 100644 --- a/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php +++ b/modules/custom/az_ranking/src/Plugin/Field/FieldWidget/AZRankingWidget.php @@ -489,8 +489,8 @@ public function afterBuildRebuildPreview(array $element, FormStateInterface $for elseif (is_numeric($media_input)) { $media_id = $media_input; } - // Fallback for initial load (no user input yet). - if ($media_id === NULL && !$form_state->isRebuilding()) { + // Fallback for initial load (no user input for this field yet). + if ($media_id === NULL && $media_input === NULL) { $media_id = $details['media']['#default_value'] ?? NULL; } @@ -624,7 +624,7 @@ public function afterBuildRebuildPreview(array $element, FormStateInterface $for '#link_url' => $link_url, '#link_title' => $link_title, '#attributes' => [ - 'class' => $ranking_classes . ' widget-preview-ranking', + 'class' => array_merge(array_filter(explode(' ', $ranking_classes)), ['widget-preview-ranking']), 'style' => 'transform: scale(0.8); transform-origin: center;', ], ];