You are here

editor.module in Editor 5

Same filename and directory in other branches
  1. 6 editor.module
  2. 7 editor.module

Extendable WYSIWYG editor @author Tj Holowaychuk <tj@vision-media.ca> @link http://vision-media.ca @package Editor

File

editor.module
View source
<?php

/**
 * @file 
 * Extendable WYSIWYG editor
 * @author Tj Holowaychuk <tj@vision-media.ca>
 * @link http://vision-media.ca
 * @package Editor
 */

// --------------------- ALPHA:
// Completed
// --------------------- BETA:
// @todo: restore settings, and have a proxy method which detects if you are editing or initially creating/wrapping an element
// @todo: test with multiple editors on a single page
// @todo: execCommand issues with newly created nodes and redo/undo etc
// @todo: ie 6/7 maintain selection
// @todo: wrap method; issue with converting htmlentities;
// @todo: methods for removing parent or children node types when wrapping..
// This sort of functionality will be needed to insure that duplicates of <strong> tags etc do not occur.
// Also potentially creating cleaner markup preventing <em> and <strong> tags from surrounding images etc.
// @todo: documentation, js properties and methods, hooks, etc, readme
// @todo: fix CSS selector exclusion
// @todo: test with multiples... #editor-preview needs to be specific
// --------------------- BETA2:
// @todo: require that contrib plugins supply a filename for the javascript which needs to be included
// then work this into the editor_attach() logic
// @todo: fix click-clear
// @todo: path to optional stylesheet(s)
// @todo: allow plugins to display a help section explaining what it does or how to use it
// @todo: paste proxy
// @todo: right click content menus (optional)
// @todo: add jQuery goodness with the new 1.2 methods available
// @todo: visibility API integration in D5, refactor for D6, or use input format
// @todo: new png style which would be usable for EVERY theme, transparent glass look
// @todo: ability to load page with editors in textarea mode
// @todo: check plugin dependencies
// @todo: CSS sprites for core plugin images
// @todo: disable anchor tags in preview
// @todo: initialize hidden plugins without adding them to the profile string?
// @todo: plugin settings hooks
// @todo: crossbrowser support :D
// @todo: abstract out the option validation, but still make it optional so each plugin is not required
// to use the packaged validation. Show errors in the fields (error class).
// @todo: integrate css with sprites module? use specific comment syntax...
// @todo: use tabs/sub-tabs instead of the menu as is now
// @todo: filter all selects through a settings gui so that users can choose
// which options do or do not show up
// @todo: do not submit forms when clicking enter; plugins options must be entire form then use $().submit not $().click
// --------------------- BETA3:
// @todo: queryCommandEnabled()
// @todo: default style sheet(s)
// @todo: allow grip to resize editor as well?
// @todo: mouse down indication for plugins
// @todo: status bar, method in JS to add/change status
// @todo: resizeable
// @todo: support the textarea resizeing
// @todo: repopulate option dialogs... automatically, no plugin code
// @todo: more core plugins
// @todo: show revision info in status bar
// @todo: dispatch more events, make sure all potential plugin requirements are handled
// @todo: try catch blocks & Drupal.js implementations
// @todo: dependency logic, theme args etc
// @todo: abstract out dialog handler so it can be replced by popups etc
// @todo: test hooks, test everything!!
// @todo: prep for translations, get Yura to translate to german/ukrainian
// @todo: roll over a plugin and it gives you the description in the status bar?
// @todo: migrate to jQuery's new UI toolbar / RTE
// @todo: wrap validation conditionals with Drupal.t() such as if (uri == Drupal.t('Location')){ // Do something }
// @todo: refactor performance issues with needing modules to have plugins available at all times etc
// @todo: remove dependency for plugins module, and port the designMode jQuery plugin into editor.js
// @todo: abstract hidden upload plugin so other modules can provide image uploading etc
// @todo: unified plugin settings/configuration page(s)?
// @todo: spell check doc :)

/* -----------------------------------------------------------------

  Hook Implementations 

------------------------------------------------------------------ */

/**
* Implementation of hook_init().
*/
function editor_init() {
  editor_attach();
}

/**
 * Implementation of hook_perm();
 */
function editor_perm() {
  return array(
    'administer editor visibility',
    'administer editor profiles',
    'access editor',
  );
}

/**
 * Implementation of hook_menu().
 */
function editor_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/editor',
      'title' => t('Text Editor'),
      'description' => t('Visual text editor options and configuration.'),
      'callback' => 'editor_settings_overview',
      'access' => user_access('administer site configuration'),
    );
    $items[] = array(
      'path' => 'admin/editor/visibility',
      'title' => t('Visibility'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'editor_settings_visibility',
      'access' => user_access('administer editor visibility'),
    );
    $items[] = array(
      'path' => 'admin/editor/profiles',
      'title' => t('Profiles'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'editor_settings_profiles',
      'access' => user_access('administer editor profiles'),
    );
  }
  return $items;
}

/**
 * Implementation of hook_plugins().
 */
function editor_plugins() {
  return array(
    array(
      '#name' => 'design_mode',
      '#download_uri' => 'http://plugins.jquery.com/files/jquery.designmode.js_0.txt',
      '#description' => t('This plugin is required to supply cross-browser compatible methods needed for Editor.'),
    ),
  );
}

/* -----------------------------------------------------------------

  General Functionality 

------------------------------------------------------------------ */

/**
 * Initialize the editor attachment process.
 * 
 * This function essentially adds required files
 * and settings needed to 'attach' Editor via JavaScript.
 */
function editor_attach() {

  // Make sure design_mode plugin is available
  if (!($design_mode = plugins_available('design_mode'))) {
    return;
  }

  // Make sure visibility validates
  if (!visibility_api_access('editor')) {
    return;
  }

  // Gather plugins and profiles
  $plugins = editor_invoke_plugins();
  if (!($profile = editor_profile_get(variable_get('editor_default_profile', 'small')))) {
    return;
  }

  // Settings
  $data = array(
    'editor' => array(
      'toolbars' => editor_display_toolbars($profile),
    ),
  );

  // Plugin settings
  if (count($plugins)) {
    foreach ($plugins as $plugin) {
      $data['editor']['plugins'][$plugin->pid] = $plugin;
    }
  }

  // Add required css and javascript
  $module_path = drupal_get_path('module', 'editor');
  drupal_add_js($design_mode);
  drupal_add_css($module_path . '/editor.css');
  drupal_add_js($module_path . '/editor.js');
  drupal_add_js($module_path . '/editor.plugins.js');
  drupal_add_js($data, 'setting');
}

/**
 * Generate toolbar markup.
 * 
 * - Plugins required for display within the $profile passed are gathered. 
 * - Spacer characters; '|' are replaced with theme_editor_spacer().
 * - Plugins are validated for dependencies. 
 * - hook_plugin_api('alter') is invoked. 
 * - Plugin themes are applied.   
 * 
 * @param object $profile
 *   Plugin profile object
 * 
 * @return string
 *   Markup.
 * 
 * @see theme_editor_toolbar()
 * @see theme_editor_spacer()
 */
function editor_display_toolbars($profile) {
  $items = array();
  $plugins = editor_profile_plugins($profile);
  $plugin_count = count($plugins);
  if (count($plugins)) {
    foreach ($plugins as $i => $plugin) {

      // Ignore processing hidden plugins
      if ($plugin->class == 'hidden') {
        continue;
      }

      // Check for spacers
      if ($plugin == '|') {

        // No need for spacers at the beginning or end or the toolbars
        if ($i != 0 && $i != $plugin_count - 1) {
          $items[] = theme('editor_spacer');
        }
        continue;
      }

      // Invoke alter op
      editor_invoke_plugin_api('alter', $plugin);
      $plugin_class = editor_plugin_class_get($plugin->class);

      // Class must be available
      if ($plugin && $plugin_class) {

        // Theme must be available
        $theme = 'theme_' . $plugin_class->theme;
        if (function_exists($theme)) {
          $args = array();
          $args[] = $plugin_class->theme;
          $args[] = $plugin;
          if (count($plugin->theme_args)) {
            foreach ($plugin->theme_args as $arg) {
              $args[] = $arg;
            }
          }
          $items[] = @call_user_func_array('theme', $args);
        }
      }
    }
  }
  return theme('editor_toolbar', implode(' ', $items));
}

/**
 * Get a plugin profile object.
 * 
 * @param int $prid
 *   Profile id
 * 
 * @return mixed
 *   - success: Profile object
 *   - failure: FALSE
 */
function editor_profile_get($prid) {
  $profiles = editor_invoke_profiles();
  if (count($profiles)) {
    foreach ($profiles as $i => $profile) {
      if ($profile->prid == $prid) {
        return $profile;
      }
    }
  }
  return FALSE;
}

/**
 * Get plugins based on a profile's placeholders.
 * 
 * @param object $profile
 *   Profile object
 * 
 * @return array
 *   Plugin objects
 */
function editor_profile_plugins($profile) {
  $plugins = array();
  if (count($profile->profile_array)) {
    foreach ($profile->profile_array as $pid) {
      if ($pid != '|') {
        $plugin = editor_plugin_get($pid);
        if ($plugin->pid) {
          $plugins[] = $plugin;
        }
      }
      else {
        $plugins[] = '|';
      }
    }
  }
  return $plugins;
}

/**
 * Parase a profile string into a usable array of pid's and placeholders.
 * 
 * @return array
 * 
 * @todo: document
 * @todo: rename profile_string and profile_array
 */
function editor_profile_parse_string($profile_string) {
  if (is_string($profile_string)) {
    if ($profile_array = preg_split('/[\\s,]+/', $profile_string)) {
      return $profile_array;
    }
    else {
      return FALSE;
    }
  }
}

/**
 * Create a profile object.
 *
 * @param string $prid  
 *   A unique identifier. This may be something similar to
 *   'basic', or 'full'.
 * 
 * @param string $profile_string 
 *   A string of pid's and placeholders used which is parsed 
 *   later parsed into an array. The following place holders
 *   are available. 
 * 
 *   - '|': spacer
 * 
 * @param string $name 
 *   A human readable string which should be wrapped in t().
 *    
 * @param string $description 
 *   (optional) Description wrapped in t(). 
 *   
 * @returns object
 */
function editor_profile_create($prid, $name, $profile_string, $description = '') {
  $profile = new stdClass();
  $profile->prid = $prid;
  $profile->name = $name;
  $profile->profile_string = $profile_string;
  $profile->description = $description;
  return $profile;
}

/**
 * Get a plugin class object.
 * 
 * @param int $pcid
 *   Plugin class id
 * 
 * @return mixed
 *   - success: Plugin class object
 *   - failure: FALSE
 */
function editor_plugin_class_get($pcid) {
  $plugin_classes = editor_invoke_plugin_classes();
  if (count($plugin_classes)) {
    foreach ($plugin_classes as $i => $plugin_class) {
      if ($plugin_class->pcid == $pcid) {
        return $plugin_class;
      }
    }
  }
  return FALSE;
}

/**
 * Get a plugin object.
 * 
 * @param int $pid
 *   Plugin id
 * 
 * @return mixed
 *   - success: Plugin object
 *   - failure: FALSE
 */
function editor_plugin_get($pid) {
  $plugins = editor_invoke_plugins();
  if (count($plugins)) {
    foreach ($plugins as $i => $plugin) {
      if ($plugin->pid == $pid) {
        return $plugin;
      }
    }
  }
  return FALSE;
}

/**
 * Create a plugin object.
 *
 * @param string $pid  
 *   A unique identifier. This may be something similar to
 *   'align_left', 'image', or 'link'.
 *   
 * @param string $name 
 *   A human readable string which should be wrapped in t().
 *    
 * @param string $class 
 *   A plugin class pcid.
 * 
 * @param string $description 
 *   (optional) Description wrapped in t(). 
 * 
 * @param string $options 
 *   (optional) Markup which is displayed within the options panel. 
 * 
 * @param array $theme_args   
 *   (optional) An array of arguments which will be passed to the theme function.
 * 
 * @param array $dependencies   
 *   (optional) An array of plugins to which this plugin is dependant of. 
 *   
 * @returns object
 */
function editor_plugin_create($pid, $name, $class, $description = '', $options = array(), $theme_args = array(), $dependencies = array()) {
  $plugin = new stdClass();
  $plugin->pid = $pid;
  $plugin->name = $name;
  $plugin->class = $class;
  $plugin->description = $description;
  $plugin->options = $options;
  $plugin->theme_args = $theme_args;
  $plugin->dependencies = $dependencies;
  return $plugin;
}

/**
 * Create a plugin class object.
 *
 * @param string $pcid  
 *   A machine-readable unique identifier. This may be something similar 
 *   to 'button', or 'select'. 
 *   
 * @param string $name 
 *   A human readable string which should be wrapped in t().
 *    
 * @param string $theme 
 *   (optiona) A theme function name without the 'theme_' prefix.
 *   
 * @returns object
 */
function editor_plugin_class_create($pcid, $name, $theme = NULL) {
  $plugin_class = new stdClass();
  $plugin_class->pcid = $pcid;
  $plugin_class->name = $name;
  $plugin_class->theme = $theme;
  return $plugin_class;
}

/**
 * Invokes hook_editor_plugin_api().
 * 
 * This hook provides modules with access to different 
 * phases which fire during the plugin process allowing 
 * manipulation. These phases are detailed below see the
 * $op parameter.
 * 
 * @param string $op
 *   - 'alter': Allows altering of the plugin object before it is rendered.
 * 
 * @param object $plugin
 * 
 * @param mixed $a1
 *   Reserved for future use.
 * 
 * @param mixed $a2
 *   Reserved for future use
 */
function editor_invoke_plugin_api($op, &$plugin, $a1 = NULL, $a2 = NULL) {
  foreach (module_implements('editor_plugin_api') as $module) {
    $function = $module . '_editor_plugin_api';
    $function($op, $plugin, $a1, $a2);
  }
}

/**
 * Invokes hook_editor_profiles().
 * 
 * This hook provides modules with access to provide
 * their own editor profiles.
 * 
 * @see editor_profile_create()
 * 
 * @returns array
 *   Profiles
 */
function editor_invoke_profiles() {
  static $profiles;
  if (!isset($profiles)) {
    $path = drupal_get_path('module', 'editor');
    require_once $path . '/editor.profiles.inc';
    $profiles = module_invoke_all('editor_profiles');
    if (count($profiles)) {
      foreach ($profiles as &$profile) {

        // Check if any setting is overriding our profile string
        if ($profile_string = variable_get($profile->prid . '_profile_string', FALSE)) {
          $profile->profile_string = $profile_string;
        }
        $profile->profile_array = editor_profile_parse_string($profile->profile_string);
      }
    }
  }
  return $profiles;
}

/**
 * Invokes hook_editor_plugins().
 * 
 * The editor plugins hook allows for 'plugins'
 * which are displayed within the editor toolbar. This may 
 * range from buttons, select fields, etc. Each providing
 * unique funtionality.
 * 
 * @see editor_plugin_create()
 * 
 * @returns array
 *   Plugins
 */
function editor_invoke_plugins() {
  static $plugins;
  if (!isset($plugins)) {
    $path = drupal_get_path('module', 'editor');
    require_once $path . '/editor.plugins.inc';
    $plugins = module_invoke_all('editor_plugins');
  }
  return $plugins;
}

/**
 * Invokes hook_editor_plugin_classes().
 * 
 * The editor plugin class hook allows modules to provide
 * 'classes' of plugins such as buttons or select fields.
 * 
 * @see editor_plugin_class_create()
 * 
 * @returns array
 *   Plugins
 */
function editor_invoke_plugin_classes() {
  static $plugin_classes;
  if (!isset($plugin_classes)) {
    $path = drupal_get_path('module', 'editor');
    require_once $path . '/editor.plugins.classes.inc';
    $plugin_classes = module_invoke_all('editor_plugin_classes');
  }
  return $plugin_classes;
}

/* -----------------------------------------------------------------

  Forms 

------------------------------------------------------------------ */

/**
 * Editor settings overview.
 */
function editor_settings_overview() {

  // Check database setup if necessary
  if (function_exists('db_check_setup') && empty($_POST)) {
    db_check_setup();
  }
  $menu = menu_get_item(NULL, 'admin/editor');
  $content = system_admin_menu_block($menu);
  return theme('admin_block_content', $content);
}

/**
 * Visibility settings form.
 */
function editor_settings_visibility() {
  $form = array();
  $form['visibility'] = array(
    '#type' => 'fieldset',
    '#title' => t('Visibility'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  visibility_api_form_configuration('editor', $form['visibility'], TRUE, TRUE, TRUE);
  return system_settings_form($form);
}

/**
 * Profile settings form.
 */
function editor_settings_profiles() {
  $form = array();
  $profile_options = array();
  $profiles = editor_invoke_profiles();
  $plugins = editor_invoke_plugins();
  $plugin_help = theme('editor_plugin_help', $plugins);
  foreach ((array) $profiles as $profile) {
    $profile_options[$profile->prid] = $profile->name;
  }

  // Default profile
  $form['editor_default_profile'] = array(
    '#type' => 'select',
    '#title' => t('Default Profile'),
    '#default_value' => variable_get('editor_default_profile', 'small'),
    '#options' => $profile_options,
  );

  // Profile settings
  foreach ((array) $profiles as $i => $profile) {
    $form[$profile->prid] = array(
      '#type' => 'fieldset',
      '#title' => $profile->name,
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#description' => $profile->description,
    );
    $form[$profile->prid][$profile->prid . '_profile_string'] = array(
      '#type' => 'textarea',
      '#title' => t('Profile Plugins'),
      '#default_value' => $profile->profile_string,
      '#description' => t('Simply add the plugins \'token\' to this string to place it in the editor toolbar. The | character is transformed into a seperator and is simply for organization.'),
    );
    $form[$profile->prid]['tokens'] = array(
      '#type' => 'fieldset',
      '#title' => t('Plugin Tokens'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $form[$profile->prid]['tokens']['help'] = array(
      '#type' => 'markup',
      '#value' => $plugin_help,
    );
  }
  return system_settings_form($form);
}

/* -----------------------------------------------------------------

  Themes 

------------------------------------------------------------------ */

/**
 * @defgroup themeable Themeable functions
 * @{                        
 */

/**
 * Theme editor plugin token help section.
 * 
 * @param array $plugins
 * 
 * @return string
 *  Markup. 
 */
function theme_editor_plugin_help($plugins) {
  $header = array(
    t('Name'),
    t('Token'),
    t('Description'),
  );
  $rows = array();
  foreach ((array) $plugins as $plugin) {
    $rows[] = array(
      $plugin->name,
      $plugin->pid,
      $plugin->description,
    );
  }
  return theme('table', $header, $rows);
}

/**
 * Theme an editor toolbar.
 * 
 * @param string $content
 * 
 * @return string
 *   Markup.
 */
function theme_editor_toolbar($content) {
  return '<div class="editor-toolbar">' . $content . '<div class="clear-block"></div></div>';
}

/**
 * Theme a spacer.
 * 
 * @return string
 *   Markup.
 */
function theme_editor_spacer() {
  return '<div class="editor-spacer"></div>';
}

/**
 * @} End of "Themeable functions"
 */

Functions

Namesort descending Description
editor_attach Initialize the editor attachment process.
editor_display_toolbars Generate toolbar markup.
editor_init Implementation of hook_init().
editor_invoke_plugins Invokes hook_editor_plugins().
editor_invoke_plugin_api Invokes hook_editor_plugin_api().
editor_invoke_plugin_classes Invokes hook_editor_plugin_classes().
editor_invoke_profiles Invokes hook_editor_profiles().
editor_menu Implementation of hook_menu().
editor_perm Implementation of hook_perm();
editor_plugins Implementation of hook_plugins().
editor_plugin_class_create Create a plugin class object.
editor_plugin_class_get Get a plugin class object.
editor_plugin_create Create a plugin object.
editor_plugin_get Get a plugin object.
editor_profile_create Create a profile object.
editor_profile_get Get a plugin profile object.
editor_profile_parse_string Parase a profile string into a usable array of pid's and placeholders.
editor_profile_plugins Get plugins based on a profile's placeholders.
editor_settings_overview Editor settings overview.
editor_settings_profiles Profile settings form.
editor_settings_visibility Visibility settings form.
theme_editor_plugin_help Theme editor plugin token help section.
theme_editor_spacer Theme a spacer.
theme_editor_toolbar Theme an editor toolbar.