inline_documentation-2.0.0-alpha1/inline_documentation.module
inline_documentation.module
<?php
/**
* @file
* Contains Drupal\inline_documentation\inline_documentation.module.
*/
use Drupal\Core\Form\FormState;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\Component\Utility\Html;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
/**
* Implements hook_form_FORM_ID_alter().
*
* Perform some alterations on the inline_documentation node forms.
*/
function inline_documentation_form_node_form_alter(&$form, FormState $form_state, $form_id) {
switch ($form_id) {
case 'node_inline_documentation_form':
case 'node_inline_documentation_edit_form':
// Create a visibility group in the right-hand column.
$form['visibility'] = [
'#type' => 'details',
'#title' => t('Visibility'),
'#group' => 'advanced',
'#attributes' => [
'class' => ['node-form-options'],
],
'#attached' => [
'library' => ['node/drupal.node'],
],
'#weight' => -10,
'#optional' => FALSE,
'#open' => TRUE,
];
// Move the visibility settings to the new group.
$form['field_inline_doc_page_element']['#group'] = 'visibility';
$form['field_inline_doc_color']['#group'] = 'visibility';
$form['field_inline_doc_pages']['#group'] = 'visibility';
$form['field_inline_doc_weight']['#group'] = 'visibility';
break;
case 'node_inline_documentation_inline_documentation_form':
// Create a button for activating the element selector.
$select_button = [
'select_element' => [
'#type' => 'button',
'#value' => t('Select element'),
'#attributes' => [
'class' => ['inline-documentation-select-element'],
],
],
];
// Add the button as first widget to the field_inline_doc_page_element
// field.
array_unshift($form['field_inline_doc_page_element']['widget'], $select_button);
// Change the field type of the field_inline_doc_color field to 'color'.
$form['field_inline_doc_color']['widget'][0]['value']['#type'] = 'color';
// Get the node object.
$form_object = $form_state->getFormObject();
$node = $form_object->getEntity();
// Set the current url as default value in the field_inline_doc_pages
// field in new nodes.
if ($node->isNew()) {
$form['field_inline_doc_pages']['widget'][0]['value']['#default_value'] = Url::fromRoute('<current>');
}
// Add a custom submit handler for our redirect.
$form['actions']['submit']['#submit'][] = 'inline_documentation_node_edit_form_submit';
// Change the advanced group to the accordion type.
$form['advanced']['#type'] = 'accordion';
// Close the revision information group by default.
$form['revision_information']['#open'] = FALSE;
// Move the meta group to the bottom of the form.
$form['meta']['#weight'] = 21;
// Create a new visibility group.
$form['visibility'] = [
'#type' => 'details',
'#title' => t('Visibility'),
'#group' => 'advanced',
'#attributes' => [
'class' => ['node-form-options'],
],
'#attached' => [
'library' => ['node/drupal.node'],
],
'#weight' => 10,
'#optional' => FALSE,
'#open' => FALSE,
];
// Add these fields to the visibility group.
$form['field_inline_doc_pages']['#group'] = 'visibility';
$form['field_inline_doc_weight']['#group'] = 'visibility';
// Breaky breaky.
break;
}
}
/**
* Submit handler for the node_inline_documentation_inline_documentation_form.
*/
function inline_documentation_node_edit_form_submit($form, FormState &$form_state) {
// Redirect to the current url after submitting.
$form_state->setRedirectUrl(Url::fromRoute('<current>'));
}
/**
* Implements hook_page_bottom().
*/
function inline_documentation_page_bottom(array &$page_bottom) {
// Get the current user.
$user = \Drupal::currentUser();
// Stop if the user hasn't got the permission to view inline documentation.
if (!$user->hasPermission('view inline documentation overview')) {
return;
}
$page_whitelist = \Drupal::config('inline_documentation.settings')->get('inline_documentation_page_whitelist');
$page_blacklist = \Drupal::config('inline_documentation.settings')->get('inline_documentation_page_blacklist');
$show_inline_documentation_overview = FALSE;
$pages = inline_documentation_get_current_page();
if (!empty($pages)) {
foreach ($pages as $page) {
if (empty($page_whitelist) || \Drupal::service('path.matcher')->matchPath($page, $page_whitelist)) {
$show_inline_documentation_overview = TRUE;
}
if (!empty($page_blacklist) && \Drupal::service('path.matcher')->matchPath($page, $page_blacklist)) {
$show_inline_documentation_overview = FALSE;
break;
}
}
}
if (!$show_inline_documentation_overview) {
return;
}
// This array will contain the inline documentation nodes in a renderable
// array.
$renderable_nodes = [];
// Load inline documentation for the current page.
$nodes = inline_documentation_load_by_page();
// Check if there's documentation.
if (!empty($nodes)) {
foreach ($nodes as $node) {
// Only show unpublished nodes to its owner or anyone with the
// administer nodes permission.
if (
!$node->isPublished() &&
$node->getOwnerId() != $user->id() &&
!$user->hasPermission('administer nodes')
) {
continue;
}
// Load the inline_documentation view display.
$view_builder = \Drupal::entityTypeManager()->getViewBuilder('node');
$renderable_nodes[] = $view_builder->view($node, 'inline_documentation', $node->language()->getId());
}
}
$form = inline_documentation_get_node_form();
// Don't show anything if no nodes and no edit form are found.
if (empty($renderable_nodes) && empty($form)) {
return;
}
// Add the inline documentation to the page.
$page_bottom['inline_documentation'] = [
'#theme' => 'inline_documentation_overview',
'#nodes' => $renderable_nodes,
'#form' => $form,
'#attached' => [
'library' => 'inline_documentation/inline_documentation',
],
];
}
/**
* Loads the node add/edit form for an inline documentation node.
*
* @return array|NULL
* A renderable array containing a form or NULL.
*/
function inline_documentation_get_node_form() {
// Get the current user.
$user = \Drupal::currentUser();
// First, create an empty node of the type inline_documentation.
$node = \Drupal::entityTypeManager()
->getStorage('node')
->create(['type' => 'inline_documentation']);
// Try to retrieve the edit-inline-doc param from the url.
$edit_nid = \Drupal::request()->get('edit-inline-doc');
// Check if the user may create a new node if no nid to edit is given.
if (empty($edit_nid) && !$node->access('create', $user)) {
return NULL;
}
// Check if it's not empty.
if (!empty($edit_nid)) {
// Check if the user may update the node.
if (!$node->access('update', $user)) {
return NULL;
}
// Load the node.
$node = \Drupal::entityTypeManager()
->getStorage('node')
->load($edit_nid);
// Check if the node is of the type inline_documentation.
if ($node->getType() != 'inline_documentation') {
return NULL;
}
}
// Generate the form.
$form = \Drupal::service('entity.form_builder')->getForm($node, 'inline_documentation');
// Create a form title.
$form_title = t('Add Inline Documentation');
if (!$node->isNew()) {
$form_title = t('Edit Inline Documentation');
$form['#attributes']['class'][] = 'edit-form';
}
// Add the title to the form.
$form['form_title'] = [
'#markup' => '<h2>' . $form_title . '</h2>',
'#weight' => -10,
];
// Force the form to use the not-admin theme.
$form['#theme'] = ['node_inline_documentation_inline_documentation_form', 'node_form'];
// Force the meta group to become a details element.
if (isset($form['meta'])) {
$form['meta']['#theme_wrappers'] = ['details'];
$form['meta']['#summary_attributes'] = [];
unset($form['meta']['#attributes']['class']);
}
return $form;
}
/**
* Creates html containing an error message.
*
* @param string $message
* The error message.
*
* @return array
* Renderable array with a title, error message and back link.
*/
function inline_documentation_form_error($message) {
$form = [
'title' => [
'#markup' => '<h2>' . t('Error') . '</h2>',
],
'error' => [
'#markup' => '<p>' . $message . '</p>',
'#prefix' => '<div class="error">',
'#suffix' => '</div>',
],
'back' => [
'#type' => 'link',
'#title' => t('Go back'),
'#url' => Url::fromRoute('<current>'),
],
];
return $form;
}
/**
* Implements hook_entity_type_build().
*/
function inline_documentation_entity_type_build(array &$entity_types) {
// Add our custom form display mode to the classes so the display can be
// called programmatically.
$entity_types['node']->setFormClass('inline_documentation', 'Drupal\node\NodeForm');
}
/**
* Implements hook_preprocess_node().
*/
function inline_documentation_preprocess_node(&$variables) {
// Check for the inline_documentation node type.
if ($variables['node']->getType() == 'inline_documentation' && $variables['view_mode'] == 'inline_documentation') {
// Get the value of field_inline_doc_page_element.
$field_inline_doc_page_element = $variables['node']->get('field_inline_doc_page_element')->getValue();
// Add the value as a data attribute.
if (!empty($field_inline_doc_page_element[0]['value'])) {
$variables['attributes']['data-field-inline-doc-page-element'] = $field_inline_doc_page_element[0]['value'];
}
// Get the value of field_inline_doc_color.
$field_inline_doc_color = $variables['node']->get('field_inline_doc_color')->getValue();
// Add the value as a data attribute.
if (!empty($field_inline_doc_color[0]['value'])) {
$variables['attributes']['data-field-inline-doc-color'] = $field_inline_doc_color[0]['value'];
$variables['attributes']['style'] = 'border-color: ' . $field_inline_doc_color[0]['value'];
}
// Add extra class if the node is not published.
if (!$variables['node']->isPublished()) {
$variables['attributes']['class'][] = 'unpublished';
}
}
}
/**
* Helper function to load inline documentation by page(s).
*
* @param array $pages
* Contains page paths to load inline documentation for.
* @param bool $load_entities
* Defines if node IDs or fully loaded node objects should be returned.
*
* @return array
* Inline documentations IDs or node objects for the given page(s).
*/
function inline_documentation_load_by_page(array $pages = [], $load_entities = TRUE) {
// Get the current path if no pages are given.
if (empty($pages)) {
$pages = inline_documentation_get_current_page();
}
// Set up a node entity query.
$query = \Drupal::entityQuery('node')
->accessCheck(TRUE)
->condition('type', 'inline_documentation');
// Create an or group because we want to check each path individually.
$orGroup = $query->orConditionGroup();
foreach ($pages as $page) {
$orGroup->condition('field_inline_doc_pages', '%' . $page . "\r\n%", 'LIKE');
}
// Also, check for an empty string indicating the documentation should be
// displayed on all pages.
$orGroup->condition('field_inline_doc_pages', NULL, 'IS NULL');
// Add the or group to the query.
$query->condition($orGroup);
$query->sort('field_inline_doc_weight', 'ASC');
// Execute the query and retrieve the node ids.
$query->accessCheck();
$nids = $query->execute();
// Load entity objects if enabled.
if ($load_entities) {
$nodes = Node::loadMultiple($nids);
return $nodes;
}
return $nids;
}
/**
* Gets the current page url in multiple formats.
*
* A page array will be created containing the following formats:
* - <front> if it's the frontpage.
* - The request uri as shown in the browser.
* - The internal path.
* - Placeholderized versions of the request uri and internal path.
*
* Example: /blog/category/title
* Returns: [
* /blog/category/title
* /blog/category/*
* /blog/*
* /node/3
* /node/*
* ]
*
* @return array
* An array containing all possible urls of the current page.
*/
function inline_documentation_get_current_page() {
$pages = [];
$current_url = Url::fromRoute('<current>');
$internal_path = '/' . $current_url->getInternalPath();
$request_uri = $current_url->toString();
// Check if the current page is the frontpage.
if (\Drupal::service('path.matcher')->isFrontPage()) {
$pages['<front>'] = '<front>';
}
else {
// Add the internal path to the pages array.
$pages[$internal_path] = $internal_path;
// Replace all parts with *.
$pages += inline_documentation_placeholderize_path($internal_path);
// Add the uri as shown in the browser to the pages array.
$pages[$request_uri] = $request_uri;
// Replace all parts with *.
$pages += inline_documentation_placeholderize_path($request_uri);
}
return $pages;
}
/**
* Break down a path into multiple paths with placeholders.
*
* Example: /blog/category/title becomes:
* - /blog/category/*
* - /blog/*
* - /blog/*\/title
*
* @param string $path
* Contains a url path starting with a slash.
*
* @return array
* An array containing multiple paths with placeholders.
*/
function inline_documentation_placeholderize_path($path) {
$pages = [];
// Split the path in slashes.
$slashes = explode('/', substr($path, 1));
// For every slash, create a new path with placeholder to search for.
foreach ($slashes as $key => $path_part) {
$page = '';
foreach ($slashes as $key2 => $path_part2) {
if ($key2 < $key) {
$page .= '/' . $path_part2;
}
elseif (!empty($page) && substr($page, -2) != '/*') {
$page .= '/*';
}
}
if (!empty($page)) {
$pages[$page] = $page;
}
}
// Go through the path one more time to replace the middle parts with *.
foreach ($slashes as $key => $path_part) {
if ($key > 0) {
$slashes2 = $slashes;
$slashes2[$key] = '*';
$page = '/' . implode('/', $slashes2);
$pages[$page] = $page;
}
}
return $pages;
}
/**
* Implements hook_entity_presave().
*
* Force a newline after the field_inline_doc_pages value.
*/
function inline_documentation_entity_presave(EntityInterface $entity) {
// Only do this on inline_documentation nodes.
if ($entity->bundle() == 'inline_documentation') {
// Explode the pages on newlines.
$pages = explode("\r\n", $entity->field_inline_doc_pages->value);
// Create new string for the sanitized pages.
$new_pages = '';
// Check if there are pages.
if (!empty($pages)) {
// Iterate through them.
foreach ($pages as $page) {
// Remove surrounding whitespaces.
$page = trim($page);
// Add it to the new var if it's not empty.
if (!empty($page)) {
$new_pages .= $page . "\r\n";
}
}
}
// Save the new value.
$entity->field_inline_doc_pages->value = $new_pages;
}
}
/**
* Implements hook_theme().
*/
function inline_documentation_theme($existing, $type, $theme, $path) {
return [
'inline_documentation_overview' => [
'variables' => [
'nodes' => NULL,
'form' => NULL,
],
],
'node__inline_documentation__inline_documentation' => [
'template' => 'node--inline-documentation--inline-documentation',
'base hook' => 'node',
],
];
}
/**
* Implements hook_node_view_alter().
*
* Add metadata to the contextual links so the node edit url can be changed in
* hook_contextual_view_alter().
*/
function inline_documentation_node_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
$bundle = $entity->bundle();
// Check if the node is of the type inline_documentation and the view mode
// also is inline_documentation. Also check if the node has contextual links.
if ($bundle == 'inline_documentation' && $build['#view_mode'] == 'inline_documentation' && isset($build['#contextual_links']['node'])) {
// Put the node type and view mode in the contextual metadata.
$build['#contextual_links']['node']['metadata']['bundle'] = $bundle;
$build['#contextual_links']['node']['metadata']['view_mode'] = $build['#view_mode'];
// Get the current url + query as shown in the browser.
$edit_url = \Drupal::request()->getRequestUri();
// Add a ? or & to the url.
if (strstr($edit_url, '?')) {
$edit_url .= '&';
}
else {
$edit_url .= '?';
}
// Add the inline doc to edit to the url.
$edit_url .= 'edit-inline-doc=' . $entity->id();
// Add the url to the metadata so it can be read by the alter hook.
$build['#contextual_links']['node']['metadata']['edit_url'] = $edit_url;
}
}
/**
* Implements hook_contextual_links_view_alter().
*
* Set a new node edit url in the contextual links for inline documentation
* nodes in the overview.
*/
function inline_documentation_contextual_links_view_alter(&$element, $items) {
// Check if the node bundle and view mode are set in the metadata and check
// if they contain 'inline_documentation'. Also check if the edit url is set.
if (
isset($element['#contextual_links']['node']['metadata']['bundle']) &&
$element['#contextual_links']['node']['metadata']['bundle'] == 'inline_documentation' &&
isset($element['#contextual_links']['node']['metadata']['view_mode']) &&
$element['#contextual_links']['node']['metadata']['view_mode'] == 'inline_documentation' &&
isset($element['#contextual_links']['node']['metadata']['edit_url'])
) {
// Get the node edit url.
$edit_url = $element['#contextual_links']['node']['metadata']['edit_url'];
// Overwrite the node edit url with ours.
if (!empty($element['#links']['entitynodeedit-form']['url'])) {
$element['#links']['entitynodeedit-form']['url'] = Url::fromUserInput($edit_url);
}
}
}
/**
* Implements hook_help().
*/
function inline_documentation_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.inline_documentation':
$text = file_get_contents(__DIR__ . '/README.md');
if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
return '<pre>' . Html::escape($text) . '</pre>';
}
else {
// Use the Markdown filter to render the README.
$filter_manager = \Drupal::service('plugin.manager.filter');
$settings = \Drupal::configFactory()->get('markdown.settings')->getRawData();
$config = ['settings' => $settings];
$filter = $filter_manager->createInstance('markdown', $config);
return $filter->process($text, 'en');
}
}
return NULL;
}
