You are here

sheetnode.module in Sheetnode 7.2

Same filename and directory in other branches
  1. 5 sheetnode.module
  2. 6 sheetnode.module
  3. 7 sheetnode.module

Module file for the sheetnode module.

File

sheetnode.module
View source
<?php

/**
 * @file
 * Module file for the sheetnode module.
 */

/**
 * Implements hook_node_info().
 */
function sheetnode_node_info() {
  return array(
    'sheetnode' => array(
      'name' => t('Sheetnode'),
      'module' => 'sheetnode',
      'description' => t('A spreadsheet node.'),
      'base' => 'sheetnode',
    ),
  );
}

/**
 * Implements hook_permission().
 */
function sheetnode_permission() {
  return array(
    'create sheetnode' => array(
      'title' => t('Create new sheetnode'),
      'description' => t('Create new sheetnodes.'),
    ),
    'edit own sheetnode' => array(
      'title' => t('Edit own sheetnode'),
      'description' => t('Edit own sheetnodes.'),
    ),
    'edit any sheetnode' => array(
      'title' => t('Edit any sheetnode'),
      'description' => t('Edit any sheetnodes.'),
    ),
    'delete own sheetnode' => array(
      'title' => t('Delete own sheetnode'),
      'description' => t('Delete own sheetnodes.'),
    ),
    'delete any sheetnode' => array(
      'title' => t('Delete any sheetnode'),
      'description' => t('Delete any sheetnodes.'),
    ),
    'create sheetnode template' => array(
      'title' => t('Create sheetnode template'),
      'description' => t('Create sheetnode templates.'),
    ),
    'edit sheetnode settings' => array(
      'title' => t('Edit sheetnode settings'),
      'description' => t('Modify sheetnode settings and formatting.'),
    ),
  );
}

/**
 * Implements hook_node_access().
 */
function sheetnode_node_access($op, $node, $account) {
  if (is_string($node)) {
    if ($node != 'sheetnode') {
      return NODE_ACCESS_IGNORE;
    }
  }
  if ($op == 'create') {
    return user_access('create sheetnode', $account) ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY;
  }
  if ($op == 'update') {
    return user_access('edit any sheetnode', $account) || user_access('edit own sheetnode', $account) && $account->uid == $node->uid ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY;
  }
  if ($op == 'delete') {
    return user_access('delete any sheetnode', $account) || user_access('delete own sheetnode', $account) && $account->uid == $node->uid ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY;
  }
}

/**
 * Implements hook_load().
 */
function sheetnode_load($nodes) {
  $vids = array();
  foreach ($nodes as $node) {
    $vids[] = $node->vid;
    $nodes[$node->nid]->sheetnode = array(
      'nid' => $node->nid,
      'vid' => $node->vid,
      'value' => NULL,
    );
  }
  if (empty($vids)) {
    return;
  }
  $sheetnodes = db_query("SELECT * FROM {sheetnode} WHERE vid IN (:vids)", array(
    ':vids' => $vids,
  ));
  foreach ($sheetnodes as $sheetnode) {
    $nodes[$sheetnode->nid]->sheetnode = (array) $sheetnode;
  }
}

/**
 * Implements hook_view().
 */
function sheetnode_view($node, $view_mode) {

  // SocialCalc sheet.
  if ($view_mode == 'full') {
    $output = _sheetnode_inject(drupal_clean_css_identifier('sheetnode-' . $node->nid), 'sheetnode', $node->sheetnode['value'], FALSE, array(
      'entity-type' => 'node',
      'oid' => $node->nid,
    ));
    $node->content['sheetnode'] = array(
      '#markup' => $output,
      '#weight' => -1,
    );
  }
  return $node;
}

/**
 * Implements hook_delete().
 */
function sheetnode_delete(&$node) {
  db_delete('sheetnode')
    ->condition('nid', $node->nid)
    ->execute();
}

/**
 * Implements hook_node_revision_delete().
 */
function sheetnode_node_revision_delete($node) {
  if ($node->type == 'sheetnode') {
    db_delete('sheetnode')
      ->condition('vid', $node->vid)
      ->execute();
  }
}

/**
 * Implements hook_form().
 */
function sheetnode_form($node, &$form_state) {
  $types = node_type_get_types();
  $type = $types['sheetnode'];

  // Generate the default title.
  $form = node_content_form(!empty($node) ? $node : 'sheetnode', $form_state);

  // SocialCalc sheet.
  if (!empty($form_state['input']['sheetnode']['value'])) {
    $value = $form_state['input']['sheetnode']['value'];
    $nid = @$node->nid;
  }
  elseif (!empty($node->nid)) {
    $value = $node->sheetnode['value'];
    $nid = $node->nid;
  }
  elseif (!empty($node->clone_from_original_nid)) {

    // Support node_clone.module.
    $original_node = node_load($node->clone_from_original_nid);
    $value = $original_node->sheetnode['value'];
    $nid = $node->clone_from_original_nid;
  }
  else {
    $value = '';
    $nid = NULL;
  }
  $output = _sheetnode_inject(drupal_clean_css_identifier('sheetnode-' . (empty($node->nid) ? 'new' : $node->nid)), 'sheetnode', $value, drupal_clean_css_identifier('edit-sheetnode-value'), array(
    'entity-type' => 'node',
    'oid' => $nid,
  ));
  $form['sheetnode'] = array(
    '#tree' => TRUE,
    '#weight' => -1,
    'socialcalc' => array(
      '#markup' => $output,
    ),
    'value' => array(
      '#type' => 'hidden',
      '#attributes' => array(
        'id' => 'edit-sheetnode-value',
      ),
    ),
  );

  // Template.
  if (user_access('create sheetnode template')) {
    $form['sheetnode']['template'] = array(
      '#type' => 'textfield',
      '#title' => t('Save as template'),
      '#description' => t('When saving this sheet, also keep a copy under the name you specify here. Later, this copy can be used as a template for other sheets.'),
      '#required' => FALSE,
    );
  }
  return $form;
}

/**
 * Implements hook_validate().
 */
function sheetnode_validate($node, $form, &$form_state) {
  $errors = form_get_errors();
  if (!empty($errors)) {
    _sheetnode_inject(drupal_clean_css_identifier('sheetnode-' . (empty($form['nid']['#value']) ? 'new' : $form['nid']['#value'])), 'sheetnode', $form['sheetnode']['value']['#value'], drupal_clean_css_identifier('edit-sheetnode-value'), array(
      'entity-type' => 'node',
      'oid' => $form['nid']['#value'],
    ));
  }
}

/**
 * Helper function to inject Sheetnode JavaScript.
 */
function _sheetnode_inject($sheet_id, $sheet_aliases, $value, $save_element, $context) {
  $library_path = libraries_get_path('socialcalc');
  $module_path = drupal_get_path('module', 'sheetnode');
  drupal_add_js($library_path . '/socialcalc.min.js', array(
    'weight' => 1,
  ));
  drupal_add_js($module_path . '/js/sheetnode.js', array(
    'weight' => 2,
  ));
  drupal_add_css($library_path . '/socialcalc.min.css');
  drupal_add_css($module_path . '/css/sheetnode.css');

  // Allow other modules to add their own JS/CSS.
  module_invoke_all('sheetnode_plugins', $value, $save_element, $context);

  // Lower-case sheet aliases.
  $sheet_aliases = array_map('strtolower', (array) $sheet_aliases);
  static $sheets = NULL;
  if (!isset($sheets[$sheet_id])) {
    drupal_add_js(array(
      'sheetnode' => array(
        $sheet_id => array(
          'aliases' => $sheet_aliases,
          'value' => $value,
          'imagePrefix' => base_path() . $library_path . '/images/sc-',
          'containerElement' => $sheet_id,
          'saveElement' => $save_element,
          'viewMode' => variable_get('sheetnode_view_mode', SHEETNODE_VIEW_FIDDLE),
          'showToolbar' => variable_get('sheetnode_view_toolbar', FALSE),
          'permissions' => array(
            'edit sheetnode settings' => user_access('edit sheetnode settings'),
          ),
          'context' => $context,
        ),
      ),
    ), 'setting');
    $sheets[$sheet_id] = TRUE;
  }
  return '<div class="sheetview" id="' . $sheet_id . '"></div>';
}

/**
 * Implements hook_insert().
 */
function sheetnode_insert($node) {
  if ($node->sheetnode['value']) {
    _sheetnode_save($node->nid, $node->vid, $node->sheetnode['value']);
  }
  if ($node->sheetnode['template'] && user_access('create sheetnode template')) {
    _sheetnode_template_save($node->vid, $node->sheetnode['template'], $node->sheetnode['value']);
  }
}

/**
 * Implements hook_update().
 */
function sheetnode_update($node) {
  if (!empty($node->sheetnode['value'])) {
    _sheetnode_save($node->nid, $node->vid, $node->sheetnode['value']);
  }
  elseif (!empty($node->revision)) {

    // Reverting a revision.
    $value = db_query("SELECT value FROM {sheetnode} WHERE vid = :old", array(
      ':old' => $node->old_vid,
    ))
      ->fetchField();
    db_insert('sheetnode')
      ->fields(array(
      'vid' => $node->vid,
      'nid' => $node->nid,
      'value' => $value,
    ))
      ->execute();
  }
  if (!empty($node->sheetnode['template']) && user_access('create sheetnode template')) {
    _sheetnode_template_save($node->vid, $node->sheetnode['template'], $node->sheetnode['value']);
  }
}

/**
 * Implements hook_node_presave().
 */
function sheetnode_node_presave($node) {
  if ($node->type == 'sheetnode' && isset($node->devel_generate)) {

    // Generate random spreadsheets for sheetnodes.
    $node->sheetnode = array(
      'template' => NULL,
    ) + _sheetnode_devel_generate($node, NULL, NULL, NULL);
  }
}

/**
 * Implements hook_node_update_index().
 */
function sheetnode_node_update_index($node) {
  $output = '';
  if ($node->type == 'sheetnode') {
    $output .= _sheetnode_update_index($node->sheetnode['value']);
  }
  foreach (sheetnode_get_sheetfields($node->type) as $field_name => $field) {
    foreach ($node->{$field_name}[LANGUAGE_NONE] as $item) {
      $output .= _sheetnode_update_index($item['value']);
    }
  }
  return $output;
}

/**
 * Helper function to return an indexable version of the spreadsheet.
 */
function _sheetnode_update_index($value) {
  module_load_include('inc', 'sheetnode', 'socialcalc');
  $output = '<table>';
  $socialcalc = socialcalc_parse($value);
  $sc = $socialcalc['sheet'];
  $row = -1;
  if (!empty($sc['cells'])) {
    foreach ($sc['cells'] as $c) {
      if ($c['pos'][1] > $row) {

        // New row? - this assumes cells are ordered by col/row.
        if ($row != -1) {
          $output .= '</tr>';
        }
        $row = $c['pos'][1];
        $output .= '<tr>';
      }
      $output .= '<td>' . (isset($c['datavalue']) ? $c['datavalue'] : '&nbsp;') . (isset($c['comment']) ? ' (' . check_plain($c['comment']) . ')' : '') . '</td>';
    }
    if ($row != -1) {
      $output .= '</tr>';
    }
    $output .= '</table>';
    return $output;
  }
}

/**
 * Implements hook_token_info().
 */
function sheetnode_token_info() {
  $info['types']['sheet'] = array(
    'name' => t('Sheet'),
    'description' => t('Tokens related to sheets.'),
    'needs-data' => 'sheet',
  );
  $info['tokens']['sheet']['cell'] = array(
    'name' => t('Cell reference'),
    'description' => t('A cell reference such as B52, C79, etc.'),
    'dynamic' => TRUE,
  );
  $info['tokens']['node']['sheetnode'] = array(
    'name' => t('Sheetnode'),
    'description' => t('The sheet of the node.'),
    'type' => 'sheet',
  );
  return $info;
}

/**
 * Implements hook_tokens().
 */
function sheetnode_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  $sanitize = !empty($options['sanitize']);
  if ($type == 'sheet' && !empty($data['sheet'])) {
    module_load_include('inc', 'sheetnode', 'socialcalc');
    $socialcalc = socialcalc_parse($data['sheet']);
    $sc = $socialcalc['sheet'];
    foreach ($tokens as $name => $original) {
      switch ($name) {
        default:
          list($token, $coord) = explode(':', $name, 2);
          if ($token == 'cell' && isset($sc['cells'][$coord])) {
            $c = $sc['cells'][$coord];
            $replacements[$original] = isset($c['datavalue']) ? $c['datavalue'] : '';
          }
          break;
      }
    }
  }
  if ($type == 'node' && !empty($data['node']) && $data['node']->type == 'sheetnode') {
    $node = $data['node'];
    foreach ($tokens as $name => $original) {
      switch ($name) {
        case 'sheetnode':
          $replacements[$original] = $node->sheetnode['value'];
          break;
      }
    }
    if ($sheet_tokens = token_find_with_prefix($tokens, 'sheetnode')) {
      $replacements += token_generate('sheet', $sheet_tokens, array(
        'sheet' => $node->sheetnode['value'],
      ), $options);
    }
  }
  return $replacements;
}

/**
 * Implements hook_token_info_alter().
 */
function sheetnode_token_info_alter(&$info) {
  $fields = field_info_fields();
  foreach ($info['tokens']['node'] as $name => &$token_info) {
    if (isset($fields[$name]) && $fields[$name]['type'] == 'sheetfield') {
      $token_info['type'] = 'sheet';
    }
  }
}

/**
 * Implements hook_tokens_alter().
 */
function sheetnode_tokens_alter(array &$replacements, array $context) {
  if ($context['type'] == 'node' && !empty($context['data']['node'])) {
    $node = $context['data']['node'];
    $tokens = $context['tokens'];
    foreach (sheetnode_get_sheetfields($node->type) as $field_name => $field_info) {
      if ($sheet_tokens = token_find_with_prefix($tokens, $field_name)) {

        // TODO: how to handle multi-valued fields here?
        $replacements += token_generate('sheet', $sheet_tokens, array(
          'sheet' => $node->{$field_name}[LANGUAGE_NONE][0]['value'],
        ), $context['options']);
      }
    }
  }
}

/**
 * Implements hook_menu().
 */
function sheetnode_menu() {
  $items = array();
  $items['sheetnode/load'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => '_sheetnode_ajax_load',
    'access arguments' => array(
      'access content',
    ),
  );
  $items['sheetnode/field'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => '_sheetnode_ajax_field',
    'access arguments' => array(
      'access content',
    ),
  );
  $items['sheetnode/token'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => '_sheetnode_ajax_token',
    'access arguments' => array(
      'access content',
    ),
  );
  $items['node/add/sheetnode_template'] = array(
    'title' => 'Sheetnode import from template',
    'access arguments' => array(
      'create sheetnode',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sheetnode_import_template',
    ),
    'description' => 'Create a new sheetnode from an existing template.',
  );
  $items['admin/config/content/sheetnode'] = array(
    'title' => 'Sheetnode',
    'access arguments' => array(
      'administer site configuration',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sheetnode_admin_settings',
    ),
    'description' => 'Configure sheetnodes and sheetnode components.',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/system/content/general'] = array(
    'title' => 'General',
    'weight' => -10,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  return $items;
}
define('SHEETNODE_VIEW_READONLY', 0);
define('SHEETNODE_VIEW_FIDDLE', 1);
define('SHEETNODE_VIEW_HTML', 2);

/**
 * Form function for admin/settings/sheetnode.
 */
function sheetnode_admin_settings() {
  $form['sheetnode_view_mode'] = array(
    '#type' => 'radios',
    '#title' => t('View mode'),
    '#description' => t('Select the way sheetnodes should be displayed in view mode.'),
    '#options' => array(
      SHEETNODE_VIEW_READONLY => t('Read-only spreadsheet'),
      SHEETNODE_VIEW_FIDDLE => t('Fiddle mode (interactive spreadsheet without save functionality)'),
      SHEETNODE_VIEW_HTML => t('HTML table'),
    ),
    '#default_value' => variable_get('sheetnode_view_mode', SHEETNODE_VIEW_FIDDLE),
  );
  $form['sheetnode_view_toolbar'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show toolbar in view mode'),
    '#default_value' => variable_get('sheetnode_view_toolbar', FALSE),
  );
  return system_settings_form($form);
}

/**
 * Form function for node/add/sheetnode_template.
 */
function sheetnode_import_template($form, &$form_state, $tid = NULL) {
  if (!empty($tid)) {
    $form_state['values']['template'] = $tid;
    sheetnode_import_template_submit($form, $form_state);
    drupal_goto($form_state['redirect']);
  }
  $options[0] = t('- Select a template -');
  $templates = db_query("SELECT tid, name FROM {sheetnode_template}");
  foreach ($templates as $template) {
    $options[$template->tid] = $template->name;
  }
  $form['template'] = array(
    '#type' => 'select',
    '#title' => t('Template'),
    '#description' => t('Please select the template to load into your new sheetnode.'),
    '#options' => $options,
    '#default_value' => 0,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

/**
 * Validate function for sheetnode_import_template form.
 */
function sheetnode_import_template_validate($form, &$form_state) {
  if (!$form_state['values']['template']) {
    form_set_error('template', t('Please select a template.'));
  }
}

/**
 * Submit function for sheetnode_import_template form.
 */
function sheetnode_import_template_submit($form, &$form_state) {
  global $user;
  module_load_include('inc', 'node', 'node.pages');
  $template = _sheetnode_template_load($form_state['values']['template']);
  $node = new StdClass();
  $node->type = 'sheetnode';
  node_object_prepare($node);
  $node->title = $template->name;
  $node->name = $user->name;
  $node->language = LANGUAGE_NONE;
  $node->sheetnode['value'] = $template->value;
  $node->sheetnode['template'] = NULL;

  // Let other modules alter the sheetnode or do other work.
  $context = array(
    'template' => $template,
  );
  drupal_alter('sheetnode_import', $node, $context);

  // Save the sheetnode.
  $node = node_submit($node);
  node_save($node);
  if (!empty($node->nid)) {
    $form_state['redirect'] = 'node/' . $node->nid . '/edit';
  }
}

/**
 * Implements hook_views_api().
 */
function sheetnode_views_api() {
  return array(
    'api' => 2.0,
  );
}
define('SHEETNODE_RANGE_LABELS_ROW', 1);
define('SHEETNODE_RANGE_LABELS_COL', 2);
define('SHEETNODE_RANGE_LABELS_ROW_COL', 3);

/**
 * Implements hook_theme().
 */
function sheetnode_theme($existing, $type, $theme, $path) {
  return array(
    'sheetnode_range' => array(
      'variables' => array(
        'range' => NULL,
        'labels' => SHEETNODE_RANGE_LABELS_ROW_COL,
      ),
    ),
  );
}

/**
 * Theme function for sheetnode_range.
 */
function theme_sheetnode_range($variables) {
  $range = $variables['range'];
  $labels = $variables['labels'];
  if (empty($range)) {
    return '';
  }
  module_load_include('inc', 'sheetnode', 'socialcalc');
  list($c, $r) = socialcalc_coord_to_cr(key($range));
  $row = array();
  if ($labels & SHEETNODE_RANGE_LABELS_ROW) {
    $row[] = array(
      'data' => $r,
      'header' => TRUE,
    );
  }
  $header = array();
  if ($labels & SHEETNODE_RANGE_LABELS_ROW) {
    $header[] = array();
  }
  $rows = array();
  foreach ($range as $coord => $value) {
    $pos = socialcalc_coord_to_cr($coord);
    if ($pos[1] > $r) {
      $rows[] = $row;
      $r = $pos[1];
      $row = array();
      if ($labels & SHEETNODE_RANGE_LABELS_ROW) {
        $row[] = array(
          'data' => $r,
          'header' => TRUE,
        );
      }
    }
    $row[] = $value;
    if (empty($rows)) {
      $coord = socialcalc_cr_to_coord($pos[0], $pos[1], TRUE);
      $header[] = $coord[0];
    }
  }
  $rows[] = $row;
  return theme('table', array(
    'header' => $labels & SHEETNODE_RANGE_LABELS_COL ? $header : NULL,
    'rows' => $rows,
  ));
}

/**
 * Implements hook_field_extra_fields().
 */
function sheetnode_field_extra_fields() {
  $extra['node']['sheetnode'] = array(
    'form' => array(
      'sheetnode' => array(
        'label' => t('Spreadsheet'),
        'description' => t('Spreadsheet control'),
        'weight' => -1,
      ),
    ),
    'display' => array(
      'sheetnode' => array(
        'label' => t('Spreadsheet'),
        'description' => t('Spreadsheet control'),
        'weight' => -1,
      ),
    ),
  );
  return $extra;
}

/**
 * Implements hook_field_info().
 */
function sheetnode_field_info() {
  return array(
    'sheetfield' => array(
      'label' => t('Spreadsheet'),
      'description' => t('Store a spreadsheet.'),
      'default_widget' => 'sheetfield_spreadsheet',
      'default_formatter' => 'sheetfield_default',
      'translatable' => FALSE,
    ),
  );
}

/**
 * Implements hook_node_validate().
 *
 * Catch form errors to re-initialize sheetfields.
 */
function sheetnode_node_validate($node, $form, &$form_state) {
  $errors = form_get_errors();
  if (!empty($errors) && !empty($form_state['field'])) {
    foreach ($form_state['field'] as $field_name => $field) {
      if ($field[LANGUAGE_NONE]['field']['type'] == 'sheetfield' && !empty($form_state['input'][$field_name])) {
        foreach ($form_state['input'][$field_name][LANGUAGE_NONE] as $delta => $item) {
          $element_id = drupal_clean_css_identifier('edit-' . $field_name . '-' . LANGUAGE_NONE . '-' . $delta);
          $output = _sheetnode_inject(drupal_clean_css_identifier('sheetfield-' . $element_id), _sheetnode_sheetfield_aliases($field_name, $field[LANGUAGE_NONE]['instance']['label'], $item, $delta), $item['value'], $element_id, array(
            'entity-type' => 'node',
            'oid' => @$node->nid,
          ));
        }
      }
    }
  }
}

/**
 * Implements hook_field_is_empty().
 */
function sheetnode_field_is_empty($item, $field) {
  if (empty($item['value'])) {
    return TRUE;
  }
  module_load_include('inc', 'sheetnode', 'socialcalc');
  $sc = socialcalc_parse($item['value']);
  return empty($sc['sheet']['cells']);
}

/**
 * Implements hook_devel_generate().
 */
function sheetnode_devel_generate($object, $field, $instance, $bundle) {
  if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_CUSTOM) {
    return devel_generate_multiple('_sheetnode_devel_generate', $object, $field, $instance, $bundle);
  }
  else {
    return _sheetnode_devel_generate($object, $field, $instance, $bundle);
  }
}

/**
 * Helper to generate random spreadsheet.
 */
function _sheetnode_devel_generate($object, $field, $instance, $bundle) {
  module_load_include('inc', 'sheetnode', 'socialcalc');
  $sc = array(
    'edit' => array(),
    'audit' => array(),
    'sheet' => array(),
  );
  for ($row = 1; $row < mt_rand(10, 50); $row++) {
    $sc['sheet']['cells'][] = array(
      'pos' => array(
        1,
        $row,
      ),
      'datavalue' => devel_generate_word(mt_rand(6, 12)),
      'datatype' => 't',
      'valuetype' => 't',
    );
    $sc['sheet']['cells'][] = array(
      'pos' => array(
        2,
        $row,
      ),
      'datavalue' => _sheetnode_lcg_randf(-100, 100),
      'datatype' => 'v',
      'valuetype' => 'n',
    );
  }
  $sc['edit']['ecell']['coord'] = 'A1';
  return array(
    'value' => socialcalc_save($sc),
  );
}

/**
 * Helper function to return random number using combined linear congruential generator.
 */
function _sheetnode_lcg_randf($min, $max) {
  return $min + lcg_value() * abs($max - $min);
}

/**
 * Implements hook_field_formatter_info().
 */
function sheetnode_field_formatter_info() {
  $formatters = array(
    'sheetfield_default' => array(
      'label' => t('Default'),
      'field types' => array(
        'sheetfield',
      ),
    ),
  );
  if (module_exists('date')) {
    $formatters['socialcalc_date'] = array(
      'label' => t('SocialCalc'),
      'field types' => array(
        'date',
        'datetime',
        'datestamp',
      ),
    );
  }
  return $formatters;
}

/**
 * Implements hook_field_formatter_view().
 */
function sheetnode_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();
  switch ($display['type']) {
    case 'sheetfield_default':
      foreach ($items as $delta => $item) {
        $output = '';
        if (!empty($item['name'])) {
          $output .= '<div class="sheetfield-title">' . $item['name'] . '</div>';
        }
        $output .= _sheetnode_inject(drupal_clean_css_identifier('sheetfield-' . $field['field_name'] . '-' . $langcode . '-' . $delta), _sheetnode_sheetfield_aliases($field['field_name'], $instance['label'], $item, $delta), $item['value'], FALSE, isset($entity) && $entity == 'node' ? array(
          'entity-type' => $entity_type,
          'oid' => @$entity->nid,
        ) : NULL);
        $element[$delta] = array(
          '#markup' => $output,
        );
      }
      break;
    case 'socialcalc_date':
      module_load_include('inc', 'sheetnode', 'socialcalc');
      foreach ($items as $delta => $item) {
        $value = $item['value'];
        $timezone = isset($item['timezone']) ? $item['timezone'] : '';
        $timezone = date_get_timezone($field['settings']['tz_handling'], $timezone);
        $timezone_db = date_get_timezone_db($field['settings']['tz_handling']);
        $db_format = date_type_format($field['type']);
        $date = new DateObject($value, $timezone_db, $db_format);
        $date
          ->limitGranularity($field['settings']['granularity']);
        date_timezone_set($date, timezone_open($timezone));
        $element[$delta] = array(
          '#markup' => socialcalc_import_date($date),
        );
      }
      break;
  }
  return $element;
}

/**
 * Implements hook_field_widget_info().
 */
function sheetnode_field_widget_info() {
  return array(
    'sheetfield_spreadsheet' => array(
      'label' => t('Spreadsheet'),
      'field types' => array(
        'sheetfield',
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function sheetnode_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $widget = $element;
  $widget['#delta'] = $delta;
  $defaults = isset($items[$delta]) ? $items[$delta] : array(
    'name' => '',
    'value' => '',
  );
  switch ($instance['widget']['type']) {
    case 'sheetfield_spreadsheet':
      $element_id = drupal_clean_css_identifier('edit-' . $element['#field_name'] . '-' . $langcode . '-' . $delta);
      $output = _sheetnode_inject(drupal_clean_css_identifier('sheetfield-' . $element_id), _sheetnode_sheetfield_aliases($element['#field_name'], $instance['label'], $defaults, $delta), $defaults['value'], $element_id, isset($form['#node']) ? array(
        'entity-type' => 'node',
        'oid' => @$form['#node']->nid,
      ) : NULL);
      $widget['name'] = array(
        '#type' => 'textfield',
        '#title' => t('Title'),
        '#default_value' => @$defaults['name'],
      );
      $widget['value'] = array(
        '#type' => 'hidden',
        '#attributes' => array(
          'id' => $element_id,
        ),
        '#prefix' => $output,
      );
      break;
  }
  return $widget;
}

/**
 * Implements hook_form_alter().
 */
function sheetnode_form_alter(&$form, $form_state, $form_id) {

  // Sheetfield settings:
  // * Insert extra template element for default values.
  if ($form_id == 'field_ui_field_edit_form' && $form['#field']['type'] == 'sheetfield') {
    $options[0] = t('- Select a template -');
    $templates = db_query("SELECT tid, name FROM {sheetnode_template}");
    foreach ($templates as $template) {
      $options[$template->tid] = $template->name;
    }
    $form['instance']['default_value_widget']['sheetfield_template'] = array(
      '#type' => 'select',
      '#title' => t('Template'),
      '#description' => t('The template to load into your new sheetfield. This setting will override the sheet above but will be overridden by the PHP code below, if any.'),
      '#options' => $options,
      '#default_value' => variable_get('sheetfield_template_' . $form['#field']['field_name'], 0),
      '#weight' => 1,
    );
    array_unshift($form['#submit'], '_sheetnode_field_ui_field_edit_form_submit');
  }
}

/**
 * Submit function for field_ui_field_edit_form form.
 */
function _sheetnode_field_ui_field_edit_form_submit($form, &$form_state) {
  if (!empty($form_state['values']['sheetfield_template'])) {
    $template = _sheetnode_template_load($form_state['values']['sheetfield_template']);
    if (!empty($template)) {

      // WARNING: LANGUAGE_NONE is an assumption.
      $form_state['values'][$form['#field']['field_name']][LANGUAGE_NONE][0]['value'] = $template->value;
    }
  }
  variable_set('sheetfield_template_' . $form['#field']['field_name'], $form_state['values']['sheetfield_template']);
}

/**
 * Implements hook_element_info().
 */
function sheetnode_element_info() {
  return array(
    'spreadsheet' => array(
      '#input' => TRUE,
      '#process' => array(
        '_sheetnode_spreadsheet_process',
      ),
    ),
  );
}

/**
 * Process function for spreadsheet element.
 */
function _sheetnode_spreadsheet_process($element, &$form_state, $form) {
  $value = $element['#value'];
  $output = _sheetnode_inject(drupal_clean_css_identifier('sheetfield-' . $element['#id']), array(
    $element['#name'],
    $element['#title'],
  ), $value, drupal_clean_css_identifier($element['#id']), NULL);
  $element['#tree'] = TRUE;
  $element['value'] = array(
    '#type' => 'hidden',
    '#attributes' => array(
      'id' => drupal_clean_css_identifier($element['#id']),
    ),
    '#prefix' => $output,
  );
  return $element;
}

/**
 * API function to return a sheet given a name.
 */
function sheetnode_find_sheet($sheetname) {
  module_load_include('inc', 'sheetnode', 'socialcalc');
  $args = explode('/', $sheetname);
  $sheetname = array_shift($args);

  // No value found by default.
  $value = NULL;
  $title = NULL;

  // Try to find a node.
  $node = NULL;
  if (is_numeric($sheetname)) {
    $node = node_load(intval($sheetname));
  }
  else {
    $nid = db_query("SELECT nid FROM {node} WHERE title = :title LIMIT 1", array(
      ':title' => $sheetname,
    ))
      ->fetchField();
    if ($nid) {
      $node = node_load($nid);
    }
  }

  // Do we have access to this node?
  if ($node && !node_access('view', $node)) {
    return array(
      NULL,
      NULL,
    );
  }

  // Is it a sheetnode?
  if ($node && $node->type === 'sheetnode' && empty($args[0])) {
    $value = $node->sheetnode['value'];
    $title = $node->title;
  }

  // Is it a sheetfield?
  if (!$value && $node) {
    $target_field = empty($args[0]) ? FALSE : $args[0];
    $target_delta = empty($args[1]) ? 0 : intval($args[1]);
    foreach (sheetnode_get_sheetfields($node->type) as $field_name => $field_info) {

      // Check that it's the field we want:
      // * It's accessible to this user.
      // * Its label matches the input.
      // * Its delta matches the input.
      if (field_access('view', $field_info, 'node', $node) && (!$target_field || 0 === strcasecmp($target_field, $field_name) || 0 === strcasecmp($target_field, $field_info['instance']['label']) || 0 === strcasecmp($target_field, @$node->{$field_name}[LANGUAGE_NONE][$target_delta]['name'])) && !empty($node->{$field_name}[LANGUAGE_NONE][$target_delta])) {
        $value = $node->{$field_name}[LANGUAGE_NONE][$target_delta]['value'];
        $title = trim(sprintf("%s %s %s", $node->title, $field_info['instance']['label'], isset($node->{$field_name}[LANGUAGE_NONE][$target_delta]['name']) ? $node->{$field_name}[LANGUAGE_NONE][$target_delta]['name'] : ($target_delta ? strval($target_delta) : '')));
        break;
      }
    }
  }

  // Is it a sheetview?
  if (!$value && module_exists('views')) {

    // Try a view feed with our SocialCalc output plugin style.
    $view = views_get_view($sheetname);
    if ($view) {
      foreach (array_keys($view->display) as $display_id) {
        $display = $view->display[$display_id];
        if (isset($display->display_options['style_plugin']) && $display->display_options['style_plugin'] === 'sheet_raw' && $view
          ->access($display_id)) {
          if (!empty($args)) {
            $view
              ->set_arguments($args);
          }
          $value = $view
            ->render($display_id);
          $title = $display->handler
            ->get_option('title');

          // Our style plugin sets this to text/plain, so reset it.
          drupal_add_http_header('Content-type', 'text/html; charset=utf-8');
        }
      }
    }
  }
  return array(
    $value,
    $title,
  );
}

/**
 * AJAX function to return a sheet value.
 */
function _sheetnode_ajax_load($sheetname = NULL) {

  // Parse the sheetname which might contain extra args separated by forward-slash.
  if (empty($sheetname)) {
    $sheetname = $_REQUEST['sheetname'];
  }

  // Look for the sheet.
  list($value, $title) = sheetnode_find_sheet($sheetname);

  // Found a spreadsheet: return it.
  drupal_add_http_header('Content-type', 'text/plain; charset=utf-8');
  if ($value) {
    $parts = socialcalc_parse_parts($value);
    if (isset($parts['sheet'])) {
      echo $parts['sheet'];
    }
  }
  drupal_exit();
}

/**
 * AJAX function to return a token value.
 */
function _sheetnode_ajax_token($oid = NULL, $entity_type = NULL, $token = NULL) {
  if (!$oid) {
    $oid = $_REQUEST['oid'];
  }
  if (!$entity_type) {
    $entity_type = $_REQUEST['entity_type'];
  }
  if (!$token) {
    $token = $_REQUEST['token'];
  }
  $value = NULL;
  $entity = _sheetnode_entity_load($entity_type, $oid);
  if ($entity) {

    // Do the token replacement.
    $value = token_replace($token, array(
      $entity_type => $entity,
    ));

    // If found, send it back.
    if ($value) {
      drupal_json_output(array(
        'type' => is_numeric($value) ? 'n' : 'th',
        'value' => $value,
      ));
      drupal_exit();
    }
  }
  drupal_json_output(array(
    'value' => '',
    'type' => 'e#NAME?',
  ));
  drupal_exit();
}

/**
 * AJAX function to return a field value.
 */
function _sheetnode_ajax_field($oid = NULL, $entity_type = NULL, $field = NULL) {
  if (!$oid) {
    $oid = $_REQUEST['oid'];
  }
  if (!$entity_type) {
    $entity_type = $_REQUEST['entity_type'];
  }
  if (!$field) {
    $field = $_REQUEST['field'];
  }
  $value = NULL;
  $entity = _sheetnode_entity_load($entity_type, $oid);
  if ($entity) {

    // Try field.
    $field_info = field_info_instance($entity_type, $field, field_extract_bundle($entity_type, $entity));
    if ($field_info) {
      if (!field_access('view', $field_info, $entity_type, $entity)) {
        drupal_json_output(array(
          'value' => '',
          'type' => 'e#NAME?',
        ));
        drupal_exit();
      }
      $value = field_view_field($entity_type, $entity, $field, array(
        'label' => 'hidden',
      ));
      if ($value) {
        $value = drupal_render($value);
      }
    }

    // Try rendered field with raw build functions.
    $build_func = $entity_type . '_build_content';
    if (!$value && function_exists($build_func)) {
      $build_func($entity);
      if (isset($entity->content[$field])) {
        $value = drupal_render($entity->content[$field]);
      }
    }

    // Try raw object field.
    if (!$value && isset($entity->{$field})) {
      if (is_object($entity->{$field}) || is_array($entity->{$field})) {
        $value = print_r($entity->{$field}, TRUE);
      }
      else {
        $value = $entity->{$field};
      }
    }

    // If found, send it back.
    if ($value) {
      drupal_json_output(array(
        'type' => is_numeric($value) ? 'n' : 'th',
        'value' => $value,
      ));
      drupal_exit();
    }
  }
  drupal_json_output(array(
    'value' => '',
    'type' => 'e#NAME?',
  ));
  drupal_exit();
}

/**
 * Helper function to save a sheetnode.
 */
function _sheetnode_save($nid, $vid, $value) {
  db_delete('sheetnode')
    ->condition('vid', $vid)
    ->execute();
  db_insert('sheetnode')
    ->fields(array(
    'nid' => $nid,
    'vid' => $vid,
    'value' => $value,
  ))
    ->execute();
}

/**
 * Helper function to save a sheetnode template.
 */
function _sheetnode_template_save($vid, $name, $value) {
  db_merge('sheetnode_template')
    ->key(array(
    'name' => $name,
  ))
    ->fields(array(
    'vid' => $vid,
    'value' => $value,
  ))
    ->execute();
}

/**
 * Helper function to load a sheetnode template.
 */
function _sheetnode_template_load($tid) {
  $template = db_select('sheetnode_template', 's')
    ->fields('s')
    ->condition('tid', $tid, '=')
    ->execute()
    ->fetchAssoc();
  return (object) $template;
}

/**
 * Helper function to load a Drupal entity.
 *
 * Also checks for access right.
 */
function _sheetnode_entity_load($entity_type, $oid, $op = 'view', $account = NULL) {
  $entities = entity_load($entity_type, array(
    $oid,
  ));
  if (isset($entities[$oid])) {
    $entity = $entities[$oid];

    // Taken from entity.module/entity_access() - not included to reduce dependencies.
    // @see http://drupalcontrib.org/api/drupal/contributions!entity!entity.module/function/entity_access/7
    if (($info = entity_get_info()) && isset($info[$entity_type]['access callback'])) {
      return $info[$entity_type]['access callback']($op, $entity, $account, $entity_type) ? $entity : FALSE;
    }
    return $entity;
  }
  return FALSE;
}

/**
 * API function to get all sheetfields of a node type.
 */
function sheetnode_get_sheetfields($type) {
  $sheetfields = array();
  $instances = field_info_instances('node', $type);
  $fields = field_info_fields();
  foreach ($instances as $field_name => $instance) {
    if ($fields[$field_name]['type'] === 'sheetfield') {
      $sheetfields[$field_name] = $fields[$field_name];
      $sheetfields[$field_name]['instance'] = $instance;
    }
  }
  return $sheetfields;
}

/**
 * Helper function to retrieve possible sheetfield aliases.
 */
function _sheetnode_sheetfield_aliases($field_name, $label, $value, $delta) {
  $names = array();
  $names[] = $field_name . '/' . $delta;
  if (!empty($label)) {
    $names[] = $label . '/' . $delta;
  }
  if (!empty($value['name'])) {
    $names[] = $value['name'];
  }
  return $names;
}

Functions

Namesort descending Description
sheetnode_admin_settings Form function for admin/settings/sheetnode.
sheetnode_delete Implements hook_delete().
sheetnode_devel_generate Implements hook_devel_generate().
sheetnode_element_info Implements hook_element_info().
sheetnode_field_extra_fields Implements hook_field_extra_fields().
sheetnode_field_formatter_info Implements hook_field_formatter_info().
sheetnode_field_formatter_view Implements hook_field_formatter_view().
sheetnode_field_info Implements hook_field_info().
sheetnode_field_is_empty Implements hook_field_is_empty().
sheetnode_field_widget_form Implements hook_field_widget_form().
sheetnode_field_widget_info Implements hook_field_widget_info().
sheetnode_find_sheet API function to return a sheet given a name.
sheetnode_form Implements hook_form().
sheetnode_form_alter Implements hook_form_alter().
sheetnode_get_sheetfields API function to get all sheetfields of a node type.
sheetnode_import_template Form function for node/add/sheetnode_template.
sheetnode_import_template_submit Submit function for sheetnode_import_template form.
sheetnode_import_template_validate Validate function for sheetnode_import_template form.
sheetnode_insert Implements hook_insert().
sheetnode_load Implements hook_load().
sheetnode_menu Implements hook_menu().
sheetnode_node_access Implements hook_node_access().
sheetnode_node_info Implements hook_node_info().
sheetnode_node_presave Implements hook_node_presave().
sheetnode_node_revision_delete Implements hook_node_revision_delete().
sheetnode_node_update_index Implements hook_node_update_index().
sheetnode_node_validate Implements hook_node_validate().
sheetnode_permission Implements hook_permission().
sheetnode_theme Implements hook_theme().
sheetnode_tokens Implements hook_tokens().
sheetnode_tokens_alter Implements hook_tokens_alter().
sheetnode_token_info Implements hook_token_info().
sheetnode_token_info_alter Implements hook_token_info_alter().
sheetnode_update Implements hook_update().
sheetnode_validate Implements hook_validate().
sheetnode_view Implements hook_view().
sheetnode_views_api Implements hook_views_api().
theme_sheetnode_range Theme function for sheetnode_range.
_sheetnode_ajax_field AJAX function to return a field value.
_sheetnode_ajax_load AJAX function to return a sheet value.
_sheetnode_ajax_token AJAX function to return a token value.
_sheetnode_devel_generate Helper to generate random spreadsheet.
_sheetnode_entity_load Helper function to load a Drupal entity.
_sheetnode_field_ui_field_edit_form_submit Submit function for field_ui_field_edit_form form.
_sheetnode_inject Helper function to inject Sheetnode JavaScript.
_sheetnode_lcg_randf Helper function to return random number using combined linear congruential generator.
_sheetnode_save Helper function to save a sheetnode.
_sheetnode_sheetfield_aliases Helper function to retrieve possible sheetfield aliases.
_sheetnode_spreadsheet_process Process function for spreadsheet element.
_sheetnode_template_load Helper function to load a sheetnode template.
_sheetnode_template_save Helper function to save a sheetnode template.
_sheetnode_update_index Helper function to return an indexable version of the spreadsheet.

Constants