You are here

toc_node.module in TOC Node 7

Same filename and directory in other branches
  1. 8 toc_node.module

File

toc_node.module
View source
<?php

/**
 * Implementation of hook_form_node_type_form_alter().
 */
function toc_node_form_node_type_form_alter(&$form, $form_state) {
  $type = $form['#node_type'];
  $form['toc_node'] = array(
    '#type' => 'fieldset',
    '#title' => t('Table of contents'),
    '#description' => t('Settings for adding TOC to nodes of this content type.'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#group' => 'additional_settings',
    '#attached' => array(
      'js' => array(
        drupal_get_path('module', 'menu') . '/menu.admin.js',
      ),
    ),
  );
  $form['toc_node']['toc_node_enabled'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable table of contents'),
    '#default_value' => variable_get('toc_node_enabled_' . $type->type, 0),
  );
  $form['toc_node']['toc_node_back_to_top_links'] = array(
    '#type' => 'select',
    '#title' => t('Back to top links'),
    '#default_value' => variable_get('toc_node_back_to_top_links_' . $type->type, 0),
    '#empty_option' => t('- Select -'),
    '#options' => array(
      t('Disabled - always'),
      t('Enabled - always'),
      t('Enabled - per node option'),
      t('Disabled - per node option'),
    ),
  );
  $form['toc_node']['toc_node_level'] = array(
    '#type' => 'select',
    '#title' => t('Level'),
    '#description' => t('Up to what heading level to include.'),
    '#default_value' => variable_get('toc_node_level_' . $type->type, 'h2'),
    '#empty_option' => t('- Select -'),
    '#options' => array(
      2 => 'h2',
      3 => 'h3',
      4 => 'h4',
      5 => 'h5',
      6 => 'h6',
      7 => 'h7',
    ),
  );
  $form['toc_node']['toc_node_styles'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Styles available'),
    '#default_value' => variable_get('toc_node_styles_' . $type->type, array()),
    '#options' => array(
      'none' => t('No TOC'),
      'bullets' => t('Bullets'),
      'numbers' => t('Numbered'),
    ),
  );
  $form['toc_node']['toc_node_style_default'] = array(
    '#type' => 'select',
    '#title' => t('Display default'),
    '#default_value' => variable_get('toc_node_style_default_' . $type->type, 'bullets'),
    '#empty_option' => t('- Select -'),
    '#options' => array(
      'none' => t('No TOC'),
      'bullets' => t('Bullets'),
      'numbers' => t('Numbered'),
    ),
  );
}

/**
 * Implementation of hook_form_node_form_alter().
 */
function toc_node_form_node_form_alter(&$form, &$form_state, $form_id) {
  $type = $form['type']['#value'];
  $toc_enabled = variable_get('toc_node_enabled_' . $type, 0);

  // Only show options if TOC enabled for this content type.
  if (empty($toc_enabled)) {
    return;
  }
  $options_all = array(
    'none' => t('No TOC'),
    'bullets' => t('Bullets'),
    'numbers' => t('Numbered'),
  );
  $options_enabled = variable_get('toc_node_styles_' . $type, array());
  $options = array_intersect_key($options_all, array_flip($options_enabled));
  $style = variable_get('toc_node_style_default_' . $type, 'bullets');
  $back_to_top_links_defaults = variable_get('toc_node_back_to_top_links_' . $type, 0);
  $level = variable_get('toc_node_level_' . $type, 'h2');
  if (isset($form['nid']['#value'])) {
    $node_settings = db_query('SELECT * FROM {toc_node} WHERE nid = :nid', array(
      ':nid' => $form['nid']['#value'],
    ))
      ->fetchAssoc();
    if (!empty($node_settings) && isset($options[$node_settings['style']])) {
      $style = $node_settings['style'];
      $back_to_top_links = $node_settings['back_links'];
      $level = $node_settings['level'];
    }
  }
  $form['toc_node'] = array(
    '#type' => 'fieldset',
    '#title' => t('Table of contents'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#group' => 'additional_settings',
  );
  $form['toc_node']['toc_node_style'] = array(
    '#type' => 'select',
    '#title' => t('TOC Display'),
    '#default_value' => $style,
    '#empty_option' => t('- Select -'),
    '#options' => $options,
  );

  // Check if back to top links should be on as default.
  if (!isset($back_to_top_links) && ($back_to_top_links_defaults == 1 || $back_to_top_links_defaults == 2)) {
    $back_to_top_links = 1;
  }
  elseif (!isset($back_to_top_links) && ($back_to_top_links_defaults == 0 || $back_to_top_links_defaults == 3)) {
    $back_to_top_links = 0;
  }
  $form['toc_node']['toc_node_back_to_top_links'] = array(
    '#type' => 'checkbox',
    '#title' => t('Back to top links'),
    '#default_value' => $back_to_top_links,
  );

  // Check if there is per node options.
  if ($back_to_top_links_defaults < 2) {
    $form['toc_node']['toc_node_back_to_top_links']['#access'] = FALSE;
  }
  $form['toc_node']['toc_node_level'] = array(
    '#type' => 'select',
    '#title' => t('Level'),
    '#description' => t('Up to what heading level to include.'),
    '#default_value' => $level,
    '#empty_option' => t('- Select -'),
    '#options' => array(
      2 => 'h2',
      3 => 'h3',
      4 => 'h4',
      5 => 'h5',
      6 => 'h6',
      7 => 'h7',
    ),
  );
}

/**
 * Implementation of hook_node_insert().
 */
function toc_node_node_insert($node) {
  $toc_enabled = variable_get('toc_node_enabled_' . $node->type, 0);
  if (empty($toc_enabled)) {
    return;
  }
  db_insert('toc_node')
    ->fields(array(
    'nid' => $node->nid,
    'style' => $node->toc_node_style,
    'level' => $node->toc_node_level,
    'back_links' => $node->toc_node_back_to_top_links,
  ))
    ->execute();
}

/**
 * Implementation of hook_node_update().
 */
function toc_node_node_update($node) {
  $toc_enabled = variable_get('toc_node_enabled_' . $node->type, 0);
  if (empty($toc_enabled)) {
    return;
  }

  // check defaults
  if (!isset($node->toc_node_style)) {
    $node->toc_node_style = variable_get('toc_node_style_default_' . $node->type, 'bullets');
  }
  if (!isset($node->toc_node_level)) {
    $node->toc_node_level = variable_get('toc_node_level_' . $node->type, 'h2');
  }
  if (!isset($node->toc_node_back_to_top_links)) {
    $node->toc_node_back_to_top_links = variable_get('toc_node_back_to_top_links_' . $node->type, 0);
  }
  db_merge('toc_node')
    ->key(array(
    'nid' => $node->nid,
  ))
    ->fields(array(
    'nid' => $node->nid,
    'style' => $node->toc_node_style,
    'level' => $node->toc_node_level,
    'back_links' => $node->toc_node_back_to_top_links,
  ))
    ->execute();
}

/**
 * Implementation of hook_node_delete().
 */
function toc_node_node_delete($node) {
  db_delete('toc_node')
    ->condition('nid', $node->nid)
    ->execute();
}

/**
 * Implementation of hook_node_load().
 */
function toc_node_node_load($nodes, $types) {
  $skip = TRUE;
  foreach ($types as $type) {
    $toc_enabled = variable_get('toc_node_enabled_' . $type, 0);
    if (!empty($toc_enabled)) {
      $skip = FALSE;
    }
  }
  if ($skip) {
    return;
  }

  // Gather our extra data for each of these nodes.
  $result = db_query('SELECT * FROM {toc_node} WHERE nid IN(:nids)', array(
    ':nids' => array_keys($nodes),
  ));

  // Add our extra data to the node objects.
  foreach ($result as $record) {
    $nodes[$record->nid]->toc_node_style = $record->style;
    $nodes[$record->nid]->toc_node_level = $record->level;
    $nodes[$record->nid]->toc_node_back_to_top_links = $record->back_links;
  }
}

/**
 * Implementation of hook_theme().
 */
function toc_node_theme() {
  $theme = array(
    'toc_node_move_to_top_link' => array(
      'variables' => array(
        'content' => NULL,
      ),
    ),
    'toc_node_links' => array(
      'variables' => array(
        'links' => NULL,
      ),
      'template' => 'templates/toc_node_links',
    ),
  );
  return $theme;
}

/**
 * Theming functions for 'Move to top' links.
 */
function theme_toc_node_move_to_top_link($variables) {
  $content = $variables['content'];
  $path_alias = drupal_get_path_alias($_GET['q']);
  $link = '<div class="toc-top-links">';
  $link .= l(t('Move to top'), $path_alias, array(
    'fragment' => 'table-of-contents',
  ));
  $link .= '</div>';
  $output = str_ireplace('</h2>', '</h2>' . $link, $content);
  return $output;
}

/**
 * Implements hook_block_info().
 */
function toc_node_block_info() {
  $return = array();
  $return['toc_node'] = array(
    'info' => t('Table Of Contents'),
    'cache' => DRUPAL_CACHE_PER_PAGE,
  );
  return $return;
}

/**
 * Implements hook_block_view().
 */
function toc_node_block_view($delta = '') {
  $block = array();
  switch ($delta) {
    case 'toc_node':
      if (arg(0) != 'node' || !is_numeric(arg(1))) {
        return;
      }

      // Get the nid
      $nid = arg(1);
      $node = node_load($nid);
      $toc_enabled = variable_get('toc_node_enabled_' . $node->type, 0);

      // Check if TOC is enabled for this content type.
      if (empty($toc_enabled)) {
        return;
      }

      // Check if TOC is set to not display.
      if (!isset($node->toc_node_style) || $node->toc_node_style == 'none') {
        return;
      }
      $node_rendered = node_view($node);
      $node_content = drupal_render($node_rendered);
      $content = toc_node_generate($node_content, $node->toc_node_style, $node->toc_node_level, $node->toc_node_back_to_top_links, 'toc_list');
      if ($content != $node_content) {
        $block['subject'] = t('Contents');
        $block['content'] = $content;
      }
      break;
  }
  return $block;
}

/**
 * Generate the TOC.
 *
 * @param $title
 * @param $content
 * @param $type
 * @return string
 */
function toc_node_generate($content, $style, $heading_levels, $back_to_top_links, $return = 'all') {
  static $toc_list_outputed = FALSE;
  $links = array();
  $anchor_count = 0;
  $dom_document = new DOMDocument('1.0', 'utf-8');
  @$dom_document
    ->loadHTML('<?xml encoding="UTF-8"><div id="toc-node">' . $content . '</div>');

  // Add anchors, and TOC tags which are used to work out the order of headings later.
  for ($level = 2; $level <= $heading_levels; $level++) {
    toc_node_add_anchors($dom_document, $anchor_count, $links, $level);
  }
  if (empty($links)) {
    return $content;
  }
  $title = NULL;

  // Create the TOC links.
  $toc = toc_node_links($dom_document, $links, $style);

  // Remove TOC tags.
  toc_node_remove_toc_tags($dom_document, $heading_levels);

  // Output DOM to a string.
  // Unfortunately below PHP 5.3.6 saveHTML() doesn't expect a parameter.
  $content_updated = $dom_document
    ->saveHTML();

  // So we have to remove wrapping tags ourseleves.
  $content_fragments = explode('<div id="toc-node">', $content_updated);
  $output = str_replace('</div></body></html>', '', $content_fragments[1]);

  // Check if table list is being output in a block.
  if ($return == 'toc_list') {
    $toc_list_outputed = TRUE;
    return $toc;
  }

  // If table list has already been output in a block don't output again.
  if (!$toc_list_outputed) {
    $output = $toc . $output;
  }

  // Back to top links.
  if (!empty($back_to_top_links)) {
    $output = theme('toc_node_move_to_top_link', array(
      'content' => $output,
    ));
  }
  return $output;
}

/**
 * Add anchors, gather link data,
 * add TOC tags which are used to work out the order of headings later.
 *
 * @param $dom_document
 * @param $anchor_count
 * @param $links
 * @param $heading_level
 */
function toc_node_add_anchors(&$dom_document, &$anchor_count, &$links, $heading_level) {
  $headers = $dom_document
    ->getElementsByTagName('h' . $heading_level);
  foreach ($headers as $header) {
    $anchor_count++;

    // TOC links.
    $label = trim($header->nodeValue);
    $links['toc-' . $anchor_count]['label'] = $label;
    $links['toc-' . $anchor_count]['level'] = $heading_level;

    // Headings, add class.
    $classes = $header
      ->getAttribute('class');
    $header
      ->setAttribute('class', $classes . ' toc-headings');

    // Anchors above headings.
    $anchor_div = $dom_document
      ->createElement('div');
    $anchor_div
      ->setAttribute('class', 'toc-item-anchor');
    $target = $dom_document
      ->createElement('a');
    $target
      ->setAttribute('name', 'toc-' . $anchor_count);
    $anchor_div
      ->appendChild($target);
    $header->parentNode
      ->insertBefore($anchor_div, $header);
    $toc_tag = $dom_document
      ->createElement('toc');
    $toc_tag
      ->setAttribute('class', 'toc-' . $anchor_count);
    $header
      ->appendChild($toc_tag);
  }
}

/**
 * Create the TOC links, including numbering of content items.
 *
 * @param $dom_document
 * @param $links_data
 * @param $style
 * @return array
 */
function toc_node_links($dom_document, $links_data, $style) {
  $path = drupal_get_path_alias($_GET['q']);
  $toc_number = array();
  $toc_tags = $dom_document
    ->getElementsByTagName('toc');
  foreach ($toc_tags as $toc_tag) {
    $toc_id = $toc_tag->attributes
      ->getNamedItem('class')->value;
    $link = $links_data[$toc_id];

    // TOC always starts at H2, so we need to adjust level here.
    $toc_level = $link['level'] - 1;
    $toc_level_count = count($toc_number);
    for ($i = $link['level']; $i <= $toc_level_count; $i++) {
      unset($toc_number[$i]);
    }
    if (empty($toc_number[$toc_level])) {
      $toc_number[$toc_level] = 1;
    }
    else {
      $toc_number[$toc_level] = $toc_number[$toc_level] + 1;
    }
    $link_number = implode('.', $toc_number);
    $data = '';
    if ($style == 'numbers') {
      $data .= '<span class="toc-number">' . $link_number . '</span> ';
    }
    $data .= '<span class="toc-text">' . $link['label'] . '</span>';
    $links[] = array(
      'data' => l($data, $path, array(
        'fragment' => $toc_id,
        'html' => TRUE,
      )),
      'class' => array(
        'toc-node-level-' . $toc_level,
      ),
    );
  }
  if (empty($links)) {
    return '';
  }
  $item_list = theme('item_list', array(
    'items' => $links,
    'type' => 'ul',
    'attributes' => array(
      'class' => 'toc-node-' . $style,
    ),
  ));
  return theme('toc_node_links', array(
    'links' => $item_list,
  ));
}

/**
 * Remove TOC tags, which were used to work out order and numbering of content items.
 *
 * @param $dom_document
 * @param $heading_levels
 */
function toc_node_remove_toc_tags(&$dom_document, $heading_levels) {
  for ($level = 2; $level <= $heading_levels; $level++) {
    $headers = $dom_document
      ->getElementsByTagName('h' . $level);
    foreach ($headers as $header) {
      $toc = $header
        ->getElementsByTagName('toc');
      $header
        ->removeChild($toc
        ->item(0));
    }
  }
}

/**
 * Implementation of hook_form_node_view_alter().
 */
function toc_node_node_view_alter(&$build) {
  $toc_enabled = variable_get('toc_node_enabled_' . $build['#bundle'], 0);
  $node = $build['#node'];
  if (empty($toc_enabled) || empty($node->toc_node_style) || $node->toc_node_style == 'none') {
    return;
  }

  // Only show TOC on node page.
  if (arg(0) != 'node' || !is_numeric(arg(1)) || arg(1) != $node->nid) {
    return;
  }

  // Add a #post_render callback to act on the rendered HTML of the node.
  $build['#post_render'][] = 'toc_node_node_post_render';
}

/**
 * Post render callback to act on the rendered HTML of the node.
 */
function toc_node_node_post_render($content, $element) {
  $node = $element['#node'];
  $content = toc_node_generate($content, $node->toc_node_style, $node->toc_node_level, $node->toc_node_back_to_top_links);
  return $content;
}

Functions

Namesort descending Description
theme_toc_node_move_to_top_link Theming functions for 'Move to top' links.
toc_node_add_anchors Add anchors, gather link data, add TOC tags which are used to work out the order of headings later.
toc_node_block_info Implements hook_block_info().
toc_node_block_view Implements hook_block_view().
toc_node_form_node_form_alter Implementation of hook_form_node_form_alter().
toc_node_form_node_type_form_alter Implementation of hook_form_node_type_form_alter().
toc_node_generate Generate the TOC.
toc_node_links Create the TOC links, including numbering of content items.
toc_node_node_delete Implementation of hook_node_delete().
toc_node_node_insert Implementation of hook_node_insert().
toc_node_node_load Implementation of hook_node_load().
toc_node_node_post_render Post render callback to act on the rendered HTML of the node.
toc_node_node_update Implementation of hook_node_update().
toc_node_node_view_alter Implementation of hook_form_node_view_alter().
toc_node_remove_toc_tags Remove TOC tags, which were used to work out order and numbering of content items.
toc_node_theme Implementation of hook_theme().