// and Richard Bennett /** * @file * Integrate the TinyMCE editor (http://tinymce.moxiecode.com/) into Drupal. */ /** * Implementation of hook_menu(). */ function tinymce_menu($may_cache) { $items = array(); if ($may_cache) { $items[] = array('path' => 'admin/settings/tinymce', 'title' => t('tinymce'), 'callback' => 'tinymce_admin', 'access' => user_access('administer tinymce')); } return $items; } /** * Implementation of hook_help(). */ function tinymce_help($section) { switch ($section) { case 'admin/modules#description': return t('The TinyMCE Javascript HTML WYSIWYG editor.'); case 'admin/settings/tinymce#pages': return "node/*\nuser/*\ncomment/*"; } } /** * Implementation of hook_perm(). */ function tinymce_perm() { return array('administer tinymce', 'access tinymce'); } /** * Implementation of hook_img_assist_head(). */ function tinymce_img_assist_head() { global $base_url; // The tinymce docs say to include tiny_mce_popup.js, but this was killing IE! $popup_path = $base_url .'/'. drupal_get_path('module', 'tinymce'). '/tinymce/jscripts/tiny_mce/tiny_mce_popup.js'; $img_assist_prop = $base_url .'/'. drupal_get_path('module', 'img_assist'). '/properties.js'; $clean_url = variable_get('clean_url', 0); $img_template = variable_get('img_assist_img_html', img_assist_help('img_assist/template')); $img_template = str_replace("\r\n", "\n", $img_template); $img_template = str_replace("\n", '\n', addslashes($img_template)); $output = << EOD; return $output; } /** * Implementation of hook_img_assist_on_submit(). */ function tinymce_img_assist_on_submit() { return 'parent.insertImage(this.form);'; } /** * Implementation of hook_textarea(). */ function tinymce_textarea($op, $textarea_name) { static $is_running = FALSE; if (!user_access('access tinymce')) return NULL; global $user; global $base_url; static $profile_name; // Since tinymce_config() makes a db hit, only call it when we're pretty sure // we're gonna render tinymce. $valid_so_far = FALSE; if ($op == 'post') { if (!$profile_name) { $profile_name = db_result(db_query('SELECT s.name FROM {tinymce_settings} s INNER JOIN {tinymce_role} r ON r.name = s.name WHERE r.rid IN (%s)', implode(',', array_keys($user->roles)))); } $profile = tinymce_profile_load($profile_name); $init = tinymce_config($profile); $init['elements'] = 'edit['. $textarea_name .']'; $valid_so_far = TRUE; } if ($valid_so_far && _tinymce_page_match($profile)) { // Merge and overrivide user-defined TinyMCE settings. $init = array_merge($init, (array) theme('tinymce_theme', $init, $textarea_name, $init['theme'], $is_running)); foreach ($init as $k => $v) { if (strtolower($v) != 'true' && strtolower($v) != 'false') { $v = '"'. $v. '"'; } $settings[] = $k. ' : '. $v; } $tinymce_settings = implode(",\n ", $settings); if (function_exists('img_assist_help')) { $img_assist_js_on = $base_url .'/'. url('img_assist/add&editor=tinymce') .'&textarea='; $img_assist_js_off = $base_url .'/'. url('img_assist/add') .'&textarea='; $img_assist_on = $base_url .'/'. url('img_assist/add&editor=tinymce') .'&textarea=edit['. $textarea_name .']'; $img_assist_off = $base_url .'/'. url('img_assist/add') .'&textarea=edit['. $textarea_name .']'; } $enable = t('enable rich-text'); $disable = t('disable rich-text'); $tinymce_invoke = << tinyMCE.init({ $tinymce_settings }); EOD; $js_toggle = << function mceToggle(id, linkid) { element = document.getElementById(id); link = document.getElementById(linkid); img_assist = document.getElementById('img_assist-link-'+ id); if (tinyMCE.getEditorId(element.name) == null) { tinyMCE.addMCEControl(element, element.name); element.togg = 'on'; link.innerHTML = '$disable'; link.href = "javascript:mceToggle('" +id+ "', '" +linkid+ "');"; if (img_assist) img_assist.href = "$img_assist_js_on"+ element.name; link.blur(); } else { tinyMCE.removeMCEControl(tinyMCE.getEditorId(element.name)); element.togg = 'off'; link.innerHTML = '$enable'; link.href = "javascript:mceToggle('" +id+ "', '" +linkid+ "');"; if (img_assist) img_assist.href = "$img_assist_js_off"+ element.name; link.blur(); } } EOD; $status = isset($user->tinymce_status) ? $user->tinymce_status : variable_get('tinymce_default_state', 0); $link_text = $status == 1 ? $disable : $enable; $no_wysiwyg = t('Your current web browser does not support WYSIWYG editing.'); $wysiwyg_link = << img_assist = document.getElementById('img_assist-link-edit-$textarea_name'); if (img_assist) { img_assist.href = tinyMCE.getEditorId('edit-$textarea_name') == null ? "$img_assist_on" : "$img_assist_off"; } if (typeof(document.execCommand) == 'undefined') { img_assist.href = "$img_assist_off"; document.write('
$no_wysiwyg
'); } else { document.write(""); } EOD; // We only load the TinyMCE js file once per request if (!$is_running) { $is_running = TRUE; // For some crazy reason IE will only load this JS file if the absolute reference is given to it. drupal_set_html_head(''); drupal_set_html_head($js_toggle); // We have to do this becuase of some unfocused CSS in certain themes. See http://drupal.org/node/18879 for details drupal_set_html_head(''); } // Load a TinyMCE init for each textarea. drupal_set_html_head($tinymce_invoke); return $wysiwyg_link; } } /** * Implementation of hook_user(). */ function tinymce_user($type, &$edit, &$user, $category = NULL) { if ($type == 'form' && $category == 'account' && user_access('access tinymce')) { $user_status = $edit['tinymce_status'] != NULL ? $edit['tinymce_status'] : ($user->tinymce_status != NULL ? $user->tinymce_status : variable_get('tinymce_default_state', 0)); $form = form_radios(t('Default status'), 'tinymce_status', $user_status, array(t('Off'), t('On')), t('Should rich-text editing be enabled or disabled by default in textarea fields?')); return array(array('title' => t('TinyMCE settings'), 'data' => $form)); } if ($type == 'validate') { return array('tinymce_status' => $edit['tinymce_status']); } } /** * @addtogroup themeable * @{ */ /** * Customize a TinyMCE theme. * * @param init * An array of settings TinyMCE should invoke a theme. You may override any * of the TinyMCE settings. Details here: * * http://tinymce.moxiecode.com/wrapper.php?url=tinymce/docs/using.htm * * @param textarea_name * The name of the textarea TinyMCE wants to enable. * * @param theme_name * The default theme name to be enabled for this textarea. The sitewide * default is 'simple', but the user may also override this. * * @param is_running * A boolean flag that identifies id TinyMCE is currently running for this * request life cycle. If it's already running then anything returned by this * will be ignored. This is necessary since TinyMCE works by being invoked * once per request and not once per textarea. */ function theme_tinymce_theme($init, $textarea_name, $theme_name, $is_running) { // Force the 'simple' theme for some of the smaller textareas. switch ($textarea_name) { case 'log': case 'signature': case 'site_mission': case 'site_footer': case 'settings][access_pages': $init['theme'] = 'simple'; unset($init['theme_advanced_toolbar_location']); unset($init['theme_advanced_toolbar_align']); unset($init['theme_advanced_path_location']); unset($init['theme_advanced_blockformats']); unset($init['theme_advanced_styles']); return $init; } switch ($theme_name) { case 'advanced': $init['extended_valid_elements'] = 'a[href|target|name|title|onclick]'; $init['theme_advanced_buttons3_add_before'] = 'tablecontrols,separator'; $init['plugins'] = file_exists(drupal_get_path('module', 'tinymce'). '/tinymce/jscripts/tiny_mce/plugins/drupalimage') ? 'drupalimage,table,emotions,print' : 'table,emotions,print'; $init['theme_advanced_buttons3_add'] = 'drupalimage,emotions,separator,print'; return $init; } } /** @} End of addtogroup themeable */ /** * Grab the themes available to TinyMCE. * * TinyMCE themes control the functionality and buttons that are available to a * user. Themes are only looked for within the default TinyMCE theme directory. * * @return * An array of theme names. */ function _tinymce_get_themes() { static $themes = array(); if (!$themes) { $theme_loc = drupal_get_path('module', 'tinymce') .'/tinymce/jscripts/tiny_mce/themes/'; if (is_dir($theme_loc) && $dh = opendir($theme_loc)) { while (($file = readdir($dh)) !== false) { if (!in_array($file, array('.', '..', 'CVS')) && is_dir($theme_loc . $file)) { $themes[$file] = $file; } } closedir($dh); asort($themes); } } return $themes; } /******************************************************************** * Module Functions :: Public ********************************************************************/ /** * Controller for tinymce administrative settings. */ function tinymce_admin($arg = NULL) { $edit = $_POST['edit']; $op = $_POST['op']; $op = $arg && !$op ? $arg : $op; switch ($op) { case 'add': $breadcrumb[] = array('path' => 'admin', 'title' => t('administer')); $breadcrumb[] = array('path' => 'admin/settings/tinymce', 'title' => t('tinymce')); $breadcrumb[] = array('path' => 'admin/settings/tinymce/add', 'title' => t('Add new tinymce profile')); menu_set_location($breadcrumb); $output = tinymce_profile_form($edit); break; case 'edit': drupal_set_title(t('Edit tinymce profile')); $output = tinymce_profile_form(tinymce_profile_load(urldecode(arg(4)))); break; case 'delete': tinymce_profile_delete(urldecode(arg(4))); drupal_set_message(t('Deleted profile')); drupal_goto('admin/settings/tinymce'); break; case t('Create profile'); case t('Update profile'); if (tinymce_profile_validate($edit)) { tinymce_profile_save($edit); $edit['old_name'] ? drupal_set_message(t('Your tinymce profile has been updated.')) : drupal_set_message(t('Your tinymce profile has been created.')); drupal_goto('admin/settings/tinymce'); } else { $output = tinymce_profile_form($edit); } break; case t('Save settings'): variable_set('tinymce_default_state', $edit['tinymce_default_state']); drupal_set_message(t('Settings updated')); drupal_goto('admin/settings/tinymce'); break; default: drupal_set_title(t('TinyMCE settings (%revision)', array('%revision' => '$Revision$'))); //Check if TinyMCE is installed. $tinymce_loc = drupal_get_path('module', 'tinymce') .'/tinymce/'; if (!is_dir($tinymce_loc)) { drupal_set_message(t('Could not find the TinyMCE engine installed at %tinymce-directory. Please download TinyMCE, uncompress it and copy the folder into %tinymce-path.', array('%tinymce-path' => drupal_get_path('module', 'tinymce'), '%tinymce-directory' => $tinymce_loc)), 'error'); } $output = tinymce_profile_overview(); } print theme('page', $output); } /** * Return an array of initial tinymce config options from the current role. */ function tinymce_config($profile) { global $base_url; global $user; $settings = $profile->settings; // Build a default list of TinyMCE settings. // Is tinymce on by default? $init['mode'] = variable_get('tinymce_default_state', 0) == 1 ? 'exact' : 'none'; $status = isset($user->tinymce_status) ? $user->tinymce_status : variable_get('tinymce_default_state', 0); $init['mode'] = $status == 1 ? 'exact' : 'none'; $init['theme'] = $settings['theme'] ? $settings['theme'] : 'simple'; $init['document_base_url'] = "$base_url/"; $init['verify_html'] = $settings['verify_html'] ? $settings['verify_html'] : 'false'; $init['auto_cleanup_word'] = $settings['msword'] ? $settings['msword'] : 'false'; $init['preformatted'] = $settings['preformatted'] ? $settings['preformatted'] : 'false'; $init['force_br_newlines'] = $settings['force_br'] ? $settings['force_br'] : 'false'; $init['force_p_newlines'] = $settings['force_p'] ? $settings['force_p'] : 'false'; if ($init['theme'] == 'advanced') { $init['theme_advanced_toolbar_location'] = $settings['toolbar_loc'] ? $settings['toolbar_loc'] : 'bottom'; $init['theme_advanced_toolbar_align'] = $settings['toolbar_align'] ? $settings['toolbar_align'] : 'left'; $init['theme_advanced_path_location'] = $settings['path_loc'] ? $settings['path_loc'] : 'none'; $init['theme_advanced_blockformats'] = $settings['block_formats'] ? $settings['block_formats'] : 'p,address,pre,h1,h2,h3,h4,h5,h6'; } if ($edit['css_classes']) $init['theme_advanced_styles'] = $settings['css_classes']; if ($settings['width']) $init['width'] = $settings['width']; if ($settings['height']) $init['height'] = $settings['height']; if ($settings['css_setting'] == 'theme') { $css = drupal_get_path('theme', init_theme()) . '/style.css'; if (file_exists($css)) { $init['content_css'] = $base_url .'/'. $css; } else if ($settings['css_setting'] == 'self') { $init['content_css'] = $edit['css_path']; } } return $init; } /** * Remove a profile from the database. */ function tinymce_profile_delete($name) { db_query("DELETE FROM {tinymce_settings} WHERE name = '%s'", $name); db_query("DELETE FROM {tinymce_role} WHERE name = '%s'", $name); } /** * Return an HTML form for profile configuration. */ function tinymce_profile_form($edit) { $edit = array2object($edit); // Only display the roles that currently don't have a tinymce profile. One // profile per role. $orig_roles = user_roles(); $roles = $orig_roles; if (arg(3) == 'add') { $result = db_query('SELECT DISTINCT(rid) FROM {tinymce_role}'); while ($data = db_fetch_object($result)) { unset($roles[$data->rid]); } if (!$roles) { drupal_set_message(t('You will not be allowed to create a new profile since all user roles have already been assigned profiles. First remove an existing tinymce profile from at least one role in order to create a new profile.'), 'error'); } else if (count($orig_roles) != count($roles)) { drupal_set_message(t('Not all user roles are shown since they already have tinymce profiles. You must first unassign profiles in order to add them to a new one.')); } $btn = t('Create profile'); } else { $output = form_hidden('old_name', $edit->name); $btn = t('Update profile'); } $group = form_textfield(t('Profile name'), 'name', $edit->name, 40, 128, t('Enter an unique name for this profile. This name is only visible in the tinymce administration page.'), NULL, TRUE); $group .= form_checkboxes(t('Roles allowed to use this profile'), 'rids', array_keys((array) $edit->rids), $roles, t('Select at least one role.'), NULL, TRUE); $group .= form_radios(t('Theme'), 'settings][theme', $edit->settings['theme'] ? $edit->settings['theme'] : 'simple', _tinymce_get_themes(), t('Select the tinymce theme.')); $group .= form_radios(t('Make tinymce visible on'), 'settings][access', $edit->settings['access'], array(t('specific pages'), t('all textareas'))); $group .= form_textarea(t('Specific pages'), 'settings][access_pages', $edit->settings['access_pages'] ? $edit->settings['access_pages'] : tinymce_help('admin/settings/tinymce#pages'), 40, 5, t("Enter one page per line as Drupal paths. The '*' character is a wildcard. Example paths are 'blog' for the blog page and 'blog/*' for every personal blog. '<front>' is the front page.")); $output .= form_group(t('Basic setup'), $group); $output .= t('

Advanced options

'); $group = form_textfield(t('Editor width'), 'settings][width', $edit->settings['width'], 3, 5, t('Set width of editor. Leave blank to use size of textarea being replaced.')); $group .= form_textfield(t('Editor height'), 'settings][height', $edit->settings['height'], 3, 5, t('Set height of editor. Leave blank to use size of textarea being replaced.')); $group .= form_select(t('Toolbar location'), 'settings][toolbar_loc', $edit->settings['toolbar_loc'], array('bottom' => 'bottom', 'top' => 'top'), t('Show toolbar at the top or bottom of the editor area?')); $group .= form_select(t('Toolbar alignment'), 'settings][toolbar_align', $edit->settings['toolbar_align'], array('center' => 'center', 'left' => 'left', 'right' => 'right'), t('Align tool icons left, center, or right within the toolbar.')); $group .= form_select(t('Path location'), 'settings][path_loc', $edit->settings['path_loc'], array('none' => 'none', 'top' => 'top', 'bottom' => 'bottom'), t('Path to html elements (i.e. "body>table>tr>td"). Show at top, bottom, or not at all.')); $output .= form_group(t('Editor display'), $group); $group = form_select(t('Auto cleanup Word'), 'settings][msword', $edit->settings['msword'], array('true' => 'true', 'false' => 'false'), t('Automatically cleanup MS Office/Word HTML will be executed automatically on paste operations. (Only works in Internet Explorer)')); $group .= form_select(t('Verify HTML'), 'settings][verify_html', $edit->settings['verify_html'], array('true' => 'true', 'false' => 'false'), t('Should the HTML contents be verified or not? Verifying will strip <head> tags, so choose false if you will be editing full page HTML.')); $group .= form_select(t('Preformatted'), 'settings][preformatted', $edit->settings['preformatted'], array('false' => 'false', 'true' => 'true'), t('If this option is set to true, the editor will insert TAB characters on tab and preserve other whitespace characters just like a PRE HTML element does.')); $output .= form_group(t('On save'), $group); $group = form_select(t('Editor CSS'), 'settings][css_setting', $edit->settings['css_setting'] ? $edit->settings['css_setting'] : 'theme', array('theme' => 'use theme css', 'self' => 'define css', 'none' => 'tinyMCE default'), t('Defines the CSS to be used in the editor area.
use theme css - get css from current Drupal theme.
define css - enter path for css file below.
tinyMCE default - uses default CSS from editor.')); $group .= form_textfield(t('CSS path'), 'settings][css_path', $edit->settings['css_path'], 40, 255, t('Enter path to CSS file (example: "/css/editor.css"). Select "define css" above.')); $group .= form_textfield(t('CSS classes'), 'settings][css_classes', $edit->settings['css_classes'], 40, 255, t('Adds CSS classes to the "styles" droplist. Format is "<title>=<class>;"
Example: "Header 1=header1;Header 2=header2;Header 3=header3;"
Leave blank to automatically import list of CSS classes from style sheet.')); $output .= form_group(t('CSS'), $group); $group = form_textfield(t('Block formats'), 'settings][block_formats', $edit->settings['block_formats'] ? $edit->settings['block_formats'] : 'p,address,pre,h1,h2,h3,h4,h5,h6', 40, 250, t('Comma separated list of HTML block formats. You can only remove elements, not add.')); $group .= form_select(t('Force BR new lines'), 'settings][force_br', $edit->settings['force_br'] ? $edit->settings['force_br'] : 'false', array('true' => 'true', 'false' => 'false'), t('Use BR tags for new lines rather than P.')); $group .= form_select(t('Force P new lines'), 'settings][force_p', $edit->settings['force_p'] ? $edit->settings['force_p'] : 'true', array('true' => 'true', 'false' => 'false'), t('When enabled, Mozilla/Firefox will generate P elements on Enter/Return key and BR elements on Shift+Enter/Return..')); $output .= form_group(t('Formatting'), $group); $output .= form_submit($btn); return form($output); } /** * Load all profiles. */ function tinymce_profile_load($name = '') { static $profiles = array(); if (!$profiles) { $roles = user_roles(); $result = db_query('SELECT * FROM {tinymce_settings}'); while ($data = db_fetch_object($result)) { $data->settings = unserialize($data->settings); $result2 = db_query("SELECT rid FROM {tinymce_role} WHERE name = '%s'", $data->name); $role = array(); while ($r = db_fetch_object($result2)) { $role[$r->rid] = $roles[$r->rid]; } $data->rids = $role; $profiles[$data->name] = $data; } } return ($name ? $profiles[$name] : $profiles); } /** * Controller for tinymce profiles. */ function tinymce_profile_overview() { $output = ''; $output .= t('

Create new profile

', array('%create-profile-url' => url('admin/settings/tinymce/add'))); $profiles = tinymce_profile_load(); if ($profiles) { $roles = user_roles(); $header = array(t('Profile'), t('Roles'), t('Operations')); foreach ($profiles as $p) { $rows[] = array(array('data' => $p->name, 'valign' => 'top'), array('data' => implode("
\n", $p->rids)), array('data' => l(t('edit'), 'admin/settings/tinymce/edit/'. urlencode($p->name)) . ' '. l(t('delete'), 'admin/settings/tinymce/delete/'. urlencode($p->name)), 'valign' => 'top')); } $output .= theme('table', $header, $rows). '

 

'; } else { $output .= t('

No profiles found.

'); } $group = form_radios(t('Default tinymce state'), 'tinymce_default_state', variable_get('tinymce_default_state', 0), array(t('Off'), t('On')), t('Should tinymce be enabled or disabled by default when it\'s first loaded from a textarea? Note: The user may override this setting in their profile.')); $output .= form_group(t('Default settings'), $group); $output .= form_submit(t('Save settings')); return form($output); } /** * Save a profile to the database. */ function tinymce_profile_save($edit) { db_query("DELETE FROM {tinymce_settings} WHERE name = '%s' or name = '%s'", $edit['name'], $edit['old_name']); db_query("DELETE FROM {tinymce_role} WHERE name = '%s' or name = '%s'", $edit['name'], $edit['old_name']); db_query("INSERT INTO {tinymce_settings} (name, settings) VALUES ('%s', '%s')", $edit['name'], serialize($edit['settings'])); foreach ($edit['rids'] as $rid) { db_query("INSERT INTO {tinymce_role} (name, rid) VALUES ('%s', %d)", $edit['name'], $rid); } } /** * Profile validation. */ function tinymce_profile_validate($edit) { $errors = array(); if (!$edit['name']) { $errors['name'] = t('You must give a profile name.'); } if (!$edit['rids']) { $errors['rids'] = t('You must select at least one role.'); } foreach ($errors as $name => $message) { form_set_error($name, $message); } return count($errors) == 0; } /******************************************************************** * Module Functions :: Private ********************************************************************/ /** * Determine if TinyMCE has permission to be used on the current page. * * @return * TRUE if can render, FALSE if not allowed. */ function _tinymce_page_match($edit) { //Kill TinyMCE if we're editing a textarea with PHP in it! if ($_POST['edit']['format'] == 2) { return FALSE; } else { // PHP input formats are #2 in the filters table. preg_match("|^node/(\d+)(/edit)$|", $_GET['q'], $match); if (intval($match[1]) > 0) { if (db_result(db_query('SELECT format FROM {node} WHERE nid = %d AND format = 2', $match[1]))) { return FALSE; } } } if ($edit->settings['access'] == 1) { return TRUE; } else { $page_match = FALSE; $pages = $edit->settings['access_pages'] ? $edit->settings['access_pages'] : tinymce_help('admin/settings/tinymce#pages'); if ($pages) { $path = drupal_get_path_alias($_GET['q']); $regexp = '/^('. preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/', '/(^|\|)\\\\($|\|)/'), array('|', '.*', '\1'. variable_get('site_frontpage', 'node') .'\2'), preg_quote($pages, '/')) .')$/'; $page_match = preg_match($regexp, $path); } return $page_match; } } ?>