clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\tour\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a tour item annotation object.
*
* Plugin Namespace: Plugin\tour\tip
*
* For a working example, see \Drupal\tour\Plugin\tour\tip\TipPluginText
*
* @see \Drupal\tour\TipPluginBase
* @see \Drupal\tour\TipPluginInterface
* @see \Drupal\tour\TipPluginManager
* @see plugin_api
*
* @Annotation
*/
class Tip extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The title of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title;
}

View File

@@ -0,0 +1,189 @@
<?php
namespace Drupal\tour\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\tour\TipsPluginCollection;
use Drupal\tour\TourInterface;
/**
* Defines the configured tour entity.
*
* @ConfigEntityType(
* id = "tour",
* label = @Translation("Tour"),
* label_collection = @Translation("Tours"),
* label_singular = @Translation("tour"),
* label_plural = @Translation("tours"),
* label_count = @PluralTranslation(
* singular = "@count tour",
* plural = "@count tours",
* ),
* handlers = {
* "view_builder" = "Drupal\tour\TourViewBuilder",
* "access" = "Drupal\tour\TourAccessControlHandler",
* },
* admin_permission = "administer site configuration",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "module",
* "routes",
* "tips",
* },
* lookup_keys = {
* "routes.*.route_name"
* }
* )
*/
class Tour extends ConfigEntityBase implements TourInterface {
/**
* The name (plugin ID) of the tour.
*
* @var string
*/
protected $id;
/**
* The module which this tour is assigned to.
*
* @var string
*/
protected $module;
/**
* The label of the tour.
*
* @var string
*/
protected $label;
/**
* The routes on which this tour should be displayed.
*
* @var array
*/
protected $routes = [];
/**
* The routes on which this tour should be displayed, keyed by route id.
*
* @var array
*/
protected $keyedRoutes;
/**
* Holds the collection of tips that are attached to this tour.
*
* @var \Drupal\tour\TipsPluginCollection
*/
protected $tipsCollection;
/**
* The array of plugin config, only used for export and to populate the $tipsCollection.
*
* @var array
*/
protected $tips = [];
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
$this->tipsCollection = new TipsPluginCollection(\Drupal::service('plugin.manager.tour.tip'), $this->tips);
}
/**
* {@inheritdoc}
*/
public function getRoutes() {
return $this->routes;
}
/**
* {@inheritdoc}
*/
public function getTip($id) {
return $this->tipsCollection->get($id);
}
/**
* {@inheritdoc}
*/
public function getTips() {
$tips = [];
foreach ($this->tips as $id => $tip) {
$tips[] = $this->getTip($id);
}
uasort($tips, function ($a, $b) {
return $a->getWeight() <=> $b->getWeight();
});
\Drupal::moduleHandler()->alter('tour_tips', $tips, $this);
return array_values($tips);
}
/**
* {@inheritdoc}
*/
public function getModule() {
return $this->module;
}
/**
* {@inheritdoc}
*/
public function hasMatchingRoute($route_name, $route_params) {
if (!isset($this->keyedRoutes)) {
$this->keyedRoutes = [];
foreach ($this->getRoutes() as $route) {
$this->keyedRoutes[$route['route_name']] = $route['route_params'] ?? [];
}
}
if (!isset($this->keyedRoutes[$route_name])) {
// We don't know about this route.
return FALSE;
}
if (empty($this->keyedRoutes[$route_name])) {
// We don't need to worry about route params, the route name is enough.
return TRUE;
}
foreach ($this->keyedRoutes[$route_name] as $key => $value) {
// If a required param is missing or doesn't match, return FALSE.
if (empty($route_params[$key]) || $route_params[$key] !== $value) {
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function resetKeyedRoutes() {
unset($this->keyedRoutes);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
foreach ($this->tipsCollection as $instance) {
$definition = $instance->getPluginDefinition();
$this->addDependency('module', $definition['provider']);
}
$this->addDependency('module', $this->module);
return $this;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Drupal\tour\Plugin\HelpSection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the tours list section for the help page.
*
* @HelpSection(
* id = "tour",
* title = @Translation("Tours"),
* weight = 10,
* description = @Translation("Tours guide you through workflows or explain concepts on various user interface pages. The tours with links in this list are on user interface landing pages; the tours without links will show on individual pages (such as when editing a View using the Views UI module). Available tours:"),
* permission = "access tour"
* )
*/
class TourHelpSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a TourHelpSection object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// The calculation of which URL (if any) gets put on which tour depends
// on a route access check. This can have a lot of inputs, including user
// permissions and other factors. Rather than doing a complicated
// accounting of the cache metadata for all of these possible factors, set
// the max age of the cache to zero to prevent using incorrect cached
// information.
return 0;
}
/**
* {@inheritdoc}
*/
public function listTopics() {
/** @var \Drupal\tour\TourInterface[] $tours */
$tours = $this->entityTypeManager->getStorage('tour')->loadMultiple();
// Sort in the manner defined by Tour.
uasort($tours, ['Drupal\tour\Entity\Tour', 'sort']);
// Make a link to each tour, using the first of its routes that can
// be linked to by this user, if any.
$topics = [];
foreach ($tours as $tour) {
$title = $tour->label();
$id = $tour->id();
$routes = $tour->getRoutes();
$made_link = FALSE;
foreach ($routes as $route) {
// Some tours are for routes with parameters. For instance, there is
// currently a tour in the Language module for the language edit page,
// which appears on all pages with URLs like:
// /admin/config/regional/language/edit/LANGCODE.
// There is no way to make a link to the page that displays the tour,
// because it is a set of pages. The easiest way to detect this is to
// use a try/catch exception -- try to make a link, and it will error
// out with a missing parameter exception if the route leads to a set
// of pages instead of a single page.
try {
$params = $route['route_params'] ?? [];
$url = Url::fromRoute($route['route_name'], $params);
// Skip this route if the current user cannot access it.
if (!$url->access()) {
continue;
}
// Generate the link HTML directly, using toString(), to catch
// missing parameter exceptions now instead of at render time.
$topics[$id] = Link::fromTextAndUrl($title, $url)->toString();
// If the line above didn't generate an exception, we have a good
// link that the user can access.
$made_link = TRUE;
break;
}
catch (\Exception $e) {
// Exceptions are normally due to routes that need parameters. If
// there is an exception, just try the next route and see if we can
// find one that will work for us.
}
}
if (!$made_link) {
// None of the routes worked to make a link, so at least display the
// tour title.
$topics[$id] = $title;
}
}
return $topics;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Drupal\tour\Plugin\tour\tip;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Utility\Token;
use Drupal\tour\TipPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Displays some text as a tip.
*
* @Tip(
* id = "text",
* title = @Translation("Text")
* )
*/
class TipPluginText extends TipPluginBase implements ContainerFactoryPluginInterface {
/**
* The body text which is used for render of this Text Tip.
*
* @var string
*/
protected $body;
/**
* Token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* Constructs a \Drupal\tour\Plugin\tour\tip\TipPluginText object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Utility\Token $token
* The token service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Token $token) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->token = $token;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('token'));
}
/**
* {@inheritdoc}
*/
public function getBody(): array {
return [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->token->replace($this->get('body')),
'#attributes' => [
'class' => ['tour-tip-body'],
],
];
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Drupal\tour;
use Drupal\Core\Plugin\PluginBase;
/**
* Defines a base tour item implementation.
*
* @see \Drupal\tour\Annotation\Tip
* @see \Drupal\tour\TipPluginInterface
* @see \Drupal\tour\TipPluginManager
* @see plugin_api
*/
abstract class TipPluginBase extends PluginBase implements TipPluginInterface {
/**
* The label which is used for render of this tip.
*
* @var string
*/
protected $label;
/**
* Allows tips to take more priority that others.
*
* @var string
*/
protected $weight;
/**
* {@inheritdoc}
*/
public function id() {
return $this->get('id');
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->get('label');
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->get('weight');
}
/**
* {@inheritdoc}
*/
public function get($key) {
if (!empty($this->configuration[$key])) {
return $this->configuration[$key];
}
}
/**
* {@inheritdoc}
*/
public function set($key, $value) {
$this->configuration[$key] = $value;
}
/**
* {@inheritdoc}
*/
public function getLocation(): ?string {
$location = $this->get('position');
// The location values accepted by PopperJS, the library used for
// positioning the tip.
assert(in_array(trim($location ?? ''), [
'auto',
'auto-start',
'auto-end',
'top',
'top-start',
'top-end',
'bottom',
'bottom-start',
'bottom-end',
'right',
'right-start',
'right-end',
'left',
'left-start',
'left-end',
'',
], TRUE), "$location is not a valid Tour Tip position value");
return $location;
}
/**
* {@inheritdoc}
*/
public function getSelector(): ?string {
return $this->get('selector');
}
/**
* {@inheritdoc}
*/
public function getBody(): array {
return [];
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Drupal\tour;
/**
* Defines an interface for tour items.
*
* @see \Drupal\tour\Annotation\Tip
* @see \Drupal\tour\TipPluginBase
* @see \Drupal\tour\TipPluginManager
* @see plugin_api
*/
interface TipPluginInterface {
/**
* Returns id of the tip.
*
* @return string
* The id of the tip.
*/
public function id();
/**
* Returns label of the tip.
*
* @return string
* The label of the tip.
*/
public function getLabel();
/**
* Returns weight of the tip.
*
* @return string
* The weight of the tip.
*/
public function getWeight();
/**
* Used for returning values by key.
*
* @var string
* Key of the value.
*
* @return string
* Value of the key.
*/
public function get($key);
/**
* Returns the selector the tour tip will attach to.
*
* This typically maps to the Shepherd Step options `attachTo.element`
* property.
*
* @return null|string
* A selector string, or null for an unattached tip.
*
* @see https://shepherdjs.dev/docs/Step.html
*/
public function getSelector(): ?string;
/**
* Returns the body content of the tooltip.
*
* This typically maps to the Shepherd Step options `text` property.
*
* @return array
* A render array.
*
* @see https://shepherdjs.dev/docs/Step.html
*/
public function getBody(): array;
/**
* Returns the configured placement of the tip relative to the element.
*
* If null, the tip will automatically determine the best position based on
* the element's position in the viewport.
*
* This typically maps to the Shepherd Step options `attachTo.on` property.
*
* @return string|null
* The tip placement relative to the element.
*
* @see https://shepherdjs.dev/docs/Step.html
*/
public function getLocation(): ?string;
/**
* Used for returning values by key.
*
* @var string
* Key of the value.
*
* @var string
* Value of the key.
*/
public function set($key, $value);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\tour;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Provides a plugin manager for tour items.
*
* @see \Drupal\tour\Annotation\Tip
* @see \Drupal\tour\TipPluginBase
* @see \Drupal\tour\TipPluginInterface
* @see plugin_api
*/
class TipPluginManager extends DefaultPluginManager {
/**
* Constructs a new TipPluginManager.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations,
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/tour/tip', $namespaces, $module_handler, 'Drupal\tour\TipPluginInterface', 'Drupal\tour\Annotation\Tip');
$this->alterInfo('tour_tips_info');
$this->setCacheBackend($cache_backend, 'tour_plugins');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\tour;
use Drupal\Core\Plugin\DefaultLazyPluginCollection;
/**
* A collection of tips.
*/
class TipsPluginCollection extends DefaultLazyPluginCollection {
/**
* {@inheritdoc}
*/
protected $pluginKey = 'plugin';
/**
* {@inheritdoc}
*
* @return \Drupal\tour\TipPluginInterface
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\tour;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the tour entity type.
*
* @see \Drupal\tour\Entity\Tour
*/
class TourAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($operation === 'view') {
return AccessResult::allowedIfHasPermissions($account, ['access tour', 'administer site configuration'], 'OR');
}
return parent::checkAccess($entity, $operation, $account);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Drupal\tour;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a tour entity.
*/
interface TourInterface extends ConfigEntityInterface {
/**
* The routes that this tour will appear on.
*
* @return array
* Returns array of routes for the tour.
*/
public function getRoutes();
/**
* Whether the tour matches a given set of route parameters.
*
* @param string $route_name
* The route name the parameters are for.
* @param array $route_params
* Associative array of raw route params.
*
* @return bool
* TRUE if the tour matches the route parameters.
*/
public function hasMatchingRoute($route_name, $route_params);
/**
* Returns tip plugin.
*
* @param string $id
* The identifier of the tip.
*
* @return \Drupal\tour\TipPluginInterface
* The tip plugin.
*/
public function getTip($id);
/**
* Returns the tips for this tour.
*
* @return array
* An array of tip plugins.
*/
public function getTips();
/**
* Gets the module this tour belongs to.
*
* @return string
* The module this tour belongs to.
*/
public function getModule();
/**
* Resets the statically cached keyed routes.
*/
public function resetKeyedRoutes();
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\tour;
@trigger_error('The ' . __NAMESPACE__ . '\TourTipPluginInterface is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Implement ' . __NAMESPACE__ . '\TipPluginInterface instead. See https://www.drupal.org/node/3340701', E_USER_DEPRECATED);
/**
* Defines an interface for tour items.
*
* @see \Drupal\tour\Annotation\Tip
* @see \Drupal\tour\TipPluginBase
* @see \Drupal\tour\TipPluginManager
* @see plugin_api
*
* @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Implements
* TipPluginInterface instead.
*
* @see https://www.drupal.org/node/3340701
*/
interface TourTipPluginInterface extends TipPluginInterface {}

View File

@@ -0,0 +1,139 @@
<?php
namespace Drupal\tour;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Component\Utility\Html;
/**
* Provides a Tour view builder.
*
* Note: Does not invoke any alter hooks. In other view
* builders, the view alter hooks are run later in the process
*/
class TourViewBuilder extends EntityViewBuilder {
/**
* {@inheritdoc}
*/
public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) {
/** @var \Drupal\tour\TourInterface[] $entities */
$tour = [];
$cache_tags = [];
$total_tips = 0;
foreach ($entities as $entity_id => $entity) {
$tour[$entity_id] = $entity->getTips();
$total_tips += count($tour[$entity_id]);
$cache_tags = Cache::mergeTags($cache_tags, $entity->getCacheTags());
}
$items = [];
foreach ($tour as $tour_id => $tips) {
$tourEntity = $entities[$tour_id];
foreach ($tips as $index => $tip) {
$classes = [
'tip-module-' . Html::getClass($tourEntity->getModule()),
'tip-type-' . Html::getClass($tip->getPluginId()),
'tip-' . Html::getClass($tip->id()),
];
$selector = $tip->getSelector();
$location = $tip->getLocation();
$body_render_array = $tip->getBody();
$body = (string) \Drupal::service('renderer')->renderInIsolation($body_render_array);
$output = [
'body' => $body,
'title' => $tip->getLabel(),
];
$selector = $tip->getSelector();
if ($output) {
$items[] = [
'id' => $tip->id(),
'selector' => $selector,
'module' => $tourEntity->getModule(),
'type' => $tip->getPluginId(),
'counter' => $this->t('@tour_item of @total', [
'@tour_item' => $index + 1,
'@total' => $total_tips,
]),
'attachTo' => [
'element' => $selector,
'on' => $location ?? 'bottom-start',
],
// Shepherd expects classes to be provided as a string.
'classes' => implode(' ', $classes),
] + $output;
}
}
}
// If there is at least one tour item, build the tour.
if ($items) {
$key = array_key_last($items);
$items[$key]['cancelText'] = t('End tour');
}
$build = [
'#cache' => [
'tags' => $cache_tags,
],
];
// If at least one tour was built, attach tips and the tour library.
if ($items) {
$build['#attached']['drupalSettings']['tourShepherdConfig'] = [
'defaultStepOptions' => [
'classes' => 'drupal-tour',
'cancelIcon' => [
'enabled' => TRUE,
'label' => $this->t('Close'),
],
'modalOverlayOpeningPadding' => 3,
'scrollTo' => [
'behavior' => 'smooth',
'block' => 'center',
],
'popperOptions' => [
'modifiers' => [
// Prevent overlap with the element being highlighted.
[
'name' => 'offset',
'options' => [
'offset' => [-10, 20],
],
],
// Pad the arrows so they don't hit the edge of rounded corners.
[
'name' => 'arrow',
'options' => [
'padding' => 12,
],
],
// Disable Shepherd's focusAfterRender modifier, which results in
// the tour item container being focused on any scroll or resize
// event.
[
'name' => 'focusAfterRender',
'enabled' => FALSE,
],
],
],
],
'useModalOverlay' => TRUE,
];
// This property is used for storing the tour items. It may change without
// notice and should not be extended or modified in contrib.
// see: https://www.drupal.org/project/drupal/issues/3214593
$build['#attached']['drupalSettings']['_tour_internal'] = $items;
$build['#attached']['library'][] = 'tour/tour';
}
return $build;
}
}