editor.module in Editor 5
Same filename and directory in other branches
Extendable WYSIWYG editor @author Tj Holowaychuk <tj@vision-media.ca> @link http://vision-media.ca @package Editor
File
editor.moduleView 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
Name | 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. |