wysiwyg.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. (function($) {
  2. // Check if this file has already been loaded.
  3. if (typeof Drupal.wysiwygAttach !== 'undefined') {
  4. return;
  5. }
  6. // Keeps track of editor status during AJAX operations, active format and more.
  7. // Always use getFieldInfo() to get a valid reference to the correct data.
  8. var _fieldInfoStorage = {};
  9. // Keeps track of information relevant to each format, such as editor settings.
  10. // Always use getFormatInfo() to get a reference to a format's data.
  11. var _formatInfoStorage = {};
  12. // Keeps track of global and per format plugin configurations.
  13. // Always use getPluginInfo() tog get a valid reference to the correct data.
  14. var _pluginInfoStorage = {'global': {'drupal': {}, 'native': {}}};
  15. // Keeps track of private instance information.
  16. var _internalInstances = {};
  17. // Keeps track of initialized editor libraries.
  18. var _initializedLibraries = {};
  19. // Keeps a map between format selectboxes and fields.
  20. var _selectToField = {};
  21. /**
  22. * Returns field specific editor data.
  23. *
  24. * @throws Error
  25. * Exception thrown if data for an unknown field is requested.
  26. * Summary fields are expected to use the same data as the main field.
  27. *
  28. * If a field id contains the delimiter '--', anything after that is dropped and
  29. * the remainder is assumed to be the id of an original field replaced by an
  30. * AJAX operation, due to how Drupal generates unique ids.
  31. * @see drupal_html_id()
  32. *
  33. * Do not modify the returned object unless you really know what you're doing.
  34. * No external code should need access to this, and it may likely change in the
  35. * future.
  36. *
  37. * @param fieldId
  38. * The id of the field to get data for.
  39. *
  40. * @returns
  41. * A reference to an object with the following properties:
  42. * - activeFormat: A string with the active format id.
  43. * - enabled: A boolean, true if the editor is attached.
  44. * - formats: An object with one sub-object for each available format, holding
  45. * format specific state data for this field.
  46. * - summary: An optional string with the id of a corresponding summary field.
  47. * - trigger: A string with the id of the format selector for the field.
  48. * - getFormatInfo: Shortcut method to getFormatInfo(fieldInfo.activeFormat).
  49. */
  50. function getFieldInfo(fieldId) {
  51. if (_fieldInfoStorage[fieldId]) {
  52. return _fieldInfoStorage[fieldId];
  53. }
  54. var baseFieldId = (fieldId.indexOf('--') === -1 ? fieldId : fieldId.substr(0, fieldId.indexOf('--')));
  55. if (_fieldInfoStorage[baseFieldId]) {
  56. return _fieldInfoStorage[baseFieldId];
  57. }
  58. throw new Error('Wysiwyg module has no information about field "' + fieldId + '"');
  59. }
  60. /**
  61. * Returns format specific editor data.
  62. *
  63. * Do not modify the returned object unless you really know what you're doing.
  64. * No external code should need access to this, and it may likely change in the
  65. * future.
  66. *
  67. * @param formatId
  68. * The id of a format to get data for.
  69. *
  70. * @returns
  71. * A reference to an object with the following properties:
  72. * - editor: A string with the id of the editor attached to the format.
  73. * 'none' if no editor profile is associated with the format.
  74. * - enabled: True if the editor is active.
  75. * - toggle: True if the editor can be toggled on/off by the user.
  76. * - editorSettings: A structure holding editor settings for this format.
  77. * - getPluginInfo: Shortcut method to get plugin config for the this format.
  78. */
  79. function getFormatInfo(formatId) {
  80. if (_formatInfoStorage[formatId]) {
  81. return _formatInfoStorage[formatId];
  82. }
  83. return {
  84. editor: 'none',
  85. getPluginInfo: function () {
  86. return getPluginInfo(formatId);
  87. }
  88. };
  89. }
  90. /**
  91. * Returns plugin configuration for a specific format, or the global values.
  92. *
  93. * @param formatId
  94. * The id of a format to get data for, or 'global' to get data common to all
  95. * formats and editors. Use 'global:editorname' to limit it to one editor.
  96. *
  97. * @return
  98. * The returned object will have the sub-objects 'drupal' and 'native', each
  99. * with properties matching names of plugins.
  100. * Global data for Drupal (cross-editor) plugins will have the following keys:
  101. * - title: A human readable name for the button.
  102. * - internalName: The unique name of a native plugin wrapper, used in editor
  103. * profiles and when registering the plugin with the editor API to avoid
  104. * possible id conflicts with native plugins.
  105. * - css: A stylesheet needed by the plugin.
  106. * - icon path: The path where button icons are stored.
  107. * - path: The path to the plugin's main folder.
  108. * - buttons: An object with button data, keyed by name with the properties:
  109. * - description: A human readable string describing the button's function.
  110. * - title: A human readable string with the name of the button.
  111. * - icon: An object with one or more of the following properties:
  112. * - src: An absolute (begins with '/') or relative path to the icon.
  113. * - path: An absolute path to a folder containing the button.
  114. *
  115. * When formatId matched a format with an assigned editor, values for plugins
  116. * match the return value of the editor integration's [proxy] plugin settings
  117. * callbacks.
  118. *
  119. * @see Drupal.wysiwyg.utilities.getPluginInfo()
  120. * @see Drupal.wyswiyg.utilities.extractButtonSettings()
  121. */
  122. function getPluginInfo(formatId) {
  123. var match, editor;
  124. if ((match = formatId.match(/^global:(\w+)$/))) {
  125. formatId = 'global';
  126. editor = match[1];
  127. }
  128. if (!_pluginInfoStorage[formatId]) {
  129. return {};
  130. }
  131. if (formatId === 'global' && typeof editor !== 'undefined') {
  132. return { 'drupal': _pluginInfoStorage.global.drupal, 'native': (_pluginInfoStorage.global['native'][editor]) };
  133. }
  134. return _pluginInfoStorage[formatId];
  135. }
  136. /**
  137. * Attach editors to input formats and target elements (f.e. textareas).
  138. *
  139. * This behavior searches for input format selectors and formatting guidelines
  140. * that have been preprocessed by Wysiwyg API. All CSS classes of those elements
  141. * with the prefix 'wysiwyg-' are parsed into input format parameters, defining
  142. * the input format, configured editor, target element id, and variable other
  143. * properties, which are passed to the attach/detach hooks of the corresponding
  144. * editor.
  145. *
  146. * Furthermore, an "enable/disable rich-text" toggle link is added after the
  147. * target element to allow users to alter its contents in plain text.
  148. *
  149. * This is executed once, while editor attach/detach hooks can be invoked
  150. * multiple times.
  151. *
  152. * @param context
  153. * A DOM element, supplied by Drupal.attachBehaviors().
  154. */
  155. Drupal.behaviors.attachWysiwyg = {
  156. attach: function (context, settings) {
  157. // This breaks in Konqueror. Prevent it from running.
  158. if (/KDE/.test(navigator.vendor)) {
  159. return;
  160. }
  161. var wysiwygs = $('.wysiwyg:input', context);
  162. if (!wysiwygs.length) {
  163. // No new fields, nothing to update.
  164. return;
  165. }
  166. updateInternalState(settings.wysiwyg, context);
  167. wysiwygs.once('wysiwyg', function () {
  168. // Skip processing if the element is unknown or does not exist in this
  169. // document. Can happen after a form was removed but Drupal.ajax keeps a
  170. // lingering reference to the form and calls Drupal.attachBehaviors().
  171. var $this = $('#' + this.id, document);
  172. if (!$this.length) {
  173. return;
  174. }
  175. // Directly attach this editor, if the input format is enabled or there is
  176. // only one input format at all.
  177. Drupal.wysiwygAttach(context, this.id);
  178. })
  179. .closest('form').submit(function (event) {
  180. // Detach any editor when the containing form is submitted.
  181. // Do not detach if the event was cancelled.
  182. if (event.isDefaultPrevented()) {
  183. return;
  184. }
  185. var form = this;
  186. $('.wysiwyg:input', this).each(function () {
  187. Drupal.wysiwygDetach(form, this.id, 'serialize');
  188. });
  189. });
  190. },
  191. detach: function (context, settings, trigger) {
  192. var wysiwygs;
  193. // The 'serialize' trigger indicates that we should simply update the
  194. // underlying element with the new text, without destroying the editor.
  195. if (trigger == 'serialize') {
  196. // Removing the wysiwyg-processed class guarantees that the editor will
  197. // be reattached. Only do this if we're planning to destroy the editor.
  198. wysiwygs = $('.wysiwyg-processed:input', context);
  199. }
  200. else {
  201. wysiwygs = $('.wysiwyg:input', context).removeOnce('wysiwyg');
  202. }
  203. wysiwygs.each(function () {
  204. Drupal.wysiwygDetach(context, this.id, trigger);
  205. });
  206. }
  207. };
  208. /**
  209. * Attach an editor to a target element.
  210. *
  211. * Detaches any existing instance for the field before attaching a new instance
  212. * based on the current state of the field. Editor settings and state
  213. * information is fetched based on the element id and get cloned first, so they
  214. * cannot be overridden. After attaching the editor, the toggle link is shown
  215. * again, except in case we are attaching no editor.
  216. *
  217. * Also attaches editors to the summary field, if available.
  218. *
  219. * @param context
  220. * A DOM element, supplied by Drupal.attachBehaviors().
  221. * @param fieldId
  222. * The id of an element to attach an editor to.
  223. */
  224. Drupal.wysiwygAttach = function(context, fieldId) {
  225. var fieldInfo = getFieldInfo(fieldId),
  226. formatInfo = fieldInfo.getFormatInfo(),
  227. editorSettings = formatInfo.editorSettings,
  228. editor = formatInfo.editor,
  229. previousStatus = false,
  230. previousEditor = 'none',
  231. doSummary = (fieldInfo.summary && (!fieldInfo.formats[fieldInfo.activeFormat] || !fieldInfo.formats[fieldInfo.activeFormat].skip_summary));
  232. if (_internalInstances[fieldId]) {
  233. previousStatus = _internalInstances[fieldId]['status'];
  234. previousEditor = _internalInstances[fieldId].editor;
  235. }
  236. // Detach any previous editor instance if enabled, else remove the grippie.
  237. detachFromField(context, {'editor': previousEditor, 'status': previousStatus, 'field': fieldId, 'resizable': fieldInfo.resizable}, 'unload');
  238. if (doSummary) {
  239. // Summary instances may have a different status if no real editor was
  240. // attached yet because the field was hidden.
  241. if (_internalInstances[fieldInfo.summary]) {
  242. previousStatus = _internalInstances[fieldInfo.summary]['status'];
  243. }
  244. detachFromField(context, {'editor': previousEditor, 'status': previousStatus, 'field': fieldInfo.summary, 'resizable': fieldInfo.resizable}, 'unload');
  245. }
  246. // Store this field id, so (external) plugins can use it.
  247. // @todo Wrong point in time. Probably can only supported by editors which
  248. // support an onFocus() or similar event.
  249. Drupal.wysiwyg.activeId = fieldId;
  250. // Attach or update toggle link, if enabled.
  251. Drupal.wysiwygAttachToggleLink(context, fieldId);
  252. // Attach to main field.
  253. attachToField(context, {'status': fieldInfo.enabled, 'editor': editor, 'field': fieldId, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings);
  254. // Attach to summary field.
  255. if (doSummary) {
  256. // If the summary wrapper is visible, attach immediately.
  257. if ($('#' + fieldInfo.summary).parents('.text-summary-wrapper').is(':visible')) {
  258. attachToField(context, {'status': fieldInfo.enabled, 'editor': editor, 'field': fieldInfo.summary, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings);
  259. }
  260. else {
  261. // Attach an instance of the 'none' editor to have consistency while the
  262. // summary is hidden, then switch to a real editor instance when shown.
  263. attachToField(context, {'status': false, 'editor': editor, 'field': fieldInfo.summary, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings);
  264. // Unbind any existing click handler to avoid double toggling.
  265. $('#' + fieldId).parents('.text-format-wrapper').find('.link-edit-summary').unbind('click.wysiwyg').bind('click.wysiwyg', function () {
  266. detachFromField(context, {'status': false, 'editor': editor, 'field': fieldInfo.summary, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings);
  267. attachToField(context, {'status': fieldInfo.enabled, 'editor': editor, 'field': fieldInfo.summary, 'format': fieldInfo.activeFormat, 'resizable': fieldInfo.resizable}, editorSettings);
  268. $(this).unbind('click.wysiwyg');
  269. });
  270. }
  271. }
  272. };
  273. /**
  274. * The public API exposed for an editor-enabled field.
  275. *
  276. * Properties should be treated as read-only state and changing them will not
  277. * have any effect on how the instance behaves.
  278. *
  279. * Note: The attach() and detach() methods are not part of the public API and
  280. * should not be called directly to avoid synchronization issues.
  281. * Use Drupal.wysiwygAttach() and Drupal.wysiwygDetach() to activate or
  282. * deactivate editor instances. Externally switching the active editor is not
  283. * supported other than changing the format using the select element.
  284. */
  285. function WysiwygInstance(internalInstance) {
  286. // The id of the field the instance manipulates.
  287. this.field = internalInstance.field;
  288. // The internal name of the attached editor.
  289. this.editor = internalInstance.editor;
  290. // If the editor is currently enabled or not.
  291. this['status'] = internalInstance['status'];
  292. // The id of the text format the editor is attached to.
  293. this.format = internalInstance.format;
  294. // If the field is resizable without an editor attached.
  295. this.resizable = internalInstance.resizable;
  296. // Methods below here redirect to the 'none' editor which handles plain text
  297. // fields when the editor is disabled.
  298. /**
  299. * Insert content at the cursor position.
  300. *
  301. * @param content
  302. * An HTML markup string.
  303. */
  304. this.insert = function (content) {
  305. return internalInstance['status'] ? internalInstance.insert(content) : Drupal.wysiwyg.editor.instance.none.insert.call(internalInstance, content);
  306. }
  307. /**
  308. * Get all content from the editor.
  309. *
  310. * @return
  311. * An HTML markup string.
  312. */
  313. this.getContent = function () {
  314. return internalInstance['status'] ? internalInstance.getContent() : Drupal.wysiwyg.editor.instance.none.getContent.call(internalInstance);
  315. }
  316. /**
  317. * Replace all content in the editor.
  318. *
  319. * @param content
  320. * An HTML markup string.
  321. */
  322. this.setContent = function (content) {
  323. return internalInstance['status'] ? internalInstance.setContent(content) : Drupal.wysiwyg.editor.instance.none.setContent.call(internalInstance, content);
  324. }
  325. /**
  326. * Check if the editor is in fullscreen mode.
  327. *
  328. * @return bool
  329. * True if the editor is considered to be in fullscreen mode.
  330. */
  331. this.isFullscreen = function (content) {
  332. return internalInstance['status'] && $.isFunction(internalInstance.isFullscreen) ? internalInstance.isFullscreen() : false;
  333. }
  334. // @todo The methods below only work for TinyMCE, deprecate?
  335. /**
  336. * Open a native editor dialog.
  337. *
  338. * Use of this method i not recomended due to limited editor support.
  339. *
  340. * @param dialog
  341. * An object with dialog settings. Keys used:
  342. * - url: The url of the dialog template.
  343. * - width: Width in pixels.
  344. * - height: Height in pixels.
  345. */
  346. this.openDialog = function (dialog, params) {
  347. if ($.isFunction(internalInstance.openDialog)) {
  348. return internalInstance.openDialog(dialog, params)
  349. }
  350. }
  351. /**
  352. * Close an opened dialog.
  353. *
  354. * @param dialog
  355. * Same options as for opening a dialog.
  356. */
  357. this.closeDialog = function (dialog) {
  358. if ($.isFunction(internalInstance.closeDialog)) {
  359. return internalInstance.closeDialog(dialog)
  360. }
  361. }
  362. }
  363. /**
  364. * The private base for editor instances.
  365. *
  366. * An instance of this object is used as the context for all calls into the
  367. * editor instances (including attach() and detach() when only one instance is
  368. * asked to detach).
  369. *
  370. * Anything added to Drupal.wysiwyg.editor.instance[editorName] is cloned into
  371. * an instance of this function.
  372. *
  373. * Editor state parameters are cloned into the instance after that.
  374. */
  375. function WysiwygInternalInstance(params) {
  376. $.extend(true, this, Drupal.wysiwyg.editor.instance[params.editor]);
  377. $.extend(true, this, params);
  378. this.pluginInfo = {
  379. 'global': getPluginInfo('global:' + params.editor),
  380. 'instances': getPluginInfo(params.format)
  381. };
  382. // Keep track of the public face to keep it synced.
  383. this.publicInstance = new WysiwygInstance(this);
  384. }
  385. /**
  386. * Updates internal settings and state caches with new information.
  387. *
  388. * Attaches selection change handler to format selector to track state changes.
  389. *
  390. * @param settings
  391. * A structure like Drupal.settigns.wysiwyg.
  392. * @param context
  393. * The context given from Drupal.attachBehaviors().
  394. */
  395. function updateInternalState(settings, context) {
  396. var pluginData = settings.plugins;
  397. for (var plugin in pluginData.drupal) {
  398. if (!(plugin in _pluginInfoStorage.global.drupal)) {
  399. _pluginInfoStorage.global.drupal[plugin] = pluginData.drupal[plugin];
  400. }
  401. }
  402. // To make sure we don't rely on Drupal.settings, uncomment these for testing.
  403. //pluginData.drupal = {};
  404. for (var editorId in pluginData['native']) {
  405. for (var plugin in pluginData['native'][editorId]) {
  406. _pluginInfoStorage.global['native'][editorId] = (_pluginInfoStorage.global['native'][editorId] || {});
  407. if (!(plugin in _pluginInfoStorage.global['native'][editorId])) {
  408. _pluginInfoStorage.global['native'][editorId][plugin] = pluginData['native'][editorId][plugin];
  409. }
  410. }
  411. }
  412. //pluginData['native'] = {};
  413. for (var fmatId in pluginData) {
  414. if (fmatId.substr(0, 6) !== 'format') {
  415. continue;
  416. }
  417. _pluginInfoStorage[fmatId] = (_pluginInfoStorage[fmatId] || {'drupal': {}, 'native': {}});
  418. for (var plugin in pluginData[fmatId].drupal) {
  419. if (!(plugin in _pluginInfoStorage[fmatId].drupal)) {
  420. _pluginInfoStorage[fmatId].drupal[plugin] = pluginData[fmatId].drupal[plugin];
  421. }
  422. }
  423. for (var plugin in pluginData[fmatId]['native']) {
  424. if (!(plugin in _pluginInfoStorage[fmatId]['native'])) {
  425. _pluginInfoStorage[fmatId]['native'][plugin] = pluginData[fmatId]['native'][plugin];
  426. }
  427. }
  428. delete pluginData[fmatId];
  429. }
  430. // Build the cache of format/profile settings.
  431. for (var editor in settings.configs) {
  432. if (!settings.configs.hasOwnProperty(editor)) {
  433. continue;
  434. }
  435. for (var format in settings.configs[editor]) {
  436. if (_formatInfoStorage[format] || !settings.configs[editor].hasOwnProperty(format)) {
  437. continue;
  438. }
  439. _formatInfoStorage[format] = {
  440. editor: editor,
  441. toggle: true, // Overridden by triggers.
  442. editorSettings: processObjectTypes(settings.configs[editor][format])
  443. };
  444. }
  445. // Initialize editor libraries if not already done.
  446. if (!_initializedLibraries[editor] && typeof Drupal.wysiwyg.editor.init[editor] === 'function') {
  447. // Clone, so original settings are not overwritten.
  448. Drupal.wysiwyg.editor.init[editor](jQuery.extend(true, {}, settings.configs[editor]), getPluginInfo('global:' + editor));
  449. _initializedLibraries[editor] = true;
  450. }
  451. // Update libraries, in case new plugins etc have not been initialized yet.
  452. else if (typeof Drupal.wysiwyg.editor.update[editor] === 'function') {
  453. Drupal.wysiwyg.editor.update[editor](jQuery.extend(true, {}, settings.configs[editor]), getPluginInfo('global:' + editor));
  454. }
  455. }
  456. //settings.configs = {};
  457. for (var triggerId in settings.triggers) {
  458. var trigger = settings.triggers[triggerId];
  459. var fieldId = trigger.field;
  460. var baseFieldId = (fieldId.indexOf('--') === -1 ? fieldId : fieldId.substr(0, fieldId.indexOf('--')));
  461. var fieldInfo = null;
  462. if (!(fieldInfo = _fieldInfoStorage[baseFieldId])) {
  463. fieldInfo = _fieldInfoStorage[baseFieldId] = {
  464. formats: {},
  465. select: trigger.select,
  466. resizable: trigger.resizable,
  467. summary: trigger.summary,
  468. getFormatInfo: function () {
  469. if (this.select) {
  470. this.activeFormat = 'format' + $('#' + this.select + ':input').val();
  471. }
  472. return getFormatInfo(this.activeFormat);
  473. }
  474. // 'activeFormat' and 'enabled' added below.
  475. }
  476. };
  477. for (var format in trigger) {
  478. if (format.indexOf('format') != 0 || fieldInfo.formats[format]) {
  479. continue;
  480. }
  481. fieldInfo.formats[format] = {
  482. 'enabled': trigger[format].status
  483. }
  484. if (!_formatInfoStorage[format]) {
  485. _formatInfoStorage[format] = {
  486. editor: trigger[format].editor,
  487. editorSettings: {},
  488. getPluginInfo: function () {
  489. return getPluginInfo(formatId);
  490. }
  491. };
  492. }
  493. // Always update these since they are stored as state.
  494. _formatInfoStorage[format].toggle = trigger[format].toggle;
  495. if (trigger[format].skip_summary) {
  496. fieldInfo.formats[format].skip_summary = true;
  497. }
  498. }
  499. var $selectbox = null;
  500. // Always update these since Drupal generates new ids on AJAX calls.
  501. fieldInfo.summary = trigger.summary;
  502. if (trigger.select) {
  503. _selectToField[trigger.select.replace(/--\d+$/,'')] = trigger.field;
  504. fieldInfo.select = trigger.select;
  505. // Specifically target input elements in case selectbox wrappers have
  506. // hidden the real element and cloned its attributes.
  507. $selectbox = $('#' + trigger.select + ':input', context).filter('select');
  508. // Attach onChange handlers to input format selector elements.
  509. $selectbox.unbind('change.wysiwyg').bind('change.wysiwyg', formatChanged);
  510. }
  511. // Always update the active format to ensure the righ profile is used if a
  512. // field was removed and gets re-added and the instance was left behind.
  513. fieldInfo.activeFormat = 'format' + ($selectbox ? $selectbox.val() : trigger.activeFormat);
  514. fieldInfo.enabled = fieldInfo.formats[fieldInfo.activeFormat] && fieldInfo.formats[fieldInfo.activeFormat].enabled;
  515. }
  516. //settings.triggers = {};
  517. }
  518. /**
  519. * Helper to prepare and attach an editor for a single field.
  520. *
  521. * Creates the 'instance' object under Drupal.wysiwyg.instances[fieldId].
  522. *
  523. * @param context
  524. * A DOM element, supplied by Drupal.attachBehaviors().
  525. * @param params
  526. * An object containing state information for the editor with the following
  527. * properties:
  528. * - 'status': A boolean stating whether the editor is currently active. If
  529. * false, the default textarea behaviors will be attached instead (aka the
  530. * 'none' editor implementation).
  531. * - 'editor': The internal name of the editor to attach when active.
  532. * - 'field': The field id to use as an output target for the editor.
  533. * - 'format': The name of the active text format (prefixed 'format').
  534. * - 'resizable': A boolean indicating whether the original textarea was
  535. * resizable.
  536. * Note: This parameter is passed directly to the editor implementation and
  537. * needs to have been reconstructed or cloned before attaching.
  538. * @param editorSettings
  539. * An object containing all the settings the editor needs for this field.
  540. * Settings are automatically cloned to prevent editors from modifying them.
  541. */
  542. function attachToField(context, params, editorSettings) {
  543. // If the editor isn't active, attach default behaviors instead.
  544. var editor = (params.status ? params.editor : 'none');
  545. // Settings are deep merged (cloned) to prevent editor implementations from
  546. // permanently modifying them while attaching.
  547. var clonedSettings = jQuery.extend(true, {}, editorSettings);
  548. // (Re-)initialize field instance.
  549. var internalInstance = new WysiwygInternalInstance(params);
  550. _internalInstances[params.field] = internalInstance;
  551. Drupal.wysiwyg.instances[params.field] = internalInstance.publicInstance;
  552. if ($.isFunction(Drupal.wysiwyg.editor.attach[editor])) {
  553. Drupal.wysiwyg.editor.attach[editor].call(internalInstance, context, params, params.status ? clonedSettings : {});
  554. }
  555. }
  556. /**
  557. * Detach all editors from a target element.
  558. *
  559. * Ensures Drupal's original textfield resize functionality is restored if
  560. * enabled and the triggering reason is 'unload'.
  561. *
  562. * Also detaches editors from the summary field, if available.
  563. *
  564. * @param context
  565. * A DOM element, supplied by Drupal.detachBehaviors().
  566. * @param fieldId
  567. * The id of an element to attach an editor to.
  568. * @param trigger
  569. * A string describing what is causing the editor to be detached.
  570. * - 'serialize': The editor normally just syncs its contents to the original
  571. * textarea for value serialization before an AJAX request.
  572. * - 'unload': The editor is to be removed completely and the original
  573. * textarea restored.
  574. *
  575. * @see Drupal.detachBehaviors()
  576. */
  577. Drupal.wysiwygDetach = function (context, fieldId, trigger) {
  578. var fieldInfo = getFieldInfo(fieldId),
  579. editor = fieldInfo.getFormatInfo().editor,
  580. trigger = trigger || 'unload',
  581. previousStatus = (_internalInstances[fieldId] && _internalInstances[fieldId]['status']);
  582. // Detach from main field.
  583. detachFromField(context, {'editor': editor, 'status': previousStatus, 'field': fieldId, 'resizable': fieldInfo.resizable}, trigger);
  584. if (trigger == 'unload') {
  585. // Attach the resize behavior by forcing status to false. Other values are
  586. // intentionally kept the same to show which editor is normally attached.
  587. attachToField(context, {'editor': editor, 'status': false, 'format': fieldInfo.activeFormat, 'field': fieldId, 'resizable': fieldInfo.resizable});
  588. Drupal.wysiwygAttachToggleLink(context, fieldId);
  589. }
  590. // Detach from summary field.
  591. if (fieldInfo.summary && _internalInstances[fieldInfo.summary]) {
  592. // The "Edit summary" click handler could re-enable the editor by mistake.
  593. $('#' + fieldId).parents('.text-format-wrapper').find('.link-edit-summary').unbind('click.wysiwyg');
  594. // Summary instances may have a different status if no real editor was
  595. // attached yet because the field was hidden.
  596. if (_internalInstances[fieldInfo.summary]) {
  597. previousStatus = _internalInstances[fieldInfo.summary]['status'];
  598. }
  599. detachFromField(context, {'editor': editor, 'status': previousStatus, 'field': fieldInfo.summary, 'resizable': fieldInfo.resizable}, trigger);
  600. if (trigger == 'unload') {
  601. attachToField(context, {'editor': editor, 'status': false, 'format': fieldInfo.activeFormat, 'field': fieldInfo.summary, 'resizable': fieldInfo.resizable});
  602. }
  603. }
  604. };
  605. /**
  606. * Helper to detach and clean up after an editor for a single field.
  607. *
  608. * Removes the 'instance' object under Drupal.wysiwyg.instances[fieldId].
  609. *
  610. * @param context
  611. * A DOM element, supplied by Drupal.detachBehaviors().
  612. * @param params
  613. * An object containing state information for the editor with the following
  614. * properties:
  615. * - 'status': A boolean stating whether the editor is currently active. If
  616. * false, the default textarea behaviors will be attached instead (aka the
  617. * 'none' editor implementation).
  618. * - 'editor': The internal name of the editor to attach when active.
  619. * - 'field': The field id to use as an output target for the editor.
  620. * - 'format': The name of the active text format (prefixed 'format').
  621. * - 'resizable': A boolean indicating whether the original textarea was
  622. * resizable.
  623. * Note: This parameter is passed directly to the editor implementation and
  624. * needs to have been reconstructed or cloned before detaching.
  625. * @param trigger
  626. * A string describing what is causing the editor to be detached.
  627. * - 'serialize': The editor normally just syncs its contents to the original
  628. * textarea for value serialization before an AJAX request.
  629. * - 'unload': The editor is to be removed completely and the original
  630. * textarea restored.
  631. *
  632. * @see Drupal.wysiwygDetach()
  633. */
  634. function detachFromField(context, params, trigger) {
  635. var editor = (params.status ? params.editor : 'none');
  636. if (jQuery.isFunction(Drupal.wysiwyg.editor.detach[editor])) {
  637. Drupal.wysiwyg.editor.detach[editor].call(_internalInstances[params.field], context, params, trigger);
  638. }
  639. if (trigger == 'unload') {
  640. delete Drupal.wysiwyg.instances[params.field];
  641. delete _internalInstances[params.field];
  642. }
  643. }
  644. /**
  645. * Append or update an editor toggle link to a target element.
  646. *
  647. * @param context
  648. * A DOM element, supplied by Drupal.attachBehaviors().
  649. * @param fieldId
  650. * The id of an element to attach an editor to.
  651. */
  652. Drupal.wysiwygAttachToggleLink = function(context, fieldId) {
  653. var fieldInfo = getFieldInfo(fieldId),
  654. editor = fieldInfo.getFormatInfo().editor;
  655. if (!fieldInfo.getFormatInfo().toggle) {
  656. // Otherwise, ensure that toggle link is hidden.
  657. $('#wysiwyg-toggle-' + fieldId).hide();
  658. return;
  659. }
  660. if (!$('#wysiwyg-toggle-' + fieldId, context).length) {
  661. var text = document.createTextNode(fieldInfo.enabled ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable),
  662. a = document.createElement('a'),
  663. div = document.createElement('div');
  664. $(a).attr({ id: 'wysiwyg-toggle-' + fieldId, href: 'javascript:void(0);' }).append(text);
  665. $(div).addClass('wysiwyg-toggle-wrapper').append(a);
  666. if ($('#' + fieldInfo.select).closest('.fieldset-wrapper').prepend(div).length == 0) {
  667. // Fall back to inserting the link right after the field.
  668. $('#' + fieldId).after(div);
  669. };
  670. }
  671. $('#wysiwyg-toggle-' + fieldId, context)
  672. .html(fieldInfo.enabled ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable).show()
  673. .unbind('click.wysiwyg')
  674. .bind('click.wysiwyg', { 'fieldId': fieldId, 'context': context }, Drupal.wysiwyg.toggleWysiwyg);
  675. // Hide toggle link in case no editor is attached.
  676. if (editor == 'none') {
  677. $('#wysiwyg-toggle-' + fieldId).hide();
  678. }
  679. };
  680. /**
  681. * Callback for the Enable/Disable rich editor link.
  682. */
  683. Drupal.wysiwyg.toggleWysiwyg = function (event) {
  684. var context = event.data.context,
  685. fieldId = event.data.fieldId,
  686. fieldInfo = getFieldInfo(fieldId);
  687. // Toggling the enabled state indirectly toggles use of the 'none' editor.
  688. if (fieldInfo.enabled) {
  689. fieldInfo.enabled = false;
  690. Drupal.wysiwygDetach(context, fieldId, 'unload');
  691. }
  692. else {
  693. fieldInfo.enabled = true;
  694. Drupal.wysiwygAttach(context, fieldId);
  695. }
  696. fieldInfo.formats[fieldInfo.activeFormat].enabled = fieldInfo.enabled;
  697. }
  698. /**
  699. * Event handler for when the selected format is changed.
  700. */
  701. function formatChanged(event) {
  702. var fieldId = _selectToField[this.id.replace(/--\d+$/,'')];
  703. var context = $(this).closest('form');
  704. // Field state is fetched by reference.
  705. var currentField = getFieldInfo(fieldId);
  706. // Save the state of the current format.
  707. if (currentField.formats[currentField.activeFormat]) {
  708. currentField.formats[currentField.activeFormat].enabled = currentField.enabled;
  709. }
  710. // Switch format/profile.
  711. currentField.activeFormat = 'format' + this.value;
  712. // Load the state from the new format.
  713. if (currentField.formats[currentField.activeFormat]) {
  714. currentField.enabled = currentField.formats[currentField.activeFormat].enabled;
  715. }
  716. else {
  717. currentField.enabled = false;
  718. }
  719. // Attaching again will use the changed field state.
  720. Drupal.wysiwygAttach(context, fieldId);
  721. }
  722. /**
  723. * Convert JSON type placeholders into the actual types.
  724. *
  725. * Recognizes function references (callbacks) and Regular Expressions.
  726. *
  727. * To create a callback, pass in an object with the following properties:
  728. * - 'drupalWysiwygType': Must be set to 'callback'.
  729. * - 'name': A string with the name of the callback, use
  730. * 'object.subobject.method' syntax for methods in nested objects.
  731. * - 'context': An optional string with the name of an object for overriding
  732. * 'this' inside the function. Use 'object.subobject' syntax for nested
  733. * objects. Defaults to the window object.
  734. *
  735. * To create a RegExp, pass in an object with the following properties:
  736. * - 'drupalWysiwygType: Must be set to 'regexp'.
  737. * - 'regexp': The Regular Expression as a string, without / wrappers.
  738. * - 'modifiers': An optional string with modifiers to set on the RegExp object.
  739. *
  740. * @param json
  741. * The json argument with all recognized type placeholders replaced by the real
  742. * types.
  743. *
  744. * @return The JSON object with placeholder types replaced.
  745. */
  746. function processObjectTypes(json) {
  747. var out = null;
  748. if (typeof json != 'object') {
  749. return json;
  750. }
  751. out = new json.constructor();
  752. if (json.drupalWysiwygType) {
  753. switch (json.drupalWysiwygType) {
  754. case 'callback':
  755. out = callbackWrapper(json.name, json.context);
  756. break;
  757. case 'regexp':
  758. out = new RegExp(json.regexp, json.modifiers ? json.modifiers : undefined);
  759. break;
  760. default:
  761. out.drupalWysiwygType = json.drupalWysiwygType;
  762. }
  763. }
  764. else {
  765. for (var i in json) {
  766. if (json.hasOwnProperty(i) && json[i] && typeof json[i] == 'object') {
  767. out[i] = processObjectTypes(json[i]);
  768. }
  769. else {
  770. out[i] = json[i];
  771. }
  772. }
  773. }
  774. return out;
  775. }
  776. /**
  777. * Convert function names into function references.
  778. *
  779. * @param name
  780. * The name of a function to use as callback. Use the 'object.subobject.method'
  781. * syntax for methods in nested objects.
  782. * @param context
  783. * An optional string with the name of an object for overriding 'this' inside
  784. * the function. Use 'object.subobject' syntax for nested objects. Defaults to
  785. * the window object.
  786. *
  787. * @return
  788. * A function which will call the named function or method in the proper
  789. * context, passing through arguments and return values.
  790. */
  791. function callbackWrapper(name, context) {
  792. var namespaces = name.split('.'), func = namespaces.pop(), obj = window;
  793. for (var i = 0; obj && i < namespaces.length; i++) {
  794. obj = obj[namespaces[i]];
  795. }
  796. if (!obj) {
  797. throw "Wysiwyg: Unable to locate callback " + namespaces.join('.') + "." + func + "()";
  798. }
  799. if (!context) {
  800. context = obj;
  801. }
  802. else if (typeof context == 'string'){
  803. namespaces = context.split('.');
  804. context = window;
  805. for (i = 0; context && i < namespaces.length; i++) {
  806. context = context[namespaces[i]];
  807. }
  808. if (!context) {
  809. throw "Wysiwyg: Unable to locate context object " + namespaces.join('.');
  810. }
  811. }
  812. if (typeof obj[func] != 'function') {
  813. throw "Wysiwyg: " + func + " is not a callback function";
  814. }
  815. return function () {
  816. return obj[func].apply(context, arguments);
  817. }
  818. }
  819. var oldBeforeSerialize = (Drupal.ajax ? Drupal.ajax.prototype.beforeSerialize : false);
  820. if (oldBeforeSerialize) {
  821. /**
  822. * Filter the ajax_html_ids list sent in AJAX requests.
  823. *
  824. * This overrides part of the form serializer to not include ids we know will
  825. * not collide because editors are removed before those ids are reused.
  826. *
  827. * This avoids hitting like max_input_vars, which defaults to 1000,
  828. * even with just a few active editor instances.
  829. */
  830. Drupal.ajax.prototype.beforeSerialize = function (element, options) {
  831. var ret = oldBeforeSerialize.call(this, element, options);
  832. var excludeSelectors = [];
  833. $.each(Drupal.wysiwyg.excludeIdSelectors, function () {
  834. if ($.isArray(this)) {
  835. excludeSelectors = excludeSelectors.concat(this);
  836. }
  837. });
  838. if (excludeSelectors.length > 0) {
  839. var ajaxHtmlIdsArray = options.data['ajax_html_ids[]'];
  840. if (!ajaxHtmlIdsArray || ajaxHtmlIdsArray.length === 0) {
  841. return ret;
  842. }
  843. options.data['ajax_html_ids[]'] = [];
  844. $('[id]:not(' + excludeSelectors.join(',') + ')').each(function () {
  845. if ($.inArray(this.id, ajaxHtmlIdsArray) !== -1) {
  846. options.data['ajax_html_ids[]'].push(this.id);
  847. }
  848. });
  849. }
  850. return ret;
  851. };
  852. }
  853. // Respond to CTools detach behaviors event.
  854. $(document).unbind('CToolsDetachBehaviors.wysiwyg').bind('CToolsDetachBehaviors.wysiwyg', function(event, context) {
  855. $('.wysiwyg:input', context).removeOnce('wysiwyg').each(function () {
  856. Drupal.wysiwygDetach(context, this.id, 'unload');
  857. // The 'none' instances are destroyed with the dialog.
  858. delete Drupal.wysiwyg.instances[this.id];
  859. delete _internalInstances[this.id];
  860. var baseFieldId = (this.id.indexOf('--') === -1 ? this.id : this.id.substr(0, this.id.indexOf('--')));
  861. delete _fieldInfoStorage[baseFieldId];
  862. });
  863. });
  864. })(jQuery);