Commit 203ead62 authored by alexpott's avatar alexpott

Issue #2418017 by Wim Leers, YesCT, dawehner, webchick, jibran, amateescu,...

Issue #2418017 by Wim Leers, YesCT, dawehner, webchick, jibran, amateescu, hussainweb: Implement autocomplete UI for the link widget
parent e6967af9
......@@ -59,9 +59,14 @@
* @return {Boolean}
*/
function searchHandler(event) {
// Only search when the term is two characters or larger.
var options = autocomplete.options;
var term = autocomplete.extractLastTerm(event.target.value);
return term.length >= autocomplete.minLength;
// Abort search if the first character is in firstCharacterBlacklist.
if (term.length > 0 && options.firstCharacterBlacklist.indexOf(term[0]) !== -1) {
return false;
}
// Only search when the term is at least the minimum length.
return term.length >= options.minLength;
}
/**
......@@ -174,6 +179,11 @@
// Act on textfields with the "form-autocomplete" class.
var $autocomplete = $(context).find('input.form-autocomplete').once('autocomplete');
if ($autocomplete.length) {
// Allow options to be overriden per instance.
var blacklist = $autocomplete.attr('data-autocomplete-first-character-blacklist');
$.extend(autocomplete.options, {
firstCharacterBlacklist: (blacklist) ? blacklist : ''
});
// Use jQuery UI Autocomplete on the textfield.
$autocomplete.autocomplete(autocomplete.options)
.data("ui-autocomplete")
......@@ -194,8 +204,7 @@
*/
autocomplete = {
cache: {},
// Exposes methods to allow overriding by contrib.
minLength: 1,
// Exposes options to allow overriding by contrib.
splitValues: autocompleteSplitValues,
extractLastTerm: extractLastTerm,
// jQuery UI autocomplete options.
......@@ -204,7 +213,10 @@
focus: focusHandler,
search: searchHandler,
select: selectHandler,
renderItem: renderItem
renderItem: renderItem,
minLength: 1,
// Custom options, used by Drupal.autocomplete.
firstCharacterBlacklist: ''
},
ajax: {
dataType: 'json'
......
......@@ -25,7 +25,7 @@ class LinkFieldTest extends WebTestBase {
*
* @var array
*/
public static $modules = ['entity_test', 'link'];
public static $modules = ['entity_test', 'link', 'node'];
/**
* A field to use in this test class.
......@@ -93,27 +93,61 @@ function testURLValidation() {
// Create a path alias.
\Drupal::service('path.alias_storage')->save('admin', 'a/path/alias');
// Define some valid URLs.
// Create a node to test the link widget.
$node = $this->drupalCreateNode();
// Define some valid URLs (keys are the entered values, values are the
// strings displayed to the user).
$valid_external_entries = array(
'http://www.example.com/',
'http://www.example.com/' => 'http://www.example.com/',
);
$valid_internal_entries = array(
'entity_test/add',
'a/path/alias',
'entity:user/1',
'/entity_test/add' => '/entity_test/add',
'/a/path/alias' => '/a/path/alias',
// Front page, with query string and fragment.
'/' => '<front>',
'/?example=llama' => '<front>?example=llama',
'/#example' => '<front>#example',
// @todo '<front>' is valid input for BC reasons, may be removed by
// https://www.drupal.org/node/2421941
'<front>' => '&lt;front&gt;',
'<front>#example' => '&lt;front&gt;#example',
'<front>?example=llama' =>'&lt;front&gt;?example=llama',
// Query string and fragment.
'?example=llama' => '?example=llama',
'#example' => '#example',
// Entity reference autocomplete value.
$node->label() . ' (1)' => $node->label() . ' (1)',
// Entity URI displayed as ER autocomplete value when displayed in a form.
'entity:node/1' => $node->label() . ' (1)',
// URI for an entity that exists, but is not accessible by the user.
'entity:user/1' => '- Restricted access - (1)',
// URI for an entity that doesn't exist, but with a valid ID.
'entity:user/999999' => 'entity:user/999999',
// URI for an entity that doesn't exist, with an invalid ID.
'entity:user/invalid-parameter' => 'entity:user/invalid-parameter',
);
// Define some invalid URLs.
$validation_error_1 = "The path '@link_path' is either invalid or you do not have access to it.";
$validation_error_2 = 'Manually entered paths should start with /, ? or #.';
$invalid_external_entries = array(
// Missing protcol
'not-an-url',
'not-an-url' => $validation_error_2,
// Invalid protocol
'invalid://not-a-valid-protocol',
'invalid://not-a-valid-protocol' => $validation_error_1,
// Missing host name
'http://',
'http://' => $validation_error_1,
);
$invalid_internal_entries = array(
'non/existing/path',
'/non/existing/path' => $validation_error_1,
'no-leading-slash' => $validation_error_2,
'entity:non_existing_entity_type/yar' => $validation_error_1,
);
// Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC.
......@@ -142,15 +176,15 @@ function testURLValidation() {
* An array of valid URL entries.
*/
protected function assertValidEntries($field_name, array $valid_entries) {
foreach ($valid_entries as $value) {
foreach ($valid_entries as $uri => $string) {
$edit = array(
"{$field_name}[0][uri]" => $value,
"{$field_name}[0][uri]" => $uri,
);
$this->drupalPostForm('entity_test/add', $edit, t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
$id = $match[1];
$this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
$this->assertRaw($value);
$this->assertRaw($string);
}
}
......@@ -163,12 +197,12 @@ protected function assertValidEntries($field_name, array $valid_entries) {
* An array of invalid URL entries.
*/
protected function assertInvalidEntries($field_name, array $invalid_entries) {
foreach ($invalid_entries as $invalid_value) {
foreach ($invalid_entries as $invalid_value => $error_message) {
$edit = array(
"{$field_name}[0][uri]" => $invalid_value,
);
$this->drupalPostForm('entity_test/add', $edit, t('Save'));
$this->assertText(t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $invalid_value)));
$this->assertText(t($error_message, array('@link_path' => $invalid_value)));
}
}
......
......@@ -65,7 +65,7 @@ function testMenuLanguage() {
$this->assertOptionSelected('edit-langcode', $edit['langcode'], 'The menu language was correctly selected.');
// Test menu link language.
$link_path = '<front>';
$link_path = '/';
// Add a menu link.
$link_title = $this->randomString();
......
......@@ -266,7 +266,7 @@ function doMenuTests() {
$this->clickLink(t('Add link'));
$link_title = $this->randomString();
$this->drupalPostForm(NULL, array('link[0][uri]' => '<front>', 'title[0][value]' => $link_title), t('Save'));
$this->drupalPostForm(NULL, array('link[0][uri]' => '/', 'title[0][value]' => $link_title), t('Save'));
$this->assertUrl(Url::fromRoute('entity.menu.edit_form', ['menu' => $menu_name]));
// Test the 'Edit' operation.
$this->clickLink(t('Edit'));
......@@ -301,9 +301,9 @@ function doMenuTests() {
$this->doMenuLinkFormDefaultsTest();
// Add menu links.
$item1 = $this->addMenuLink('', 'node/' . $node1->id(), $menu_name, TRUE);
$item2 = $this->addMenuLink($item1->getPluginId(), 'node/' . $node2->id(), $menu_name, FALSE);
$item3 = $this->addMenuLink($item2->getPluginId(), 'node/' . $node3->id(), $menu_name);
$item1 = $this->addMenuLink('', '/node/' . $node1->id(), $menu_name, TRUE);
$item2 = $this->addMenuLink($item1->getPluginId(), '/node/' . $node2->id(), $menu_name, FALSE);
$item3 = $this->addMenuLink($item2->getPluginId(), '/node/' . $node3->id(), $menu_name);
// Hierarchy
// <$menu_name>
......@@ -337,10 +337,10 @@ function doMenuTests() {
$this->verifyMenuLink($item3, $node3, $item2, $node2);
// Add more menu links.
$item4 = $this->addMenuLink('', 'node/' . $node4->id(), $menu_name);
$item5 = $this->addMenuLink($item4->getPluginId(), 'node/' . $node5->id(), $menu_name);
$item4 = $this->addMenuLink('', '/node/' . $node4->id(), $menu_name);
$item5 = $this->addMenuLink($item4->getPluginId(), '/node/' . $node5->id(), $menu_name);
// Create a menu link pointing to an alias.
$item6 = $this->addMenuLink($item4->getPluginId(), 'node5', $menu_name, TRUE, '0');
$item6 = $this->addMenuLink($item4->getPluginId(), '/node5', $menu_name, TRUE, '0');
// Hierarchy
// <$menu_name>
......@@ -427,7 +427,7 @@ function doMenuTests() {
// item's weight doesn't get changed because of the old hardcoded delta=50.
$items = array();
for ($i = -50; $i <= 51; $i++) {
$items[$i] = $this->addMenuLink('', 'node/' . $node1->id(), $menu_name, TRUE, strval($i));
$items[$i] = $this->addMenuLink('', '/node/' . $node1->id(), $menu_name, TRUE, strval($i));
}
$this->assertMenuLink($items[51]->getPluginId(), array('weight' => '51'));
......@@ -454,7 +454,7 @@ function doMenuTests() {
$this->assertMenuLink($item7->getPluginId(), array('url' => 'http://drupal.org'));
// Add <front> menu item.
$item8 = $this->addMenuLink('', '<front>', $menu_name);
$item8 = $this->addMenuLink('', '/', $menu_name);
$this->assertMenuLink($item8->getPluginId(), array('route_name' => '<front>'));
$this->drupalGet('');
$this->assertResponse(200);
......@@ -494,20 +494,19 @@ function testMenuQueryAndFragment() {
$this->drupalLogin($this->adminUser);
// Make a path with query and fragment on.
$path = 'test-page?arg1=value1&arg2=value2';
$path = '/test-page?arg1=value1&arg2=value2';
$item = $this->addMenuLink('', $path);
$this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit');
$this->assertFieldByName('link[0][uri]', $path, 'Path is found with both query and fragment.');
// Now change the path to something without query and fragment.
$path = 'test-page';
$path = '/test-page';
$this->drupalPostForm('admin/structure/menu/item/' . $item->id() . '/edit', array('link[0][uri]' => $path), t('Save'));
$this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit');
$this->assertFieldByName('link[0][uri]', $path, 'Path no longer has query or fragment.');
// Use <front>#fragment and ensure that saving it does not loose its
// content.
// Use <front>#fragment and ensure that saving it does not lose its content.
$path = '<front>?arg1=value#fragment';
$item = $this->addMenuLink('', $path);
......@@ -547,7 +546,7 @@ function testUnpublishedNodeMenuItem() {
'status' => NODE_NOT_PUBLISHED,
));
$item = $this->addMenuLink('', 'node/' . $node->id());
$item = $this->addMenuLink('', '/node/' . $node->id());
$this->modifyMenuLink($item);
// Test that a user with 'administer menu' but without 'bypass node access'
......@@ -564,7 +563,7 @@ function testUnpublishedNodeMenuItem() {
public function testBlockContextualLinks() {
$this->drupalLogin($this->drupalCreateUser(array('administer menu', 'access contextual links', 'administer blocks')));
$custom_menu = $this->addCustomMenu();
$this->addMenuLink('', '<front>', $custom_menu->id());
$this->addMenuLink('', '/', $custom_menu->id());
$block = $this->drupalPlaceBlock('system_menu_block:' . $custom_menu->id(), array('label' => 'Custom menu', 'provider' => 'system'));
$this->drupalGet('test-page');
......@@ -606,7 +605,7 @@ public function testBlockContextualLinks() {
* @return \Drupal\menu_link_content\Entity\MenuLinkContent
* A menu link entity.
*/
function addMenuLink($parent = '', $path = '<front>', $menu_name = 'tools', $expanded = FALSE, $weight = '0') {
function addMenuLink($parent = '', $path = '/', $menu_name = 'tools', $expanded = FALSE, $weight = '0') {
// View add menu link page.
$this->drupalGet("admin/structure/menu/manage/$menu_name/add");
$this->assertResponse(200);
......@@ -640,7 +639,7 @@ function addMenuLink($parent = '', $path = '<front>', $menu_name = 'tools', $exp
* Attempts to add menu link with invalid path or no access permission.
*/
function addInvalidMenuLink() {
foreach (array('-&-', 'admin/people/permissions') as $link_path) {
foreach (array('/-&-', '/admin/people/permissions') as $link_path) {
$edit = array(
'link[0][uri]' => $link_path,
'title[0][value]' => 'title',
......@@ -666,7 +665,7 @@ function checkInvalidParentMenuLinks() {
$parent = $last_link ? 'tools:' . $last_link->getPluginId() : 'tools:';
$title = 'title' . $i;
$edit = array(
'link[0][uri]' => '<front>',
'link[0][uri]' => '/',
'title[0][value]' => $title,
'menu_parent' => $parent,
'description[0][value]' => '',
......
......@@ -41,18 +41,18 @@ public function testShortcutLinkAdd() {
// Create some paths to test.
$test_cases = [
'<front>',
'admin',
'admin/config/system/site-information',
'node/' . $this->node->id() . '/edit',
$path['alias'],
'router_test/test2',
'router_test/test3/value',
'/',
'/admin',
'/admin/config/system/site-information',
'/node/' . $this->node->id() . '/edit',
'/' . $path['alias'],
'/router_test/test2',
'/router_test/test3/value',
];
$test_cases_non_access = [
'admin',
'admin/config/system/site-information',
'/admin',
'/admin/config/system/site-information',
];
// Check that each new shortcut links where it should.
......@@ -66,7 +66,7 @@ public function testShortcutLinkAdd() {
$this->assertResponse(200);
$saved_set = ShortcutSet::load($set->id());
$paths = $this->getShortcutInformation($saved_set, 'link');
$this->assertTrue(in_array('user-path:/' . ($test_path == '<front>' ? '' : $test_path), $paths), 'Shortcut created: ' . $test_path);
$this->assertTrue(in_array('user-path:' . $test_path, $paths), 'Shortcut created: ' . $test_path);
if (in_array($test_path, $test_cases_non_access)) {
$this->assertNoLink($title, String::format('Shortcut link %url not accessible on the page.', ['%url' => $test_path]));
......@@ -92,15 +92,15 @@ public function testShortcutLinkAdd() {
$title = $this->randomMachineName();
$form_data = [
'title[0][value]' => $title,
'link[0][uri]' => 'admin',
'link[0][uri]' => '/admin',
];
$this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save'));
$this->assertResponse(200);
$this->assertRaw(t("The path '@link_path' is either invalid or you do not have access to it.", ['@link_path' => 'admin']));
$this->assertRaw(t("The path '@link_path' is either invalid or you do not have access to it.", ['@link_path' => '/admin']));
$form_data = [
'title[0][value]' => $title,
'link[0][uri]' => 'node',
'link[0][uri]' => '/node',
];
$this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save'));
$this->assertLink($title, 0, 'Shortcut link found on the page.');
......@@ -147,7 +147,7 @@ public function testShortcutLinkRename() {
$shortcuts = $set->getShortcuts();
$shortcut = reset($shortcuts);
$this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $new_link_name, 'link[0][uri]' => $shortcut->link->uri), t('Save'));
$this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $new_link_name), t('Save'));
$saved_set = ShortcutSet::load($set->id());
$titles = $this->getShortcutInformation($saved_set, 'title');
$this->assertTrue(in_array($new_link_name, $titles), 'Shortcut renamed: ' . $new_link_name);
......@@ -161,14 +161,14 @@ public function testShortcutLinkChangePath() {
$set = $this->set;
// Tests changing a shortcut path.
$new_link_path = 'admin/config';
$new_link_path = '/admin/config';
$shortcuts = $set->getShortcuts();
$shortcut = reset($shortcuts);
$this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $shortcut->getTitle(), 'link[0][uri]' => $new_link_path), t('Save'));
$saved_set = ShortcutSet::load($set->id());
$paths = $this->getShortcutInformation($saved_set, 'link');
$this->assertTrue(in_array('user-path:/' . $new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path);
$this->assertTrue(in_array('user-path:' . $new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path);
$this->assertLinkByHref($new_link_path, 0, 'Shortcut with new path appears on the page.');
}
......
......@@ -187,7 +187,7 @@ function testBreadCrumbs() {
$menu = 'tools';
$edit = array(
'title[0][value]' => 'Root',
'link[0][uri]' => 'node',
'link[0][uri]' => '/node',
);
$this->drupalPostForm("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
$menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => 'Root'));
......@@ -240,7 +240,7 @@ function testBreadCrumbs() {
$term = $data['term'];
$edit = array(
'title[0][value]' => "$name link",
'link[0][uri]' => "taxonomy/term/{$term->id()}",
'link[0][uri]' => "/taxonomy/term/{$term->id()}",
'menu_parent' => "$menu:{$parent_mlid}",
'enabled[value]' => 1,
);
......
......@@ -382,7 +382,8 @@ system.theme_settings_theme:
path: ''
options:
_only_fragment: TRUE
requirements:
_access: 'TRUE'
'<current>':
path: '<current>'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment