CRM-16444 - Add CKEditor Toolbar Configurator
authorColeman Watts <coleman@civicrm.org>
Fri, 10 Jul 2015 14:33:03 +0000 (10:33 -0400)
committerColeman Watts <coleman@civicrm.org>
Fri, 10 Jul 2015 15:41:18 +0000 (11:41 -0400)
CRM/Admin/Form/Preferences/Display.php
CRM/Admin/Page/CKEditorConfig.php [new file with mode: 0644]
CRM/Core/Resources.php
CRM/Core/xml/Menu/Admin.xml
CRM/Logging/ReportDetail.php
js/wysiwyg/admin.ckeditor-configurator.js [new file with mode: 0644]
js/wysiwyg/crm.ckeditor.js
templates/CRM/Admin/Form/Preferences/Display.tpl
templates/CRM/Admin/Page/CKEditorConfig.tpl [new file with mode: 0644]

index 599272b50b69217f8838d7909bd0f41f879113c1..0c79ba3e630d7e0f0fbac293d9d221082a1fba09 100644 (file)
@@ -143,6 +143,7 @@ class CRM_Admin_Form_Preferences_Display extends CRM_Admin_Form_Preferences {
     $extra = array();
 
     $this->addElement('select', 'editor_id', ts('WYSIWYG Editor'), $wysiwyg_options, $extra);
+    $this->addElement('submit', 'ckeditor_config', ts('Configure CKEditor'));
 
     $editOptions = CRM_Core_OptionGroup::values('contact_edit_options', FALSE, FALSE, FALSE, 'AND v.filter = 0');
     $this->assign('editOptions', $editOptions);
@@ -188,6 +189,15 @@ class CRM_Admin_Form_Preferences_Display extends CRM_Admin_Form_Preferences {
     $this->_config->editor_id = $this->_params['editor_id'];
 
     $this->postProcessCommon();
+
+    // If "Configure CKEditor" button was clicked
+    if (!empty($this->_params['ckeditor_config'])) {
+      // Suppress the "Saved" status message and redirect to the CKEditor Config page
+      $session = CRM_Core_Session::singleton();
+      $session->getStatus(TRUE);
+      $url = CRM_Utils_System::url('civicrm/admin/ckeditor', 'reset=1');
+      $session->pushUserContext($url);
+    }
   }
 
 }
diff --git a/CRM/Admin/Page/CKEditorConfig.php b/CRM/Admin/Page/CKEditorConfig.php
new file mode 100644 (file)
index 0000000..ef3e167
--- /dev/null
@@ -0,0 +1,235 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.6                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2015                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2015
+ * $Id$
+ *
+ */
+
+/**
+ * Page for configuring CKEditor options
+ *
+ * Note that while this is implemented as a CRM_Core_Page, it is actually a form.
+ * Because the form needs to be submitted and refreshed via javascrit, it seemed like
+ * Quickform and CRM_Core_Form/Controller might get in the way.
+ */
+class CRM_Admin_Page_CKEditorConfig extends CRM_Core_Page {
+
+  const CONFIG_FILENAME = 'crm-ckeditor-config.js';
+
+  /**
+   * Default settings if config file has not been initialized
+   *
+   * @var array
+   */
+  public $defaultSettings = array(
+    'skin' => 'moono',
+    'extraPlugins' => '',
+  );
+
+  /**
+   * @return string
+   */
+  public function run() {
+    // If the form was submitted, take appropriate action.
+    if (!empty($_POST['revert'])) {
+      self::deleteConfigFile();
+    }
+    elseif (!empty($_POST['config'])) {
+      $this->save($_POST);
+    }
+
+    CRM_Core_Resources::singleton()
+      ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/fulltoolbareditor.js', 1)
+      ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js', 2)
+      ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/toolbarmodifier.js', 3)
+      ->addScriptFile('civicrm', 'js/wysiwyg/admin.ckeditor-configurator.js', 10)
+      ->addStyleFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/css/fontello.css')
+      ->addStyleFile('civicrm', 'bower_components/ckeditor/samples/css/samples.css')
+      ->addVars('ckConfig', array(
+        'plugins' => array_values($this->getCKPlugins()),
+      ));
+
+    $this->assign('skins', $this->getCKSkins());
+    $this->assign('skin', $this->getConfigSetting('skin'));
+    $this->assign('extraPlugins', $this->getConfigSetting('extraPlugins'));
+    $this->assign('configUrl', self::getConfigUrl());
+    $this->assign('revertConfirm', htmlspecialchars(ts('Are you sure you want to revert all changes?', array('escape' => 'js'))));
+
+    CRM_Utils_System::appendBreadCrumb(array(array(
+      'url' => CRM_Utils_System::url('civicrm/admin/setting/preferences/display', 'reset=1'),
+      'title' => ts('Display Preferences'),
+    )));
+
+    return parent::run();
+  }
+
+  /**
+   * Generate the config js file based on posted data.
+   *
+   * @param array $params
+   */
+  public function save($params) {
+    $config = "/**\n"
+      . " * CKEditor config file auto-generated by CiviCRM.\n"
+      . " *\n"
+      . " * Note: This file will be overwritten if settings are modified at:\n"
+      . " * @link " . CRM_Utils_System::url(CRM_Utils_System::currentPath(), NULL, TRUE, NULL, FALSE) . "\n"
+      . " */\n\n"
+      // Standardize line-endings
+      . preg_replace('~\R~u', "\n", $params['config']);
+
+    // Use defaultSettings as a whitelist so we don't just insert any old junk into the file
+    foreach ($this->defaultSettings as $key => $default) {
+      if (isset($params[$key]) && strlen($params[$key])) {
+        $pos = strrpos($config, '};');
+        $setting = "\n\tconfig.$key = '{$params[$key]}';\n";
+        $config = substr_replace($config, $setting, $pos, 0);
+      }
+    }
+    self::saveConfigFile($config);
+    if (!empty($params['save'])) {
+      CRM_Core_Session::setStatus(ts("You may need to clear your browser's cache to see the changes in CiviCRM."), ts('CKEditor Saved'), 'success');
+    }
+  }
+
+  /**
+   * @return array
+   */
+  private function getCKPlugins() {
+    $plugins = array();
+    global $civicrm_root;
+    $pluginDir = CRM_Utils_file::addTrailingSlash($civicrm_root, '/') . 'bower_components/ckeditor/plugins';
+
+    foreach (glob($pluginDir . '/*', GLOB_ONLYDIR) as $dir) {
+      $dir = rtrim(str_replace('\\', '/', $dir), '/');
+      $name = substr($dir, strrpos($dir, '/') + 1);
+      $dir = CRM_Utils_file::addTrailingSlash($dir, '/');
+      if (is_file($dir . 'plugin.js')) {
+        $plugins[$name] = array(
+          'id' => $name,
+          'text' => ucfirst($name),
+          'icon' => NULL,
+        );
+        if (is_dir($dir . "icons")) {
+          if (is_file($dir . "icons/$name.png")) {
+            $plugins[$name]['icon'] = "bower_components/ckeditor/plugins/$name/icons/$name.png";
+          }
+          elseif (glob($dir . "icons/*.png")) {
+            $icon = CRM_Utils_Array::first(glob($dir . "icons/*.png"));
+            $icon = rtrim(str_replace('\\', '/', $icon), '/');
+            $plugins[$name]['icon'] = "bower_components/ckeditor/plugins/$name/icons/" . substr($icon, strrpos($icon, '/') + 1);
+          }
+        }
+      }
+    }
+
+    return $plugins;
+  }
+
+  /**
+   * @return array
+   */
+  private function getCKSkins() {
+    $skins = array();
+    global $civicrm_root;
+    $skinDir = CRM_Utils_file::addTrailingSlash($civicrm_root, '/') . 'bower_components/ckeditor/skins';
+    foreach (glob($skinDir . '/*', GLOB_ONLYDIR) as $dir) {
+      $dir = rtrim(str_replace('\\', '/', $dir), '/');
+      $skins[] = substr($dir, strrpos($dir, '/') + 1);
+    }
+    return $skins;
+  }
+
+  /**
+   * @param $setting
+   * @return string
+   */
+  private function getConfigSetting($setting) {
+    $value = CRM_Utils_Array::value($setting, $this->defaultSettings, '');
+    $file = self::getConfigFile();
+    if ($file) {
+      $contents = file_get_contents($file);
+      $matches = array();
+      preg_match("/\sconfig\.$setting\s?=\s?'([^']*)'/", $contents, $matches);
+      if ($matches) {
+        $value = $matches[1];
+      }
+    }
+    return $value;
+  }
+
+  /**
+   * @return null|string
+   */
+  public static function getConfigUrl() {
+    if (self::getConfigFile()) {
+      // FIXME: Basing file path off imageUploadURL sucks, but it's all we got
+      $url = CRM_Utils_file::addTrailingSlash(CRM_Core_Config::singleton()->imageUploadURL, '/');
+      $url = str_replace('/persist/contribute/', '/persist/', $url);
+      return $url . SELF::CONFIG_FILENAME;
+    }
+    return NULL;
+  }
+
+  /**
+   * @param bool $checkIfFileExists
+   *   If false, this fn will return fileName even if it doesn't exist
+   *
+   * @return null|string
+   */
+  public static function getConfigFile($checkIfFileExists = TRUE) {
+    // FIXME: Basing file path off imageUploadDir sucks, but it's all we got
+    $dir = CRM_Core_Config::singleton()->imageUploadDir;
+    $dir = CRM_Utils_file::addTrailingSlash(str_replace('\\', '/', $dir), '/');
+    $dir = str_replace('/persist/contribute/', '/persist/', $dir);
+    $fileName = $dir . SELF::CONFIG_FILENAME;
+    return !$checkIfFileExists || is_file($fileName) ? $fileName : NULL;
+  }
+
+  /**
+   * @param string $contents
+   */
+  public static function saveConfigFile($contents) {
+    $file = self::getConfigFile(FALSE);
+    file_put_contents($file, $contents);
+  }
+
+  /**
+   * Delete SELF::CONFIG_FILENAME
+   */
+  public static function deleteConfigFile() {
+    $file = self::getConfigFile();
+    if ($file) {
+      unlink($file);
+    }
+  }
+
+}
index cd2945b5c577ad1732c87355a11c786acaf4d533..67034a9e6ec247adc10895ed5cfb3f88765d983f 100644 (file)
@@ -582,14 +582,17 @@ class CRM_Core_Resources {
 
       // Add resources from coreResourceList
       $jsWeight = -9999;
-      foreach ($this->coreResourceList() as $file) {
-        if (substr($file, -2) == 'js') {
+      foreach ($this->coreResourceList() as $item) {
+        if (is_array($item)) {
+          $this->addSetting($item);
+        }
+        elseif (substr($item, -2) == 'js') {
           // Don't bother  looking for ts() calls in packages, there aren't any
-          $translate = (substr($file, 0, 3) == 'js/');
-          $this->addScriptFile('civicrm', $file, $jsWeight++, $region, $translate);
+          $translate = (substr($item, 0, 3) == 'js/');
+          $this->addScriptFile('civicrm', $item, $jsWeight++, $region, $translate);
         }
         else {
-          $this->addStyleFile('civicrm', $file, -100, $region);
+          $this->addStyleFile('civicrm', $item, -100, $region);
         }
       }
 
@@ -717,6 +720,10 @@ class CRM_Core_Resources {
     if ($editor == "CKEditor") {
       $items[] = "bower_components/ckeditor/ckeditor.js";
       $items[] = "js/wysiwyg/crm.ckeditor.js";
+      $ckConfig = CRM_Admin_Page_CKEditorConfig::getConfigUrl();
+      if ($ckConfig) {
+        $items[] = array('config' => array('CKEditorCustomConfig' => $ckConfig));
+      }
     }
 
     // These scripts are only needed by back-office users
index 632d87913e3a856f1716fefcc061fb1b7220885c..971f80849c58b73aac56fd2614f56225cb10ae24 100644 (file)
     <page_callback>CRM_Badge_Form_Layout</page_callback>
     <access_arguments>administer CiviCRM</access_arguments>
   </item>
+  <item>
+    <path>civicrm/admin/ckeditor</path>
+    <title>Configure CKEditor</title>
+    <page_callback>CRM_Admin_Page_CKEditorConfig</page_callback>
+    <access_arguments>administer CiviCRM</access_arguments>
+  </item>
 </menu>
index 921c35bf9468b409becb6da13afff022cf293d74..d76664fb78f5dcf55ce4c3ff5c6d3cd2b9aecaf5 100644 (file)
@@ -239,7 +239,7 @@ class CRM_Logging_ReportDetail extends CRM_Report_Form {
 
     $q = "reset=1&log_conn_id={$this->log_conn_id}&log_date={$this->log_date}";
     $this->assign('revertURL', CRM_Report_Utils_Report::getNextUrl($this->detail, "$q&revert=1", FALSE, TRUE));
-    $this->assign('revertConfirm', ts('Are you sure you want to revert all these changes?'));
+    $this->assign('revertConfirm', ts('Are you sure you want to revert all changes?'));
   }
 
 }
diff --git a/js/wysiwyg/admin.ckeditor-configurator.js b/js/wysiwyg/admin.ckeditor-configurator.js
new file mode 100644 (file)
index 0000000..0ebbb59
--- /dev/null
@@ -0,0 +1,65 @@
+// https://civicrm.org/licensing
+(function($, _) {
+  'use strict';
+
+  // Weird conflict with drupal styles
+  $('body').removeClass('toolbar');
+
+  function format(item) {
+    var icon = '<span class="ui-icon ui-icon-gear"></span>';
+    if (item.icon) {
+      icon = '<img src="' + CRM.config.resourceBase + item.icon + '" />';
+    }
+    return icon + '&nbsp;' + item.text;
+  }
+
+  $('#extraPlugins').crmSelect2({
+    multiple: true,
+    closeOnSelect: false,
+    data: CRM.vars.ckConfig.plugins,
+    escapeMarkup: _.identity,
+    formatResult: format,
+    formatSelection: format
+  });
+
+  var toolbarModifier = new ToolbarConfigurator.ToolbarModifier( 'editor-basic' );
+
+  toolbarModifier.init(_.noop);
+
+  CKEDITOR.document.getById( 'toolbarModifierWrapper' ).append( toolbarModifier.mainContainer );
+
+  $('.toolbar button:last', '#toolbarModifierWrapper').hide();
+
+  $(function() {
+    var selectorOpen = false,
+      changedWhileOpen = false;
+
+    $('#toolbarModifierForm')
+      // The buttons in the configurator are not submit buttons!
+      .on('click', 'button', function(e) {
+        e.preventDefault();
+      })
+      .on('submit', function(e) {
+        $('.toolbar button:last', '#toolbarModifierWrapper')[0].click();
+        $('.configContainer textarea', '#toolbarModifierWrapper').attr('name', 'config');
+      })
+      .on('change', '.config-param', function(e) {
+        changedWhileOpen = true;
+        if (!selectorOpen) {
+          $('#toolbarModifierForm').submit().block();
+        }
+      })
+      // Debounce the change event so it only fires after the multiselect is closed
+      .on('select2-open', 'input.config-param', function(e) {
+        selectorOpen = true;
+        changedWhileOpen = false;
+      })
+      .on('select2-close', 'input.config-param', function(e) {
+        selectorOpen = false;
+        if (changedWhileOpen) {
+          $(this).change();
+        }
+      });
+  });
+
+})(CRM.$, CRM._);
\ No newline at end of file
index fe7fc499d8ff881744f9e07cc61258ee9feaf849..5d418e57b39a93cb942ebf42eab332c54918e72c 100644 (file)
       browseUrl = CRM.config.userFrameworkResourceURL + "packages/kcfinder/browse.php?cms=civicrm",
       uploadUrl = CRM.config.userFrameworkResourceURL + "packages/kcfinder/upload.php?cms=civicrm";
     if ($(item).length) {
-      editor = CKEDITOR.replace($(item)[0]);
+      editor = CKEDITOR.replace($(item)[0], {
+        filebrowserBrowseUrl: browseUrl + '&type=files',
+        filebrowserImageBrowseUrl: browseUrl + '&type=images',
+        filebrowserFlashBrowseUrl: browseUrl + '&type=flash',
+        filebrowserUploadUrl: uploadUrl + '&type=files',
+        filebrowserImageUploadUrl: uploadUrl + '&type=images',
+        filebrowserFlashUploadUrl: uploadUrl + '&type=flash',
+        customConfig: CRM.config.CKEditorCustomConfig
+      });
     }
     if (editor) {
-      editor.config.filebrowserBrowseUrl = browseUrl + '&type=files';
-      editor.config.filebrowserImageBrowseUrl = browseUrl + '&type=images';
-      editor.config.filebrowserFlashBrowseUrl = browseUrl + '&type=flash';
-      editor.config.filebrowserUploadUrl = uploadUrl + '&type=files';
-      editor.config.filebrowserImageUploadUrl = uploadUrl + '&type=images';
-      editor.config.filebrowserFlashUploadUrl = uploadUrl + '&type=flash';
       editor.on('focus', function() {
         $(item).trigger('focus');
       });
index de6c92fb271f2effe03cd12190ee04b475ca10a4..7ae0f08fb8d7ba3b5ee03bcaebd206827998c3e6 100644 (file)
     </tr>
     <tr class="crm-preferences-display-form-block-editor_id">
       <td class="label">{$form.editor_id.label}</td>
-      <td>{$form.editor_id.html}</td>
+      <td>
+        {$form.editor_id.html}
+        &nbsp;
+        <span class="crm-button crm-icon-button" style="display:inline-block;vertical-align:middle;float:none!important;">
+          <span class="crm-button-icon ui-icon-gear"> </span>
+          {$form.ckeditor_config.html}
+        </span>
+      </td>
     </tr>
     <tr class="crm-preferences-display-form-block-description">
       <td>&nbsp;</td>
           placeholder: 'ui-state-highlight',
           update: getSorting
         });
+
+        function showCKEditorConfig() {
+          console.log($(this).val());
+          $('.crm-preferences-display-form-block-editor_id .crm-button').toggle($(this).val() == '2');
+        }
+        $('select[name=editor_id]').each(showCKEditorConfig).change(showCKEditorConfig);
       });
     </script>
   {/literal}
diff --git a/templates/CRM/Admin/Page/CKEditorConfig.tpl b/templates/CRM/Admin/Page/CKEditorConfig.tpl
new file mode 100644 (file)
index 0000000..049fbcc
--- /dev/null
@@ -0,0 +1,77 @@
+{*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.6                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2015                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+*}
+<style>{literal}
+  .select2-results .ui-icon,
+  .select2-container .ui-icon,
+  .select2-results img,
+  .select2-container img {
+    display: inline-block;
+    position: relative;
+    top: 2px;
+  }
+  #toolbarModifierWrapper .toolbar button:last-child,
+  #toolbarModifierWrapper .toolbar button[data-group=config] {
+    display: none;
+  }
+{/literal}</style>
+{* Force the custom config file to reload by appending a new query string *}
+<script type="text/javascript">
+  {if $configUrl}CKEDITOR.config.customConfig = '{$configUrl}?{php}print str_replace(array(' ', '.'), array('', '='), microtime());{/php}'{/if};
+</script>
+
+<form method="post" action="{crmURL}" id="toolbarModifierForm">
+  <div class="crm-block crm-form-block">
+    <label for="skin">{ts}Skin{/ts}</label>
+    <select id="skin" name="skin" class="crm-select2 eight config-param">
+      {foreach from=$skins item='s'}
+        <option value="{$s}" {if $s == $skin}selected{/if}>{$s|ucfirst}</option>
+      {/foreach}
+    </select>
+    &nbsp;&nbsp;
+    <label for="extraPlugins">{ts}Plugins{/ts}</label>
+    <input id="extraPlugins" name="extraPlugins" class="huge config-param" value="{$extraPlugins}" placeholder="{ts}Select optional extra features{/ts}">
+  </div>
+
+  <div class="editors-container">
+    <div id="editor-basic"></div>
+    <div id="editor-advanced"></div>
+  </div>
+
+  <div class="configurator">
+    <div>
+      <div id="toolbarModifierWrapper" class="active"></div>
+    </div>
+  </div>
+
+  <div class="crm-submit-buttons">
+    <span class="crm-button crm-icon-button">
+      <span class="crm-button-icon ui-icon-check"> </span> <input type="submit" value="{ts}Save{/ts}" name="save" class="crm-form-submit" accesskey="S"/>
+    </span>
+    <span class="crm-button crm-icon-button">
+      <span class="crm-button-icon ui-icon-cancel"> </span> <input type="submit" value="{ts}Revert to Default{/ts}" name="revert" class="crm-form-submit" onclick="return confirm('{$revertConfirm}');"/>
+    </span>
+  </div>
+</form>