911 lines
34 KiB
JavaScript
911 lines
34 KiB
JavaScript
(function($) {
|
|
|
|
// Check if this file has already been loaded.
|
|
if (typeof Drupal.wysiwygAttach !== 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// Keeps track of editor status during AJAX operations, active format and more.
|
|
// Always use getFieldInfo() to get a valid reference to the correct data.
|
|
var _fieldInfoStorage = {};
|
|
// Keeps track of information relevant to each format, such as editor settings.
|
|
// Always use getFormatInfo() to get a reference to a format's data.
|
|
var _formatInfoStorage = {};
|
|
|
|
// Keeps track of global and per format plugin configurations.
|
|
// Always use getPluginInfo() tog get a valid reference to the correct data.
|
|
var _pluginInfoStorage = {'global': {'drupal': {}, 'native': {}}};
|
|
|
|
// Keeps track of private instance information.
|
|
var _internalInstances = {};
|
|
|
|
// Keeps track of initialized editor libraries.
|
|
var _initializedLibraries = {};
|
|
|
|
// Keeps a map between format selectboxes and fields.
|
|
var _selectToField = {};
|
|
|
|
/**
|
|
* Returns field specific editor data.
|
|
*
|
|
* @throws Error
|
|
* Exception thrown if data for an unknown field is requested.
|
|
* Summary fields are expected to use the same data as the main field.
|
|
*
|
|
* If a field id contains the delimiter '--', anything after that is dropped and
|
|
* the remainder is assumed to be the id of an original field replaced by an
|
|
* AJAX operation, due to how Drupal generates unique ids.
|
|
* @see drupal_html_id()
|
|
*
|
|
* Do not modify the returned object unless you really know what you're doing.
|
|
* No external code should need access to this, and it may likely change in the
|
|
* future.
|
|
*
|
|
* @param fieldId
|
|
* The id of the field to get data for.
|
|
*
|
|
* @returns
|
|
* A reference to an object with the following properties:
|
|
* - activeFormat: A string with the active format id.
|
|
* - enabled: A boolean, true if the editor is attached.
|
|
* - formats: An object with one sub-object for each available format, holding
|
|
* format specific state data for this field.
|
|
* - summary: An optional string with the id of a corresponding summary field.
|
|
* - trigger: A string with the id of the format selector for the field.
|
|
* - getFormatInfo: Shortcut method to getFormatInfo(fieldInfo.activeFormat).
|
|
*/
|
|
function getFieldInfo(fieldId) {
|
|
if (_fieldInfoStorage[fieldId]) {
|
|
return _fieldInfoStorage[fieldId];
|
|
}
|
|
var baseFieldId = (fieldId.indexOf('--') === -1 ? fieldId : fieldId.substr(0, fieldId.indexOf('--')));
|
|
if (_fieldInfoStorage[baseFieldId]) {
|
|
return _fieldInfoStorage[baseFieldId];
|
|
}
|
|
throw new Error('Wysiwyg module has no information about field "' + fieldId + '"');
|
|
}
|
|
|
|
/**
|
|
* Returns format specific editor data.
|
|
*
|
|
* Do not modify the returned object unless you really know what you're doing.
|
|
* No external code should need access to this, and it may likely change in the
|
|
* future.
|
|
*
|
|
* @param formatId
|
|
* The id of a format to get data for.
|
|
*
|
|
* @returns
|
|
* A reference to an object with the following properties:
|
|
* - editor: A string with the id of the editor attached to the format.
|
|
* 'none' if no editor profile is associated with the format.
|
|
* - enabled: True if the editor is active.
|
|
* - toggle: True if the editor can be toggled on/off by the user.
|
|
* - editorSettings: A structure holding editor settings for this format.
|
|
* - getPluginInfo: Shortcut method to get plugin config for the this format.
|
|
*/
|
|
function getFormatInfo(formatId) {
|
|
if (_formatInfoStorage[formatId]) {
|
|
return _formatInfoStorage[formatId];
|
|
}
|
|
return {
|
|
editor: 'none',
|
|
getPluginInfo: function () {
|
|
return getPluginInfo(formatId);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns plugin configuration for a specific format, or the global values.
|
|
*
|
|
* @param formatId
|
|
* The id of a format to get data for, or 'global' to get data common to all
|
|
* formats and editors. Use 'global:editorname' to limit it to one editor.
|
|
*
|
|
* @return
|
|
* The returned object will have the sub-objects 'drupal' and 'native', each
|
|
* with properties matching names of plugins.
|
|
* Global data for Drupal (cross-editor) plugins will have the following keys:
|
|
* - title: A human readable name for the button.
|
|
* - internalName: The unique name of a native plugin wrapper, used in editor
|
|
* profiles and when registering the plugin with the editor API to avoid
|
|
* possible id conflicts with native plugins.
|
|
* - css: A stylesheet needed by the plugin.
|
|
* - icon path: The path where button icons are stored.
|
|
* - path: The path to the plugin's main folder.
|
|
* - buttons: An object with button data, keyed by name with the properties:
|
|
* - description: A human readable string describing the button's function.
|
|
* - title: A human readable string with the name of the button.
|
|
* - icon: An object with one or more of the following properties:
|
|
* - src: An absolute (begins with '/') or relative path to the icon.
|
|
* - path: An absolute path to a folder containing the button.
|
|
*
|
|
* When formatId matched a format with an assigned editor, values for plugins
|
|
* match the return value of the editor integration's [proxy] plugin settings
|
|
* callbacks.
|
|
*
|
|
* @see Drupal.wysiwyg.utilities.getPluginInfo()
|
|
* @see Drupal.wyswiyg.utilities.extractButtonSettings()
|
|
*/
|
|
function getPluginInfo(formatId) {
|
|
var match, editor;
|
|
if ((match = formatId.match(/^global:(\w+)$/))) {
|
|
formatId = 'global';
|
|
editor = match[1];
|
|
}
|
|
if (!_pluginInfoStorage[formatId]) {
|
|
return {};
|
|
}
|
|
if (formatId === 'global' && typeof editor !== 'undefined') {
|
|
return { 'drupal': _pluginInfoStorage.global.drupal, 'native': (_pluginInfoStorage.global['native'][editor]) };
|
|
}
|
|
return _pluginInfoStorage[formatId];
|
|
}
|
|
|
|
/**
|
|
* Attach editors to input formats and target elements (f.e. textareas).
|
|
*
|
|
* This behavior searches for input format selectors and formatting guidelines
|
|
* that have been preprocessed by Wysiwyg API. All CSS classes of those elements
|
|
* with the prefix 'wysiwyg-' are parsed into input format parameters, defining
|
|
* the input format, configured editor, target element id, and variable other
|
|
* properties, which are passed to the attach/detach hooks of the corresponding
|
|
* editor.
|
|
*
|
|
* Furthermore, an "enable/disable rich-text" toggle link is added after the
|
|
* target element to allow users to alter its contents in plain text.
|
|
*
|
|
* This is executed once, while editor attach/detach hooks can be invoked
|
|
* multiple times.
|
|
*
|
|
* @param context
|
|
* A DOM element, supplied by Drupal.attachBehaviors().
|
|
*/
|
|
Drupal.behaviors.attachWysiwyg = {
|
|
attach: function (context, settings) {
|
|
// This breaks in Konqueror. Prevent it from running.
|
|
if (/KDE/.test(navigator.vendor)) {
|
|
return;
|
|
}
|
|
var wysiwygs = $('.wysiwyg:input', context);
|
|
if (!wysiwygs.length) {
|
|
// No new fields, nothing to update.
|
|
return;
|
|
}
|
|
updateInternalState(settings.wysiwyg, context);
|
|
wysiwygs.once('wysiwyg', function () {
|
|
// Skip processing if the element is unknown or does not exist in this
|
|
// document. Can happen after a form was removed but Drupal.ajax keeps a
|
|
// lingering reference to the form and calls Drupal.attachBehaviors().
|
|
var $this = $('#' + this.id, document);
|
|
if (!$this.length) {
|
|
return;
|
|
}
|
|
// Directly attach this editor, if the input format is enabled or there is
|
|
// only one input format at all.
|
|
Drupal.wysiwygAttach(context, this.id);
|
|
})
|
|
.closest('form').submit(function (event) {
|
|
// Detach any editor when the containing form is submitted.
|
|
// Do not detach if the event was cancelled.
|
|
if (event.isDefaultPrevented()) {
|
|
return;
|
|
}
|
|
var form = this;
|
|
$('.wysiwyg:input', this).each(function () {
|
|
Drupal.wysiwygDetach(form, this.id, 'serialize');
|
|
});
|
|
});
|
|
},
|
|
|
|
detach: function (context, settings, trigger) {
|
|
var wysiwygs;
|
|
// The 'serialize' trigger indicates that we should simply update the
|
|
// underlying element with the new text, without destroying the editor.
|
|
if (trigger == 'serialize') {
|
|
// Removing the wysiwyg-processed class guarantees that the editor will
|
|
// be reattached. Only do this if we're planning to destroy the editor.
|
|
wysiwygs = $('.wysiwyg-processed:input', context);
|
|
}
|
|
else {
|
|
wysiwygs = $('.wysiwyg:input', context).removeOnce('wysiwyg');
|
|
}
|
|
wysiwygs.each(function () {
|
|
Drupal.wysiwygDetach(context, this.id, trigger);
|
|
if (trigger === 'unload') {
|
|
// Delete the instance in case the field is removed. This is safe since
|
|
// detaching with the unload trigger is reverts to the 'none' "editor".
|
|
delete _internalInstances[this.id];
|
|
delete Drupal.wysiwyg.instances[this.id];
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Attach an editor to a target element.
|
|
*
|
|
* Detaches any existing instance for the field before attaching a new instance
|
|
* based on the current state of the field. Editor settings and state
|
|
* information is fetched based on the element id and get cloned first, so they
|
|
* cannot be overridden. After attaching the editor, the toggle link is shown
|
|
* again, except in case we are attaching no editor.
|
|
*
|
|
* Also attaches editors to the summary field, if available.
|
|
*
|
|
* @param context
|
|
* A DOM element, supplied by Drupal.attachBehaviors().
|
|
* @param fieldId
|
|
* The id of an element to attach an editor to.
|
|
*/
|
|
Drupal.wysiwygAttach = function(context, fieldId) {
|
|
var fieldInfo = getFieldInfo(fieldId),
|
|
doSummary = (fieldInfo.summary && (!fieldInfo.formats[fieldInfo.activeFormat] || !fieldInfo.formats[fieldInfo.activeFormat].skip_summary));
|
|
// Detach any previous editor instance if enabled, else remove the grippie.
|
|
detachFromField(fieldId, context, 'unload');
|
|
var wasSummary = !!_internalInstances[fieldInfo.summary];
|
|
if (doSummary || wasSummary) {
|
|
detachFromField(fieldId, context, 'unload', {summary: true});
|
|
}
|
|
// Store this field id, so (external) plugins can use it.
|
|
// @todo Wrong point in time. Probably can only supported by editors which
|
|
// support an onFocus() or similar event.
|
|
Drupal.wysiwyg.activeId = fieldId;
|
|
// Attach or update toggle link, if enabled.
|
|
Drupal.wysiwygAttachToggleLink(context, fieldId);
|
|
// Attach to main field.
|
|
attachToField(fieldId, context);
|
|
// Attach to summary field.
|
|
if (doSummary || wasSummary) {
|
|
// If the summary wrapper is visible, attach immediately.
|
|
if ($('#' + fieldInfo.summary).parents('.text-summary-wrapper').is(':visible')) {
|
|
attachToField(fieldId, context, {summary: true, forceDisabled: !doSummary});
|
|
}
|
|
else {
|
|
// Attach an instance of the 'none' editor to have consistency while the
|
|
// summary is hidden, then switch to a real editor instance when shown.
|
|
attachToField(fieldId, context, {summary: true, forceDisabled: true});
|
|
// Unbind any existing click handler to avoid double toggling.
|
|
$('#' + fieldId).parents('.text-format-wrapper').find('.link-edit-summary').closest('.field-edit-link').unbind('click.wysiwyg').bind('click.wysiwyg', function () {
|
|
detachFromField(fieldId, context, 'unload', {summary: true});
|
|
attachToField(fieldId, context, {summary: true, forceDisabled: !doSummary});
|
|
$(this).unbind('click.wysiwyg');
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The public API exposed for an editor-enabled field.
|
|
*
|
|
* Properties should be treated as read-only state and changing them will not
|
|
* have any effect on how the instance behaves.
|
|
*
|
|
* Note: The attach() and detach() methods are not part of the public API and
|
|
* should not be called directly to avoid synchronization issues.
|
|
* Use Drupal.wysiwygAttach() and Drupal.wysiwygDetach() to activate or
|
|
* deactivate editor instances. Externally switching the active editor is not
|
|
* supported other than changing the format using the select element.
|
|
*/
|
|
function WysiwygInstance(internalInstance) {
|
|
// The id of the field the instance manipulates.
|
|
this.field = internalInstance.field;
|
|
// The internal name of the attached editor.
|
|
this.editor = internalInstance.editor;
|
|
// If the editor is currently enabled or not.
|
|
this['status'] = internalInstance['status'];
|
|
// The id of the text format the editor is attached to.
|
|
this.format = internalInstance.format;
|
|
// If the field is resizable without an editor attached.
|
|
this.resizable = internalInstance.resizable;
|
|
|
|
// Methods below here redirect to the 'none' editor which handles plain text
|
|
// fields when the editor is disabled.
|
|
|
|
/**
|
|
* Insert content at the cursor position.
|
|
*
|
|
* @param content
|
|
* An HTML markup string.
|
|
*/
|
|
this.insert = function (content) {
|
|
return internalInstance['status'] ? internalInstance.insert(content) : Drupal.wysiwyg.editor.instance.none.insert.call(internalInstance, content);
|
|
}
|
|
|
|
/**
|
|
* Get all content from the editor.
|
|
*
|
|
* @return
|
|
* An HTML markup string.
|
|
*/
|
|
this.getContent = function () {
|
|
return internalInstance['status'] ? internalInstance.getContent() : Drupal.wysiwyg.editor.instance.none.getContent.call(internalInstance);
|
|
}
|
|
|
|
/**
|
|
* Replace all content in the editor.
|
|
*
|
|
* @param content
|
|
* An HTML markup string.
|
|
*/
|
|
this.setContent = function (content) {
|
|
return internalInstance['status'] ? internalInstance.setContent(content) : Drupal.wysiwyg.editor.instance.none.setContent.call(internalInstance, content);
|
|
}
|
|
|
|
/**
|
|
* Check if the editor is in fullscreen mode.
|
|
*
|
|
* @return bool
|
|
* True if the editor is considered to be in fullscreen mode.
|
|
*/
|
|
this.isFullscreen = function (content) {
|
|
return internalInstance['status'] && $.isFunction(internalInstance.isFullscreen) ? internalInstance.isFullscreen() : false;
|
|
}
|
|
|
|
// @todo The methods below only work for TinyMCE, deprecate?
|
|
|
|
/**
|
|
* Open a native editor dialog.
|
|
*
|
|
* Use of this method i not recommended due to limited editor support.
|
|
*
|
|
* @param dialog
|
|
* An object with dialog settings. Keys used:
|
|
* - url: The url of the dialog template.
|
|
* - width: Width in pixels.
|
|
* - height: Height in pixels.
|
|
*/
|
|
this.openDialog = function (dialog, params) {
|
|
if ($.isFunction(internalInstance.openDialog)) {
|
|
return internalInstance.openDialog(dialog, params)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close an opened dialog.
|
|
*
|
|
* @param dialog
|
|
* Same options as for opening a dialog.
|
|
*/
|
|
this.closeDialog = function (dialog) {
|
|
if ($.isFunction(internalInstance.closeDialog)) {
|
|
return internalInstance.closeDialog(dialog)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The private base for editor instances.
|
|
*
|
|
* An instance of this object is used as the context for all calls into the
|
|
* editor instances (including attach() and detach() when only one instance is
|
|
* asked to detach).
|
|
*
|
|
* Anything added to Drupal.wysiwyg.editor.instance[editorName] is cloned into
|
|
* an instance of this function.
|
|
*
|
|
* Editor state parameters are cloned into the instance after that.
|
|
*/
|
|
function WysiwygInternalInstance(params) {
|
|
$.extend(true, this, Drupal.wysiwyg.editor.instance[params.editor]);
|
|
$.extend(true, this, params);
|
|
this.pluginInfo = {
|
|
'global': getPluginInfo('global:' + params.editor),
|
|
'instances': getPluginInfo(params.format)
|
|
};
|
|
// Keep track of the public face to keep it synced.
|
|
this.publicInstance = new WysiwygInstance(this);
|
|
}
|
|
|
|
/**
|
|
* Updates internal settings and state caches with new information.
|
|
*
|
|
* Attaches selection change handler to format selector to track state changes.
|
|
*
|
|
* @param settings
|
|
* A structure like Drupal.settigns.wysiwyg.
|
|
* @param context
|
|
* The context given from Drupal.attachBehaviors().
|
|
*/
|
|
function updateInternalState(settings, context) {
|
|
var pluginData = settings.plugins;
|
|
for (var plugin in pluginData.drupal) {
|
|
if (!(plugin in _pluginInfoStorage.global.drupal)) {
|
|
_pluginInfoStorage.global.drupal[plugin] = pluginData.drupal[plugin];
|
|
}
|
|
}
|
|
// To make sure we don't rely on Drupal.settings, uncomment these for testing.
|
|
//pluginData.drupal = {};
|
|
for (var editorId in pluginData['native']) {
|
|
for (var plugin in pluginData['native'][editorId]) {
|
|
_pluginInfoStorage.global['native'][editorId] = (_pluginInfoStorage.global['native'][editorId] || {});
|
|
if (!(plugin in _pluginInfoStorage.global['native'][editorId])) {
|
|
_pluginInfoStorage.global['native'][editorId][plugin] = pluginData['native'][editorId][plugin];
|
|
}
|
|
}
|
|
}
|
|
//pluginData['native'] = {};
|
|
for (var fmatId in pluginData) {
|
|
if (fmatId.substr(0, 6) !== 'format') {
|
|
continue;
|
|
}
|
|
_pluginInfoStorage[fmatId] = (_pluginInfoStorage[fmatId] || {'drupal': {}, 'native': {}});
|
|
for (var plugin in pluginData[fmatId].drupal) {
|
|
if (!(plugin in _pluginInfoStorage[fmatId].drupal)) {
|
|
_pluginInfoStorage[fmatId].drupal[plugin] = pluginData[fmatId].drupal[plugin];
|
|
}
|
|
}
|
|
for (var plugin in pluginData[fmatId]['native']) {
|
|
if (!(plugin in _pluginInfoStorage[fmatId]['native'])) {
|
|
_pluginInfoStorage[fmatId]['native'][plugin] = pluginData[fmatId]['native'][plugin];
|
|
}
|
|
}
|
|
delete pluginData[fmatId];
|
|
}
|
|
// Build the cache of format/profile settings.
|
|
for (var editor in settings.configs) {
|
|
if (!settings.configs.hasOwnProperty(editor)) {
|
|
continue;
|
|
}
|
|
for (var format in settings.configs[editor]) {
|
|
if (_formatInfoStorage[format] || !settings.configs[editor].hasOwnProperty(format)) {
|
|
continue;
|
|
}
|
|
_formatInfoStorage[format] = {
|
|
editor: editor,
|
|
toggle: true, // Overridden by triggers.
|
|
editorSettings: processObjectTypes(settings.configs[editor][format])
|
|
};
|
|
}
|
|
// Initialize editor libraries if not already done.
|
|
if (!_initializedLibraries[editor] && typeof Drupal.wysiwyg.editor.init[editor] === 'function') {
|
|
// Clone, so original settings are not overwritten.
|
|
Drupal.wysiwyg.editor.init[editor](jQuery.extend(true, {}, settings.configs[editor]), getPluginInfo('global:' + editor));
|
|
_initializedLibraries[editor] = true;
|
|
}
|
|
// Update libraries, in case new plugins etc have not been initialized yet.
|
|
else if (typeof Drupal.wysiwyg.editor.update[editor] === 'function') {
|
|
Drupal.wysiwyg.editor.update[editor](jQuery.extend(true, {}, settings.configs[editor]), getPluginInfo('global:' + editor));
|
|
}
|
|
}
|
|
//settings.configs = {};
|
|
for (var triggerId in settings.triggers) {
|
|
var trigger = settings.triggers[triggerId];
|
|
var fieldId = trigger.field;
|
|
var baseFieldId = (fieldId.indexOf('--') === -1 ? fieldId : fieldId.substr(0, fieldId.indexOf('--')));
|
|
var fieldInfo = null;
|
|
if ($('#' + triggerId, context).length === 0) {
|
|
// Skip fields which may have been removed or are not in this context.
|
|
continue;
|
|
}
|
|
if (!(fieldInfo = _fieldInfoStorage[baseFieldId])) {
|
|
fieldInfo = _fieldInfoStorage[baseFieldId] = {
|
|
formats: {},
|
|
select: trigger.select,
|
|
resizable: trigger.resizable,
|
|
summary: trigger.summary,
|
|
getFormatInfo: function () {
|
|
if (this.select) {
|
|
this.activeFormat = 'format' + $('#' + this.select + ':input').val();
|
|
}
|
|
return getFormatInfo(this.activeFormat);
|
|
}
|
|
// 'activeFormat' and 'enabled' added below.
|
|
};
|
|
}
|
|
for (var format in trigger) {
|
|
if (format.indexOf('format') != 0 || fieldInfo.formats[format]) {
|
|
continue;
|
|
}
|
|
fieldInfo.formats[format] = {
|
|
'enabled': trigger[format].status
|
|
};
|
|
if (!_formatInfoStorage[format]) {
|
|
_formatInfoStorage[format] = {
|
|
editor: trigger[format].editor,
|
|
editorSettings: {},
|
|
getPluginInfo: function () {
|
|
return getPluginInfo(formatId);
|
|
}
|
|
};
|
|
}
|
|
// Always update these since they are stored as state.
|
|
_formatInfoStorage[format].toggle = trigger[format].toggle;
|
|
if (trigger[format].skip_summary) {
|
|
fieldInfo.formats[format].skip_summary = true;
|
|
}
|
|
}
|
|
var $selectbox = null;
|
|
// Always update these since Drupal generates new ids on AJAX calls.
|
|
fieldInfo.summary = trigger.summary;
|
|
if (trigger.select) {
|
|
_selectToField[trigger.select.replace(/--\d+$/,'')] = trigger.field;
|
|
fieldInfo.select = trigger.select;
|
|
// Specifically target input elements in case selectbox wrappers have
|
|
// hidden the real element and cloned its attributes.
|
|
$selectbox = $('#' + trigger.select + ':input', context).filter('select');
|
|
// Attach onChange handlers to input format selector elements.
|
|
$selectbox.unbind('change.wysiwyg').bind('change.wysiwyg', formatChanged);
|
|
}
|
|
// Always update the active format to ensure the righ profile is used if a
|
|
// field was removed and gets re-added and the instance was left behind.
|
|
fieldInfo.activeFormat = 'format' + ($selectbox ? $selectbox.val() : trigger.activeFormat);
|
|
fieldInfo.enabled = fieldInfo.formats[fieldInfo.activeFormat] && fieldInfo.formats[fieldInfo.activeFormat].enabled;
|
|
}
|
|
//settings.triggers = {};
|
|
}
|
|
|
|
/**
|
|
* Helper to prepare and attach an editor for a single field.
|
|
*
|
|
* Creates the 'instance' object under Drupal.wysiwyg.instances[fieldId].
|
|
*
|
|
* @param mainFieldId
|
|
* The id of the field's main element, for fetching field info.
|
|
* @param context
|
|
* A DOM element, supplied by Drupal.attachBehaviors().
|
|
* @param params
|
|
* An optional object for overriding state information for the editor with the
|
|
* following properties:
|
|
* - 'summary': Set to true to indicate to attach to the summary instead of
|
|
* the main element. Defaults to false.
|
|
* - 'forceDisabled': Set to true to override the current state of the field
|
|
* and assume it is disabled. Useful for hidden summary instances.
|
|
*
|
|
* @see Drupal.wysiwygAttach()
|
|
*/
|
|
function attachToField(mainFieldId, context, params) {
|
|
params = params || {};
|
|
var fieldInfo = getFieldInfo(mainFieldId);
|
|
var fieldId = (params.summary ? fieldInfo.summary : mainFieldId);
|
|
var formatInfo = fieldInfo.getFormatInfo();
|
|
// If the editor isn't active, attach default behaviors instead.
|
|
var enabled = (fieldInfo.enabled && !params.forceDisabled);
|
|
var editor = (enabled ? formatInfo.editor : 'none');
|
|
// Settings are deep merged (cloned) to prevent editor implementations from
|
|
// permanently modifying them while attaching.
|
|
var clonedSettings = (enabled ? jQuery.extend(true, {}, formatInfo.editorSettings) : {});
|
|
// (Re-)initialize field instance.
|
|
var stateParams = {
|
|
field: fieldId,
|
|
editor: formatInfo.editor,
|
|
'status': enabled,
|
|
format: fieldInfo.activeFormat,
|
|
resizable: fieldInfo.resizable
|
|
};
|
|
var internalInstance = new WysiwygInternalInstance(stateParams);
|
|
_internalInstances[fieldId] = internalInstance;
|
|
Drupal.wysiwyg.instances[fieldId] = internalInstance.publicInstance;
|
|
// Attach editor, if enabled by default or last state was enabled.
|
|
Drupal.wysiwyg.editor.attach[editor].call(internalInstance, context, stateParams, clonedSettings);
|
|
}
|
|
|
|
/**
|
|
* Detach all editors from a target element.
|
|
*
|
|
* Ensures Drupal's original textfield resize functionality is restored if
|
|
* enabled and the triggering reason is 'unload'.
|
|
*
|
|
* Also detaches editors from the summary field, if available.
|
|
*
|
|
* @param context
|
|
* A DOM element, supplied by Drupal.detachBehaviors().
|
|
* @param fieldId
|
|
* The id of an element to attach an editor to.
|
|
* @param trigger
|
|
* A string describing what is causing the editor to be detached.
|
|
* - 'serialize': The editor normally just syncs its contents to the original
|
|
* textarea for value serialization before an AJAX request.
|
|
* - 'unload': The editor is to be removed completely and the original
|
|
* textarea restored.
|
|
*
|
|
* @see Drupal.detachBehaviors()
|
|
*/
|
|
Drupal.wysiwygDetach = function (context, fieldId, trigger) {
|
|
var fieldInfo = getFieldInfo(fieldId),
|
|
trigger = trigger || 'unload';
|
|
// Detach from main field.
|
|
detachFromField(fieldId, context, trigger);
|
|
if (trigger == 'unload') {
|
|
// Attach the resize behavior by forcing status to false. Other values are
|
|
// intentionally kept the same to show which editor is normally attached.
|
|
attachToField(fieldId, context, {forceDisabled: true});
|
|
Drupal.wysiwygAttachToggleLink(context, fieldId);
|
|
}
|
|
// Detach from summary field.
|
|
if (fieldInfo.summary && _internalInstances[fieldInfo.summary]) {
|
|
// The "Edit summary" click handler could re-enable the editor by mistake.
|
|
$('#' + fieldId).parents('.text-format-wrapper').find('.link-edit-summary').unbind('click.wysiwyg');
|
|
detachFromField(fieldId, context, trigger, {summary: true});
|
|
if (trigger == 'unload') {
|
|
attachToField(fieldId, context, {summary: true});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Helper to detach and clean up after an editor for a single field.
|
|
*
|
|
* Removes the 'instance' object under Drupal.wysiwyg.instances[fieldId].
|
|
*
|
|
* @param mainFieldId
|
|
* The id of the field's main element, for fetching field info.
|
|
* @param context
|
|
* A DOM element, supplied by Drupal.detachBehaviors().
|
|
* @param trigger
|
|
* A string describing what is causing the editor to be detached.
|
|
* - 'serialize': The editor normally just syncs its contents to the original
|
|
* textarea for value serialization before an AJAX request.
|
|
* - 'unload': The editor is to be removed completely and the original
|
|
* textarea restored.
|
|
* @param params
|
|
* An optional object for overriding state information for the editor with the
|
|
* following properties:
|
|
* - 'summary': Set to true to indicate to detach from the summary instead of
|
|
* the main element. Defaults to false.
|
|
*
|
|
* @see Drupal.wysiwygDetach()
|
|
*/
|
|
function detachFromField(mainFieldId, context, trigger, params) {
|
|
params = params || {};
|
|
var fieldInfo = getFieldInfo(mainFieldId);
|
|
var fieldId = (params.summary ? fieldInfo.summary : mainFieldId);
|
|
var enabled = false;
|
|
var editor = 'none';
|
|
if (_internalInstances[fieldId]) {
|
|
enabled = _internalInstances[fieldId]['status'];
|
|
editor = (enabled ? _internalInstances[fieldId].editor : 'none');
|
|
}
|
|
var stateParams = {
|
|
field: fieldId,
|
|
'status': enabled,
|
|
editor: fieldInfo.editor,
|
|
format: fieldInfo.activeFormat,
|
|
resizable: fieldInfo.resizable
|
|
};
|
|
if (jQuery.isFunction(Drupal.wysiwyg.editor.detach[editor])) {
|
|
Drupal.wysiwyg.editor.detach[editor].call(_internalInstances[fieldId], context, stateParams, trigger);
|
|
}
|
|
if (trigger == 'unload') {
|
|
delete Drupal.wysiwyg.instances[fieldId];
|
|
delete _internalInstances[fieldId];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append or update an editor toggle link to a target element.
|
|
*
|
|
* @param context
|
|
* A DOM element, supplied by Drupal.attachBehaviors().
|
|
* @param fieldId
|
|
* The id of an element to attach an editor to.
|
|
*/
|
|
Drupal.wysiwygAttachToggleLink = function(context, fieldId) {
|
|
var fieldInfo = getFieldInfo(fieldId),
|
|
editor = fieldInfo.getFormatInfo().editor;
|
|
if (!fieldInfo.getFormatInfo().toggle) {
|
|
// Otherwise, ensure that toggle link is hidden.
|
|
$('#wysiwyg-toggle-' + fieldId).hide();
|
|
return;
|
|
}
|
|
if (!$('#wysiwyg-toggle-' + fieldId, context).length) {
|
|
var text = document.createTextNode(fieldInfo.enabled ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable),
|
|
a = document.createElement('a'),
|
|
div = document.createElement('div');
|
|
$(a).attr({ id: 'wysiwyg-toggle-' + fieldId, href: 'javascript:void(0);' }).append(text);
|
|
$(div).addClass('wysiwyg-toggle-wrapper').append(a);
|
|
if ($('#' + fieldInfo.select).closest('.fieldset-wrapper').prepend(div).length == 0) {
|
|
// Fall back to inserting the link right after the field.
|
|
$('#' + fieldId).after(div);
|
|
};
|
|
}
|
|
$('#wysiwyg-toggle-' + fieldId, context)
|
|
.html(fieldInfo.enabled ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable).show()
|
|
.unbind('click.wysiwyg')
|
|
.bind('click.wysiwyg', { 'fieldId': fieldId, 'context': context }, Drupal.wysiwyg.toggleWysiwyg);
|
|
|
|
// Hide toggle link in case no editor is attached.
|
|
if (editor == 'none') {
|
|
$('#wysiwyg-toggle-' + fieldId).hide();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Callback for the Enable/Disable rich editor link.
|
|
*/
|
|
Drupal.wysiwyg.toggleWysiwyg = function (event) {
|
|
var context = event.data.context,
|
|
fieldId = event.data.fieldId,
|
|
fieldInfo = getFieldInfo(fieldId);
|
|
// Toggling the enabled state indirectly toggles use of the 'none' editor.
|
|
if (fieldInfo.enabled) {
|
|
fieldInfo.enabled = false;
|
|
Drupal.wysiwygDetach(context, fieldId, 'unload');
|
|
}
|
|
else {
|
|
fieldInfo.enabled = true;
|
|
Drupal.wysiwygAttach(context, fieldId);
|
|
}
|
|
fieldInfo.formats[fieldInfo.activeFormat].enabled = fieldInfo.enabled;
|
|
}
|
|
|
|
|
|
/**
|
|
* Event handler for when the selected format is changed.
|
|
*/
|
|
function formatChanged(event) {
|
|
var fieldId = _selectToField[this.id.replace(/--\d+$/, '')];
|
|
var context = $(this).closest('form');
|
|
var newFormat = 'format' + $(this).val();
|
|
// Field state is fetched by reference.
|
|
var currentField = getFieldInfo(fieldId);
|
|
// Prevent double-attaching if change event is triggered manually.
|
|
if (newFormat === currentField.activeFormat) {
|
|
return;
|
|
}
|
|
// Save the state of the current format.
|
|
if (currentField.formats[currentField.activeFormat]) {
|
|
currentField.formats[currentField.activeFormat].enabled = currentField.enabled;
|
|
}
|
|
// Switch format/profile.
|
|
currentField.activeFormat = newFormat;
|
|
// Load the state from the new format.
|
|
if (currentField.formats[currentField.activeFormat]) {
|
|
currentField.enabled = currentField.formats[currentField.activeFormat].enabled;
|
|
}
|
|
else {
|
|
currentField.enabled = false;
|
|
}
|
|
// Attaching again will use the changed field state.
|
|
Drupal.wysiwygAttach(context, fieldId);
|
|
}
|
|
|
|
/**
|
|
* Convert JSON type placeholders into the actual types.
|
|
*
|
|
* Recognizes function references (callbacks) and Regular Expressions.
|
|
*
|
|
* To create a callback, pass in an object with the following properties:
|
|
* - 'drupalWysiwygType': Must be set to 'callback'.
|
|
* - 'name': A string with the name of the callback, use
|
|
* 'object.subobject.method' syntax for methods in nested objects.
|
|
* - 'context': An optional string with the name of an object for overriding
|
|
* 'this' inside the function. Use 'object.subobject' syntax for nested
|
|
* objects. Defaults to the window object.
|
|
*
|
|
* To create a RegExp, pass in an object with the following properties:
|
|
* - 'drupalWysiwygType: Must be set to 'regexp'.
|
|
* - 'regexp': The Regular Expression as a string, without / wrappers.
|
|
* - 'modifiers': An optional string with modifiers to set on the RegExp object.
|
|
*
|
|
* @param json
|
|
* The json argument with all recognized type placeholders replaced by the real
|
|
* types.
|
|
*
|
|
* @return The JSON object with placeholder types replaced.
|
|
*/
|
|
function processObjectTypes(json) {
|
|
var out = null;
|
|
if (typeof json != 'object') {
|
|
return json;
|
|
}
|
|
out = new json.constructor();
|
|
if (json.drupalWysiwygType) {
|
|
switch (json.drupalWysiwygType) {
|
|
case 'callback':
|
|
out = callbackWrapper(json.name, json.context);
|
|
break;
|
|
case 'regexp':
|
|
out = new RegExp(json.regexp, json.modifiers ? json.modifiers : undefined);
|
|
break;
|
|
default:
|
|
out.drupalWysiwygType = json.drupalWysiwygType;
|
|
}
|
|
}
|
|
else {
|
|
for (var i in json) {
|
|
if (json.hasOwnProperty(i) && json[i] && typeof json[i] == 'object') {
|
|
out[i] = processObjectTypes(json[i]);
|
|
}
|
|
else {
|
|
out[i] = json[i];
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Convert function names into function references.
|
|
*
|
|
* @param name
|
|
* The name of a function to use as callback. Use the 'object.subobject.method'
|
|
* syntax for methods in nested objects.
|
|
* @param context
|
|
* An optional string with the name of an object for overriding 'this' inside
|
|
* the function. Use 'object.subobject' syntax for nested objects. Defaults to
|
|
* the window object.
|
|
*
|
|
* @return
|
|
* A function which will call the named function or method in the proper
|
|
* context, passing through arguments and return values.
|
|
*/
|
|
function callbackWrapper(name, context) {
|
|
var namespaces = name.split('.'), func = namespaces.pop(), obj = window;
|
|
for (var i = 0; obj && i < namespaces.length; i++) {
|
|
obj = obj[namespaces[i]];
|
|
}
|
|
if (!obj) {
|
|
throw "Wysiwyg: Unable to locate callback " + namespaces.join('.') + "." + func + "()";
|
|
}
|
|
if (!context) {
|
|
context = obj;
|
|
}
|
|
else if (typeof context == 'string'){
|
|
namespaces = context.split('.');
|
|
context = window;
|
|
for (i = 0; context && i < namespaces.length; i++) {
|
|
context = context[namespaces[i]];
|
|
}
|
|
if (!context) {
|
|
throw "Wysiwyg: Unable to locate context object " + namespaces.join('.');
|
|
}
|
|
}
|
|
if (typeof obj[func] != 'function') {
|
|
throw "Wysiwyg: " + func + " is not a callback function";
|
|
}
|
|
return function () {
|
|
return obj[func].apply(context, arguments);
|
|
}
|
|
}
|
|
|
|
var oldBeforeSerialize = (Drupal.ajax ? Drupal.ajax.prototype.beforeSerialize : false);
|
|
if (oldBeforeSerialize) {
|
|
/**
|
|
* Filter the ajax_html_ids list sent in AJAX requests.
|
|
*
|
|
* This overrides part of the form serializer to not include ids we know will
|
|
* not collide because editors are removed before those ids are reused.
|
|
*
|
|
* This avoids hitting like max_input_vars, which defaults to 1000,
|
|
* even with just a few active editor instances.
|
|
*/
|
|
Drupal.ajax.prototype.beforeSerialize = function (element, options) {
|
|
var ret = oldBeforeSerialize.call(this, element, options);
|
|
var excludeSelectors = [];
|
|
$.each(Drupal.wysiwyg.excludeIdSelectors, function () {
|
|
if ($.isArray(this)) {
|
|
excludeSelectors = excludeSelectors.concat(this);
|
|
}
|
|
});
|
|
if (excludeSelectors.length > 0) {
|
|
var ajaxHtmlIdsArray = options.data['ajax_html_ids[]'];
|
|
if (!ajaxHtmlIdsArray || ajaxHtmlIdsArray.length === 0) {
|
|
return ret;
|
|
}
|
|
options.data['ajax_html_ids[]'] = [];
|
|
$('[id]:not(' + excludeSelectors.join(',') + ')').each(function () {
|
|
if ($.inArray(this.id, ajaxHtmlIdsArray) !== -1) {
|
|
options.data['ajax_html_ids[]'].push(this.id);
|
|
}
|
|
});
|
|
}
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
// Respond to CTools detach behaviors event.
|
|
$(document).unbind('CToolsDetachBehaviors.wysiwyg').bind('CToolsDetachBehaviors.wysiwyg', function(event, context) {
|
|
$('.wysiwyg:input', context).removeOnce('wysiwyg').each(function () {
|
|
Drupal.wysiwygDetach(context, this.id, 'unload');
|
|
// The 'none' instances are destroyed with the dialog.
|
|
delete Drupal.wysiwyg.instances[this.id];
|
|
delete _internalInstances[this.id];
|
|
var baseFieldId = (this.id.indexOf('--') === -1 ? this.id : this.id.substr(0, this.id.indexOf('--')));
|
|
delete _fieldInfoStorage[baseFieldId];
|
|
});
|
|
});
|
|
|
|
})(jQuery);
|