(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); }); } }; /** * 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), formatInfo = fieldInfo.getFormatInfo(), editorSettings = formatInfo.editorSettings, editor = formatInfo.editor, previousStatus = false, previousEditor = 'none', doSummary = (fieldInfo.summary && (!fieldInfo.formats[fieldInfo.activeFormat] || !fieldInfo.formats[fieldInfo.activeFormat].skip_summary)); if (_internalInstances[fieldId]) { previousStatus = _internalInstances[fieldId]['status']; previousEditor = _internalInstances[fieldId].editor; } // Detach any previous editor instance if enabled, else remove the grippie. detachFromField(context, {'editor': previousEditor, 'status': previousStatus, 'field': fieldId, 'resizable': fieldInfo.resizable}, 'unload'); if (doSummary) { // Summary instances may have a different status if no real editor was // attached yet because the field was hidden. if (_internalInstances[fieldInfo.summary]) { previousStatus = _internalInstances[fieldInfo.summary]['status']; } detachFromField(context, {'editor': previousEditor, 'status': previousStatus, 'field': fieldInfo.summary, 'resizable': fieldInfo.resizable}, 'unload'); } // 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(context, {'status': fieldInfo.enabled, 'editor': editor, 'field': fieldId, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings); // Attach to summary field. if (doSummary) { // If the summary wrapper is visible, attach immediately. if ($('#' + fieldInfo.summary).parents('.text-summary-wrapper').is(':visible')) { attachToField(context, {'status': fieldInfo.enabled, 'editor': editor, 'field': fieldInfo.summary, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings); } 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(context, {'status': false, 'editor': editor, 'field': fieldInfo.summary, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings); // Unbind any existing click handler to avoid double toggling. $('#' + fieldId).parents('.text-format-wrapper').find('.link-edit-summary').unbind('click.wysiwyg').bind('click.wysiwyg', function () { detachFromField(context, {'status': false, 'editor': editor, 'field': fieldInfo.summary, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings); attachToField(context, {'status': fieldInfo.enabled, 'editor': editor, 'field': fieldInfo.summary, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings); $(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 recomended 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 (!(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 context * A DOM element, supplied by Drupal.attachBehaviors(). * @param params * An object containing state information for the editor with the following * properties: * - 'status': A boolean stating whether the editor is currently active. If * false, the default textarea behaviors will be attached instead (aka the * 'none' editor implementation). * - 'editor': The internal name of the editor to attach when active. * - 'field': The field id to use as an output target for the editor. * - 'format': The name of the active text format (prefixed 'format'). * - 'resizable': A boolean indicating whether the original textarea was * resizable. * Note: This parameter is passed directly to the editor implementation and * needs to have been reconstructed or cloned before attaching. * @param editorSettings * An object containing all the settings the editor needs for this field. * Settings are automatically cloned to prevent editors from modifying them. */ function attachToField(context, params, editorSettings) { // If the editor isn't active, attach default behaviors instead. var editor = (params.status ? params.editor : 'none'); // Settings are deep merged (cloned) to prevent editor implementations from // permanently modifying them while attaching. var clonedSettings = jQuery.extend(true, {}, editorSettings); // (Re-)initialize field instance. var internalInstance = new WysiwygInternalInstance(params); _internalInstances[params.field] = internalInstance; Drupal.wysiwyg.instances[params.field] = internalInstance.publicInstance; if ($.isFunction(Drupal.wysiwyg.editor.attach[editor])) { Drupal.wysiwyg.editor.attach[editor].call(internalInstance, context, params, params.status ? 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), editor = fieldInfo.getFormatInfo().editor, trigger = trigger || 'unload', previousStatus = (_internalInstances[fieldId] && _internalInstances[fieldId]['status']); // Detach from main field. detachFromField(context, {'editor': editor, 'status': previousStatus, 'field': fieldId, 'resizable': fieldInfo.resizable}, 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(context, {'editor': editor, 'status': false, 'format': fieldInfo.activeFormat, 'field': fieldId, 'resizable': fieldInfo.resizable}); 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'); // Summary instances may have a different status if no real editor was // attached yet because the field was hidden. if (_internalInstances[fieldInfo.summary]) { previousStatus = _internalInstances[fieldInfo.summary]['status']; } detachFromField(context, {'editor': editor, 'status': previousStatus, 'field': fieldInfo.summary, 'resizable': fieldInfo.resizable}, trigger); if (trigger == 'unload') { attachToField(context, {'editor': editor, 'status': false, 'format': fieldInfo.activeFormat, 'field': fieldInfo.summary, 'resizable': fieldInfo.resizable}); } } }; /** * Helper to detach and clean up after an editor for a single field. * * Removes the 'instance' object under Drupal.wysiwyg.instances[fieldId]. * * @param context * A DOM element, supplied by Drupal.detachBehaviors(). * @param params * An object containing state information for the editor with the following * properties: * - 'status': A boolean stating whether the editor is currently active. If * false, the default textarea behaviors will be attached instead (aka the * 'none' editor implementation). * - 'editor': The internal name of the editor to attach when active. * - 'field': The field id to use as an output target for the editor. * - 'format': The name of the active text format (prefixed 'format'). * - 'resizable': A boolean indicating whether the original textarea was * resizable. * Note: This parameter is passed directly to the editor implementation and * needs to have been reconstructed or cloned before detaching. * @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.wysiwygDetach() */ function detachFromField(context, params, trigger) { var editor = (params.status ? params.editor : 'none'); if (jQuery.isFunction(Drupal.wysiwyg.editor.detach[editor])) { Drupal.wysiwyg.editor.detach[editor].call(_internalInstances[params.field], context, params, trigger); } if (trigger == 'unload') { delete Drupal.wysiwyg.instances[params.field]; delete _internalInstances[params.field]; } } /** * 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'); // Field state is fetched by reference. var currentField = getFieldInfo(fieldId); // Save the state of the current format. if (currentField.formats[currentField.activeFormat]) { currentField.formats[currentField.activeFormat].enabled = currentField.enabled; } // Switch format/profile. currentField.activeFormat = 'format' + this.value; // 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);