diff --git a/js/autocomplete.js b/js/autocomplete.js index 3a98d78d5a1c3aeed11da8e7b961ce15a183a806..815b70ba1b5968effefe6518ff18a1fc5f4dadc9 100644 --- a/js/autocomplete.js +++ b/js/autocomplete.js @@ -26,7 +26,12 @@ * @param {object} suggestions */ function showSuggestions(suggestions) { - response(suggestions.matches); + if (suggestions.matches.length === 0) { + response([{title: Drupal.t('No results')}]); + } + else { + response(suggestions.matches); + } } /** @@ -63,11 +68,26 @@ function selectHandler(event, ui) { if (ui.item.hasOwnProperty('path')) { event.target.value = ui.item.path; + + if (ui.item.hasOwnProperty('title')) { + $('.linkit-link-information > span').text(ui.item.title); + } } - $(document).trigger('linkit.autocomplete.select', [event, ui]); return false; } + /** + * Handles an autocomplete response event. + * + * @param {jQuery.Event} event + * @param {object} ui + */ + function response(event, ui) { + if (ui.content.length !== 0) { + $('.linkit-link-information > span').text(event.target.value); + } + } + /** * Override jQuery UI _renderItem function to output HTML by default. * @@ -157,6 +177,7 @@ renderItem: renderItem, renderMenu: renderMenu, select: selectHandler, + response: response, minLength: 1 }, ajax: { diff --git a/linkit.module b/linkit.module index b4df80e13abb9f1a97b4121c7eab0c0f87af5632..fd5671e9ec1b02c824a084e4722aeca2dcd5593d 100644 --- a/linkit.module +++ b/linkit.module @@ -21,10 +21,8 @@ function linkit_ckeditor_plugin_info_alter(array &$plugins) { * Implements hook_form_FORM_ID_alter(). */ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_state, $form_id) { - $user_input = $form_state->getUserInput(); /** @var Drupal\filter\Entity\FilterFormat $filter_format */ $filter_format = $form_state->getBuildInfo()['args'][0]; - $input = isset($user_input['editor_object']) ? $user_input['editor_object'] : []; /** @var \Drupal\Core\Entity\EntityStorageInterface $editorStorage */ $editorStorage = Drupal::service('entity.manager')->getStorage('editor'); @@ -42,12 +40,51 @@ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_s /** @var \Drupal\linkit\Entity\Profile $linkit_profile */ $linkit_profile = Drupal::entityTypeManager()->getStorage('linkit_profile')->load($linkit_profile_id); + $user_input = $form_state->getUserInput(); + $input = isset($user_input['editor_object']) ? $user_input['editor_object'] : []; + $data_entity_type = isset($input['data-entity-type']) ? $input['data-entity-type'] : ''; + $data_entity_uuid = isset($input['data-entity-uuid']) ? $input['data-entity-uuid'] : ''; + + // If the filter_html filter is activated, or any other filters using + // XSS:filter(), it will remove 'entity:' from the href as it thinks it's a + // bad protocol. We therefor have to restore the URI again when editing a + // link. It is possible given that data-entity-type and data-entity-uuid is + // set on the link element. + try { + if (!empty($data_entity_type) && !empty($data_entity_uuid)) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = \Drupal::service('entity.repository') + ->loadEntityByUuid($data_entity_type, $data_entity_uuid); + $href = 'entity:' . $entity->getEntityTypeId() . '/' . $entity->id(); + $access = !$entity->access('view', NULL, TRUE)->isForbidden(); + } + } + catch (Exception $exception) { + // Do nothing, this is handled in the finally block. + } + finally { + // If the href is not set, the data- attributes might not exists, or the + // href is external. In that case, use the given href. + if (!isset($href)) { + $href = isset($input['href']) ? $input['href'] : ''; + } + } + + $form['link-information'] = [ + '#type' => 'inline_template', + '#template' => '', + '#context' => [ + 'link_target' => !empty($entity) && !empty($access) && $access ? $entity->label() : $href, + ], + '#weight' => 2, + ]; + // Everything under the "attributes" key is merged directly into the // generated link tag's attributes. $form['attributes']['href'] = [ '#title' => t('Link'), '#type' => 'linkit', - '#default_value' => isset($input['href']) ? $input['href'] : '', + '#default_value' => $href, '#description' => t('Start typing to find content or paste a URL.'), '#autocomplete_route_name' => 'linkit.autocomplete', '#autocomplete_route_parameters' => [ @@ -77,7 +114,7 @@ function linkit_form_editor_link_dialog_alter(&$form, FormStateInterface $form_s 'linkit/linkit.imce', ], ], - '#weight' => 1, + '#weight' => 10, ]; } @@ -93,7 +130,8 @@ function linkit_form_editor_link_dialog_validate(array &$form, FormStateInterfac // Check if the 'href' attribute contains a entity: URI. $href = $form_state->getValue(['attributes', 'href']); $uri_parts = parse_url($href); - if ($uri_parts['scheme'] !== 'entity') { + + if (!$uri_parts || !isset($uri_parts['scheme']) || $uri_parts['scheme'] !== 'entity') { $form_state->setValue(['attributes', 'data-entity-type'], ''); $form_state->setValue(['attributes', 'data-entity-uuid'], ''); return; @@ -102,20 +140,24 @@ function linkit_form_editor_link_dialog_validate(array &$form, FormStateInterfac // Parse the entity: URI into an entity type ID and entity ID. list($entity_type_id, $entity_id) = explode('/', $uri_parts['path'], 2); + // Check if the given entity type exists, to prevent the entity load method + // to throw exceptions. + $definition = \Drupal::entityTypeManager()->getDefinition($entity_type_id, FALSE); + if (is_null($definition)) { + $form_state->setError($form['attributes']['href'], t('Invalid URI')); + return; + } + // Load the entity and populate the data-entity-type and data-entity-uuid // attributes as expected by filters. // @see \Drupal\editor\Plugin\Filter\EditorFileReference // @see \Drupal\linkit\Plugin\Filter\LinkitFilter - try { - if (!$entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id)) { - $form_state->setError($form['attributes']['href'], t('Invalid URI')); - } - else { - $form_state->setValue(['attributes', 'data-entity-type'], $entity_type_id); - $form_state->setValue(['attributes', 'data-entity-uuid'], $entity->uuid()); - } + $entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id); + if (!empty($entity)) { + $form_state->setValue(['attributes', 'data-entity-type'], $entity->getEntityTypeId()); + $form_state->setValue(['attributes', 'data-entity-uuid'], $entity->uuid()); } - catch (Exception $exception) { + else { $form_state->setError($form['attributes']['href'], t('Invalid URI')); } diff --git a/tests/src/FunctionalJavascript/LinkitDialogTest.php b/tests/src/FunctionalJavascript/LinkitDialogTest.php index 91350e647336b08a934c5aa5d4df5ed03bd0052e..7786a1afdf319c38067f8f5acecf46cc3d1472a7 100644 --- a/tests/src/FunctionalJavascript/LinkitDialogTest.php +++ b/tests/src/FunctionalJavascript/LinkitDialogTest.php @@ -148,6 +148,11 @@ class LinkitDialogTest extends JavascriptTestBase { $autocomplete_container = $page->find('css', 'ul.linkit-ui-autocomplete'); $this->assertFalse($autocomplete_container->isVisible()); + // Make sure the link information is empty. + $javascript = "(function (){ return jQuery('.linkit-link-information > span').text(); })()"; + $link_information = $session->evaluateScript($javascript); + $this->assertEquals('', $link_information, 'Link information is empty'); + // Trigger a keydown event to active a autocomplete search. $input_field->keyDown('f'); @@ -168,6 +173,11 @@ class LinkitDialogTest extends JavascriptTestBase { // Make sure the href field is populated with the node uri. $this->assertEquals('entity:' . $this->demoEntity->getEntityTypeId() . '/' . $this->demoEntity->id(), $input_field->getValue(), 'The href field is populated with the node uri'); + // Make sure the link information is populated. + $javascript = "(function (){ return jQuery('.linkit-link-information > span').text(); })()"; + $link_information = $session->evaluateScript($javascript); + $this->assertEquals($this->demoEntity->label(), $link_information, 'Link information is populated'); + // Save the dialog input. $button = $page->find('css', '.editor-link-dialog')->find('css', '.button.form-submit span'); $button->click(); @@ -179,14 +189,14 @@ class LinkitDialogTest extends JavascriptTestBase { // have a name. foreach (['data-entity-type' => $this->demoEntity->getEntityTypeId(), 'data-entity-uuid' => $this->demoEntity->uuid()] as $attr => $value) { $javascript = <<evaluateScript($javascript); $this->assertNotNull($link_attr); diff --git a/tests/src/Kernel/LinkitEditorLinkDialogTest.php b/tests/src/Kernel/LinkitEditorLinkDialogTest.php new file mode 100644 index 0000000000000000000000000000000000000000..67bd7c1aca0a6a7265473b5cb2e1ef2b05ee6327 --- /dev/null +++ b/tests/src/Kernel/LinkitEditorLinkDialogTest.php @@ -0,0 +1,267 @@ +installEntitySchema('entity_test'); + + // Create a profile. + $this->linkitProfile = $this->createProfile(); + + /** @var \Drupal\linkit\MatcherManager $matcherManager */ + $matcherManager = $this->container->get('plugin.manager.linkit.matcher'); + + // Add the entity_test matcher to the profile. + /** @var \Drupal\linkit\MatcherInterface $plugin */ + $plugin = $matcherManager->createInstance('entity:entity_test'); + $this->linkitProfile->addMatcher($plugin->getConfiguration()); + $this->linkitProfile->save(); + + // Add a text format. + $this->format = FilterFormat::create([ + 'format' => 'filtered_html', + 'name' => 'Filtered HTML', + 'weight' => 0, + 'filters' => [], + ]); + $this->format->save(); + + // Set up editor. + $editor = Editor::create([ + 'format' => 'filtered_html', + 'editor' => 'ckeditor', + ]); + $editor->setSettings([ + 'plugins' => [ + 'drupallink' => [ + 'linkit_enabled' => TRUE, + 'linkit_profile' => $this->linkitProfile->id(), + ], + ], + ]); + $editor->save(); + } + + /** + * Tests adding a link. + */ + public function testAdd() { + $entity_label = $this->randomString(); + $entity = EntityTest::create(['name' => $entity_label]); + $entity->save(); + + $form_object = new EditorLinkDialog(); + + $input = [ + 'editor_object' => [], + 'dialogOptions' => [ + 'title' => 'Add Link', + 'dialogClass' => 'editor-link-dialog', + 'autoResize' => 'true', + ], + '_drupal_ajax' => '1', + 'ajax_page_state' => [ + 'theme' => 'bartik', + 'theme_token' => 'some-token', + 'libraries' => '', + ], + ]; + $form_state = (new FormState()) + ->setRequestMethod('POST') + ->setUserInput($input) + ->addBuildInfo('args', [$this->format]); + + /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ + $form_builder = $this->container->get('form_builder'); + $form_id = $form_builder->getFormId($form_object, $form_state); + $form = $form_builder->retrieveForm($form_id, $form_state); + $form_builder->prepareForm($form_id, $form, $form_state); + $form_builder->processForm($form_id, $form, $form_state); + + $this->assertEquals('linkit.autocomplete', $form['attributes']['href']['#autocomplete_route_name'], 'Linkit is enabled on the href field.'); + $this->assertEquals('', $form['attributes']['href']['#default_value'], 'The href attribute is empty.'); + $this->assertEquals('', $form['link-information']['#context']['link_target'], 'Link information is empty.'); + + $form_state->setValue(['attributes', 'href'], 'entity:missing_entity/1'); + $form_builder->submitForm($form_object, $form_state); + $this->assertNotEmpty($form_state->getErrors(), 'Got validation errors for none existing entity type.'); + + $form_state->setValue(['attributes', 'href'], 'url_without_schema'); + $form_builder->submitForm($form_object, $form_state); + $this->assertEmpty($form_state->getErrors(), 'Got no validation errors for url without schema.'); + $this->assertEquals('', $form_state->getValue(['attributes', 'data-entity-type'])); + $this->assertEquals('', $form_state->getValue(['attributes', 'data-entity-uuid'])); + + $form_state->setValue(['attributes', 'href'], 'entity:entity_test/1'); + $form_builder->submitForm($form_object, $form_state); + $this->assertEmpty($form_state->getErrors(), 'Got no validation errors for correct URI.'); + $this->assertEquals($entity->getEntityTypeId(), $form_state->getValue(['attributes', 'data-entity-type']), 'Attribute "data-entity-type" exists and has the correct value.'); + $this->assertEquals($entity->uuid(), $form_state->getValue(['attributes', 'data-entity-uuid']), 'Attribute "data-entity-uuid" exists and has the correct value.'); + } + + /** + * Tests editing a link with data attributes. + */ + public function testEditWithDataAttributes() { + $entity_label = $this->randomString(); + $entity = EntityTest::create(['name' => $entity_label]); + $entity->save(); + + $entity_no_access = EntityTest::create(['name' => 'forbid_access']); + $entity_no_access->save(); + + $form_object = new EditorLinkDialog(); + + $input = [ + 'editor_object' => [ + 'href' => 'entity:entity_test/1', + 'data-entity-type' => $entity->getEntityTypeId(), + 'data-entity-uuid' => $entity->uuid(), + ], + 'dialogOptions' => [ + 'title' => 'Edit Link', + 'dialogClass' => 'editor-link-dialog', + 'autoResize' => 'true', + ], + '_drupal_ajax' => '1', + 'ajax_page_state' => [ + 'theme' => 'bartik', + 'theme_token' => 'some-token', + 'libraries' => '', + ], + ]; + $form_state = (new FormState()) + ->setRequestMethod('POST') + ->setUserInput($input) + ->addBuildInfo('args', [$this->format]); + + /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ + $form_builder = $this->container->get('form_builder'); + $form_id = $form_builder->getFormId($form_object, $form_state); + $form = $form_builder->retrieveForm($form_id, $form_state); + $form_builder->prepareForm($form_id, $form, $form_state); + $form_builder->processForm($form_id, $form, $form_state); + + $this->assertEquals('linkit.autocomplete', $form['attributes']['href']['#autocomplete_route_name'], 'Linkit is enabled on the href field.'); + $this->assertEquals('entity:entity_test/1', $form['attributes']['href']['#default_value'], 'The href attribute is empty.'); + $this->assertEquals($entity->label(), $form['link-information']['#context']['link_target'], 'Link information is empty.'); + + // Make sure the dialog don't display entity labels for inaccessible + // entities. + $input = [ + 'editor_object' => [ + 'href' => 'entity:entity_test/2', + 'data-entity-type' => $entity_no_access->getEntityTypeId(), + 'data-entity-uuid' => $entity_no_access->uuid(), + ], + 'dialogOptions' => [ + 'title' => 'Edit Link', + 'dialogClass' => 'editor-link-dialog', + 'autoResize' => 'true', + ], + '_drupal_ajax' => '1', + 'ajax_page_state' => [ + 'theme' => 'bartik', + 'theme_token' => 'some-token', + 'libraries' => '', + ], + ]; + $form_state = (new FormState()) + ->setRequestMethod('POST') + ->setUserInput($input) + ->addBuildInfo('args', [$this->format]); + + /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ + $form_builder = $this->container->get('form_builder'); + $form_id = $form_builder->getFormId($form_object, $form_state); + $form = $form_builder->retrieveForm($form_id, $form_state); + $form_builder->prepareForm($form_id, $form, $form_state); + $form_builder->processForm($form_id, $form, $form_state); + + $this->assertEquals('linkit.autocomplete', $form['attributes']['href']['#autocomplete_route_name'], 'Linkit is enabled on the href field.'); + $this->assertEquals('entity:entity_test/2', $form['attributes']['href']['#default_value'], 'The href attribute is empty.'); + $this->assertEquals('entity:entity_test/2', $form['link-information']['#context']['link_target'], 'Link information is empty.'); + } + + /** + * Tests editing a link without data attributes. + */ + public function testEditWithoutDataAttributes() { + $form_object = new EditorLinkDialog(); + + $input = [ + 'editor_object' => [ + 'href' => 'http://example.com/', + ], + 'dialogOptions' => [ + 'title' => 'Edit Link', + 'dialogClass' => 'editor-link-dialog', + 'autoResize' => 'true', + ], + '_drupal_ajax' => '1', + 'ajax_page_state' => [ + 'theme' => 'bartik', + 'theme_token' => 'some-token', + 'libraries' => '', + ], + ]; + $form_state = (new FormState()) + ->setRequestMethod('POST') + ->setUserInput($input) + ->addBuildInfo('args', [$this->format]); + + /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ + $form_builder = $this->container->get('form_builder'); + $form_id = $form_builder->getFormId($form_object, $form_state); + $form = $form_builder->retrieveForm($form_id, $form_state); + $form_builder->prepareForm($form_id, $form, $form_state); + $form_builder->processForm($form_id, $form, $form_state); + + $this->assertEquals('linkit.autocomplete', $form['attributes']['href']['#autocomplete_route_name'], 'Linkit is enabled on the href field.'); + $this->assertEquals('http://example.com/', $form['attributes']['href']['#default_value'], 'The href attribute is empty.'); + $this->assertEquals('http://example.com/', $form['link-information']['#context']['link_target'], 'Link information is empty.'); + } + +}