ckeditor.inc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <?php
  2. /**
  3. * @file
  4. * Editor integration functions for CKEditor.
  5. */
  6. /**
  7. * Plugin implementation of hook_editor().
  8. */
  9. function wysiwyg_ckeditor_editor() {
  10. $editor['ckeditor'] = array(
  11. 'title' => 'CKEditor',
  12. 'vendor url' => 'http://ckeditor.com',
  13. 'download url' => 'http://ckeditor.com/download',
  14. 'libraries' => array(
  15. '' => array(
  16. 'title' => 'Default',
  17. 'files' => array(
  18. 'ckeditor.js' => array('preprocess' => FALSE),
  19. ),
  20. ),
  21. 'src' => array(
  22. 'title' => 'Source',
  23. 'files' => array(
  24. 'ckeditor_source.js' => array('preprocess' => FALSE),
  25. ),
  26. ),
  27. ),
  28. 'install note callback' => 'wysiwyg_ckeditor_install_note',
  29. 'version callback' => 'wysiwyg_ckeditor_version',
  30. 'themes callback' => 'wysiwyg_ckeditor_themes',
  31. 'settings form callback' => 'wysiwyg_ckeditor_settings_form',
  32. 'init callback' => 'wysiwyg_ckeditor_init',
  33. 'settings callback' => 'wysiwyg_ckeditor_settings',
  34. 'plugin callback' => 'wysiwyg_ckeditor_plugins',
  35. 'plugin settings callback' => 'wysiwyg_ckeditor_plugin_settings',
  36. 'proxy plugin' => array(
  37. 'drupal' => array(
  38. 'load' => TRUE,
  39. 'proxy' => TRUE,
  40. ),
  41. ),
  42. 'proxy plugin settings callback' => 'wysiwyg_ckeditor_proxy_plugin_settings',
  43. 'versions' => array(
  44. '3.0.0.3665' => array(
  45. 'js files' => array('ckeditor-3.0.js'),
  46. ),
  47. ),
  48. );
  49. return $editor;
  50. }
  51. /**
  52. * Return an install note.
  53. */
  54. function wysiwyg_ckeditor_install_note() {
  55. return '<p class="warning">' . t('Do NOT download the "CKEditor for Drupal" edition.') . '</p>';
  56. }
  57. /**
  58. * Detect editor version.
  59. *
  60. * @param $editor
  61. * An array containing editor properties as returned from hook_editor().
  62. *
  63. * @return
  64. * The installed editor version.
  65. */
  66. function wysiwyg_ckeditor_version($editor) {
  67. $library = $editor['library path'] . '/ckeditor.js';
  68. if (!file_exists($library)) {
  69. return;
  70. }
  71. $library = fopen($library, 'r');
  72. $max_lines = 8;
  73. while ($max_lines && $line = fgets($library, 500)) {
  74. // version:'CKEditor 3.0 SVN',revision:'3665'
  75. // version:'3.0 RC',revision:'3753'
  76. // version:'3.0.1',revision:'4391'
  77. if (preg_match('@version:\'(?:CKEditor )?([\d\.]+)(?:.+revision:\'([\d]+))?@', $line, $version)) {
  78. fclose($library);
  79. // Version numbers need to have three parts since 3.0.1.
  80. $version[1] = preg_replace('/^(\d+)\.(\d+)$/', '${1}.${2}.0', $version[1]);
  81. return $version[1] . '.' . $version[2];
  82. }
  83. $max_lines--;
  84. }
  85. fclose($library);
  86. }
  87. /**
  88. * Determine available editor themes or check/reset a given one.
  89. *
  90. * @param $editor
  91. * A processed hook_editor() array of editor properties.
  92. * @param $profile
  93. * A wysiwyg editor profile.
  94. *
  95. * @return
  96. * An array of theme names. The first returned name should be the default
  97. * theme name.
  98. */
  99. function wysiwyg_ckeditor_themes($editor, $profile) {
  100. // @todo Skins are not themes but this will do for now.
  101. $path = $editor['library path'] . '/skins/';
  102. if (file_exists($path) && ($dir_handle = opendir($path))) {
  103. $themes = array();
  104. while ($file = readdir($dir_handle)) {
  105. if (is_dir($path . $file) && substr($file, 0, 1) != '.' && $file != 'CVS') {
  106. $themes[] = $file;
  107. }
  108. }
  109. closedir($dir_handle);
  110. natcasesort($themes);
  111. $themes = array_values($themes);
  112. return !empty($themes) ? $themes : array('default');
  113. }
  114. else {
  115. return array('default');
  116. }
  117. }
  118. /**
  119. * Enhances the editor profile settings form for CKEditor.
  120. *
  121. * Adds support for CKEditor's advanced stylesSets, which are a more advanced
  122. * implementation and combination of block formats and font styles that allow
  123. * to adjust the HTML element, attributes, and CSS styles at once.
  124. *
  125. * @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles
  126. * @see http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.config.html#.stylesSet
  127. */
  128. function wysiwyg_ckeditor_settings_form(&$form, &$form_state) {
  129. if (version_compare($form_state['wysiwyg']['editor']['installed version'], '3.2.1', '>=')) {
  130. // Replace CSS classes element description to explain the advanced syntax.
  131. $form['css']['css_classes']['#description'] = t('Optionally define CSS classes for the "Font style" dropdown list.<br />Enter one class on each line in the format: !format. Example: !example<br />If left blank, CSS classes are automatically imported from loaded stylesheet(s).', array(
  132. '!format' => '<code>[label]=[element].[class]</code>',
  133. '!example' => '<code>Title=h1.title</code>',
  134. ));
  135. $form['css']['css_classes']['#element_validate'][] = 'wysiwyg_ckeditor_settings_form_validate_css_classes';
  136. }
  137. else {
  138. // Versions below 3.2.1 do not support Font styles at all.
  139. $form['css']['css_classes']['#access'] = FALSE;
  140. }
  141. }
  142. /**
  143. * #element_validate handler for CSS classes element altered by wysiwyg_ckeditor_settings_form().
  144. */
  145. function wysiwyg_ckeditor_settings_form_validate_css_classes($element, &$form_state) {
  146. if (wysiwyg_ckeditor_settings_parse_styles($element['#value']) === FALSE) {
  147. form_error($element, t('The specified CSS classes are syntactically incorrect.'));
  148. }
  149. }
  150. /**
  151. * Returns an initialization JavaScript for this editor library.
  152. *
  153. * @param array $editor
  154. * The editor library definition.
  155. * @param string $library
  156. * The library variant key from $editor['libraries'].
  157. * @param object $profile
  158. * The (first) wysiwyg editor profile.
  159. *
  160. * @return string
  161. * A string containing inline JavaScript to execute before the editor library
  162. * script is loaded.
  163. */
  164. function wysiwyg_ckeditor_init($editor) {
  165. // CKEditor unconditionally searches for its library filename in SCRIPT tags
  166. // on the page upon loading the library in order to determine the base path to
  167. // itself. When JavaScript aggregation is enabled, this search fails and all
  168. // relative constructed paths within CKEditor are broken. The library has a
  169. // CKEditor.basePath property, but it is not publicly documented and thus not
  170. // reliable. The official documentation suggests to solve the issue through
  171. // the global window variable.
  172. // @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Specifying_the_Editor_Path
  173. $library_path = base_path() . $editor['library path'] . '/';
  174. return <<<EOL
  175. window.CKEDITOR_BASEPATH = '$library_path';
  176. EOL;
  177. }
  178. /**
  179. * Return runtime editor settings for a given wysiwyg profile.
  180. *
  181. * @param $editor
  182. * A processed hook_editor() array of editor properties.
  183. * @param $config
  184. * An array containing wysiwyg editor profile settings.
  185. * @param $theme
  186. * The name of a theme/GUI/skin to use.
  187. *
  188. * @return
  189. * A settings array to be populated in
  190. * Drupal.settings.wysiwyg.configs.{editor}
  191. */
  192. function wysiwyg_ckeditor_settings($editor, $config, $theme) {
  193. $settings = array(
  194. 'width' => 'auto',
  195. // For better compatibility with smaller textareas.
  196. 'resize_minWidth' => 450,
  197. 'height' => 420,
  198. // @todo Do not use skins as themes and add separate skin handling.
  199. 'theme' => 'default',
  200. 'skin' => !empty($theme) ? $theme : 'kama',
  201. // By default, CKEditor converts most characters into HTML entities. Since
  202. // it does not support a custom definition, but Drupal supports Unicode, we
  203. // disable at least the additional character sets. CKEditor always converts
  204. // XML default characters '&', '<', '>'.
  205. // @todo Check whether completely disabling ProcessHTMLEntities is an option.
  206. 'entities_latin' => FALSE,
  207. 'entities_greek' => FALSE,
  208. );
  209. // Add HTML block format settings; common block formats are already predefined
  210. // by CKEditor.
  211. if (isset($config['block_formats'])) {
  212. $block_formats = explode(',', drupal_strtolower($config['block_formats']));
  213. $predefined_formats = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'address', 'div');
  214. foreach (array_diff($block_formats, $predefined_formats) as $tag) {
  215. $tag = trim($tag);
  216. $settings["format_$tag"] = array('element' => $tag);
  217. }
  218. $settings['format_tags'] = implode(';', $block_formats);
  219. }
  220. if (isset($config['apply_source_formatting'])) {
  221. $settings['apply_source_formatting'] = $config['apply_source_formatting'];
  222. }
  223. if (isset($config['css_setting'])) {
  224. // Versions below 3.0.1 could only handle one stylesheet.
  225. if (version_compare($editor['installed version'], '3.0.1.4391', '<')) {
  226. if ($config['css_setting'] == 'theme') {
  227. $settings['contentsCss'] = reset(wysiwyg_get_css());
  228. }
  229. elseif ($config['css_setting'] == 'self' && isset($config['css_path'])) {
  230. $settings['contentsCss'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => drupal_get_path('theme', variable_get('theme_default', NULL))));
  231. }
  232. }
  233. else {
  234. if ($config['css_setting'] == 'theme') {
  235. $settings['contentsCss'] = wysiwyg_get_css();
  236. }
  237. elseif ($config['css_setting'] == 'self' && isset($config['css_path'])) {
  238. $settings['contentsCss'] = explode(',', strtr($config['css_path'], array('%b' => base_path(), '%t' => drupal_get_path('theme', variable_get('theme_default', NULL)))));
  239. }
  240. }
  241. }
  242. // Parse and define the styles set for the Styles plugin (3.2.1+).
  243. // @todo This should be a plugin setting, but Wysiwyg does not support
  244. // plugin-specific settings yet.
  245. if (!empty($config['buttons']['default']['Styles']) && version_compare($editor['installed version'], '3.2.1', '>=')) {
  246. if ($styles = wysiwyg_ckeditor_settings_parse_styles($config['css_classes'])) {
  247. $settings['stylesSet'] = $styles;
  248. }
  249. }
  250. if (isset($config['language'])) {
  251. $settings['language'] = $config['language'];
  252. }
  253. if (isset($config['resizing'])) {
  254. // CKEditor performs a type-agnostic comparison on this particular setting.
  255. $settings['resize_enabled'] = (bool) $config['resizing'];
  256. }
  257. if (isset($config['toolbar_loc'])) {
  258. $settings['toolbarLocation'] = $config['toolbar_loc'];
  259. }
  260. $settings['toolbar'] = array();
  261. if (!empty($config['buttons'])) {
  262. $extra_plugins = array();
  263. $plugins = wysiwyg_get_plugins($editor['name']);
  264. foreach ($config['buttons'] as $plugin => $buttons) {
  265. foreach ($buttons as $button => $enabled) {
  266. // Iterate separately over buttons and extensions properties.
  267. foreach (array('buttons', 'extensions') as $type) {
  268. // Skip unavailable plugins.
  269. if (!isset($plugins[$plugin][$type][$button])) {
  270. continue;
  271. }
  272. // Add buttons.
  273. if ($type == 'buttons') {
  274. $settings['toolbar'][] = $button;
  275. }
  276. // Add external Drupal plugins to the list of extensions.
  277. if ($type == 'buttons' && !empty($plugins[$plugin]['proxy'])) {
  278. $extra_plugins[] = $button;
  279. }
  280. // Add external plugins to the list of extensions.
  281. elseif ($type == 'buttons' && empty($plugins[$plugin]['internal'])) {
  282. $extra_plugins[] = $plugin;
  283. }
  284. // Add internal buttons that also need to be loaded as extension.
  285. elseif ($type == 'buttons' && !empty($plugins[$plugin]['load'])) {
  286. $extra_plugins[] = $plugin;
  287. }
  288. // Add plain extensions.
  289. elseif ($type == 'extensions' && !empty($plugins[$plugin]['load'])) {
  290. $extra_plugins[] = $plugin;
  291. }
  292. // Allow plugins to add or override global configuration settings.
  293. if (!empty($plugins[$plugin]['options'])) {
  294. $settings = array_merge($settings, $plugins[$plugin]['options']);
  295. }
  296. }
  297. }
  298. }
  299. if (!empty($extra_plugins)) {
  300. $settings['extraPlugins'] = implode(',', $extra_plugins);
  301. }
  302. }
  303. // For now, all buttons are placed into one row.
  304. $settings['toolbar'] = array($settings['toolbar']);
  305. return $settings;
  306. }
  307. /**
  308. * Parses CSS classes settings string into a stylesSet JavaScript settings array.
  309. *
  310. * @param string $css_classes
  311. * A string containing CSS class definitions to add to the Style dropdown
  312. * list, separated by newlines.
  313. *
  314. * @return array|false
  315. * An array containing the parsed stylesSet definition, or FALSE on parse
  316. * error.
  317. *
  318. * @see wysiwyg_ckeditor_settings_form()
  319. * @see wysiwyg_ckeditor_settings_form_validate_css_classes()
  320. *
  321. * @todo This should be a plugin setting, but Wysiwyg does not support
  322. * plugin-specific settings yet.
  323. */
  324. function wysiwyg_ckeditor_settings_parse_styles($css_classes) {
  325. $set = array();
  326. $input = trim($css_classes);
  327. if (empty($input)) {
  328. return $set;
  329. }
  330. // Handle both Unix and Windows line-endings.
  331. foreach (explode("\n", str_replace("\r", '', $input)) as $line) {
  332. $line = trim($line);
  333. // [label]=[element].[class][.[class]][...] pattern expected.
  334. if (!preg_match('@^.+= *[a-zA-Z0-9]+(\.[a-zA-Z0-9_ -]+)*$@', $line)) {
  335. return FALSE;
  336. }
  337. list($label, $selector) = explode('=', $line, 2);
  338. $classes = explode('.', $selector);
  339. $element = array_shift($classes);
  340. $style = array();
  341. $style['name'] = trim($label);
  342. $style['element'] = trim($element);
  343. if (!empty($classes)) {
  344. $style['attributes']['class'] = implode(' ', array_map('trim', $classes));
  345. }
  346. $set[] = $style;
  347. }
  348. return $set;
  349. }
  350. /**
  351. * Build a JS settings array of native external plugins that need to be loaded separately.
  352. */
  353. function wysiwyg_ckeditor_plugin_settings($editor, $profile, $plugins) {
  354. $settings = array();
  355. foreach ($plugins as $name => $plugin) {
  356. // Register all plugins that need to be loaded.
  357. if (!empty($plugin['load'])) {
  358. $settings[$name] = array();
  359. // Add path for native external plugins.
  360. if (empty($plugin['internal']) && isset($plugin['path'])) {
  361. $settings[$name]['path'] = base_path() . $plugin['path'] . '/';
  362. }
  363. // Force native internal plugins to use the standard path.
  364. else {
  365. $settings[$name]['path'] = base_path() . $editor['library path'] . '/plugins/' . $name . '/';
  366. }
  367. // CKEditor defaults to 'plugin.js' on its own when filename is not set.
  368. if (!empty($plugin['filename'])) {
  369. $settings[$name]['fileName'] = $plugin['filename'];
  370. }
  371. }
  372. }
  373. return $settings;
  374. }
  375. /**
  376. * Build a JS settings array for Drupal plugins loaded via the proxy plugin.
  377. */
  378. function wysiwyg_ckeditor_proxy_plugin_settings($editor, $profile, $plugins) {
  379. $settings = array();
  380. foreach ($plugins as $name => $plugin) {
  381. // Populate required plugin settings.
  382. $settings[$name] = $plugin['dialog settings'] + array(
  383. 'title' => $plugin['title'],
  384. 'icon' => base_path() . $plugin['icon path'] . '/' . $plugin['icon file'],
  385. 'iconTitle' => $plugin['icon title'],
  386. // @todo These should only be set if the plugin defined them.
  387. 'css' => base_path() . $plugin['css path'] . '/' . $plugin['css file'],
  388. );
  389. }
  390. return $settings;
  391. }
  392. /**
  393. * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin().
  394. */
  395. function wysiwyg_ckeditor_plugins($editor) {
  396. $plugins = array(
  397. 'default' => array(
  398. 'buttons' => array(
  399. 'Bold' => t('Bold'), 'Italic' => t('Italic'), 'Underline' => t('Underline'),
  400. 'Strike' => t('Strike-through'),
  401. 'JustifyLeft' => t('Align left'), 'JustifyCenter' => t('Align center'), 'JustifyRight' => t('Align right'), 'JustifyBlock' => t('Justify'),
  402. 'BidiLtr' => t('Left-to-right'), 'BidiRtl' => t('Right-to-left'),
  403. 'BulletedList' => t('Bullet list'), 'NumberedList' => t('Numbered list'),
  404. 'Outdent' => t('Outdent'), 'Indent' => t('Indent'),
  405. 'Undo' => t('Undo'), 'Redo' => t('Redo'),
  406. 'Link' => t('Link'), 'Unlink' => t('Unlink'), 'Anchor' => t('Anchor'),
  407. 'Image' => t('Image'),
  408. 'TextColor' => t('Forecolor'), 'BGColor' => t('Backcolor'),
  409. 'Superscript' => t('Superscript'), 'Subscript' => t('Subscript'),
  410. 'Blockquote' => t('Blockquote'), 'Source' => t('Source code'),
  411. 'HorizontalRule' => t('Horizontal rule'),
  412. 'Cut' => t('Cut'), 'Copy' => t('Copy'), 'Paste' => t('Paste'),
  413. 'PasteText' => t('Paste Text'), 'PasteFromWord' => t('Paste from Word'),
  414. 'ShowBlocks' => t('Show blocks'),
  415. 'RemoveFormat' => t('Remove format'),
  416. 'SpecialChar' => t('Character map'),
  417. 'Format' => t('HTML block format'), 'Font' => t('Font'), 'FontSize' => t('Font size'), 'Styles' => t('Font style'),
  418. 'Table' => t('Table'),
  419. 'SelectAll' => t('Select all'), 'Find' => t('Search'), 'Replace' => t('Replace'),
  420. 'Flash' => t('Flash'), 'Smiley' => t('Smiley'),
  421. 'CreateDiv' => t('Div container'),
  422. 'Iframe' => t('iFrame'),
  423. 'Maximize' => t('Maximize'),
  424. 'SpellChecker' => t('Check spelling'), 'Scayt' => t('Check spelling as you type'),
  425. 'About' => t('About'),
  426. ),
  427. 'internal' => TRUE,
  428. ),
  429. );
  430. if (version_compare($editor['installed version'], '3.1.0.4885', '<')) {
  431. unset($plugins['default']['buttons']['CreateDiv']);
  432. }
  433. if (version_compare($editor['installed version'], '3.4.0.5808', '<')) {
  434. unset($plugins['default']['buttons']['BidiLtr']);
  435. unset($plugins['default']['buttons']['BidiRtl']);
  436. }
  437. if (version_compare($editor['installed version'], '3.5.0.6260', '<')) {
  438. unset($plugins['default']['buttons']['Iframe']);
  439. }
  440. return $plugins;
  441. }