geysir-8.x-1.2/geysir.module
geysir.module
<?php
/**
* @file
* Geysir module file.
*/
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\entity\Routing\AdminHtmlRouteProvider;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
/**
* Implements hook_theme().
*/
function geysir_theme() {
return [
'geysir_field_paragraph_wrapper' => [
'render element' => 'element',
'file' => 'geysir.theme.inc',
],
];
}
/**
* Implements hook_toolbar().
*/
function geysir_toolbar() {
$items = [];
$items['geysir'] = [
'#cache' => [
'contexts' => [
'user.permissions',
],
],
];
if (!Drupal::currentUser()
->hasPermission('geysir manage paragraphs from front-end')) {
return $items;
}
$items['geysir'] += [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => t('Geysir'),
'#attributes' => [
'class' => ['toolbar-icon', 'toolbar-icon-geysir'],
'aria-pressed' => 'false',
'type' => 'button',
],
],
'#wrapper_attributes' => [
'class' => ['geysir-toolbar-tab'],
],
'#attached' => [
'library' => [
'geysir/geysir-toolbar',
],
],
];
return $items;
}
/**
* Implements hook_help().
*/
function geysir_help($route_name, $route_match) {
$output = '';
switch ($route_name) {
// Main module help for the Geysir module.
case 'help.page.geysir':
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Geysir introduces several user interface optimisations which support content authors in their daily workflow. Focus lies on the page building process.') . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dt>' . t('Inserting new Paragraphs from the front-end') . '</dt>';
$output .= '<dd>' . t('Geysir allows authors to insert new Paragraphs without having to go to the Drupal backend. A button is available to <em>add</em> a new Paragraph between existing Paragraphs, this button opens a modal dialog which allows inserting a new Paragraph. The modal first requires you to select a Paragraph Type to add. Then it contains all the fields that are part of the related Paragraph Bundle.') . '</dd>';
$output .= '<dt>' . t('Editing existing Paragraphs from the front-end') . '</dt>';
$output .= '<dd>' . t('Geysir allows authors to edit Paragraphs without having to go to the Drupal backend. When hovering over an existing Paragraph, it gets highlighted and an <em>edit</em> button appears. This button opens a modal dialog which allows editing that particular Paragraph. The modal contains all the fields that are part of the related Paragraph Bundle.') . '</dd>';
$output .= '<dt>' . t('Deleting existing Paragraphs from the front-end') . '</dt>';
$output .= '<dd>' . t('Next to editing Paragraphs, Geysir also allows deletion of existing paragraphs. A <em>delete</em> button is available which allows fast deletion of a Paragraph in a page.') . '</dd>';
$output .= '<h3>' . t('Future additions') . '</h3>';
$output .= '<p>' . t('We plan to continuously add support for new features in the near future, like the introduction of draft versions to be able to work with a page without publishing every action, reordering of paragraphs from the front-end and the reduction of clutter for authors to provide high-fidelity page previews.') . '</p>';
break;
}
return $output;
}
/**
* Implements hook_entity_type_build().
*/
function geysir_entity_type_build(array &$entity_types) {
/* @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
// Add forms for Paragraphs without overriding the default forms.
$entity_types['paragraph']->setFormClass('geysir_delete', '\Drupal\geysir\Form\GeysirParagraphDeleteForm');
$entity_types['paragraph']->setFormClass('geysir_edit', '\Drupal\geysir\Form\GeysirParagraphForm');
$entity_types['paragraph']->setFormClass('geysir_modal_add', '\Drupal\geysir\Form\GeysirModalParagraphAddForm');
$entity_types['paragraph']->setFormClass('geysir_modal_delete', '\Drupal\geysir\Form\GeysirModalParagraphDeleteForm');
$entity_types['paragraph']->setFormClass('geysir_modal_edit', '\Drupal\geysir\Form\GeysirModalParagraphForm');
if (isset($entity_types['paragraph'])) {
$entity_types['paragraph']->setLinkTemplate('delete-form', '/paragraphs/{paragraph}/delete');
$route_provider = $entity_types['paragraph']->getHandlerClass('route_provider');
// During first install, the AdminHtmlRouteProvider class is not available, because entity module is not installed yet.
// After running all updates, clearing the cache will load the AdminHtmlRouteProvider class.
if (class_exists(AdminHtmlRouteProvider::class)) {
$route_provider['html'] = AdminHtmlRouteProvider::class;
}
$entity_types['paragraph']->setHandlerClass('route_provider', $route_provider);
}
}
/**
* Implements hook_preprocess_HOOK().
*
* Using hook_preprocess_paragraph().
*/
function geysir_preprocess_paragraph(&$vars) {
$vars['attributes']['class'][] = 'clearfix';
}
/**
* Gets the first parent that is not a paragraph.
*
* @param Drupal\Core\Entity\EntityInterface $entity
*
* @return Drupal\Core\Entity\EntityInterface $entity
*/
function geysir_get_non_paragraph_parent(EntityInterface $entity) {
if ($entity->getEntityTypeId() != 'paragraph') {
return $entity;
}
else {
/** @var Drupal\paragraphs\Entity\Paragraph $entity */
return geysir_get_non_paragraph_parent($entity->getParentEntity());
}
}
/**
* Implements hook_preprocess_HOOK().
*
* Using hook_preprocess_field().
*/
function geysir_preprocess_field(&$vars) {
if (empty($vars['field_type']) || $vars['field_type'] !== 'entity_reference_revisions') {
return;
}
// Do not apply Geysir on Admin routes. This avoids rendering the buttons
// when a paragraph is set to preview in form mode.
// At the same time check if the admin route is not a Geysir route.
$route = \Drupal::routeMatch()->getRouteName();
if (\Drupal::service('router.admin_context')->isAdminRoute() && strpos($route, 'geysir.') !== 0) {
return;
}
$element = &$vars['element'];
/** @var Drupal\Core\Entity\FieldableEntityInterface $parent */
$parent = $element['#object'];
// Skip nested paragraphs.
if (!($parent instanceof NodeInterface)) {
return;
}
// Check update access for current node + permission to use Geysir.
if (
!$parent->isLatestRevision() ||
!$parent->access('update') ||
!\Drupal::currentUser()->hasPermission('geysir manage paragraphs from front-end')
) {
return;
}
/** @var Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList $field */
$field = $element['#items'];
$field_definition = $field->getFieldDefinition();
$field_storage_definition = $field_definition->getFieldStorageDefinition();
if ($field_storage_definition->getSetting('target_type') !== 'paragraph') {
return;
}
$can_add_more_fields = ($field_storage_definition->getCardinality() == -1 ||
count($vars['items']) < $field_storage_definition->getCardinality());
$field_wrapper_id = Html::getUniqueId('geysir--' . $vars['field_name']);
$langcode = Drupal::languageManager()->getCurrentLanguage()->getId();
$translated_parent = FALSE;
$non_paragraph_parent = geysir_get_non_paragraph_parent($parent);
if ($non_paragraph_parent->isTranslatable()) {
$parent_translation = Drupal::service('entity.repository')
->getTranslationFromContext($non_paragraph_parent);
$translated_parent = !$parent_translation->isDefaultTranslation();
}
$delta = 0;
while (!empty($element[$delta])) {
$links = [];
/** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
$paragraph = $element[$delta]['#paragraph'];
$paragraph_to_cut = $paragraph;
// Use the parent revision id if available, otherwise the parent id.
$parent_revision_id = ($parent->getRevisionId()) ? $parent->getRevisionId() : $parent->id();
if (!$translated_parent && $can_add_more_fields) {
// Add link - before.
$links['add_before'] = [
'title' => t('Add before'),
'url' => Url::fromRoute('geysir.modal.add_form', [
'parent_entity_type' => $parent->getEntityType()->id(),
'parent_entity_bundle' => $parent->bundle(),
'parent_entity_revision' => $parent_revision_id,
'field' => $vars['field_name'],
'field_wrapper_id' => $field_wrapper_id,
'delta' => $delta,
'paragraph' => $paragraph->id(),
'paragraph_revision' => $paragraph->getRevisionId(),
'position' => 'before',
'js' => 'nojs',
]),
'attributes' => [
'class' => ['use-ajax', 'geysir-button'],
'data-dialog-type' => 'modal',
'title' => t('Add before'),
],
];
// Add link - after.
$links['add_after'] = [
'title' => t('Add after'),
'url' => Url::fromRoute('geysir.modal.add_form', [
'parent_entity_type' => $parent->getEntityType()->id(),
'parent_entity_bundle' => $parent->bundle(),
'parent_entity_revision' => $parent_revision_id,
'field' => $vars['field_name'],
'field_wrapper_id' => $field_wrapper_id,
'delta' => $delta,
'paragraph' => $paragraph->id(),
'paragraph_revision' => $paragraph->getRevisionId(),
'position' => 'after',
'js' => 'nojs',
]),
'attributes' => [
'class' => ['use-ajax', 'geysir-button'],
'data-dialog-type' => 'modal',
'title' => t('Add after'),
],
];
}
// Edit link.
$links['edit'] = [
'title' => t('Edit'),
'url' => Url::fromRoute('geysir.modal.edit_form', [
'parent_entity_type' => $parent->getEntityType()->id(),
'parent_entity_bundle' => $parent->bundle(),
'parent_entity_revision' => $parent_revision_id,
'field' => $vars['field_name'],
'field_wrapper_id' => $field_wrapper_id,
'delta' => $delta,
'paragraph' => $paragraph->id(),
'paragraph_revision' => $paragraph->getRevisionId(),
'js' => 'nojs',
]),
'attributes' => [
'class' => ['use-ajax', 'geysir-button'],
'data-dialog-type' => 'modal',
'title' => t('Edit'),
],
];
if ($translated_parent) {
if (
$paragraph->isDefaultTranslation()
) {
$links['edit'] = [
'title' => t('Translate'),
'url' => Url::fromRoute('geysir.modal.translate_form', [
'parent_entity_type' => $parent->getEntityType()->id(),
'parent_entity_bundle' => $parent->bundle(),
'parent_entity_revision' => $parent_revision_id,
'field' => $vars['field_name'],
'field_wrapper_id' => $field_wrapper_id,
'delta' => $delta,
'paragraph' => $paragraph->id(),
'paragraph_revision' => $paragraph->getRevisionId(),
'js' => 'nojs',
]),
'attributes' => [
'class' => ['use-ajax', 'geysir-button'],
'data-dialog-type' => 'modal',
'title' => t('Translate'),
],
];
}
if (!$paragraph->isTranslatable()) {
unset($links['edit']);
}
}
if (!$translated_parent) {
if (count($element['#items']) > 1) {
// Cut link.
$links['cut'] = [
'title' => t('Cut'),
'url' => Url::fromRoute('geysir.cut', [
'parent_entity_type' => $parent->getEntityType()->id(),
'parent_entity_bundle' => $parent->bundle(),
'parent_entity_revision' => $parent_revision_id,
'field' => $vars['field_name'],
'field_wrapper_id' => $field_wrapper_id,
'delta' => $delta,
'paragraph_to_cut' => $paragraph->id(),
'paragraph_revision' => $paragraph->getRevisionId(),
'js' => 'nojs',
]),
'attributes' => [
'data-geysir-field-paragraph-field-wrapper' => $field_wrapper_id,
'class' => ['geysir-button'],
'data-dialog-type' => 'modal',
'title' => t('Cut'),
],
];
}
// Delete link.
$links['delete'] = [
'title' => t('Delete'),
'url' => Url::fromRoute('geysir.modal.delete_form', [
'parent_entity_type' => $parent->getEntityType()->id(),
'parent_entity_bundle' => $parent->bundle(),
'parent_entity_revision' => $parent_revision_id,
'field' => $vars['field_name'],
'field_wrapper_id' => $field_wrapper_id,
'delta' => $delta,
'paragraph' => $paragraph->id(),
'paragraph_revision' => $paragraph->getRevisionId(),
'js' => 'nojs',
]),
'attributes' => [
'class' => ['use-ajax', 'geysir-button'],
'data-dialog-type' => 'modal',
'title' => t('Delete'),
],
];
if ($field_definition->isRequired() && count($element['#items']) == 1) {
$links['delete']['title'] = t('Cannot remove the last item of a required field');
$links['delete']['url'] = Url::fromUserInput('#');
$links['delete']['attributes']['class'][] = 'disabled';
$links['delete']['attributes']['title'] = t('Cannot remove the last item of a required field');
}
// Paste link - before.
$links['paste_before'] = [
'title' => t('Paste'),
'url' => Url::fromRoute('geysir.paste', [
'parent_entity_type' => $parent->getEntityType()->id(),
'parent_entity_bundle' => $parent->bundle(),
'parent_entity_revision' => $parent_revision_id,
'field' => $vars['field_name'],
'field_wrapper_id' => $field_wrapper_id,
'delta' => $delta,
'position' => 'before',
'paragraph_revision' => $paragraph->getRevisionId(),
'paragraph_to_paste' => $paragraph_to_cut->id(),
'js' => 'nojs',
]),
'attributes' => [
'data-geysir-field-paragraph-field-wrapper' => $field_wrapper_id,
'class' => ['use-ajax', 'geysir-button', 'geysir-paste'],
'data-dialog-type' => 'modal',
'title' => t('Paste'),
],
];
// Paste link - after.
$links['paste_after'] = [
'title' => t('Paste'),
'url' => Url::fromRoute('geysir.paste', [
'parent_entity_type' => $parent->getEntityType()->id(),
'parent_entity_bundle' => $parent->bundle(),
'parent_entity_revision' => $parent_revision_id,
'field' => $vars['field_name'],
'field_wrapper_id' => $field_wrapper_id,
'delta' => $delta,
'position' => 'after',
'paragraph_revision' => $paragraph->getRevisionId(),
'paragraph_to_paste' => $paragraph_to_cut->id(),
'js' => 'nojs',
]),
'attributes' => [
'data-geysir-field-paragraph-field-wrapper' => $field_wrapper_id,
'class' => ['use-ajax', 'geysir-button', 'geysir-paste'],
'data-dialog-type' => 'modal',
'title' => t('Paste'),
],
];
}
$context = [
'paragraph' => $paragraph,
'parent' => $parent,
'delta' => $delta,
'field_definition' => $field_definition,
];
\Drupal::moduleHandler()->alter('geysir_paragraph_links', $links, $context);
$links_array = [
'#theme' => 'links',
'#links' => $links,
'#attributes' => [
'class' => [
'geysir-field-paragraph-links',
'links',
],
],
'#attached' => [
'library' => [
'core/drupal.dialog.ajax',
'geysir/geysir',
],
],
];
$vars['items'][$delta]['content']['#theme_wrappers'][] = 'geysir_field_paragraph_wrapper';
$vars['items'][$delta]['content']['#geysir_field_paragraph_links'] = Drupal::service('renderer')
->render($links_array);
/** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
$paragraph = $vars['items'][$delta]['content']['#paragraph'];
if (
$paragraph->isTranslatable() &&
$paragraph->hasTranslation($langcode)
) {
$vars['items'][$delta]['content']['#paragraph'] = $paragraph->getTranslation($langcode);
}
$delta++;
}
// Attach the field wrapper ID in a data-attribute.
$vars['attributes']['data-geysir-field-paragraph-field-wrapper'] = $field_wrapper_id;
}
/**
* Implements hook_preprocess_HOOK().
*
* Using hook_preprocess_node().
*/
function geysir_preprocess_node(&$vars) {
/** @var \Drupal\node\Entity\Node $node */
$node = $vars["node"];
if (
empty($node) ||
!$node->isLatestRevision() ||
!$node->access('update') ||
!Drupal::currentUser()->hasPermission('geysir manage paragraphs from front-end') ||
!$node->isDefaultTranslation()
) {
return;
}
$field_definitions = $node->getFieldDefinitions();
// Check if multiple paragraph fields.
$paragraph_fields = [];
foreach ($field_definitions as $field_definition) {
/** @var \Drupal\Core\Field\BaseFieldDefinition $field_Definition */
if ($field_definition->getType() == 'entity_reference_revisions') {
/** @var \Drupal\field\Entity\FieldStorageConfig $field_storage_definition */
$field_storage_definition = $field_definition->getFieldStorageDefinition();
if ($field_storage_definition->getSetting('target_type') == 'paragraph') {
$paragraph_fields[$field_storage_definition->get('field_name')] = $field_definition->getLabel();
}
}
}
if (empty($paragraph_fields)) {
return;
}
foreach ($paragraph_fields as $field_name => $field_label) {
// Check if the paragraph field already has paragraphs added.
if (isset($vars['content'][$field_name]) && empty($vars['content'][$field_name]['#items'])) {
$field_wrapper_id = Html::getUniqueId('geysir--' . $field_name);
$markup = geysir_get_add_first_paragraph_markup($node, $field_name, $field_wrapper_id, $field_label);
$markup = [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => ['data-geysir-field-paragraph-field-wrapper' => $field_wrapper_id],
'#value' => $markup,
];
$vars['content'][$field_name]['#suffix'] = Drupal::service('renderer')
->render($markup);
}
}
}
/**
* Get the 'Add first paragrah' link.
*
* @param Node $node
* The parent node.
* @param $field_name
* The field name.
* @param $field_wrapper_id
* The wrapper in place for the field.
* @param $field_label
* The field label
*
* @return mixed
* The markup of the link.
*/
function geysir_get_add_first_paragraph_markup(Node $node, $field_name, $field_wrapper_id, $field_label) {
$links['add_before'] = [
'title' => t('Start adding content to %field_label', ['%field_label' => $field_label]),
'url' => Url::fromRoute('geysir.modal.add_form_first', [
'parent_entity_type' => $node->getEntityType()->id(),
'parent_entity_bundle' => $node->bundle(),
'parent_entity_revision' => $node->getRevisionId() ? $node->getRevisionId() : $node->getLoadedRevisionId(),
'field' => $field_name,
'field_wrapper_id' => $field_wrapper_id,
'delta' => 0,
'position' => 'before',
'js' => 'nojs',
]),
'attributes' => [
'class' => ['use-ajax', 'geysir-button'],
'data-dialog-type' => 'modal',
'title' => t('Start adding content to %field_label', ['%field_label' => $field_label]),
],
];
$links_array = [
'#theme' => 'links',
'#links' => $links,
'#attributes' => [
'class' => [
'geysir-field-paragraph-links',
'geysir-field-paragraph-add-first',
'links',
],
],
'#attached' => ['library' => ['core/drupal.dialog.ajax', 'geysir/geysir']],
];
return Drupal::service('renderer')->render($links_array);
}
/**
* Implements hook_preprocess_HOOK().
*
* Adds classes to link elements.
*/
function geysir_preprocess_links(&$variables) {
if (empty($variables['links']) || !is_array($variables['links'])) {
return;
}
foreach ($variables['links'] as $key => $link) {
if (!$key || !is_string($key)) {
continue;
}
$variables['links'][$key]['attributes']['class'][] = Html::cleanCssIdentifier($key);
}
}
