diff --git a/lms.routing.yml b/lms.routing.yml index 22273b24b5925920eeeae9def965d943d2f98d5a..3642eb55adc459eb25da9484a7f21201c005581c 100644 --- a/lms.routing.yml +++ b/lms.routing.yml @@ -77,3 +77,11 @@ lms.answer.evaluate: parameters: lms_answer: type: entity:lms_answer + +lms.modal_subform_endpoint: + path: 'lms/modal-reference-form' + defaults: + _controller: 'Drupal\lms\Controller\LmsReferenceSubform::endpoint' + _title: 'LMS Reference form endpoint' + requirements: + _permission: 'administer lms' diff --git a/src/Controller/LmsReferenceSubform.php b/src/Controller/LmsReferenceSubform.php new file mode 100644 index 0000000000000000000000000000000000000000..6fa3a0e97b94c1472ddc0cc62f4d68c2dd580601 --- /dev/null +++ b/src/Controller/LmsReferenceSubform.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\lms\Controller; + +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\AlertCommand; +use Drupal\Core\DependencyInjection\AutowireTrait; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Form\FormAjaxResponseBuilderInterface; +use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Form\FormState; +use Drupal\lms\Form\LmsEntityCreationForm; +use Symfony\Component\HttpFoundation\Request; + +/** + * Add doc. + */ +final class LmsReferenceSubform implements ContainerInjectionInterface { + + use AutowireTrait; + + public function __construct( + private readonly FormBuilderInterface $formBuilder, + private readonly FormAjaxResponseBuilderInterface $ajaxResponseBuilder, + ) {} + + /** + * Endpoint callback. + */ + public function endpoint(Request $request): AjaxResponse { + $form = $this->formBuilder->getForm(LmsEntityCreationForm::class); + + // Any additional code should not be executed because a FormException + // should be thrown. + $form_state = new FormState(); + $form_id = $this->formBuilder->getFormId(LmsEntityCreationForm::class, $form_state); + $form_state->setRequestMethod('POST'); + $input = $request->request->all(); + $form_state->setUserInput($input); + + $form = $this->formBuilder->getCache($input['form_build_id'], $form_state); + kdpm($form, 'FA'); + // @todo Security - if form is not available, exit. + if (!\is_array($form)) { + $form = $this->formBuilder->retrieveForm($form_id, $form_state); + $this->formBuilder->prepareForm($form_id, $form, $form_state); + $cache_form_state = $form_state->getCacheableArray(); + $cache_form_state['always_process'] = $form_state->getAlwaysProcess(); + $cache_form_state['temporary'] = $form_state->getTemporary(); + $form_state_before_retrieval = clone $form_state; + $form_state = $form_state_before_retrieval; + $form_state->setFormState($cache_form_state); + } + + $form['#build_id_old'] = $request->request->get('form_build_id'); + + $form_state->disableRedirect(); + $this->formBuilder->processForm($form_id, $form, $form_state); + + $response = $this->ajaxResponseBuilder->buildResponse($request, $form, $form_state, []); + \assert($response instanceof AjaxResponse); + $response->setStatusCode(200); + + return $response; + } + +} diff --git a/src/Form/LmsEntityCreationForm.php b/src/Form/LmsEntityCreationForm.php new file mode 100644 index 0000000000000000000000000000000000000000..8481eddfd7bfb2964ffdc634e7538be018197911 --- /dev/null +++ b/src/Form/LmsEntityCreationForm.php @@ -0,0 +1,161 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\lms\Form; + +use Drupal\Core\Ajax\AlertCommand; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Modal LMS entity creation form. + */ +final class LmsEntityCreationForm extends FormBase { + + private const FORM_SELECTOR = 'lms-entity-creation-form'; + + private array $bundleInfo; + + /** + * The constructor. + */ + public function __construct( + protected EntityTypeBundleInfoInterface $entityTypeBundleInfo, + ) { + + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.bundle.info'), + $container->get('form_builder'), + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'lms_entity_creation_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + if ($form_state->get('lms_parent_form_data') === NULL) { + $form_state->set('lms_parent_form_data', $form_state->getBuildInfo()['args']); + } + + $bundle_form = $this->getBundleForm($form, $form_state); + if ($bundle_form !== NULL) { + $form = $bundle_form; + } + else { + $form = $this->getEntityForm($form, $form_state); + } + + $form['#attributes']['data-lms-selector'] = self::FORM_SELECTOR; + + return $form; + } + + private function getBundleForm(array $form, FormStateInterface $form_state): ?array { + $bundle = $form_state->get('lms_entity_form_bundle'); + if ($bundle !== NULL) { + return NULL; + } + + $bundle = $form_state->getValue('bundle'); + if ($bundle !== NULL) { + $form_state->set('lms_entity_form_bundle', $bundle); + return NULL; + } + + $entity_type_id = $this->getEntityTypeId($form_state); + $bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + if (\count($bundle_info) === 1) { + $form_state->set('lms_entity_form_bundle', \array_keys($bundle_info)[0]); + return NULL; + } + + $bundle_options = []; + foreach ($bundle_info as $bundle_id => $data) { + $bundle_options[$bundle_id] = $data['label']; + } + + $form['bundle'] = [ + '#type' => 'radios', + '#title' => $this->t('Bundle'), + '#options' => $bundle_options, + '#ajax' => [ + 'callback' => [\get_class($this), 'bundleSelectionAjax'], + 'url' => Url::fromRoute('lms.modal_subform_endpoint'), + 'options' => [ + 'query' => [ + FormBuilderInterface::AJAX_FORM_REQUEST => TRUE, + ], + ], + ], + ]; + + return $form; + } + + public static function bundleSelectionAjax(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $form_selector = '[data-lms-selector="' . self::FORM_SELECTOR . '"]'; + $response->addCommand(new ReplaceCommand($form_selector, $form)); + return $response; + } + + private function getEntityForm(array $form, FormStateInterface $form_state) { + $form['submit'] = [ + '#type' => 'button', + '#value' => $this->t('Create and add'), + '#ajax' => [ + 'callback' => [\get_class($this), 'createEntityAjax'], + 'url' => Url::fromRoute('lms.modal_subform_endpoint'), + 'options' => [ + 'query' => [ + FormBuilderInterface::AJAX_FORM_REQUEST => TRUE, + ], + ], + ], + ]; + return $form; + } + + public static function createEntityAjax(array $form, FormStateInterface $form_state) { + kdpm($form_state->get('lms_entity_form_bundle'), 'FA'); + kdpm($form_state->getBuildInfo(), 'FA'); + $response = new AjaxResponse(); + $response->addCommand(new AlertCommand('ok.')); + return $response; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // This is + } + + /** + * dev + */ + private function getEntityTypeId(FormStateInterface $form_state) { + return $form_state->getBuildInfo()['args'][0] ?? 'lms_lesson'; + } + +} diff --git a/src/Plugin/Field/FieldWidget/LMSReferenceTable.php b/src/Plugin/Field/FieldWidget/LMSReferenceTable.php new file mode 100644 index 0000000000000000000000000000000000000000..4a1a4bf7c8adf622b464a465f83d4259e41148da --- /dev/null +++ b/src/Plugin/Field/FieldWidget/LMSReferenceTable.php @@ -0,0 +1,279 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\lms\Plugin\Field\FieldWidget; + +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\InvokeCommand; +use Drupal\Core\Ajax\OpenModalDialogCommand; +use Drupal\Core\Entity\Element\EntityAutocomplete; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Field\Attribute\FieldWidget; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Field\WidgetBase; +use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\lms\Entity\Activity; +use Drupal\lms\Entity\ActivityType; +use Drupal\lms\Form\LmsEntityCreationForm; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Plugin implementation of the 'lms_reference_table' widget. + */ +#[FieldWidget( + id: 'lms_reference_table', + label: new TranslatableMarkup('LMS reference table'), + description: new TranslatableMarkup('Improved LMS entity reference widget.'), + field_types: ['lms_reference'], + multiple_values: TRUE, +)] +final class LMSReferenceTable extends WidgetBase { + + private const TABLEDRAG_CLASS = 'lms-items-order-weight'; + + /** + * Constructor. + */ + public function __construct( + $plugin_id, + $plugin_definition, + FieldDefinitionInterface $field_definition, + array $settings, + array $third_party_settings, + protected EntityTypeBundleInfoInterface $entityTypeBundleInfo, + protected FormBuilderInterface $formBuilder, + ) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('entity_type.bundle.info'), + $container->get('form_builder') + ); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state): array { + return []; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary(): array { + return []; + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + + $table = [ + '#type' => 'table', + '#caption' => $this->t('References'), + '#header' => [ + 'weight' => $this->t('Weight'), + 'title' => $this->t('Title'), + 'type' => $this->t('Type'), + 'parameters' => $this->t('Parameters'), + 'remove' => $this->t('Remove'), + ], + '#tableselect' => FALSE, + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => self::TABLEDRAG_CLASS, + ], + ], + '#empty' => $this->t('No references.'), + '#attributes' => ['data-lms-selector' => 'lms-reference-table'], + ]; + + $target_type = $this->fieldDefinition->getSetting('target_type'); + $bundle_info = $this->entityTypeBundleInfo->getBundleInfo($target_type); + + $references = $form_state->get('lms_references'); + if ($references === NULL) { + $references = []; + foreach ($items as $item_delta => $item) { + $entity = $item->entity; + $references[$item_delta] = [ + 'weight' => $item_delta, + 'entity_id' => $item->get('target_id')->getValue(), + 'label' => $entity->label(), + 'type' => $bundle_info[$entity->bundle()]['label'], + ]; + } + } + + foreach ($references as $reference) { + $table[$reference['entity_id']] = [ + 'weight' => [ + '#type' => 'weight', + '#title' => $this->t('Weight for @title', [ + '@title' => $reference['label'], + ]), + '#title_display' => 'invisible', + '#default_value' => $reference['weight'], + '#attributes' => ['class' => [self::TABLEDRAG_CLASS]], + ], + 'title' => [ + '#markup' => $reference['label'], + ], + 'type' => [ + '#markup' => $reference['type'], + ], + 'parameters' => [], + 'remove' => [ + '#type' => 'button', + '#value' => $this->t('Remove'), + '#ajax' => [ + 'callback' => [\get_class($this), 'removeItem'], + ], + ], + ]; + + } + + $element['table'] = $table; + $element['add_item'] = [ + '#type' => 'button', + '#value' => $this->t('Add item'), + '#ajax' => [ + 'callback' => [$this, 'openAddModal'], + ], + '#limit_validation_errors' => [], + ]; + //$element['#attached']['library'] = ['drupal.dialog.ajax']; + + return $element; + } + + public function openAddModal(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + kdpm($form, 'FA'); + $target_type = $this->fieldDefinition->getSetting('target_type'); + $modal_form = $this->formBuilder->getForm(LmsEntityCreationForm::class, $target_type, $form_state); + + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand( + $this->t('Create entity'), + $modal_form, + ['width' => '80%'] + )); + return $response; + } + + /** + * LMS Activity data elements. + * + * @param mixed[] $element + * Form element. + */ + private function setActivityDataElements(array &$element): void { + $element['target_id']['#title'] = $this->t('Activity'); + $element['data']['max_score'] = [ + '#type' => 'number', + '#min' => 0, + '#max' => 100, + '#title' => $this->t('Maximum score'), + '#default_value' => 5, + ]; + $element['target_id']['#ajax'] = [ + 'callback' => [static::class, 'updateMaxScore'], + 'event' => 'autocompleteclose', + ]; + } + + /** + * LMS Lesson data elements. + * + * @param mixed[] $element + * Form element. + */ + private function setLessonDataElements(array &$element): void { + $element['target_id']['#title'] = $this->t('Lesson'); + $element['data']['mandatory'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Mandatory'), + '#default_value' => TRUE, + ]; + $element['data']['auto_repeat_failed'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Auto-repeat if failed'), + '#description' => $this->t("If a student didn't get the score required to pass this lesson, it will be restarted when trying to navigate to the next one."), + '#default_value' => FALSE, + ]; + $element['data']['required_score'] = [ + '#type' => 'number', + '#min' => 0, + '#max' => 100, + '#title' => $this->t('Required score [%]'), + '#default_value' => 50, + ]; + $element['data']['time_limit'] = [ + '#type' => 'number', + '#min' => 0, + '#title' => $this->t('Time limit [min]'), + '#description' => $this->t('After this much time has passed, the lesson will be finished with all remaining activities unanswered.'), + '#default_value' => 0, + ]; + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + $values = parent::massageFormValues($values, $form, $form_state); + return $values; + } + + /** + * Update max score value on new elements. + */ + public static function updateMaxScore(array $form, FormStateInterface $form_state): ?AjaxResponse { + $trigger = $form_state->getTriggeringElement(); + $entity_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($trigger['#value']); + if ($entity_id === NULL) { + return NULL; + } + $entity = Activity::load($entity_id); + if ($entity === NULL) { + return NULL; + } + + // Value. + $activity_type = ActivityType::load($entity->bundle()); + $default_max_score = $activity_type->getDefaultMaxScore(); + + // Selector. + $parents = $trigger['#parents']; + \array_pop($parents); + $parents[] = 'data'; + $parents[] = 'max_score'; + $selector = 'input[name="' . \array_shift($parents) . '[' . \implode('][', $parents) . ']"]'; + + // Command. + $response = new AjaxResponse(); + $response->addCommand(new InvokeCommand($selector, 'val', [$default_max_score])); + return $response; + } + +}