diff --git a/core/composer.json b/core/composer.json index 1eabf49..3e4ea8d 100644 --- a/core/composer.json +++ b/core/composer.json @@ -53,6 +53,7 @@ "drupal/breakpoint": "self.version", "drupal/ckeditor": "self.version", "drupal/classy": "self.version", + "drupal/coffee": "self.version", "drupal/color": "self.version", "drupal/comment": "self.version", "drupal/config": "self.version", diff --git a/core/modules/coffee/coffee.api.php b/core/modules/coffee/coffee.api.php new file mode 100644 index 0000000..21cb1e4 --- /dev/null +++ b/core/modules/coffee/coffee.api.php @@ -0,0 +1,55 @@ + Url::fromRoute('my.simple.route')->toString(), + 'label' => 'Simple', + // Every result should include a command. + 'command' => ':simple', + ); + + // More advanced example to include view results. + if ($view = Views::getView('frontpage')) { + $view->setDisplay(); + $view->preExecute(); + $view->execute(); + + foreach ($view->result as $row) { + $entity = $row->_entity; + $commands[] = array( + 'value' => $entity->toUrl()->toString(), + 'label' => 'Pub: ' . $entity->label(), + // You can also specify commands that if the user enters, this command + // should show. + 'command' => ':x' . ' ' . $entity->label(), + ); + } + } + + return $commands; +} + +/** + * @} End of "addtogroup hooks" + */ diff --git a/core/modules/coffee/coffee.coffee.inc b/core/modules/coffee/coffee.coffee.inc new file mode 100644 index 0000000..388dd8a --- /dev/null +++ b/core/modules/coffee/coffee.coffee.inc @@ -0,0 +1,41 @@ + Url::fromRoute('')->toString(), + 'label' => t('Go to front page'), + 'command' => ':front', + ); + + // The command is ":add" and includes the link_title to empower the + // autocompletion. + $command = ':add'; + $entity_manager = \Drupal::entityTypeManager(); + // Only use node types the user has access to. + foreach ($entity_manager->getStorage('node_type')->loadMultiple() as $type) { + $node_type = $type->id(); + if ($entity_manager->getAccessControlHandler('node')->createAccess($node_type)) { + $commands[] = array( + 'value' => Url::fromRoute('node.add', ['node_type' => $node_type])->toString(), + 'label' => $type->label(), + 'command' => $command . ' ' . $type->label(), + ); + } + } + + return $commands; +} diff --git a/core/modules/coffee/coffee.info.yml b/core/modules/coffee/coffee.info.yml new file mode 100644 index 0000000..320e0b8 --- /dev/null +++ b/core/modules/coffee/coffee.info.yml @@ -0,0 +1,7 @@ +name: Coffee +description: 'Provides an Alfred like search box to navigate within your site.' +package: Core +version: VERSION +core: 8.x +type: module +configure: coffee.configuration diff --git a/core/modules/coffee/coffee.libraries.yml b/core/modules/coffee/coffee.libraries.yml new file mode 100644 index 0000000..54fa00c --- /dev/null +++ b/core/modules/coffee/coffee.libraries.yml @@ -0,0 +1,17 @@ +drupal.coffee: + version: VERSION + js: + js/coffee.js: {} + css: + component: + css/coffee.css: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings + - core/jquery.once + - core/jquery.ui + - core/jquery.ui.autocomplete + jquery.ui.autocomplete: + version: 1.8.11 + \ No newline at end of file diff --git a/core/modules/coffee/coffee.links.menu.yml b/core/modules/coffee/coffee.links.menu.yml new file mode 100644 index 0000000..1f71c77 --- /dev/null +++ b/core/modules/coffee/coffee.links.menu.yml @@ -0,0 +1,5 @@ +coffee.configuration: + title: Coffee + description: 'Select what menus coffee should search.' + route_name: coffee.configuration + parent: system.admin_config_ui diff --git a/core/modules/coffee/coffee.module b/core/modules/coffee/coffee.module new file mode 100644 index 0000000..edfc52c --- /dev/null +++ b/core/modules/coffee/coffee.module @@ -0,0 +1,71 @@ +hasPermission('access coffee')) { + $config = \Drupal::config('coffee.configuration'); + $cache_tags = isset($attachments['#cache']['tags']) ? $attachments['#cache']['tags'] : []; + $attachments['#cache']['tags'] = Cache::mergeTags($cache_tags, $config->getCacheTags()); + + $attachments['#attached']['library'][] = 'coffee/drupal.coffee'; + $attachments['#attached']['drupalSettings']['coffee'] = [ + 'maxResults' => $config->get('max_results'), + ]; + } +} + + +/** + * Implements hook_hook_info(). + */ +function coffee_hook_info() { + $hooks = array( + 'coffee_commands' => array( + 'group' => 'coffee', + ), + ); + + return $hooks; +} + +/** + * Implements hook_toolbar(). + */ +function coffee_toolbar() { + $items['coffee'] = [ + '#cache' => [ + 'contexts' => ['user.permissions'], + ], + ]; + + if (\Drupal::currentUser()->hasPermission('access coffee')) { + $items['coffee'] += [ + '#weight' => 999, + '#type' => 'toolbar_item', + 'tab' => [ + '#type' => 'link', + '#title' => t('Go to'), + '#url' => Url::fromRoute(''), + '#attributes' => [ + 'title' => t('Use alt+d to start Coffee and search for a page to go to '), + 'class' => ['toolbar-icon', 'toolbar-icon-coffee'], + ], + ], + '#attached' => [ + 'library' => ['coffee/drupal.coffee'], + ], + ]; + } + + return $items; +} diff --git a/core/modules/coffee/coffee.permissions.yml b/core/modules/coffee/coffee.permissions.yml new file mode 100644 index 0000000..98a7d2f --- /dev/null +++ b/core/modules/coffee/coffee.permissions.yml @@ -0,0 +1,7 @@ +access coffee: + title: 'Access Coffee' + description: 'Access the Coffee search box to navigate fast between admin pages' + +administer coffee: + title: 'Administer Coffee' + description: 'Administer the Coffee search module' diff --git a/core/modules/coffee/coffee.routing.yml b/core/modules/coffee/coffee.routing.yml new file mode 100644 index 0000000..368b8f9 --- /dev/null +++ b/core/modules/coffee/coffee.routing.yml @@ -0,0 +1,14 @@ +coffee.configuration: + path: '/admin/config/user-interface/coffee' + defaults: + _form: 'Drupal\coffee\Form\CoffeeConfigurationForm' + _title: 'Coffee configuration' + requirements: + _permission: 'administer coffee' + +coffee.get_data: + path: '/admin/coffee/get-data' + defaults: + _controller: 'Drupal\coffee\Controller\CoffeeController::coffeeData' + requirements: + _permission: 'access coffee' diff --git a/core/modules/coffee/config/install/coffee.configuration.yml b/core/modules/coffee/config/install/coffee.configuration.yml new file mode 100644 index 0000000..af6e3d6 --- /dev/null +++ b/core/modules/coffee/config/install/coffee.configuration.yml @@ -0,0 +1,3 @@ +coffee_menus: + admin: admin +max_results: 7 diff --git a/core/modules/coffee/config/schema/coffee.schema.yml b/core/modules/coffee/config/schema/coffee.schema.yml new file mode 100644 index 0000000..b8664dd --- /dev/null +++ b/core/modules/coffee/config/schema/coffee.schema.yml @@ -0,0 +1,15 @@ +# Schema for the configuration files of the Coffee module. + +coffee.configuration: + type: config_object + label: 'Coffee settings' + mapping: + coffee_menus: + type: sequence + label: 'Coffee menus' + sequence: + - type: string + label: 'Menu' + max_results: + type: integer + label: 'Number of items to show in the search result' diff --git a/core/modules/coffee/css/coffee.css b/core/modules/coffee/css/coffee.css new file mode 100644 index 0000000..1842292 --- /dev/null +++ b/core/modules/coffee/css/coffee.css @@ -0,0 +1,177 @@ +/** + * @file + * Stylesheet for the Coffee module. + */ +[id^="coffee"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +#coffee-bg { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 999; +} + +.coffee-form-wrapper{ + pointer-events: none; + left: 0; + width: 100%; + position: fixed; + top: 20%; + z-index: 9999; +} +#coffee-form { + pointer-events: auto; + position:relative; + margin: 0 auto; + max-width: 500px; + width: 100%; + padding: 10px; + font: 16px/1.5 sans-serif; + background: black; + background: rgba(0, 0, 0, 0.4); + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + -ms-border-radius: 15px; + -o-border-radius: 15px; + border-radius: 15px; + opacity: 1; + -webkit-transition: opacity 0.35s; + -moz-transition: opacity 0.35s; + -ms-transition: opacity 0.35s; + -o-transition: opacity 0.35s; + transition: opacity 0.35s false; +} + +#coffee-form, +#coffee-form input, +#coffee-form a{ + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.coffee-form-wrapper.hide-form { + position: absolute; + left: -100%; + opacity: 0; +} + +#coffee-form-inner { + background: #fff; + color: #444; + padding: 10px 10px 1px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; + -webkit-box-shadow: 0px 10px 40px rgba(0, 0, 0, 0.6); + -moz-box-shadow: 0px 10px 40px rgba(0, 0, 0, 0.6); + box-shadow: 0px 10px 40px rgba(0, 0, 0, 0.6); +} + +#coffee-q { + background: #d1d1d1; + color: #000; + border: 0; + font: 36px sans-serif; + line-height: 52px; + padding: 5px 10px; + width: 100%; + height: 52px; + outline: none; + display: block; + margin: 0 0 10px; + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + -ms-border-radius: 0px; + -o-border-radius: 0px; + border-radius: 0px; +} + +#coffee-q:focus { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +#coffee-results ul { + position: static; + float: none; + list-style: none; + margin: 0; + padding: 10px 0 0; + border: 0; +} + +#coffee-results li { + margin: 0; + padding: 0; + float: none; + clear: none; + width: auto; +} + +#coffee-results a { + display: block; + border: 0; + outline: none; + color: #444; + padding: 6px 10px 4px; + line-height: normal; + font-size: 16px; + text-decoration: none; + height: auto; + -webkit-transition: none; + -moz-transition: none; + -ms-transition: none; + -o-transition: none; + transition: none; + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + -ms-border-radius: 0px; + -o-border-radius: 0px; + border-radius: 0px; +} + +#coffee-results a.ui-state-hover, +#coffee-results a:focus, +#coffee-results a:hover, +#coffee-results .ui-state-focus { + background: #d1d1d1; + color: #000; + margin: 0; + cursor: pointer; +} + +#coffee-results .description { + display: block; + font-size: 11px; + color: #888; +} + +#coffee-results a.ui-state-hover .description { + color: #666; +} + +#coffee-results .ui-widget { + font-family: sans-serif; +} + +.toolbar .toolbar-bar .toolbar-icon-coffee:before, +.toolbar .toolbar-bar .toolbar-icon-coffee.active:before { + background-image: url("../images/icons/bebebe/coffee.svg"); +} + +#coffee-q{ + background-image: url("../images/icons/bebebe/coffee.svg"); + background-position: right 10px center; + background-repeat: no-repeat; + background-size: 40px 40px; +} \ No newline at end of file diff --git a/core/modules/coffee/images/icons/bebebe/coffee.svg b/core/modules/coffee/images/icons/bebebe/coffee.svg new file mode 100644 index 0000000..4fdc71b --- /dev/null +++ b/core/modules/coffee/images/icons/bebebe/coffee.svg @@ -0,0 +1,29 @@ + + + +image/svg+xml + + + + \ No newline at end of file diff --git a/core/modules/coffee/images/icons/ffffff/coffee.svg b/core/modules/coffee/images/icons/ffffff/coffee.svg new file mode 100644 index 0000000..5971457 --- /dev/null +++ b/core/modules/coffee/images/icons/ffffff/coffee.svg @@ -0,0 +1,29 @@ + + + +image/svg+xml + + + + \ No newline at end of file diff --git a/core/modules/coffee/js/coffee.js b/core/modules/coffee/js/coffee.js new file mode 100644 index 0000000..2d59d2b --- /dev/null +++ b/core/modules/coffee/js/coffee.js @@ -0,0 +1,202 @@ +/** + * @file + * JavaScript file for the Coffee module. + */ + +(function ($, Drupal, drupalSettings, DrupalCoffee) { + // Remap the filter functions for autocomplete to recognise the + // extra value "command". + var proto = $.ui.autocomplete.prototype, + initSource = proto._initSource; + + function filter(array, term) { + var matcher = new RegExp($.ui.autocomplete.escapeRegex(term), 'i'); + return $.grep(array, function (value) { + return matcher.test(value.command) || matcher.test(value.label) || matcher.test(value.value); + }); + } + + $.extend(proto, { + _initSource: function () { + if ($.isArray(this.options.source)) { + this.source = function (request, response) { + response(filter(this.options.source, request.term)); + }; + } + else { + initSource.call(this); + } + } + }); + + DrupalCoffee = DrupalCoffee || {}; + + Drupal.behaviors.coffee = { + attach: function () { + $('body').once('coffee').each(function () { + var body = $(this); + DrupalCoffee.bg.appendTo(body).hide(); + DrupalCoffee.wrapper.appendTo('body').addClass('hide-form'); + DrupalCoffee.form + .append(DrupalCoffee.label) + .append(DrupalCoffee.field) + .append(DrupalCoffee.results) + .wrapInner('
') + .appendTo(DrupalCoffee.wrapper); + + // Load autocomplete data set, consider implementing + // caching with local storage. + DrupalCoffee.dataset = []; + DrupalCoffee.isItemSelected = false; + + var autocomplete_data_element = 'ui-autocomplete'; + + $.ajax({ + url: Drupal.url('admin/coffee/get-data'), + dataType: 'json', + success: function (data) { + DrupalCoffee.dataset = data; + + // Apply autocomplete plugin on show + var $autocomplete = $(DrupalCoffee.field).autocomplete({ + source: DrupalCoffee.dataset, + focus: function (event, ui) { + // Prevents replacing the value of the input field + DrupalCoffee.isItemSelected = true; + event.preventDefault(); + }, + 'change': function(event, ui) { + DrupalCoffee.isItemSelected = false; + }, + select: function (event, ui) { + DrupalCoffee.redirect(ui.item.value, event.metaKey); + event.preventDefault(); + return false; + }, + delay: 0, + appendTo: DrupalCoffee.results + }); + + $autocomplete.data(autocomplete_data_element)._renderItem = function (ul, item) { + // strip the basePath when displaying the link description + var description = item.value; + if(item.value.indexOf(drupalSettings.path.basePath) === 0){ + description = item.value.substring(drupalSettings.path.basePath.length); + } + return $('
  • ') + .data('item.autocomplete', item) + .append('' + item.label + '' + description + '') + .appendTo(ul); + }; + + // We want to limit the number of results. + $(DrupalCoffee.field).data(autocomplete_data_element)._renderMenu = function (ul, items) { + var self = this; + items = items.slice(0, drupalSettings.coffee.maxResults); + $.each(items, function (index, item) { + self._renderItemData(ul, item); + }); + }; + + DrupalCoffee.form.keydown(function(event) { + if (event.keyCode == 13) { + var openInNewWindow = false; + + if (event.metaKey) { + openInNewWindow = true; + } + + if (!DrupalCoffee.isItemSelected) { + var $firstItem = $(DrupalCoffee.results).find('li:first').data('item.autocomplete'); + if (typeof $firstItem === 'object') { + DrupalCoffee.redirect($firstItem.value, openInNewWindow); + event.preventDefault(); + } + } + } + }); + }, + error: function () { + DrupalCoffee.field.val('Could not load data, please refresh the page'); + } + }); + + $('.toolbar-icon-coffee').click(function(event) { + event.preventDefault(); + DrupalCoffee.coffee_show(); + }); + // Key events + $(document).keydown(function (event) { + + // Show the form with alt + D. Use 2 keycodes as 'D' can be uppercase or lowercase. + if (DrupalCoffee.wrapper.hasClass('hide-form') && + event.altKey === true && + // 68/206 = d/D, 75 = k. + (event.keyCode === 68 || event.keyCode === 206 || event.keyCode === 75)) { + DrupalCoffee.coffee_show(); + event.preventDefault(); + } + // Close the form with esc or alt + D. + else { + if (!DrupalCoffee.wrapper.hasClass('hide-form') && (event.keyCode === 27 || (event.altKey === true && (event.keyCode === 68 || event.keyCode === 206)))) { + DrupalCoffee.coffee_close(); + event.preventDefault(); + } + } + }); + }); + } + }; + + // Prefix the open and close functions to avoid + // conflicts with autocomplete plugin. + + /** + * Open the form and focus on the search field. + */ + DrupalCoffee.coffee_show = function () { + DrupalCoffee.wrapper.removeClass('hide-form'); + DrupalCoffee.bg.show(); + DrupalCoffee.field.focus(); + $(DrupalCoffee.field).autocomplete({enable: true}); + }; + + /** + * Close the form and destroy all data. + */ + DrupalCoffee.coffee_close = function () { + DrupalCoffee.field.val(''); + //DrupalCoffee.results.empty(); + DrupalCoffee.wrapper.addClass('hide-form'); + DrupalCoffee.bg.hide(); + $(DrupalCoffee.field).autocomplete({enable: false}); + }; + + /** + * Close the Coffee form and redirect. + */ + DrupalCoffee.redirect = function (path, openInNewWindow) { + DrupalCoffee.coffee_close(); + + if (openInNewWindow) { + window.open(path); + } + else { + document.location = path; + } + }; + + /** + * The HTML elements. + */ + DrupalCoffee.label = $('