mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/site_users.git
synced 2026-05-05 05:10:40 -03:00
Adiciona widget UserPhotosWidget para gerenciar fotos do usuário
Widget unificado para field_user_photos: exibe tira de thumbnails em linha com destaque (borda azul) na foto padrão; clicar num thumbnail seleciona-o como padrão. O campo field_user_default_photo é atualizado ao salvar. Edição/remoção de mídias individuais ficam a cargo do menu contextual do Drupal. O hook _site_users_add_default_photo_selector() é ignorado automaticamente quando o widget está ativo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
92
css/user-photos-widget.css
Normal file
92
css/user-photos-widget.css
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @file
|
||||
* Widget de tira de thumbnails com seletor de foto padrão.
|
||||
*/
|
||||
|
||||
/* ---- Título da seção ----------------------------------------------------- */
|
||||
|
||||
.user-photos-strip-label {
|
||||
margin: 0 0 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/* ---- Tira de thumbnails -------------------------------------------------- */
|
||||
|
||||
.user-photos-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.user-photos-empty {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ---- Card de foto -------------------------------------------------------- */
|
||||
|
||||
.user-photos-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
/* O .form-type--radio (ou .form-type-radio) envolve input + label.
|
||||
Tratamos o bloco inteiro como o card clicável. */
|
||||
.user-photos-item .form-type--radio,
|
||||
.user-photos-item .form-type-radio {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Oculta o círculo do radio sem tirá-lo do fluxo de acessibilidade */
|
||||
.user-photos-item .form-radio {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Label = thumbnail — recebe borda ao ser selecionado */
|
||||
.user-photos-item .form-type--radio label,
|
||||
.user-photos-item .form-type-radio label {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
transition: border-color 0.15s, background-color 0.15s;
|
||||
}
|
||||
|
||||
/* Destaque na foto padrão selecionada */
|
||||
.user-photos-item .form-type--radio:has(input[type="radio"]:checked) label,
|
||||
.user-photos-item .form-type-radio:has(input[type="radio"]:checked) label {
|
||||
border-color: #0678be;
|
||||
background-color: #e8f4fd;
|
||||
}
|
||||
|
||||
/* Thumbnail contido dentro do label */
|
||||
.user-photos-item .form-type--radio label .media,
|
||||
.user-photos-item .form-type-radio label .media,
|
||||
.user-photos-item .form-type--radio label img,
|
||||
.user-photos-item .form-type-radio label img {
|
||||
display: block;
|
||||
width: 120px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ---- Área de upload ------------------------------------------------------ */
|
||||
|
||||
.user-photos-widget .js-form-item-new-photo,
|
||||
.user-photos-widget .form-item-new-photo {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
@@ -3,3 +3,9 @@ user-info-block:
|
||||
css:
|
||||
component:
|
||||
css/site-user-info-block.css: {}
|
||||
|
||||
user_photos_widget:
|
||||
version: 1.x
|
||||
css:
|
||||
component:
|
||||
css/user-photos-widget.css: {}
|
||||
|
||||
@@ -327,8 +327,18 @@ function site_users_form_user_register_form_alter(&$form, FormStateInterface $fo
|
||||
|
||||
/**
|
||||
* Adiciona o seletor de foto padrão ao formulário.
|
||||
*
|
||||
* Ignorado quando o widget user_photos_widget está ativo para
|
||||
* field_user_photos, pois ele já gerencia a foto padrão.
|
||||
*/
|
||||
function _site_users_add_default_photo_selector(&$form, FormStateInterface $form_state) {
|
||||
// Verificar se o widget unificado está sendo usado.
|
||||
$form_display = $form_state->getFormObject()->getFormDisplay($form_state);
|
||||
$component = $form_display->getComponent('field_user_photos');
|
||||
if ($component && ($component['type'] ?? '') === 'user_photos_widget') {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Drupal\user\UserInterface $user */
|
||||
$user = $form_state->getFormObject()->getEntity();
|
||||
|
||||
|
||||
262
src/Plugin/Field/FieldWidget/UserPhotosWidget.php
Normal file
262
src/Plugin/Field/FieldWidget/UserPhotosWidget.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\site_users\Plugin\Field\FieldWidget;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\WidgetBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\file\FileInterface;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Widget de tira de thumbnails para gerenciar fotos de usuário.
|
||||
*
|
||||
* Exibe as fotos em linha; a foto padrão é destacada com borda colorida.
|
||||
* Clicar em um thumbnail muda a foto padrão. Ao salvar, atualiza
|
||||
* field_user_default_photo na entidade.
|
||||
*
|
||||
* @FieldWidget(
|
||||
* id = "user_photos_widget",
|
||||
* label = @Translation("Galeria de fotos com foto padrão"),
|
||||
* field_types = {
|
||||
* "entity_reference"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class UserPhotosWidget extends WidgetBase {
|
||||
|
||||
public function __construct(
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
FieldDefinitionInterface $field_definition,
|
||||
array $settings,
|
||||
array $third_party_settings,
|
||||
protected EntityTypeManagerInterface $entityTypeManager,
|
||||
) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
|
||||
}
|
||||
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
return new static(
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$configuration['field_definition'],
|
||||
$configuration['settings'],
|
||||
$configuration['third_party_settings'],
|
||||
$container->get('entity_type.manager'),
|
||||
);
|
||||
}
|
||||
|
||||
public static function isApplicable(FieldDefinitionInterface $field_definition): bool {
|
||||
$storage = $field_definition->getFieldStorageDefinition();
|
||||
if ($storage->getSetting('target_type') !== 'media') {
|
||||
return FALSE;
|
||||
}
|
||||
$handler_settings = $field_definition->getSetting('handler_settings');
|
||||
return !empty($handler_settings['target_bundles']['image']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL): array {
|
||||
$field_name = $this->fieldDefinition->getName();
|
||||
$parents = $form['#parents'] ?? [];
|
||||
|
||||
$element = [
|
||||
'#field_name' => $field_name,
|
||||
'#field_parents' => $parents,
|
||||
'#parents' => array_merge($parents, [$field_name]),
|
||||
'#array_parents' => array_merge($form['#array_parents'] ?? [], [$field_name]),
|
||||
'#tree' => TRUE,
|
||||
'#attached' => ['library' => ['site_users/user_photos_widget']],
|
||||
'#attributes' => ['class' => ['user-photos-widget']],
|
||||
];
|
||||
|
||||
$entity = $items->getEntity();
|
||||
|
||||
// Foto padrão atual: na entidade salva, ou primeira disponível.
|
||||
$current_default = '';
|
||||
if ($entity->hasField('field_user_default_photo') && !$entity->get('field_user_default_photo')->isEmpty()) {
|
||||
$current_default = (string) $entity->get('field_user_default_photo')->target_id;
|
||||
}
|
||||
|
||||
// Coleta mídias existentes na ordem atual do campo.
|
||||
$medias = [];
|
||||
foreach ($items as $item) {
|
||||
if (!$item->isEmpty() && $item->entity instanceof MediaInterface) {
|
||||
$medias[(string) $item->entity->id()] = $item->entity;
|
||||
}
|
||||
}
|
||||
|
||||
if ($current_default === '' || !isset($medias[$current_default])) {
|
||||
$current_default = (string) (array_key_first($medias) ?? '');
|
||||
}
|
||||
|
||||
// Caminho compartilhado por todos os radios do grupo "foto padrão".
|
||||
// Todos os radios com o mesmo #parents geram o mesmo HTML name, formando
|
||||
// um grupo. O Drupal calcula #checked automaticamente comparando
|
||||
// #return_value com o valor submetido / #default_value.
|
||||
$radio_parents = array_merge($element['#parents'], ['default_photo']);
|
||||
|
||||
// --- Tira de thumbnails ---
|
||||
if (!empty($medias)) {
|
||||
$element['photos_label'] = [
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'p',
|
||||
'#value' => $this->t('Selecionar foto padrão'),
|
||||
'#attributes' => ['class' => ['user-photos-strip-label']],
|
||||
];
|
||||
}
|
||||
|
||||
$element['photos'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => ['class' => ['user-photos-strip']],
|
||||
];
|
||||
|
||||
if (empty($medias)) {
|
||||
$element['photos']['empty'] = [
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'p',
|
||||
'#value' => $this->t('Nenhuma foto adicionada ainda.'),
|
||||
'#attributes' => ['class' => ['user-photos-empty']],
|
||||
];
|
||||
}
|
||||
|
||||
$view_builder = $this->entityTypeManager->getViewBuilder('media');
|
||||
|
||||
foreach ($medias as $mid_str => $media) {
|
||||
// Cada foto é um card .user-photos-item.
|
||||
$element['photos'][$mid_str] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => ['class' => ['user-photos-item']],
|
||||
];
|
||||
|
||||
// Radio oculto via CSS: o título (thumbnail) torna-se o <label> clicável.
|
||||
// Todos os radios compartilham #parents → mesmo HTML name → grupo rádio.
|
||||
// Drupal calcula #checked comparando #return_value com o valor do grupo.
|
||||
$element['photos'][$mid_str]['default'] = [
|
||||
'#type' => 'radio',
|
||||
'#title' => $view_builder->view($media, 'thumbnail'),
|
||||
'#title_display' => 'after',
|
||||
'#return_value' => $mid_str,
|
||||
'#default_value' => $current_default,
|
||||
'#parents' => $radio_parents,
|
||||
];
|
||||
}
|
||||
|
||||
// --- Upload de nova foto ---
|
||||
$uid = $entity->id() ?: 'new';
|
||||
$max_count = \Drupal::config('site_users.settings')->get('photos.max_count') ?? 5;
|
||||
|
||||
$element['new_photo'] = [
|
||||
'#type' => 'managed_file',
|
||||
'#title' => $this->t('Adicionar foto'),
|
||||
'#description' => $this->t(
|
||||
'Formatos aceitos: JPEG, PNG, GIF, WebP. Máximo @max fotos no total.',
|
||||
['@max' => $max_count],
|
||||
),
|
||||
'#upload_location' => 'public://user-photos/' . $uid . '/',
|
||||
'#upload_validators' => [
|
||||
'FileExtension' => ['extensions' => 'jpg jpeg png gif webp'],
|
||||
'FileSizeLimit' => ['fileLimit' => 10 * 1024 * 1024],
|
||||
],
|
||||
'#multiple' => FALSE,
|
||||
];
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state): void {
|
||||
$field_name = $this->fieldDefinition->getName();
|
||||
$parents = $form['#parents'] ?? [];
|
||||
|
||||
$widget_values = NestedArray::getValue(
|
||||
$form_state->getValues(),
|
||||
array_merge($parents, [$field_name]),
|
||||
) ?? [];
|
||||
|
||||
// IDs atuais na ordem do campo.
|
||||
$current_ids = [];
|
||||
foreach ($items as $item) {
|
||||
if (!$item->isEmpty()) {
|
||||
$current_ids[] = (string) $item->target_id;
|
||||
}
|
||||
}
|
||||
|
||||
$remaining_ids = $current_ids;
|
||||
|
||||
// Processa upload de nova foto.
|
||||
$new_fids = $widget_values['new_photo'] ?? [];
|
||||
if (!empty($new_fids)) {
|
||||
$fid = is_array($new_fids) ? (int) reset($new_fids) : (int) $new_fids;
|
||||
if ($fid) {
|
||||
$new_mid = $this->createMediaFromFile($fid, $items->getEntity());
|
||||
if ($new_mid) {
|
||||
$remaining_ids[] = (string) $new_mid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Atualiza field_user_photos.
|
||||
$values = array_map(fn($id) => ['target_id' => (int) $id], $remaining_ids);
|
||||
$items->setValue($values);
|
||||
$items->filterEmptyItems();
|
||||
|
||||
// Atualiza field_user_default_photo.
|
||||
// O valor do grupo de radios está em default_photo (caminho compartilhado).
|
||||
$default_photo_id = (string) ($widget_values['default_photo'] ?? '');
|
||||
$entity = $items->getEntity();
|
||||
if ($entity->hasField('field_user_default_photo')) {
|
||||
$entity->set('field_user_default_photo', $default_photo_id ?: NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria uma entidade de mídia do tipo 'image' a partir de um FID.
|
||||
*/
|
||||
protected function createMediaFromFile(int $fid, EntityInterface $owner): ?int {
|
||||
$file = $this->entityTypeManager->getStorage('file')->load($fid);
|
||||
if (!$file instanceof FileInterface) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
|
||||
$media_type = $this->entityTypeManager->getStorage('media_type')->load('image');
|
||||
if (!$media_type) {
|
||||
return NULL;
|
||||
}
|
||||
$source_field = $media_type->getSource()->getConfiguration()['source_field'];
|
||||
|
||||
$media = $this->entityTypeManager->getStorage('media')->create([
|
||||
'bundle' => 'image',
|
||||
'uid' => $owner->id() ?: \Drupal::currentUser()->id(),
|
||||
'status' => 1,
|
||||
$source_field => [
|
||||
'target_id' => $fid,
|
||||
'alt' => $file->getFilename(),
|
||||
],
|
||||
]);
|
||||
$media->save();
|
||||
|
||||
return (int) $media->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user