/* --------------------------------------------------------------
editor.js 2017-09-05
Gambio GmbH
http://www.gambio.de
Copyright (c) 2017 Gambio GmbH
Released under the GNU General Public License (Version 2)
[http://www.gnu.org/licenses/gpl-2.0.html]
--------------------------------------------------------------
*/
/**
* ## Editor Widget
*
* This widget will initialize instances of CKEditor or CodeMirror depending the provided data attribute of
* each textarea, within the container the widget is bound to. Purpose of this module is to provide a common
* wrapper of the textarea and record specific editor which means that the user will be able to set an editor
* for a specific record and textarea and store this preference in the database.
*
* **Currently the available editors are "ckeditor" and "codemirror".**
*
* Important: Make sure that you provide the required options as described below. The module is flexible enough
* to provide a solution for each page code base.
*
*
* ### Options (Container)
*
* The following options are bound as data attributes to the element where the module is bound on (most of the times
* a container that includes textarea elements).
*
* **Selector | `data-editor-selector` | String | Optional**
*
* Provide a selector for the textareas to be converted to editor instances. This option defaults to "textarea" and
* will match all the textarea elements inside the container.
*
* **Event Target | `data-editor-event-target` | String | Optional**
*
* Provide a selector that will mark the element which will start the submit/save process of the page. If provided
* the selected editor preference will be saved through the user configuration service with an AJAX request.
*
* Important: There is no default value for this option.
*
* **Event Type | `data-editor-event-type` | String | Optional**
*
* Provide a JavaScript event that will mark the submit/save process of the page. If provided an event handler
* will be bound on the element marked by the "event-target" option and AJAX requests will save the current
* editor preference in the user configuration table.
*
* Important: There is no default value for this option.
*
* **AutoHide | `data-editor-auto-hide` | Boolean | Optional**
*
* Provide "true" or "false" in order to auto hide the editors, if the textareas are not visible at the beginning.
* Defaults value is "false"
*
* **AutoUpdate | `data-editor-auto-update` | Boolean | Optional**
*
* Indicates if the corresponding textarea of the editor should be updated automatically.
* Default value is "true"
*
* **Initialization Event Type | `data-editor-init-event-type` | String | Optional**
*
* Provide a custom initialization event which will trigger the start of the editor conversion. By default the
* editor instances will be created once the engine is ready 'JSENGINE_INIT_FINISHED', but there are cases where
* a custom event is required (e.g. initialization of editor widget dynamically within a dialog).
*
*
* ### Options (Textarea)
*
* The following options are bound as data attributes to each textarea element within the parent container.
*
* **Editor Identifier | `data-editor-identifier` | String | Required**
*
* Each child textarea element needs to have a unique identifier string which needs to apply with the following
* naming convention the "editor-{record type}-{id}-{textarea name}-{language code}
* (e.g. editor-products-2-short_description-de). In cases where the record ID is not set yet (new record creation),
* it is advised that you leave the {id} placeholder and replace it later on whenever the record is generated into
* the database (more information about this edge case in the examples below).
*
* **Editor Type | `data-editor-type` | String | Optional**
*
* This option can have one of the available editor values which will also state the selected editor upon
* initialization. It is optional and the default value is "ckeditor".
*
*
* ### Events
*
* The '#editor-container' element is where the widget is bound on.
*
* ```javascript
* // Fires up when all textareas are ready.
* $('#editor-container').on('editor:ready', (event, $textareas) => { ... });
*
* // Fires up each time a single textarea is ready.
* $('#editor-container').on('editor:textarea_ready', (event, $textarea) => { ... });
* ```
*
*
* ### Examples
*
* **Simple Usage**
*
* Notice that this example showcases the creation of a new customer which means that the customer's ID is not known
* yet. After its initialization, the widget will create a hidden field in the form with the
* "editor_identifiers[textarea-identifier]" name. This hidden field will have the selected editor type as value which
* be used by the backend callback to store the correct editor identifier value, once the customer's ID is generated
* (record inserted). Use the "UserConfigurationService" in backend for adding the value to the database.
*
* ```html
* <div data-gx-widget="editor" data-editor-event-target="#customer-form" data-editor-event-type="submit">
* <form id="customer-form">
* <!-- Other Fields ... ->
* <textarea class="wysiwyg" data-editor-identifier="editor-customers-{id}-notes-de"></textarea>
* </form>
* </div>
* ```
*
* @module Admin/Widgets/editor
* @requires CKEditor, CodeMirror
*/
gx.widgets.module(
'editor',
[
`${jse.source}/vendor/codemirror/codemirror.min.css`,
`${jse.source}/vendor/codemirror/codemirror.min.js`,
`${gx.source}/libs/editor_instances`,
`${gx.source}/libs/editor_values`,
`${gx.source}/widgets/quickselect`,
'user_configuration_service'
],
function (data) {
'use strict';
// ------------------------------------------------------------------------
// VARIABLES
// ------------------------------------------------------------------------
/**
* Module Selector
*
* @type {jQuery}
*/
const $this = $(this);
/**
* Default Options
*
* @type {Object}
*/
const defaults = {
selector: 'textarea',
autoHide: 'false',
initEventType: 'JSENGINE_INIT_FINISHED',
autoUpdate: 'true'
};
/**
* Final Options
*
* @type {Object}
*/
const options = $.extend(true, {}, defaults, data);
/**
* Module Object
*
* @type {Object}
*/
const module = {};
/**
* Editor Instances
*
* Identifier -> instance mapping
*
* @type {Object}
*/
const editors = {};
/**
* Available Editor Types
*
* @type {String[]}
*/
const editorTypes = ['ckeditor', 'codemirror'];
// ------------------------------------------------------------------------
// FUNCTIONS
// ------------------------------------------------------------------------
/**
* Add Editor Switch Button
*
* This method will add the editor switch button and bind the click event handler.
*
* @param {jQuery} $textarea Textarea selector to be modified.
*/
function _addEditorSwitchButton($textarea) {
let start = 0;
if ($textarea.data('editorType') === 'codemirror') {
start = 1;
}
$textarea
.wrap('<div class="editor-wrapper" />')
.parent()
.prepend(`<div data-gx-widget="quickselect" data-quickselect-align="right" data-quickselect-start="`
+ start + `">
<div class="quickselect-headline-wrapper">
<a class="editor-switch editor-switch-html" href="#html">
${jse.core.lang.translate('BUTTON_SWITCH_EDITOR_TEXT', 'admin_buttons')}
</a>
<a class="editor-switch editor-switch-text" href="#text">
${jse.core.lang.translate('BUTTON_SWITCH_EDITOR_HTML', 'admin_buttons')}
</a>
</div>
</div>`)
.find('.editor-switch')
.on('click', _onSwitchButtonClick);
if (!$textarea.is(':visible') && options.autoHide === 'true') {
$textarea.parent().hide();
}
gx.widgets.init($textarea.parent());
}
/**
* Add a hidden editor type field.
*
* This field will contain the type of the current editor and can be used by submit callbacks whenever the
* record ID is not known yet and the user configuration entry is generated by the server.
*
* @param {jQuery} $textarea Textarea selector to be modified.
*/
function _addEditorHiddenField($textarea) {
$textarea
.parent()
.append(`
<input type="hidden"
name="editor_identifiers[${$textarea.data('editorIdentifier')}]"
value="${$textarea.data('editorType') || 'ckeditor'}" />
`);
}
/**
* Create Editor Instance
*
* This method will use the "editor" library to create the appropriate editor instance, depending the textarea
* type attribute.
*
* @param {jQuery} $textarea Textarea selector to be modified.
*/
function _createEditorInstance($textarea) {
const type = $textarea.data('editorType') || 'ckeditor';
const identifier = $textarea.data('editorIdentifier');
editors[identifier] = jse.libs.editor_instances.create($textarea, type);
}
/**
* On Switch Button Click Event Handler
*
* This method will use the "editor" library to change the current editor type and update the hidden input
* field and data attributes of the textarea. It will try to set the next available editor type.
*/
function _onSwitchButtonClick() {
const $switchButton = $(this);
const $textarea = $switchButton.parents('.editor-wrapper').find('textarea');
const identifier = $textarea.data('editorIdentifier');
const currentType = $textarea.data('editorType');
const newType = $switchButton.hasClass('editor-switch-text') ? editorTypes[1] : editorTypes[0];
$textarea.siblings(`[name="editor_identifiers[${identifier}]"]`).val(newType);
$textarea.data('editorType', newType);
editors[identifier] = jse.libs.editor_instances.switch($textarea, currentType, newType);
_bindAutoUpdate($textarea);
_updateTextArea($textarea);
}
/**
* On Page Submit Handler
*
* If the event target and type are provided this method will be triggered to save the user configuration
* values in the database with AJAX requests.
*/
function _onPageSubmit() {
for (let identifier in editors) {
jse.libs.user_configuration_service.set({
data: {
userId: 0,
configurationKey: identifier,
configurationValue: editors[identifier].type
}
});
}
}
/**
* Bind Auto Update
*
* Binds an event handler to an editor instance to automatically update the
* corresponding textarea.
*
* @param {jQuery} $textarea Textarea the auto update should be bound to
*/
function _bindAutoUpdate($textarea) {
if (options.autoUpdate === 'false') {
return;
}
const instance = editors[$textarea.data('editorIdentifier')];
instance.on('change', () => _updateTextArea($textarea));
}
/**
* Update Text Area Value
*
* Transfers the value of the editor instance of the given textarea to its corresponding textarea.
*
* @param {jQuery} $textarea The textarea to be updated.
*/
function _updateTextArea($textarea) {
const editorType = $textarea.data('editorType');
const instance = editors[$textarea.data('editorIdentifier')];
switch (editorType) {
case 'ckeditor':
instance.updateElement();
break;
case 'codemirror':
$textarea.val(jse.libs.editor_values.getValue($textarea));
break;
default:
throw new Error('Editor type not recognized.', editorType);
}
$textarea.trigger('change');
}
// ------------------------------------------------------------------------
// INITIALIZATION
// ------------------------------------------------------------------------
module.init = function (done) {
$(document).on('JSENGINE_INIT_FINISHED', () => {
const dependencies = [
`${jse.source}/vendor/codemirror/css.min.js`,
`${jse.source}/vendor/codemirror/htmlmixed.min.js`,
`${jse.source}/vendor/codemirror/javascript.min.js`,
`${jse.source}/vendor/codemirror/xml.min.js`
];
jse.core.module_loader.require(dependencies);
});
// Initialize the editors after a specific event in order to make sure that other modules will be
// already initialized and nothing else will change the markup.
$(window).on(options.initEventType, () => {
const $textareas = $this.find(options.selector);
$textareas.each((index, textarea) => {
const $textarea = $(textarea);
if (editorTypes.indexOf($textarea.data('editorType')) === -1) {
$textarea.data('editorType', editorTypes[0]);
}
_addEditorSwitchButton($textarea);
_addEditorHiddenField($textarea);
_createEditorInstance($textarea);
_bindAutoUpdate($textarea);
$this.trigger('editor:textarea_ready', [$textarea]);
});
$this.trigger('editor:ready', [$textareas]);
});
// If the event target and type options are available, bind the page submit handler.
if (options.eventTarget !== undefined && options.eventType !== undefined) {
$(options.eventTarget).on(options.eventType, _onPageSubmit);
}
done();
};
// Return data to module engine.
return module;
});