diff --git a/css/user-photos-widget.css b/css/user-photos-widget.css new file mode 100644 index 0000000..47689f5 --- /dev/null +++ b/css/user-photos-widget.css @@ -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; +} diff --git a/site_users.libraries.yml b/site_users.libraries.yml index 5c8e96d..3622f38 100644 --- a/site_users.libraries.yml +++ b/site_users.libraries.yml @@ -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: {} diff --git a/site_users.module b/site_users.module index 44305c8..64cea82 100644 --- a/site_users.module +++ b/site_users.module @@ -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(); diff --git a/src/Plugin/Field/FieldWidget/UserPhotosWidget.php b/src/Plugin/Field/FieldWidget/UserPhotosWidget.php new file mode 100644 index 0000000..7f5565f --- /dev/null +++ b/src/Plugin/Field/FieldWidget/UserPhotosWidget.php @@ -0,0 +1,262 @@ +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