Source: editors/TinyMCE.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/*globals tinymce*/
/**
 * This module can be used to create a TinyMCE editor it is primarily used by the 
 * [TinyMCE form control]{@link module:alfresco/forms/controls/TinyMCE} but can be used independently
 * if required. Without any additional configuration it will instantiate an editor using the Alfresco
 * preferred configuration, however this can be overridden by providing 
 * [specific configuration]{@link module:alfresco/forms/controls/TinyMCE#editorConfig} that will
 * augment or override the [default configuration]{@link module:alfresco/forms/controls/TinyMCE#defaultEditorConfig}.
 *
 * @module alfresco/editors/TinyMCE
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/core/Core
 * @author Dave Draper
 */
define(["dojo/_base/declare", 
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin", 
        "dojo/text!./templates/TinyMCE.html", 
        "alfresco/core/Core", 
        "alfresco/core/ResizeMixin",
        "alfresco/core/topics",
        "service/constants/Default", 
        "dojo/_base/lang",
        "jquery",
        "jqueryui"], 
        function(declare, _WidgetBase, _TemplatedMixin, template, AlfCore, ResizeMixin, topics, AlfConstants, lang, $) {


   return declare([_WidgetBase, _TemplatedMixin, AlfCore, ResizeMixin], {

      /**
       * An array of the i18n files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{i18nFile: "./i18n/TinyMCE.properties"}]
       */
      i18nRequirements: [{i18nFile: "./i18n/TinyMCE.properties"}],

      /**
       * Make sure TinyMCE is included on the page.
       *
       * @instance
       * @type {String[]}
       */
      nonAmdDependencies: ["/js/lib/tinymce/tinymce.js"],

      /**
       * The HTML template to use for the widget.
       * @instance
       * @type {String}
       */
      templateString: template,

      /**
       * This indicates whether the size should be adjusted on resize events.
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.47
       */
      autoResize: false,

      /**
       * A function that should be called whenever the content of the editor changes. The function will be bound to the
       * supplied [contentChangeScope]{@link module:alfresco/editors/TinyMCE#contentChangeScope}.
       *
       * @instance
       * @type {object}
       * @default null
       */
      contentChangeHandler: null,

      /**
       * A scope for calling the [contentChangeHandler]{@link module:alfresco/editors/TinyMCE#contentChangeHandler}
       * against.
       *
       * @instance
       * @type {object}
       * @default null
       */
      contentChangeScope: null,

      /**
       * Should be used to override the [defaultEditorConfig]{@link module:alfresco/editors/TinyMCE#defaultEditorConfig}
       *
       * @instance
       * @type {object}
       * @default null
       */
      editorConfig: null,

      /**
       * Indicates whether or not the editor should be initialized as soon as it is created. This defaults to
       * true but should be configured to false if the editor is being created in a DOM fragment and the init
       * function should only be called once the DOM fragment has been placed into the document.
       *
       * @instance
       * @type {boolean}
       * @default true
       */
      immediateInit: true,

      /**
       * The content with which to intially populate the editor
       *
       * @instance
       * @type {string}
       * @default ""
       */
      intialContent: "",

      /**
       * Indicates whether or not the editor should be initially disabled
       *
       * @instance
       * @type {boolean}
       * @default false
       */
      initiallyDisabled: false,

      /**
       * The list of support locales for the editor. Can be overridden by only if there are the message
       * bundles available to support the additional locales.
       *
       * @instance
       * @type {string}
       * @default "en,de,es,fr,it,ja,nl,zh_CN,ru,nb,pt_BR"
       */
      supportedLocales: "en,de,es,fr,it,ja,nl,zh_CN,ru,nb,pt_BR",

      /**
       * Starts as false and gets set to true on the [editorInitialized]{@link module:alfresco/editors/TinyMCE#editorInitialized}
       * callback that is bound to the TinyMCE editors "init_instance_callback" configuration options.
       *
       * @instance
       * @type {boolean}
       * @default false
       */
      _editorInitialized: false,

      /**
       * Indicates whether or not the TinyMCE editor should be focused once it has been initialized. This will
       * be set to true by the [focus]{@link module:alfresco/editors/TinyMCE#focus} function if it is called before
       * the editor has been [initialized]{@link module:alfresco/editors/TinyMCE#editorInitialized}.
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.46
       */
      _focusWhenInitialized: false,

      /**
       * Indicates whether or not the TinyMCE editor should be resized once it has been initialized. This will
       * be set to true by the [focus]{@link module:alfresco/editors/TinyMCE#onResize} function if it is called before
       * the editor has been [initialized]{@link module:alfresco/editors/TinyMCE#editorInitialized}.
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.47
       */
      _resizeWhenInitialized: false,

      /**
       * The default configuration for the editor. These settings should not be configured (as they will apply
       * to all instances of the editor). However specific overrides can be achieved by setting the value of
       * [editorConfig]{@link module:alfresco/editors/TinyMCE#editorConfig} which will be mixed into these default
       * values.
       *
       * @instance
       * @type {object}
       */
      defaultEditorConfig: {
         height: 250,
         width: 538,
         menu: {},
         toolbar: "bold italic underline | bullist numlist | forecolor backcolor | undo redo removeformat",
         language: AlfConstants.JS_LOCALE,
         statusbar: false,
         theme_advanced_resize_horizontal: false
      },

      /**
       *
       * @instance
       */
      postCreate: function alfresco_editors_TinyMCE__postCreate() {
         // jshint maxcomplexity:false
         // Mix the custom editor config overrides into the default editor config...
         var config = lang.clone(this.defaultEditorConfig);

         if (this.autoResize)
         {
            config.width = "100%";
            config.autoresize_max_height = 1024;
            config.autoresize_min_height = 250;
            config.autoresize_on_init = true;
         }

         if (this.editorConfig) {
            lang.mixin(config, this.editorConfig);
         }

         // Check that the language requested is supported...
         if (config.language) {
            var locales = this.supportedLocales.split(",");
            var locale, bestGeneralizedLocale;
            for (var i = 0, j = locales.length; i < j; i++) {
               if (locales[i] === config.language) {
                  locale = config.language;
                  break;
               }
               
               if (config.language.indexOf(locales[i]) === 0)
               {
                   if (bestGeneralizedLocale === undefined || locales[i].length > bestGeneralizedLocale.length)
                   {
                       bestGeneralizedLocale = locales[i];
                   }
               }
            }
            
            config.language = locale || bestGeneralizedLocale || "en";
         }

         tinymce.baseURL = AlfConstants.URL_RESCONTEXT + "js/lib/tinymce";

         if (this.immediateInit === true) {
            this.init(config);
         } else {
            this._delayedInitConfig = config;
         }

         if (this.autoResize)
         {
            this.alfSetupResizeSubscriptions(this.onResize, this);
         }
      },

      /**
       * When [autoResize]{@link module:alfresco/editors/TinyMCE} is configured to true this will
       * respond to resize events by finding the first ancestor with height and width dimensions
       * and then increasing the size of the TinyMCE editor to fill the available space as best
       * it can.
       * 
       * @instance
       * @since 1.0.47
       */
      onResize: function alfresco_editors_TinyMCE__onResize() {
         if (this.autoResize)
         {
            if (this._editorInitialized)
            {
               // Work our way back up through the current node ancestors to find
               // the first ancestors that have height and width set on them. Height
               // and width do not need to come from the same node, but we only want
               // to take the first values we find of each (i.e. if we find a height but
               // not a width, don't take the height from the node that we DO find a width
               // on later)...
               var height, width;
               $(this.domNode).parents().each(function() {
                  // Check the style attribute returned by the JQuery function to see if
                  // height/width is set... then get the actual height/width of that
                  // element.
                  var style = $(this).attr("style");
                  if (!height && style && style.indexOf("height:") !== -1)
                  {
                     height = $(this).height();
                  }
                  if (!width && style && style.indexOf("width:") !== -1)
                  {
                     width = $(this).width();
                  }
                  // When both height and width have been set exit the loop...
                  if (height && width)
                  {
                     return false;
                  }
               });
               
               // Update the dimensions of the main node (so that the TinyMCE editor
               // can grow into it - the auto resize plugin will automatically take
               // care of the width)...
               $(this.domNode).height(height - 10); // Deduct 10 to compensate for margin

               // We need to handle the height manually, and it needs to be set on the
               // .mce-edit-area node. We need to compensate for the toolbar when 
               var editAreaNode = $(this.domNode).find(".mce-edit-area");
               $(editAreaNode).height(height - 42); // Deduct 42 to compenstate for toolbar and margin
               $(this.domNode).width(width - 2); // Deduct 2 to compensate for border
            }
            else
            {
               this._resizeWhenInitialized = true;
            }
         }
      },

      /**
       *
       * @instance
       * @param {object} config The configuration to initialise the editor with
       */
      init: function alfresco_editors_TinyMCE__init(config) {
         config = config || this._delayedInitConfig;
         config.theme = "modern";
         if (!config.toolbar) {
            config.toolbar = "styleselect | bold italic | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | print preview fullscreen";
         }
         if (!config.menu) {
            config.menu = {
               file: {
                  title: this.message("TinyMCE.toolbar.file.title"),
                  items: "newdocument | print"
               },
               edit: {
                  title: this.message("TinyMCE.toolbar.edit.title"),
                  items: "undo redo | cut copy paste pastetext | selectall | searchreplace"
               },
               insert: {
                  title: this.message("TinyMCE.toolbar.insert.title"),
                  items: "link image | charmap hr anchor pagebreak inserttime nonbreaking"
               },
               view: {
                  title: this.message("TinyMCE.toolbar.view.title"),
                  items: "fullscreen preview visualblocks code"
               },
               format: {
                  title: this.message("TinyMCE.toolbar.format.title"),
                  items: "bold italic underline strikethrough superscript subscript | formats | removeformat"
               },
               table: {
                  title: this.message("TinyMCE.toolbar.table.title"),
                  items: "inserttable tableprops deletetable | cell row column"
               }
            };
         }
         if (!config.plugins)
         {
            config.plugins = [
               "advlist autolink link image lists charmap print preview hr anchor pagebreak",
               "searchreplace code fullscreen insertdatetime nonbreaking",
               "table contextmenu paste textcolor visualblocks autoresize"
            ];
         }
         if (config.additionalPlugins)
         {
            config.plugins = config.plugins.concat(config.additionalPlugins);
         }
         
         config.init_instance_callback = lang.hitch(this, this.editorInitialized);

         this.updateEditorConfig(config);
         this.editor = new tinymce.Editor(this.editorNode, config, tinymce.EditorManager);

         // Allow back the "embed" tag as TinyMCE now removes it - this is allowed by our this.editors
         // if the HTML stripping is disabled via the "allowUnfilteredHTML" config attribute
         var extValidElements = config.extended_valid_elements;
         extValidElements = (extValidElements && extValidElements + ",") || "";
         config.extended_valid_elements = extValidElements + "embed[src|type|width|height|flashvars|wmode]";
         this.editor.render();
         this.editor.save();
         return this;
      },

      /**
       * This is an extension point function that provides the opportunity for extending widgets to 
       * make updates to the default configuration. This allows non-configurable options to be added
       * to the configuration such as specific callback overrides for configuration plugins.
       * 
       * @instance
       * @param {object} config The configuration object to be updated
       * @since 1.0.66
       * @overridable
       */
      updateEditorConfig: function alfresco_editors_TinyMCE__createEditor(/* jshint unused:false*/ config) {
         // No action by default
      },

      /**
       * This is bound to the TinyMCE editors "init_instance_callback" configuration option. This then sets up the
       * various events required to manage the editor.
       *
       * @instance
       * @param {object} editor The initialized editor.
       */
      editorInitialized: function alfresco_editors_TinyMCE__editorInitialized(editor) {
         this.alfLog("log", "TinyMCE Editor intialized!", editor);
         if (this.contentChangeScope && this.contentChangeHandler) {
            editor.on("change", lang.hitch(this.contentChangeScope, this.contentChangeHandler));
         }
         editor.setContent(this.initialContent);
         this._editorInitialized = true;

         if (this._focusWhenInitialized)
         {
            this.focus();
         }
         if (this._resizeWhenInitialized)
         {
            // See AKU-775 - adding a short timeout here allows everything to complete initialization before resizing.
            setTimeout(lang.hitch(this, this.onResize), 100);
         }
         this.setDisabled(this.initiallyDisabled);

      },

      /**
       * Give focus to the TinyMCE editor
       * 
       * @instance
       * @since 1.0.46
       * @fires module:alfresco/core/topics#TINYMCE_EDITOR_FOCUSED
       */
      focus: function alfresco_editors_TinyMCE__focus() {
         if (this._editorInitialized)
         {
            this.editor.focus();
            this.alfPublish(topics.TINYMCE_EDITOR_FOCUSED);
         }
         else
         {
            this._focusWhenInitialized = true;
         }
      },

      /**
       * This function has been added to support the use of this widget within the [TinyMCE form control]
       * {@link module:alfresco/forms/controls/TinyMCE} so that it can be disabled as necessary during
       * form state changes.
       *
       * @instance
       * @param {boolean} isDisabled Indicates whether or not to move the editor into disabled mode
       */
      setDisabled: function alfresco_editors_TinyMCE__disable(isDisabled) {
         if (this._editorInitialized) {
            this.editor.getBody()
               .setAttribute("contenteditable", !isDisabled);
         } else {
            this.initiallyDisabled = isDisabled;
         }
      },

      /**
       * This function has been added to support the use of this widget within the [TinyMCE form control]
       * {@link module:alfresco/forms/controls/TinyMCE} so that the content can be easily retrieved.
       *
       * @instance
       * @param {boolean} isDisabled Indicates whether or not to move the editor into disabled mode
       */
      getValue: function alfresco_editors_TinyMCE__getValue() {
         if (this._editorInitialized) {
            return this.editor.getContent();
         } else {
            return "";
         }
      },

      /**
       * This function has been added to support the use of this widget within the [TinyMCE form control]
       * {@link module:alfresco/forms/controls/TinyMCE} so that the content can be easily retrieved.
       *
       * @instance
       * @param {boolean} isDisabled Indicates whether or not to move the editor into disabled mode
       */
      setValue: function alfresco_editors_TinyMCE__setContent(html) {
         if (this._editorInitialized) {
            this.editor.setContent(html);
         } else {
            this.initialContent = html;
         }
      }
   });
});