(function($) { CKEDITOR.disableAutoInline = true; // Exclude every id starting with 'cke_' in ajax_html_ids during AJAX requests. Drupal.wysiwyg.excludeIdSelectors.wysiwyg_ckeditor = ['[id^="cke_"]']; // Keeps track of private instance data. var instanceMap; /** * Initialize the editor library. * * This method is called once the first time a library is needed. If new * WYSIWYG fields are added later, update() will be called instead. * * @param settings * An object containing editor settings for each input format. * @param pluginInfo * An object containing global plugin configuration. */ Drupal.wysiwyg.editor.init.ckeditor = function(settings, pluginInfo) { instanceMap = {}; // Manually set the cache-busting string to the same value as Drupal. if (Drupal.settings.wysiwyg.ckeditor.hasOwnProperty('timestamp')) { CKEDITOR.timestamp = Drupal.settings.wysiwyg.ckeditor.timestamp; } // Nothing to do here other than register new plugins etc. Drupal.wysiwyg.editor.update.ckeditor(settings, pluginInfo); }; /** * Update the editor library when new settings are available. * * This method is called instead of init() when at least one new WYSIWYG field * has been added to the document and the library has already been initialized. * * $param settings * An object containing editor settings for each input format. * $param pluginInfo * An object containing global plugin configuration. */ Drupal.wysiwyg.editor.update.ckeditor = function(settings, pluginInfo) { // Register native external plugins. // Array syntax required; 'native' is a predefined token in JavaScript. for (var pluginId in pluginInfo['native']) { if (pluginInfo['native'].hasOwnProperty(pluginId) && (!CKEDITOR.plugins.externals || !CKEDITOR.plugins.externals[pluginId])) { var plugin = pluginInfo['native'][pluginId]; CKEDITOR.plugins.addExternal(pluginId, plugin.path, plugin.fileName); } } // Build and register Drupal plugin wrappers. for (var pluginId in pluginInfo.drupal) { if (pluginInfo.drupal.hasOwnProperty(pluginId) && (!CKEDITOR.plugins.registered || !CKEDITOR.plugins.registered[pluginId])) { Drupal.wysiwyg.editor.instance.ckeditor.addPlugin(pluginId, pluginInfo.drupal[pluginId]); } } // Register Font styles (versions 3.2.1 and above). for (var format in settings) { if (settings[format].stylesSet && (!CKEDITOR.stylesSet || !CKEDITOR.stylesSet.registered[format])) { CKEDITOR.stylesSet.add(format, settings[format].stylesSet); } } }; /** * Attach this editor to a target element. */ Drupal.wysiwyg.editor.attach.ckeditor = function(context, params, settings) { // Apply editor instance settings. CKEDITOR.config.customConfig = ''; if (!settings.height) { settings.height = $('#' + params.field).height(); } // Handler for any change-related event. function changed(ev) { instanceMap[ev.editor.name].contentsChanged(); } settings.on = { // Versions 4.x has a change event, 3.x does not. change: function (ev) { changed(ev); }, contentDom: function (ev) { var version_parts = CKEDITOR.version.split('.'); if (version_parts[0] === '3' || (version_parts[0] === '4' && parseInt(version_parts[1], 10) < 2)) { ev.editor.on('key', function (ev) { // Do not capture modifiers. if (ev.data.ctrlKey || ev.data.metaKey) return; var keyCode = ev.data.keyCode; // Filter out movement keys and related. if (keyCode == 8 || keyCode == 13 || keyCode == 32 || (keyCode >= 46 && keyCode <= 90) || (keyCode >= 96 && keyCode <= 111) || (keyCode >= 186 && keyCode <= 222) || keyCode == 229) { changed(ev); } }); ev.editor.on('paste', changed); ev.editor.on('saveSnapshot', function (ev) { if (instanceMap[ev.editor.name].firstSaveSnapshot) { // The first save snapshot event is triggered when the editor is // focused and before anything has changed. instanceMap[ev.editor.name].firstSaveSnapshot = false; return; } changed(ev); }); } }, instanceReady: function(ev) { var editor = ev.editor; // Get a list of block, list and table tags from CKEditor's XHTML DTD. // @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Output_Formatting. var dtd = CKEDITOR.dtd; var tags = CKEDITOR.tools.extend({}, dtd.$block, dtd.$listItem, dtd.$tableContent); // Set source formatting rules for each listed tag except
. // Linebreaks can be inserted before or after opening and closing tags. if (settings.simple_source_formatting) { // Mimic FCKeditor output, by breaking lines between tags. for (var tag in tags) { if (tag == 'pre') { continue; } this.dataProcessor.writer.setRules(tag, { indent: true, breakBeforeOpen: true, breakAfterOpen: false, breakBeforeClose: false, breakAfterClose: true }); } } else { // CKEditor adds default formatting to
, so we want to remove that // here too. tags.br = 1; // No indents or linebreaks; for (var tag in tags) { if (tag == 'pre') { continue; } this.dataProcessor.writer.setRules(tag, { indent: false, breakBeforeOpen: false, breakAfterOpen: false, breakBeforeClose: false, breakAfterClose: false }); } } }, pluginsLoaded: function(ev) { var wysiwygInstance = instanceMap[this.name]; var enabledPlugins = wysiwygInstance.pluginInfo.instances.drupal; // Override the conversion methods to let Drupal plugins modify the data. var editor = ev.editor; if (editor.dataProcessor && enabledPlugins) { editor.dataProcessor.toHtml = CKEDITOR.tools.override(editor.dataProcessor.toHtml, function(originalToHtml) { // Convert raw data for display in WYSIWYG mode. return function(data, fixForBody) { for (var plugin in enabledPlugins) { if (typeof Drupal.wysiwyg.plugins[plugin].attach == 'function') { data = Drupal.wysiwyg.plugins[plugin].attach(data, wysiwygInstance.pluginInfo.global.drupal[plugin], editor.name); data = wysiwygInstance.prepareContent(data); } } return originalToHtml.call(this, data, fixForBody); }; }); editor.dataProcessor.toDataFormat = CKEDITOR.tools.override(editor.dataProcessor.toDataFormat, function(originalToDataFormat) { // Convert WYSIWYG mode content to raw data. return function(data, fixForBody) { data = originalToDataFormat.call(this, data, fixForBody); for (var plugin in enabledPlugins) { if (typeof Drupal.wysiwyg.plugins[plugin].detach == 'function') { data = Drupal.wysiwyg.plugins[plugin].detach(data, wysiwygInstance.pluginInfo.global.drupal[plugin], editor.name); } } return data; }; }); } }, selectionChange: function (event) { var wysiwygInstance = instanceMap[this.name]; var enabledPlugins = wysiwygInstance.pluginInfo.instances.drupal; for (var name in enabledPlugins) { var plugin = Drupal.wysiwyg.plugins[name]; if ($.isFunction(plugin.isNode)) { var node = event.data.selection.getSelectedElement(); var state = plugin.isNode(node ? node.$ : null) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF; event.editor.getCommand(name).setState(state); } } }, focus: function(ev) { Drupal.wysiwyg.activeId = ev.editor.name; }, afterCommandExec: function(ev) { // Fix Drupal toolbar obscuring editor toolbar in fullscreen mode. if (ev.data.name != 'maximize') { return; } if (ev.data.command.state == CKEDITOR.TRISTATE_ON) { Drupal.wysiwyg.utilities.onFullscreenEnter(); } else { Drupal.wysiwyg.utilities.onFullscreenExit(); } }, destroy: function (event) { // Free our reference to the private instance to not risk memory leaks. delete instanceMap[this.name]; } }; instanceMap[params.field] = this; // Attach editor. var editorInstance = CKEDITOR.replace(params.field, settings); }; /** * Detach a single editor instance. */ Drupal.wysiwyg.editor.detach.ckeditor = function (context, params, trigger) { var method = (trigger == 'serialize') ? 'updateElement' : 'destroy'; var instance = CKEDITOR.instances[params.field]; if (!instance) { return; } instance[method](); }; Drupal.wysiwyg.editor.instance.ckeditor = { // Flag indicating if the first save snapshot event has fired. firstSaveSnapshot: true, addPlugin: function (pluginName, pluginSettings) { CKEDITOR.plugins.add(pluginName, { // Wrap Drupal plugin in a proxy plugin. init: function(editor) { if (pluginSettings.css) { editor.on('mode', function(ev) { if (ev.editor.mode == 'wysiwyg') { // Inject CSS files directly into the editing area head tag. var iframe = $('#cke_contents_' + ev.editor.name + ' iframe, #' + ev.editor.id + '_contents iframe'); $('head', iframe.eq(0).contents()).append(''); } }); } if (typeof Drupal.wysiwyg.plugins[pluginName].invoke == 'function') { var pluginCommand = { exec: function (editor) { var data = { format: 'html', node: null, content: '' }; var selection = editor.getSelection(); if (selection) { data.node = selection.getSelectedElement(); if (data.node) { data.node = data.node.$; } if (selection.getType() == CKEDITOR.SELECTION_TEXT) { if (selection.getSelectedText) { data.content = selection.getSelectedText(); } else { // Pre v3.6.1. if (CKEDITOR.env.ie) { data.content = selection.getNative().createRange().text; } else { data.content = selection.getNative().toString(); } } } else if (data.node) { // content is supposed to contain the "outerHTML". data.content = data.node.parentNode.innerHTML; } } Drupal.wysiwyg.plugins[pluginName].invoke(data, pluginSettings, editor.name); } }; editor.addCommand(pluginName, pluginCommand); } editor.ui.addButton(pluginName, { label: pluginSettings.title, command: pluginName, icon: pluginSettings.icon }); // @todo Add button state handling. } }); }, prepareContent: function(content) { // @todo Don't know if we need this yet. return content; }, insert: function(content) { content = this.prepareContent(content); if (CKEDITOR.version.split('.')[0] === '3' && (CKEDITOR.env.webkit || CKEDITOR.env.chrome || CKEDITOR.env.opera || CKEDITOR.env.safari)) { // Works around a WebKit bug which removes wrapper elements. // @see https://drupal.org/node/1927968 var tmp = new CKEDITOR.dom.element('div'), children, skip = 0, item; tmp.setHtml(content); children = tmp.getChildren(); skip = 0; while (children.count() > skip) { item = children.getItem(skip); switch(item.type) { case 1: CKEDITOR.instances[this.field].insertElement(item); break; case 3: CKEDITOR.instances[this.field].insertText(item.getText()); skip++; break; case 8: CKEDITOR.instances[this.field].insertHtml(item.getOuterHtml()); skip++; break; default: skip++; } } } else { CKEDITOR.instances[this.field].insertHtml(content); } }, setContent: function (content) { CKEDITOR.instances[this.field].setData(content); }, getContent: function () { return CKEDITOR.instances[this.field].getData(); }, isFullscreen: function () { var cmd = CKEDITOR.instances[this.field].commands.maximize; return !!(cmd && cmd.state == CKEDITOR.TRISTATE_ON); } }; })(jQuery); ; (function ($) { /** * Wysiwyg plugin button implementation for NodeEmbed plugin. */ Drupal.wysiwyg.plugins.node_embed = { /** * Return whether the passed node belongs to this plugin. * * @param node * The currently focused DOM element in the editor content. */ isNode: function(node) { return ($(node).is('img.wysiwyg-node_embed')); }, /** * Execute the button. * * @param data * An object containing data about the current selection: * - format: 'html' when the passed data is HTML content, 'text' when the * passed data is plain-text content. * - node: When 'format' is 'html', the focused DOM element in the editor. * - content: The textual representation of the focused/selected editor * content. * @param settings * The plugin settings, as provided in the plugin's PHP include file. * @param instanceId * The ID of the current editor instance. */ invoke: function(data, settings, instanceId) { // Show the node selection Dialog. var iframeSrc = Drupal.settings.basePath + 'ckeditor-node-embed'; var dialogMarkup = '' + '' + '' + ''; var dialog = $(dialogMarkup).dialog({ autoOpen: false, height: 470, width: 650, modal: true, dialogClass: 'node_embed_dialog' }); $(dialog).bind('dialogclose', function(event, ui) { $(this).remove(); }); $('#buttonEmbedFromDialog').click(function(e) { // Set or updated whenever a node is selected. var node_id = window.currentActiveNid; if (node_id != null && node_id != "" ) { dialog.editor_content = '[[nid:' + node_id + ']]'; } if (window.currentActiveNid && window.currentActiveNid != "") { var edit_content = "[[nid:" + window.currentActiveNid + "]]"; if (data.format == 'html') { var content = ' '; } else { var content = edit_content; } // Write the content to the editor. if (typeof content != 'undefined') { Drupal.wysiwyg.instances[instanceId].insert(content); } } $(dialog).dialog('close'); }); $('#buttonCancelDialog').click(function(e) { $(dialog).dialog('close'); }); $(dialog).dialog('open'); } }; })(jQuery); ; (function ($) { // @todo Array syntax required; 'break' is a predefined token in JavaScript. Drupal.wysiwyg.plugins['break'] = { /** * Return whether the passed node belongs to this plugin. */ isNode: function(node) { return ($(node).is('img.wysiwyg-break')); }, /** * Execute the button. */ invoke: function(data, settings, instanceId) { if (data.format == 'html') { // Prevent duplicating a teaser break. if ($(data.node).is('img.wysiwyg-break')) { return; } var content = this._getPlaceholder(settings); } else { // Prevent duplicating a teaser break. // @todo data.content is the selection only; needs access to complete content. if (data.content.match(//)) { return; } var content = ''; } if (typeof content != 'undefined') { Drupal.wysiwyg.instances[instanceId].insert(content); } }, /** * Replace all tags with images. */ attach: function(content, settings, instanceId) { content = content.replace(//g, this._getPlaceholder(settings)); return content; }, /** * Replace images with tags in content upon detaching editor. */ detach: function(content, settings, instanceId) { var $content = $('' + content + ''); // No .outerHTML() in jQuery :( // #404532: document.createComment() required or IE will strip the comment. // #474908: IE 8 breaks when using jQuery methods to replace the elements. // @todo Add a generic implementation for all Drupal plugins for this. $.each($('img.wysiwyg-break', $content), function (i, elem) { elem.parentNode.insertBefore(document.createComment('break'), elem); elem.parentNode.removeChild(elem); }); return $content.html(); }, /** * Helper function to return a HTML placeholder. */ _getPlaceholder: function (settings) { return ''; } }; })(jQuery); ; (function ($) { Drupal.behaviors.textarea = { attach: function (context, settings) { $('.form-textarea-wrapper.resizable', context).once('textarea', function () { var staticOffset = null; var textarea = $(this).addClass('resizable-textarea').find('textarea'); var grippie = $('').mousedown(startDrag); grippie.insertAfter(textarea); function startDrag(e) { staticOffset = textarea.height() - e.pageY; textarea.css('opacity', 0.25); $(document).mousemove(performDrag).mouseup(endDrag); return false; } function performDrag(e) { textarea.height(Math.max(32, staticOffset + e.pageY) + 'px'); return false; } function endDrag(e) { $(document).unbind('mousemove', performDrag).unbind('mouseup', endDrag); textarea.css('opacity', 1); } }); } }; })(jQuery); ; (function ($) { /** * Auto-hide summary textarea if empty and show hide and unhide links. */ Drupal.behaviors.textSummary = { attach: function (context, settings) { $('.text-summary', context).once('text-summary', function () { var $widget = $(this).closest('div.field-type-text-with-summary'); var $summaries = $widget.find('div.text-summary-wrapper'); $summaries.once('text-summary-wrapper').each(function(index) { var $summary = $(this); var $summaryLabel = $summary.find('label').first(); var $full = $widget.find('.text-full').eq(index).closest('.form-item'); var $fullLabel = $full.find('label').first(); // Create a placeholder label when the field cardinality is // unlimited or greater than 1. if ($fullLabel.length == 0) { $fullLabel = $('').prependTo($full); } // Setup the edit/hide summary link. var $link = $('(' + Drupal.t('Hide summary') + ')'); var $a = $link.find('a'); var toggleClick = true; $link.bind('click', function (e) { if (toggleClick) { $summary.hide(); $a.html(Drupal.t('Edit summary')); $link.appendTo($fullLabel); } else { $summary.show(); $a.html(Drupal.t('Hide summary')); $link.appendTo($summaryLabel); } toggleClick = !toggleClick; return false; }).appendTo($summaryLabel); // If no summary is set, hide the summary field. if ($(this).find('.text-summary').val() == '') { $link.click(); } }); }); } }; })(jQuery); ; /** * @file: Popup dialog interfaces for the media project. * * Drupal.media.popups.mediaBrowser * Launches the media browser which allows users to pick a piece of media. * * Drupal.media.popups.mediaStyleSelector * Launches the style selection form where the user can choose what * format/style they want their media in. */ (function ($) { namespace('Drupal.media.popups'); /** * Media browser popup. Creates a media browser dialog. * * @param {function} * onSelect Callback for when dialog is closed, received (Array media, Object * extra); * @param {Object} * globalOptions Global options that will get passed upon initialization of * the browser. @see Drupal.media.popups.mediaBrowser.getDefaults(); * @param {Object} * pluginOptions Options for specific plugins. These are passed to the plugin * upon initialization. If a function is passed here as a callback, it is * obviously not passed, but is accessible to the plugin in * Drupal.settings.variables. Example: * pluginOptions = {library: {url_include_patterns:'/foo/bar'}}; * @param {Object} * widgetOptions Options controlling the appearance and behavior of the modal * dialog. @see Drupal.media.popups.mediaBrowser.getDefaults(); */ Drupal.media.popups.mediaBrowser = function (onSelect, globalOptions, pluginOptions, widgetOptions) { // Get default dialog options. var options = Drupal.media.popups.mediaBrowser.getDefaults(); // Add global, plugin and widget options. options.global = $.extend({}, options.global, globalOptions); options.plugins = pluginOptions; options.widget = $.extend({}, options.widget, widgetOptions); // Find the URL of the modal iFrame. var browserSrc = options.widget.src; if ($.isArray(browserSrc) && browserSrc.length) { browserSrc = browserSrc[browserSrc.length - 1]; } // Create an array of parameters to send along to the iFrame. var params = {}; // Add global field widget settings and plugin information. $.extend(params, options.global); params.plugins = options.plugins; // Append the list of parameters to the iFrame URL as query parameters. browserSrc += '&' + $.param(params); // Create an iFrame with the iFrame URL. var mediaIframe = Drupal.media.popups.getPopupIframe(browserSrc, 'mediaBrowser'); // Attach an onLoad event. mediaIframe.bind('load', options, options.widget.onLoad); // Create an array of Dialog options. var dialogOptions = options.dialog; // Setup the dialog buttons. var ok = Drupal.t('OK'); var notSelected = Drupal.t('You have not selected anything!'); dialogOptions.buttons[ok] = function () { // Find the current file selection. var selected = this.contentWindow.Drupal.media.browser.selectedMedia; // Alert the user if a selection has yet to be made. if (selected.length < 1) { alert(notSelected); return; } // Select the file. onSelect(selected); // Close the dialog. $(this).dialog('close'); }; // Create a jQuery UI dialog with the given options. var dialog = mediaIframe.dialog(dialogOptions); // Allow the dialog to react to re-sizing, scrolling, etc. Drupal.media.popups.sizeDialog(dialog); Drupal.media.popups.resizeDialog(dialog); Drupal.media.popups.scrollDialog(dialog); Drupal.media.popups.overlayDisplace(dialog.parents(".ui-dialog")); return mediaIframe; }; /** * Retrieves a list of default settings for the media browser. * * @return * An array of default settings. */ Drupal.media.popups.mediaBrowser.getDefaults = function () { return { global: { types: [], // Types to allow, defaults to all. enabledPlugins: [] // If provided, a list of plugins which should be enabled. }, widget: { // Settings for the actual iFrame which is launched. src: Drupal.settings.media.browserUrl, // Src of the media browser (if you want to totally override it) onLoad: Drupal.media.popups.mediaBrowser.mediaBrowserOnLoad // Onload function when iFrame loads. }, dialog: Drupal.media.popups.getDialogOptions() }; }; /** * Sets up the iFrame buttons. */ Drupal.media.popups.mediaBrowser.mediaBrowserOnLoad = function (e) { var options = e.data; // Ensure that the iFrame is defined. if (typeof this.contentWindow.Drupal.media === 'undefined' || typeof this.contentWindow.Drupal.media.browser === 'undefined') { return; } // Check if a selection has been made and press the 'ok' button. if (this.contentWindow.Drupal.media.browser.selectedMedia.length > 0) { var ok = Drupal.t('OK'); var ok_func = $(this).dialog('option', 'buttons')[ok]; ok_func.call(this); return; } }; /** * Finalizes the selection of a file. * * Alerts the user if a selection has yet to be made, triggers the file * selection and closes the modal dialog. */ Drupal.media.popups.mediaBrowser.finalizeSelection = function () { // Find the current file selection. var selected = this.contentWindow.Drupal.media.browser.selectedMedia; // Alert the user if a selection has yet to be made. if (selected.length < 1) { alert(notSelected); return; } // Select the file. onSelect(selected); // Close the dialog. $(this).dialog('close'); }; /** * Style chooser Popup. Creates a dialog for a user to choose a media style. * * @param mediaFile * The mediaFile you are requesting this formatting form for. * @todo: should this be fid? That's actually all we need now. * * @param Function * onSubmit Function to be called when the user chooses a media style. Takes * one parameter (Object formattedMedia). * * @param Object * options Options for the mediaStyleChooser dialog. */ Drupal.media.popups.mediaStyleSelector = function (mediaFile, onSelect, options) { var defaults = Drupal.media.popups.mediaStyleSelector.getDefaults(); // @todo: remove this awful hack :( if (typeof defaults.src === 'string' ) { defaults.src = defaults.src.replace('-media_id-', mediaFile.fid) + '&fields=' + encodeURIComponent(JSON.stringify(mediaFile.fields)); } else { var src = defaults.src.shift(); defaults.src.unshift(src); defaults.src = src.replace('-media_id-', mediaFile.fid) + '&fields=' + encodeURIComponent(JSON.stringify(mediaFile.fields)); } options = $.extend({}, defaults, options); // Create an iFrame with the iFrame URL. var mediaIframe = Drupal.media.popups.getPopupIframe(options.src, 'mediaStyleSelector'); // Attach an onLoad event. mediaIframe.bind('load', options, options.onLoad); // Create an array of Dialog options. var dialogOptions = Drupal.media.popups.getDialogOptions(); // Setup the dialog buttons. var ok = Drupal.t('OK'); var notSelected = Drupal.t('Very sorry, there was an unknown error embedding media.'); dialogOptions.buttons[ok] = function () { // Find the current file selection. var formattedMedia = this.contentWindow.Drupal.media.formatForm.getFormattedMedia(); formattedMedia.options = $.extend({}, mediaFile.attributes, formattedMedia.options); // Alert the user if a selection has yet to be made. if (!formattedMedia) { alert(notSelected); return; } // Select the file. onSelect(formattedMedia); // Close the dialog. $(this).dialog('close'); }; // Create a jQuery UI dialog with the given options. var dialog = mediaIframe.dialog(dialogOptions); // Allow the dialog to react to re-sizing, scrolling, etc. Drupal.media.popups.sizeDialog(dialog); Drupal.media.popups.resizeDialog(dialog); Drupal.media.popups.scrollDialog(dialog); Drupal.media.popups.overlayDisplace(dialog.parents(".ui-dialog")); return mediaIframe; }; Drupal.media.popups.mediaStyleSelector.mediaBrowserOnLoad = function (e) { }; Drupal.media.popups.mediaStyleSelector.getDefaults = function () { return { src: Drupal.settings.media.styleSelectorUrl, onLoad: Drupal.media.popups.mediaStyleSelector.mediaBrowserOnLoad }; }; /** * Style chooser Popup. Creates a dialog for a user to choose a media style. * * @param mediaFile * The mediaFile you are requesting this formatting form for. * @todo: should this be fid? That's actually all we need now. * * @param Function * onSubmit Function to be called when the user chooses a media style. Takes * one parameter (Object formattedMedia). * * @param Object * options Options for the mediaStyleChooser dialog. */ Drupal.media.popups.mediaFieldEditor = function (fid, onSelect, options) { var defaults = Drupal.media.popups.mediaFieldEditor.getDefaults(); // @todo: remove this awful hack :( defaults.src = defaults.src.replace('-media_id-', fid); options = $.extend({}, defaults, options); // Create an iFrame with the iFrame URL. var mediaIframe = Drupal.media.popups.getPopupIframe(options.src, 'mediaFieldEditor'); // Attach an onLoad event. mediaIframe.bind('load', options, options.onLoad); // Create an array of Dialog options. var dialogOptions = Drupal.media.popups.getDialogOptions(); // Setup the dialog buttons. var ok = Drupal.t('OK'); var notSelected = Drupal.t('Very sorry, there was an unknown error embedding media.'); dialogOptions.buttons[ok] = function () { // Find the current file selection. var formattedMedia = this.contentWindow.Drupal.media.formatForm.getFormattedMedia(); // Alert the user if a selection has yet to be made. if (!formattedMedia) { alert(notSelected); return; } // Select the file. onSelect(formattedMedia); // Close the dialog. $(this).dialog('close'); }; // Create a jQuery UI dialog with the given options. var dialog = mediaIframe.dialog(dialogOptions); // Allow the dialog to react to re-sizing, scrolling, etc. Drupal.media.popups.sizeDialog(dialog); Drupal.media.popups.resizeDialog(dialog); Drupal.media.popups.scrollDialog(dialog); Drupal.media.popups.overlayDisplace(dialog); return mediaIframe; }; Drupal.media.popups.mediaFieldEditor.mediaBrowserOnLoad = function (e) { }; Drupal.media.popups.mediaFieldEditor.getDefaults = function () { return { // @todo: do this for real src: '/media/-media_id-/edit?render=media-popup', onLoad: Drupal.media.popups.mediaFieldEditor.mediaBrowserOnLoad }; }; /** * Generic functions to both the media-browser and style selector. */ /** * Returns the commonly used options for the dialog. */ Drupal.media.popups.getDialogOptions = function () { return { title: Drupal.t('Media browser'), buttons: {}, dialogClass: Drupal.settings.media.dialogOptions.dialogclass, modal: Drupal.settings.media.dialogOptions.modal, draggable: Drupal.settings.media.dialogOptions.draggable, resizable: Drupal.settings.media.dialogOptions.resizable, minWidth: Drupal.settings.media.dialogOptions.minwidth, width: Drupal.settings.media.dialogOptions.width, height: Drupal.settings.media.dialogOptions.height, position: Drupal.settings.media.dialogOptions.position, overlay: { backgroundColor: Drupal.settings.media.dialogOptions.overlay.backgroundcolor, opacity: Drupal.settings.media.dialogOptions.overlay.opacity }, zIndex: Drupal.settings.media.dialogOptions.zindex, close: function (event, ui) { var elem = $(event.target); var id = elem.attr('id'); if(id == 'mediaStyleSelector') { $(this).dialog("destroy"); $('#mediaStyleSelector').remove(); } else { $(this).dialog("destroy"); $('#mediaBrowser').remove(); } } }; }; /** * Get an iframe to serve as the dialog's contents. Common to both plugins. */ Drupal.media.popups.getPopupIframe = function (src, id, options) { var defaults = {width: '100%', scrolling: 'auto'}; var options = $.extend({}, defaults, options); return $('') .attr('src', src) .attr('width', options.width) .attr('id', id) .attr('scrolling', options.scrolling); }; Drupal.media.popups.overlayDisplace = function (dialog) { if (parent.window.Drupal.overlay && jQuery.isFunction(parent.window.Drupal.overlay.getDisplacement)) { var overlayDisplace = parent.window.Drupal.overlay.getDisplacement('top'); if (dialog.offset().top < overlayDisplace) { dialog.css('top', overlayDisplace); } } } /** * Size the dialog when it is first loaded and keep it centered when scrolling. * * @param jQuery dialogElement * The element which has .dialog() attached to it. */ Drupal.media.popups.sizeDialog = function (dialogElement) { if (!dialogElement.is(':visible')) { return; } var windowWidth = $(window).width(); var dialogWidth = windowWidth * 0.8; var windowHeight = $(window).height(); var dialogHeight = windowHeight * 0.8; dialogElement.dialog("option", "width", dialogWidth); dialogElement.dialog("option", "height", dialogHeight); dialogElement.dialog("option", "position", 'center'); $('.media-modal-frame').width('100%'); } /** * Resize the dialog when the window changes. * * @param jQuery dialogElement * The element which has .dialog() attached to it. */ Drupal.media.popups.resizeDialog = function (dialogElement) { $(window).resize(function() { Drupal.media.popups.sizeDialog(dialogElement); }); } /** * Keeps the dialog centered when the window is scrolled. * * @param jQuery dialogElement * The element which has .dialog() attached to it. */ Drupal.media.popups.scrollDialog = function (dialogElement) { // Keep the dialog window centered when scrolling. $(window).scroll(function() { if (!dialogElement.is(':visible')) { return; } dialogElement.dialog("option", "position", 'center'); }); } })(jQuery); ; (function ($) { /** * Automatically display the guidelines of the selected text format. */ Drupal.behaviors.filterGuidelines = { attach: function (context) { $('.filter-guidelines', context).once('filter-guidelines') .find(':header').hide() .closest('.filter-wrapper').find('select.filter-list') .bind('change', function () { $(this).closest('.filter-wrapper') .find('.filter-guidelines-item').hide() .siblings('.filter-guidelines-' + this.value).show(); }) .change(); } }; })(jQuery); ; /** * @file * File with utilities to handle media in html editing. */ (function ($) { Drupal.media = Drupal.media || {}; /** * Utility to deal with media tokens / placeholders. */ Drupal.media.filter = { /** * Replaces media tokens with the placeholders for html editing. * @param content */ replaceTokenWithPlaceholder: function(content) { Drupal.media.filter.ensure_tagmap(); var matches = content.match(/\[\[.*?\]\]/g); if (matches) { for (var i = 0; i < matches.length; i++) { var match = matches[i]; if (match.indexOf('"type":"media"') == -1) { continue; } // Check if the macro exists in the tagmap. This ensures backwards // compatibility with existing media and is moderately more efficient // than re-building the element. var media = Drupal.settings.tagmap[match]; var media_json = match.replace('[[', '').replace(']]', ''); // Ensure that the media JSON is valid. try { var media_definition = JSON.parse(media_json); } catch (err) { // @todo: error logging. // Content should be returned to prevent an empty editor. return content; } // Re-build the media if the macro has changed from the tagmap. if (!media && media_definition.fid) { Drupal.media.filter.ensureSourceMap(); var source; if (source = Drupal.settings.mediaSourceMap[media_definition.fid]) { media = document.createElement(source.tagName); media.src = source.src; media.innerHTML = source.innerHTML; } else { // If the media element can't be found, leave it in to be resolved // by the user later. continue; } } // Apply attributes. var element = Drupal.media.filter.create_element(media, media_definition); var markup = Drupal.media.filter.outerHTML(element); // Use split and join to replace all instances of macro with markup. content = content.split(match).join(markup); } } return content; }, /** * Returns alt and title field attribute data from the corresponding fields. * * Specifically looks for file_entity module's file_image_alt_text and * file_image_title_text fields as those are by default used to store * override values for image alt and title attributes. * * @param options (array) * Options passed through a popup form submission. * @param includeFieldID (bool) * If set, the returned object will have extra keys with the IDs of the * found fields. * * If the alt or title fields were not found, their keys will be excluded * from the returned array. * * @return * An object with the following keys: * - alt: The value of the alt field. * - altField: The id of the alt field. * - title: The value of the title field. * - titleField: The id of the title field. */ parseAttributeFields: function(options, includeFieldID) { var attributes = {}; for (var field in options) { // If the field is set to false, use an empty string for output. options[field] = options[field] === false ? '' : options[field]; //if (field.match(/^field_file_image_alt_text/)) { if (field.match(new RegExp('^' + Drupal.settings.media.img_alt_field))) { attributes.alt = options[field]; if (includeFieldID) { attributes.altField = field; } } //if (field.match(/^field_file_image_title_text/)) { if (field.match(new RegExp('^' + Drupal.settings.media.img_title_field))) { attributes.title = options[field]; if (includeFieldID) { attributes.titleField = field; } } } return attributes; }, /** * Ensures changes made to fielded attributes are done on the fields too. * * This should be called when creating a macro tag from a placeholder. * * Changed made to attributes represented by fields are synced back to the * corresponding fields, if they exist. The alt/title attribute * values encoded in the macro will override the alt/title field values (set * in the Media dialog) during rendering of both WYSIWYG placeholders and * the final file entity on the server. Syncing makes changes applied to a * placeholder's alt/title attribute using native WYSIWYG tools visible in * the fields shown in the Media dialog. * * The reverse should be done when creating a placeholder from a macro tag * so changes made in the Media dialog are reflected in the placeholder's * alt and title attributes or the values there become stale and the change * appears uneffective. * * @param file_info (object) * A JSON decoded object of the file being inserted/updated. */ syncAttributesToFields: function(file_info) { if (!file_info) { file_info = {}; } if (!file_info.attributes) { file_info.attributes = {}; } if (!file_info.fields) { file_info.fields = {}; } var fields = Drupal.media.filter.parseAttributeFields(file_info.fields, true); // If the title attribute has changed, ensure the title field is updated. var titleAttr = file_info.attributes.title || false; if (fields.titleField && (titleAttr !== fields.title)) { file_info.fields[fields.titleField] = titleAttr; } // If the alt attribute has changed, ensure the alt field is updated. var altAttr = file_info.attributes.alt || false; if (fields.altField && (altAttr !== fields.alt)) { file_info.fields[fields.altField] = altAttr; } return file_info; }, /** * Replaces media elements with tokens. * * @param content (string) * The markup within the wysiwyg instance. */ replacePlaceholderWithToken: function(content) { Drupal.media.filter.ensure_tagmap(); // Locate and process all the media placeholders in the WYSIWYG content. var contentElements = $(''); // TODO: once baseline jQuery is 1.8+, switch to using $.parseHTML(content) contentElements.get(0).innerHTML = content; var mediaElements = contentElements.find('.media-element'); if (mediaElements) { $(mediaElements).each(function (i) { // Attempt to derive a JSON macro representation of the media placeholder. // Note: Drupal 7 ships with JQuery 1.4.4, which allows $(this).attr('outerHTML') to retrieve the eement's HTML, // but many sites use JQuery update to increate this to 1.6+, which insists on $(this).prop('outerHTML). // Until the minimum jQuery is >= 1.6, we need to do this the old-school way. // See http://stackoverflow.com/questions/2419749/get-selected-elements-outer-html var markup = $(this).get(0).outerHTML; if (markup === undefined) { // Browser does not support outerHTML DOM property. Use the more expensive clone method instead. markup = $(this).clone().wrap('').parent().html(); } var macro = Drupal.media.filter.create_macro($(markup)); if (macro) { // Replace the placeholder with the macro in the parsed content. // (Can't just replace the string section, because the outerHTML may be subtly different, // depending on the browser. Parsing tends to convert to , for instance.) Drupal.settings.tagmap[macro] = markup; $(this).replaceWith(macro); } }); content = $(contentElements).html(); } return content; }, /** * Serializes file information as a url-encoded JSON object and stores it * as a data attribute on the html element. * * @param html (string) * A html element to be used to represent the inserted media element. * @param info (object) * A object containing the media file information (fid, view_mode, etc). */ create_element: function (html, info) { if ($('').append(html).text().length === html.length) { // Element is not an html tag. Surround it in a span element so we can // pass the file attributes. html = '' + html + ''; } var element = $(html); // Parse out link wrappers. They will be re-applied when the image is // rendered on the front-end. if (element.is('a') && element.find('img').length) { element = element.children(); } // Extract attributes represented by fields and use those values to keep // them in sync, usually alt and title. var attributes = Drupal.media.filter.parseAttributeFields(info.fields); info.attributes = $.extend(info.attributes, attributes); // Move attributes from the file info array to the placeholder element. if (info.attributes) { $.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) { if (info.attributes[a]) { element.attr(a, info.attributes[a]); } else if (element.attr(a)) { // If the element has the attribute, but the value is empty, be // sure to clear it. element.removeAttr(a); } }); delete(info.attributes); // Store information to rebuild the element later, if necessary. Drupal.media.filter.ensureSourceMap(); Drupal.settings.mediaSourceMap[info.fid] = { tagName: element[0].tagName, src: element[0].src, innerHTML: element[0].innerHTML } } info.type = info.type || "media"; // Store the data in the data map. Drupal.media.filter.ensureDataMap(); // Generate a "delta" to allow for multiple embeddings of the same file. var delta = Drupal.media.filter.fileEmbedDelta(info.fid, element); if (Drupal.settings.mediaDataMap[info.fid]) { info.field_deltas = Drupal.settings.mediaDataMap[info.fid].field_deltas || {}; } else { info.field_deltas = {}; } info.field_deltas[delta] = info.fields; element.attr('data-delta', delta); Drupal.settings.mediaDataMap[info.fid] = info; // Store the fid in the DOM to retrieve the data from the info map. element.attr('data-fid', info.fid); // Add data-media-element attribute so we can find the markup element later. element.attr('data-media-element', '1') var classes = ['media-element']; if (info.view_mode) { // Remove any existing view mode classes. element.removeClass (function (index, css) { return (css.match (/\bfile-\S+/g) || []).join(' '); }); classes.push('file-' + info.view_mode.replace(/_/g, '-')); } // Check for alignment info, after removing any existing alignment class. element.removeClass (function (index, css) { return (css.match (/\bmedia-wysiwyg-align-\S+/g) || []).join(' '); }); if (info.fields && info.fields.alignment) { classes.push('media-wysiwyg-align-' + info.fields.alignment); } element.addClass(classes.join(' ')); // Attempt to override the link_title if the user has chosen to do this. info.link_text = this.overrideLinkTitle(info); // Apply link_text if present. if ((info.link_text) && (!info.fields || !info.fields.external_url || info.fields.external_url.length === 0)) { $('a', element).html(info.link_text); } return element; }, /** * Create a macro representation of the inserted media element. * * @param element (jQuery object) * A media element with attached serialized file info. */ create_macro: function (element) { var file_info = Drupal.media.filter.extract_file_info(element); if (file_info) { if (typeof file_info.link_text == 'string') { file_info.link_text = this.overrideLinkTitle(file_info); // Make sure the link_text-html-tags are properly escaped. file_info.link_text = file_info.link_text.replace(//g, '>'); } return '[[' + JSON.stringify(file_info) + ']]'; } return false; }, /** * Extract the file info from a WYSIWYG placeholder element as JSON. * * @param element (jQuery object) * A media element with associated file info via a file id (fid). */ extract_file_info: function (element) { var fid, file_info, value, delta; if (fid = element.data('fid')) { Drupal.media.filter.ensureDataMap(); if (file_info = Drupal.settings.mediaDataMap[fid]) { file_info.attributes = {}; $.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) { if (value = element.attr(a)) { // Replace " by \" to avoid error with JSON format. if (typeof value == 'string') { value = value.replace('"', '\\"'); } file_info.attributes[a] = value; } }); // Extract the link text, if there is any. file_info.link_text = (Drupal.settings.mediaDoLinkText) ? element.find('a:not(:has(img))').html() : false; // When a file is embedded, its fields can be overridden. To allow for // the edge case where the same file is embedded multiple times with // different field overrides, we look for a data-delta attribute on // the element, and use that to decide which set of data in the // "field_deltas" property to use. if (delta = element.data('delta')) { if (file_info.field_deltas && file_info.field_deltas[delta]) { file_info.fields = file_info.field_deltas[delta]; // Also look for an overridden view mode, aka "format". // Check for existance of fields to make it backward compatible. if (file_info.fields && file_info.fields.format && file_info.view_mode) { file_info.view_mode = file_info.fields.format; } } } } else { return false; } } else { return false; } return Drupal.media.filter.syncAttributesToFields(file_info); }, /** * Gets the HTML content of an element. * * @param element (jQuery object) */ outerHTML: function (element) { return element[0].outerHTML || $('').append(element.eq(0).clone()).html(); }, /** * Gets the wrapped HTML content of an element to insert into the wysiwyg. * * It also registers the element in the tag map so that the token * replacement works. * * @param element (jQuery object) The element to insert. * * @see Drupal.media.filter.replacePlaceholderWithToken() */ getWysiwygHTML: function (element) { // Create the markup and the macro. var markup = Drupal.media.filter.outerHTML(element), macro = Drupal.media.filter.create_macro(element); // Store macro/markup in the tagmap. Drupal.media.filter.ensure_tagmap(); Drupal.settings.tagmap[macro] = markup; // Return the html code to insert in an editor and use it with // replacePlaceholderWithToken() return markup; }, /** * Ensures the src tracking has been initialized and returns it. */ ensureSourceMap: function() { Drupal.settings.mediaSourceMap = Drupal.settings.mediaSourceMap || {}; return Drupal.settings.mediaSourceMap; }, /** * Ensures the data tracking has been initialized and returns it. */ ensureDataMap: function() { Drupal.settings.mediaDataMap = Drupal.settings.mediaDataMap || {}; return Drupal.settings.mediaDataMap; }, /** * Ensures the tag map has been initialized and returns it. */ ensure_tagmap: function () { Drupal.settings.tagmap = Drupal.settings.tagmap || {}; return Drupal.settings.tagmap; }, /** * Return the overridden link title based on the file_entity title field * set. * @param file the file object. * @returns the overridden link_title or the existing link text if no * overridden. */ overrideLinkTitle: function(file) { var file_title_field = Drupal.settings.media.img_title_field.replace('field_', ''); var file_title_field_machine_name = ''; if (typeof(file.fields) != 'undefined') { jQuery.each(file.fields, function(field, fieldValue) { if (field.indexOf(file_title_field) != -1) { file_title_field_machine_name = field; } }); if (typeof(file.fields[file_title_field_machine_name]) != 'undefined' && file.fields[file_title_field_machine_name] != '') { return file.fields[file_title_field_machine_name]; } else { return file.link_text; } } else { return file.link_text; } }, /** * Generates a unique "delta" for each embedding of a particular file. */ fileEmbedDelta: function(fid, element) { // Ensure we have an object to track our deltas. Drupal.settings.mediaDeltas = Drupal.settings.mediaDeltas || {}; Drupal.settings.maxMediaDelta = Drupal.settings.maxMediaDelta || 0; // Check to see if the element already has one. if (element && element.data('delta')) { var existingDelta = element.data('delta'); // If so, make sure that it is being tracked in mediaDeltas. If we're // going to create new deltas later on, make sure they do not overwrite // other mediaDeltas. if (!Drupal.settings.mediaDeltas[existingDelta]) { Drupal.settings.mediaDeltas[existingDelta] = fid; Drupal.settings.maxMediaDelta = Math.max(Drupal.settings.maxMediaDelta, existingDelta); } return existingDelta; } // Otherwise, generate a new one. var newDelta = Drupal.settings.maxMediaDelta + 1; Drupal.settings.mediaDeltas[newDelta] = fid; Drupal.settings.maxMediaDelta = newDelta; return newDelta; } } })(jQuery); ; (function ($) { /** * Attaches the autocomplete behavior to all required fields. */ Drupal.behaviors.autocomplete = { attach: function (context, settings) { var acdb = []; $('input.autocomplete', context).once('autocomplete', function () { var uri = this.value; if (!acdb[uri]) { acdb[uri] = new Drupal.ACDB(uri); } var $input = $('#' + this.id.substr(0, this.id.length - 13)) .attr('autocomplete', 'OFF') .attr('aria-autocomplete', 'list'); $($input[0].form).submit(Drupal.autocompleteSubmit); $input.parent() .attr('role', 'application') .append($('') .attr('id', $input.attr('id') + '-autocomplete-aria-live') ); new Drupal.jsAC($input, acdb[uri]); }); } }; /** * Prevents the form from submitting if the suggestions popup is open * and closes the suggestions popup when doing so. */ Drupal.autocompleteSubmit = function () { return $('#autocomplete').each(function () { this.owner.hidePopup(); }).length == 0; }; /** * An AutoComplete object. */ Drupal.jsAC = function ($input, db) { var ac = this; this.input = $input[0]; this.ariaLive = $('#' + this.input.id + '-autocomplete-aria-live'); this.db = db; $input .keydown(function (event) { return ac.onkeydown(this, event); }) .keyup(function (event) { ac.onkeyup(this, event); }) .blur(function () { ac.hidePopup(); ac.db.cancel(); }); }; /** * Handler for the "keydown" event. */ Drupal.jsAC.prototype.onkeydown = function (input, e) { if (!e) { e = window.event; } switch (e.keyCode) { case 40: // down arrow. this.selectDown(); return false; case 38: // up arrow. this.selectUp(); return false; default: // All other keys. return true; } }; /** * Handler for the "keyup" event. */ Drupal.jsAC.prototype.onkeyup = function (input, e) { if (!e) { e = window.event; } switch (e.keyCode) { case 16: // Shift. case 17: // Ctrl. case 18: // Alt. case 20: // Caps lock. case 33: // Page up. case 34: // Page down. case 35: // End. case 36: // Home. case 37: // Left arrow. case 38: // Up arrow. case 39: // Right arrow. case 40: // Down arrow. return true; case 9: // Tab. case 13: // Enter. case 27: // Esc. this.hidePopup(e.keyCode); return true; default: // All other keys. if (input.value.length > 0 && !input.readOnly) { this.populatePopup(); } else { this.hidePopup(e.keyCode); } return true; } }; /** * Puts the currently highlighted suggestion into the autocomplete field. */ Drupal.jsAC.prototype.select = function (node) { this.input.value = $(node).data('autocompleteValue'); $(this.input).trigger('autocompleteSelect', [node]); }; /** * Highlights the next suggestion. */ Drupal.jsAC.prototype.selectDown = function () { if (this.selected && this.selected.nextSibling) { this.highlight(this.selected.nextSibling); } else if (this.popup) { var lis = $('li', this.popup); if (lis.length > 0) { this.highlight(lis.get(0)); } } }; /** * Highlights the previous suggestion. */ Drupal.jsAC.prototype.selectUp = function () { if (this.selected && this.selected.previousSibling) { this.highlight(this.selected.previousSibling); } }; /** * Highlights a suggestion. */ Drupal.jsAC.prototype.highlight = function (node) { if (this.selected) { $(this.selected).removeClass('selected'); } $(node).addClass('selected'); this.selected = node; $(this.ariaLive).html($(this.selected).html()); }; /** * Unhighlights a suggestion. */ Drupal.jsAC.prototype.unhighlight = function (node) { $(node).removeClass('selected'); this.selected = false; $(this.ariaLive).empty(); }; /** * Hides the autocomplete suggestions. */ Drupal.jsAC.prototype.hidePopup = function (keycode) { // Select item if the right key or mousebutton was pressed. if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) { this.select(this.selected); } // Hide popup. var popup = this.popup; if (popup) { this.popup = null; $(popup).fadeOut('fast', function () { $(popup).remove(); }); } this.selected = false; $(this.ariaLive).empty(); }; /** * Positions the suggestions popup and starts a search. */ Drupal.jsAC.prototype.populatePopup = function () { var $input = $(this.input); var position = $input.position(); // Show popup. if (this.popup) { $(this.popup).remove(); } this.selected = false; this.popup = $('')[0]; this.popup.owner = this; $(this.popup).css({ top: parseInt(position.top + this.input.offsetHeight, 10) + 'px', left: parseInt(position.left, 10) + 'px', width: $input.innerWidth() + 'px', display: 'none' }); $input.before(this.popup); // Do search. this.db.owner = this; this.db.search(this.input.value); }; /** * Fills the suggestion popup with any matches received. */ Drupal.jsAC.prototype.found = function (matches) { // If no value in the textfield, do not show the popup. if (!this.input.value.length) { return false; } // Prepare matches. var ul = $(''); var ac = this; for (key in matches) { $('') .html($('').html(matches[key])) .mousedown(function () { ac.hidePopup(this); }) .mouseover(function () { ac.highlight(this); }) .mouseout(function () { ac.unhighlight(this); }) .data('autocompleteValue', key) .appendTo(ul); } // Show popup with matches, if any. if (this.popup) { if (ul.children().length) { $(this.popup).empty().append(ul).show(); $(this.ariaLive).html(Drupal.t('Autocomplete popup')); } else { $(this.popup).css({ visibility: 'hidden' }); this.hidePopup(); } } }; Drupal.jsAC.prototype.setStatus = function (status) { switch (status) { case 'begin': $(this.input).addClass('throbbing'); $(this.ariaLive).html(Drupal.t('Searching for matches...')); break; case 'cancel': case 'error': case 'found': $(this.input).removeClass('throbbing'); break; } }; /** * An AutoComplete DataBase object. */ Drupal.ACDB = function (uri) { this.uri = uri; this.delay = 300; this.cache = {}; }; /** * Performs a cached and delayed search. */ Drupal.ACDB.prototype.search = function (searchString) { var db = this; this.searchString = searchString; // See if this string needs to be searched for anyway. The pattern ../ is // stripped since it may be misinterpreted by the browser. searchString = searchString.replace(/^\s+|\.{2,}\/|\s+$/g, ''); // Skip empty search strings, or search strings ending with a comma, since // that is the separator between search terms. if (searchString.length <= 0 || searchString.charAt(searchString.length - 1) == ',') { return; } // See if this key has been searched for before. if (this.cache[searchString]) { return this.owner.found(this.cache[searchString]); } // Initiate delayed search. if (this.timer) { clearTimeout(this.timer); } this.timer = setTimeout(function () { db.owner.setStatus('begin'); // Ajax GET request for autocompletion. We use Drupal.encodePath instead of // encodeURIComponent to allow autocomplete search terms to contain slashes. $.ajax({ type: 'GET', url: Drupal.sanitizeAjaxUrl(db.uri + '/' + Drupal.encodePath(searchString)), dataType: 'json', jsonp: false, success: function (matches) { if (typeof matches.status == 'undefined' || matches.status != 0) { db.cache[searchString] = matches; // Verify if these are still the matches the user wants to see. if (db.searchString == searchString) { db.owner.found(matches); } db.owner.setStatus('found'); } }, error: function (xmlhttp) { Drupal.displayAjaxError(Drupal.ajaxError(xmlhttp, db.uri)); } }); }, this.delay); }; /** * Cancels the current autocomplete request. */ Drupal.ACDB.prototype.cancel = function () { if (this.owner) this.owner.setStatus('cancel'); if (this.timer) clearTimeout(this.timer); this.searchString = ''; }; })(jQuery); ; /** * @file * Provides JavaScript additions to the media field widget. * * This file provides support for launching the media browser to select existing * files and disabling of other media fields during Ajax uploads (which prevents * separate media fields from accidentally attaching files). */ (function ($) { /** * Attach behaviors to media element upload fields. */ Drupal.behaviors.mediaElement = { attach: function (context, settings) { var $context = $(context); var elements; function initMediaBrowser(selector) { var widget=$context.find(selector).once('media-browser-launch'); var browse=widget.siblings('.browse').add(widget.find('.browse')); var upload=browse.siblings('.upload').add(widget.find('.upload')); var attach=upload.siblings('.attach').add(widget.find('.attach')); browse.show(); upload.hide(); attach.hide(); browse.bind('click', {configuration: settings.media.elements[selector]}, Drupal.media.openBrowser); } if (settings.media && settings.media.elements) { elements = settings.media.elements; Object.keys(elements).forEach(initMediaBrowser); } }, detach: function (context, settings, trigger) { var $context = $(context); var elements; function removeMediaBrowser(selector) { $context.find(selector) .removeOnce('media-browser-launch') .siblings('.browse').hide() .siblings('.upload').show() .siblings('.attach').show() .siblings('.browse').unbind('click', Drupal.media.openBrowser); } if (trigger === 'unload' && settings.media && settings.media.elements) { elements = settings.media.elements; Object.keys(elements).forEach(removeMediaBrowser); } } }; /** * Attach behaviors to the media attach and remove buttons. */ Drupal.behaviors.mediaButtons = { attach: function (context) { $('input.form-submit', context).bind('mousedown', Drupal.media.disableFields); }, detach: function (context) { $('input.form-submit', context).unbind('mousedown', Drupal.media.disableFields); } }; /** * Media attach utility functions. */ Drupal.media = Drupal.media || {}; /** * Opens the media browser with the element's configuration settings. */ Drupal.media.openBrowser = function (event) { var clickedButton = this; var configuration = event.data.configuration.global; // Find the file ID, preview and upload fields. var fidField = $(this).siblings('.fid'); var previewField = $(this).siblings('.preview'); var uploadField = $(this).siblings('.upload'); // Find the edit and remove buttons. var editButton = $(this).siblings('.edit'); var removeButton = $(this).siblings('.remove'); // Launch the media browser. Drupal.media.popups.mediaBrowser(function (mediaFiles) { // Ensure that there was at least one media file selected. if (mediaFiles.length < 0) { return; } var mediaFileValue; // Process the value based on multiselect. if (mediaFiles.length > 1) { // Concatenate the array into a comma separated string. mediaFileValue = mediaFiles.map(function(file) { return file.fid; }).join(','); } else { // Grab the first of the selected media files. mediaFileValue = mediaFiles[0].fid; // Display a preview of the file using the selected media file's display. previewField.html(mediaFileValue.preview); } // Set the value of the hidden file ID field and trigger a change. uploadField.val(mediaFileValue); uploadField.trigger('change'); // Find the attach button and automatically trigger it. var attachButton = uploadField.siblings('.attach'); attachButton.trigger('mousedown'); }, configuration); return false; }; /** * Prevent media browsing when using buttons not intended to browse. */ Drupal.media.disableFields = function (event) { var clickedButton = this; // Only disable browse fields for Ajax buttons. if (!$(clickedButton).hasClass('ajax-processed')) { return; } // Check if we're working with an "Attach" button. var $enabledFields = []; if ($(this).closest('div.media-widget').length > 0) { $enabledFields = $(this).closest('div.media-widget').find('input.attach'); } // Temporarily disable attach fields other than the one we're currently // working with. Filter out fields that are already disabled so that they // do not get enabled when we re-enable these fields at the end of behavior // processing. Re-enable in a setTimeout set to a relatively short amount // of time (1 second). All the other mousedown handlers (like Drupal's Ajax // behaviors) are excuted before any timeout functions are called, so we // don't have to worry about the fields being re-enabled too soon. // @todo If the previous sentence is true, why not set the timeout to 0? var $fieldsToTemporarilyDisable = $('div.media-widget input.attach').not($enabledFields).not(':disabled'); $fieldsToTemporarilyDisable.attr('disabled', 'disabled'); setTimeout(function (){ $fieldsToTemporarilyDisable.attr('disabled', false); }, 1000); }; })(jQuery); ; /** * @file * Provides JavaScript additions to the managed file field type. * * This file provides progress bar support (if available), popup windows for * file previews, and disabling of other file fields during Ajax uploads (which * prevents separate file fields from accidentally uploading files). */ (function ($) { /** * Attach behaviors to managed file element upload fields. */ Drupal.behaviors.fileValidateAutoAttach = { attach: function (context, settings) { if (settings.file && settings.file.elements) { $.each(settings.file.elements, function(selector) { var extensions = settings.file.elements[selector]; $(selector, context).bind('change', {extensions: extensions}, Drupal.file.validateExtension); }); } }, detach: function (context, settings) { if (settings.file && settings.file.elements) { $.each(settings.file.elements, function(selector) { $(selector, context).unbind('change', Drupal.file.validateExtension); }); } } }; /** * Attach behaviors to the file upload and remove buttons. */ Drupal.behaviors.fileButtons = { attach: function (context) { $('input.form-submit', context).bind('mousedown', Drupal.file.disableFields); $('div.form-managed-file input.form-submit', context).bind('mousedown', Drupal.file.progressBar); }, detach: function (context) { $('input.form-submit', context).unbind('mousedown', Drupal.file.disableFields); $('div.form-managed-file input.form-submit', context).unbind('mousedown', Drupal.file.progressBar); } }; /** * Attach behaviors to links within managed file elements. */ Drupal.behaviors.filePreviewLinks = { attach: function (context) { $('div.form-managed-file .file a, .file-widget .file a', context).bind('click',Drupal.file.openInNewWindow); }, detach: function (context){ $('div.form-managed-file .file a, .file-widget .file a', context).unbind('click', Drupal.file.openInNewWindow); } }; /** * File upload utility functions. */ Drupal.file = Drupal.file || { /** * Client-side file input validation of file extensions. */ validateExtension: function (event) { // Remove any previous errors. $('.file-upload-js-error').remove(); // Add client side validation for the input[type=file]. var extensionPattern = event.data.extensions.replace(/,\s*/g, '|'); if (extensionPattern.length > 1 && this.value.length > 0) { var acceptableMatch = new RegExp('\\.(' + extensionPattern + ')$', 'gi'); if (!acceptableMatch.test(this.value)) { var error = Drupal.t("The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.", { // According to the specifications of HTML5, a file upload control // should not reveal the real local path to the file that a user // has selected. Some web browsers implement this restriction by // replacing the local path with "C:\fakepath\", which can cause // confusion by leaving the user thinking perhaps Drupal could not // find the file because it messed up the file path. To avoid this // confusion, therefore, we strip out the bogus fakepath string. '%filename': this.value.replace('C:\\fakepath\\', ''), '%extensions': extensionPattern.replace(/\|/g, ', ') }); $(this).closest('div.form-managed-file').prepend(' '); this.value = ''; return false; } } }, /** * Prevent file uploads when using buttons not intended to upload. */ disableFields: function (event){ var clickedButton = this; // Only disable upload fields for Ajax buttons. if (!$(clickedButton).hasClass('ajax-processed')) { return; } // Check if we're working with an "Upload" button. var $enabledFields = []; if ($(this).closest('div.form-managed-file').length > 0) { $enabledFields = $(this).closest('div.form-managed-file').find('input.form-file'); } // Temporarily disable upload fields other than the one we're currently // working with. Filter out fields that are already disabled so that they // do not get enabled when we re-enable these fields at the end of behavior // processing. Re-enable in a setTimeout set to a relatively short amount // of time (1 second). All the other mousedown handlers (like Drupal's Ajax // behaviors) are excuted before any timeout functions are called, so we // don't have to worry about the fields being re-enabled too soon. // @todo If the previous sentence is true, why not set the timeout to 0? var $fieldsToTemporarilyDisable = $('div.form-managed-file input.form-file').not($enabledFields).not(':disabled'); $fieldsToTemporarilyDisable.attr('disabled', 'disabled'); setTimeout(function (){ $fieldsToTemporarilyDisable.attr('disabled', false); }, 1000); }, /** * Add progress bar support if possible. */ progressBar: function (event) { var clickedButton = this; var $progressId = $(clickedButton).closest('div.form-managed-file').find('input.file-progress'); if ($progressId.length) { var originalName = $progressId.attr('name'); // Replace the name with the required identifier. $progressId.attr('name', originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0]); // Restore the original name after the upload begins. setTimeout(function () { $progressId.attr('name', originalName); }, 1000); } // Show the progress bar if the upload takes longer than half a second. setTimeout(function () { $(clickedButton).closest('div.form-managed-file').find('div.ajax-progress-bar').slideDown(); }, 500); }, /** * Open links to files within forms in a new window. */ openInNewWindow: function (event) { $(this).attr('target', '_blank'); window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550'); return false; } }; })(jQuery); ; (function ($) { /** * Toggle the visibility of a fieldset using smooth animations. */ Drupal.toggleFieldset = function (fieldset) { var $fieldset = $(fieldset); if ($fieldset.is('.collapsed')) { var $content = $('> .fieldset-wrapper', fieldset).hide(); $fieldset .removeClass('collapsed') .trigger({ type: 'collapsed', value: false }) .find('> legend span.fieldset-legend-prefix').html(Drupal.t('Hide')); $content.slideDown({ duration: 'fast', easing: 'linear', complete: function () { Drupal.collapseScrollIntoView(fieldset); fieldset.animating = false; }, step: function () { // Scroll the fieldset into view. Drupal.collapseScrollIntoView(fieldset); } }); } else { $fieldset.trigger({ type: 'collapsed', value: true }); $('> .fieldset-wrapper', fieldset).slideUp('fast', function () { $fieldset .addClass('collapsed') .find('> legend span.fieldset-legend-prefix').html(Drupal.t('Show')); fieldset.animating = false; }); } }; /** * Scroll a given fieldset into view as much as possible. */ Drupal.collapseScrollIntoView = function (node) { var h = document.documentElement.clientHeight || document.body.clientHeight || 0; var offset = document.documentElement.scrollTop || document.body.scrollTop || 0; var posY = $(node).offset().top; var fudge = 55; if (posY + node.offsetHeight + fudge > h + offset) { if (node.offsetHeight > h) { window.scrollTo(0, posY); } else { window.scrollTo(0, posY + node.offsetHeight - h + fudge); } } }; Drupal.behaviors.collapse = { attach: function (context, settings) { $('fieldset.collapsible', context).once('collapse', function () { var $fieldset = $(this); // Expand fieldset if there are errors inside, or if it contains an // element that is targeted by the URI fragment identifier. var anchor = location.hash && location.hash != '#' ? ', ' + location.hash : ''; if ($fieldset.find('.error' + anchor).length) { $fieldset.removeClass('collapsed'); } var summary = $(''); $fieldset. bind('summaryUpdated', function () { var text = $.trim($fieldset.drupalGetSummary()); summary.html(text ? ' (' + text + ')' : ''); }) .trigger('summaryUpdated'); // Turn the legend into a clickable link, but retain span.fieldset-legend // for CSS positioning. var $legend = $('> legend .fieldset-legend', this); $('') .append($fieldset.hasClass('collapsed') ? Drupal.t('Show') : Drupal.t('Hide')) .prependTo($legend) .after(' '); // .wrapInner() does not retain bound events. var $link = $('') .prepend($legend.contents()) .appendTo($legend) .click(function () { var fieldset = $fieldset.get(0); // Don't animate multiple times. if (!fieldset.animating) { fieldset.animating = true; Drupal.toggleFieldset(fieldset); } return false; }); $legend.append(summary); }); } }; })(jQuery); ; (function ($) { Drupal.behaviors.pathFieldsetSummaries = { attach: function (context) { $('fieldset.path-form', context).drupalSetSummary(function (context) { var path = $('.form-item-path-alias input', context).val(); var automatic = $('.form-item-path-pathauto input', context).attr('checked'); if (automatic) { return Drupal.t('Automatic alias'); } else if (path) { return Drupal.t('Alias: @alias', { '@alias': path }); } else { return Drupal.t('No alias'); } }); } }; })(jQuery); ; /** * @file * Custom JS for controlling the Metatag vertical tab. */ (function ($) { 'use strict'; Drupal.behaviors.metatagFieldsetSummaries = { attach: function (context) { $('fieldset.metatags-form', context).drupalSetSummary(function (context) { var vals = []; $("input[type='text'], select, textarea", context).each(function() { var input_field = $(this).attr('name'); // Verify the field exists before proceeding. if (input_field === undefined) { return false; } var default_name = input_field.replace(/\[value\]/, '[default]'); var default_value = $("input[type='hidden'][name='" + default_name + "']", context); if (default_value.length && default_value.val() === $(this).val()) { // Meta tag has a default value and form value matches default // value. return true; } else if (!default_value.length && !$(this).val().length) { // Meta tag has no default value and form value is empty. return true; } var label = $("label[for='" + $(this).attr('id') + "']").text(); vals.push(Drupal.t('@label: @value', { '@label': $.trim(label), '@value': Drupal.truncate($(this).val(), 25) || Drupal.t('None') })); }); if (vals.length === 0) { return Drupal.t('Using defaults'); } else { return vals.join('
'); } }); } }; /** * Encode special characters in a plain-text string for display as HTML. */ Drupal.truncate = function (str, limit) { if (str.length > limit) { return str.substr(0, limit) + '...'; } else { return str; } }; })(jQuery); ; (function ($) { Drupal.behaviors.nodeFieldsetSummaries = { attach: function (context) { $('fieldset.node-form-revision-information', context).drupalSetSummary(function (context) { var revisionCheckbox = $('.form-item-revision input', context); // Return 'New revision' if the 'Create new revision' checkbox is checked, // or if the checkbox doesn't exist, but the revision log does. For users // without the "Administer content" permission the checkbox won't appear, // but the revision log will if the content type is set to auto-revision. if (revisionCheckbox.is(':checked') || (!revisionCheckbox.length && $('.form-item-log textarea', context).length)) { return Drupal.t('New revision'); } return Drupal.t('No revision'); }); $('fieldset.node-form-author', context).drupalSetSummary(function (context) { var name = $('.form-item-name input', context).val() || Drupal.settings.anonymous, date = $('.form-item-date input', context).val(); return date ? Drupal.t('By @name on @date', { '@name': name, '@date': date }) : Drupal.t('By @name', { '@name': name }); }); $('fieldset.node-form-options', context).drupalSetSummary(function (context) { var vals = []; $('input:checked', context).parent().each(function () { vals.push(Drupal.checkPlain($.trim($(this).text()))); }); if (!$('.form-item-status input', context).is(':checked')) { vals.unshift(Drupal.t('Not published')); } return vals.join(', '); }); } }; })(jQuery); ; /** * @file */ (function ($) { 'use strict'; /** * Prompts user to save content when closing tab on node/add/* pages. */ Drupal.behaviors.kittensLifesaver = { attach: function () { var handler = function () { return '🙀 If you proceed, ALL of your content will be lost. 😿'; }; $(window).on('beforeunload', handler); $('form.node-form').submit(function () { $(window).off('beforeunload', handler); }); } }; }(jQuery)); ; /** * @file * Attaches behaviors for the Chosen module. */ (function($) { Drupal.behaviors.chosen = { attach: function(context, settings) { settings.chosen = settings.chosen || Drupal.settings.chosen; // Prepare selector and add unwantend selectors. var selector = settings.chosen.selector; // Function to prepare all the options together for the chosen() call. var getElementOptions = function (element) { var options = $.extend({}, settings.chosen.options); // The width default option is considered the minimum width, so this // must be evaluated for every option. if (settings.chosen.minimum_width > 0) { if ($(element).width() < settings.chosen.minimum_width) { options.width = settings.chosen.minimum_width + 'px'; } else { options.width = $(element).width() + 'px'; } } // Some field widgets have cardinality, so we must respect that. // @see chosen_pre_render_select() if ($(element).attr('multiple') && $(element).data('cardinality')) { options.max_selected_options = $(element).data('cardinality'); } return options; }; // Process elements that have opted-in for Chosen. // @todo Remove support for the deprecated chosen-widget class. $('select.chosen-enable, select.chosen-widget', context).once('chosen', function() { options = getElementOptions(this); $(this).chosen(options); }); $(selector, context) // Disabled on: // - Field UI // - WYSIWYG elements // - Tabledrag weights // - Elements that have opted-out of Chosen // - Elements already processed by Chosen. .not('#field-ui-field-overview-form select, #field-ui-display-overview-form select, .wysiwyg, .draggable select[name$="[weight]"], .draggable select[name$="[position]"], .chosen-disable, .chosen-processed') .filter(function() { // Filter out select widgets that do not meet the minimum number of // options. var minOptions = $(this).attr('multiple') ? settings.chosen.minimum_multiple : settings.chosen.minimum_single; if (!minOptions) { // Zero value means no minimum. return true; } else { return $(this).find('option').length >= minOptions; } }) .once('chosen', function() { options = getElementOptions(this); $(this).chosen(options); }); } }; })(jQuery); ;