Commit 95cbbbcd authored by Dries's avatar Dries

Issue #1879120 by quicksketch, Wim Leers, larowlan, frega: Use Drupal-specific...

Issue #1879120 by quicksketch, Wim Leers, larowlan, frega: Use Drupal-specific image and link plugins — use core dialogs rather than CKEditor dialogs, containing alterable Drupal forms.
parent 47233911
...@@ -8,13 +8,74 @@ ...@@ -8,13 +8,74 @@
"use strict"; "use strict";
Drupal.behaviors.dialog = { Drupal.behaviors.dialog = {
attach: function () { attach: function (context, settings) {
var $context = $(context);
// Provide a known 'drupal-modal' DOM element for Drupal-based modal // Provide a known 'drupal-modal' DOM element for Drupal-based modal
// dialogs. Non-modal dialogs are responsible for creating their own // dialogs. Non-modal dialogs are responsible for creating their own
// elements, since there can be multiple non-modal dialogs at a time. // elements, since there can be multiple non-modal dialogs at a time.
if (!$('#drupal-modal').length) { if (!$('#drupal-modal').length) {
$('<div id="drupal-modal" />').hide().appendTo('body'); $('<div id="drupal-modal" />').hide().appendTo('body');
} }
// Special behaviors specific when attaching content within a dialog.
// These behaviors usually fire after a validation error inside a dialog.
var $dialog = $context.closest('.ui-dialog-content');
if ($dialog.length) {
// Remove and replace the dialog buttons with those from the new form.
if ($dialog.dialog('option', 'drupalAutoButtons')) {
var buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
$dialog.dialog('option', 'buttons', buttons);
}
// Refocus the first input element after validation errors.
if ($context.find('form').length) {
$context.find('input:first').focus();
}
}
},
detach: function (context, settings) {
$(context).find('form').off('submit.dialogSubmit');
},
/**
* Scan a dialog for any primary buttons and move them to the button area.
*
* @param $dialog
* An jQuery object containing the element that is the dialog target.
* @return
* An array of buttons that need to be added to the button area.
*/
prepareDialogButtons: function ($dialog) {
var buttons = [];
var $buttons = $dialog.find('.form-actions input[type=submit]');
$buttons.each(function () {
// Hidden form buttons need special attention. For browser consistency,
// the button needs to be "visible" in order to have the enter key fire
// the form submit event. So instead of a simple "hide" or
// "display: none", we set its dimensions to zero.
// See http://mattsnider.com/how-forms-submit-when-pressing-enter/
var $originalButton = $(this).css({
width: 0,
height: 0,
padding: 0,
border: 0
});
buttons.push({
'text': $originalButton.html() || $originalButton.attr('value'),
'class': $originalButton.attr('class'),
'click': function (e) {
$originalButton.trigger('click');
e.preventDefault();
}
});
});
if ($buttons.length) {
$dialog.find('form').on('submit.dialogSubmit', function (e) {
$buttons.first().trigger('click');
e.preventDefault();
});
}
return buttons;
} }
}; };
...@@ -40,6 +101,12 @@ ...@@ -40,6 +101,12 @@
response.method = 'html'; response.method = 'html';
ajax.commands.insert(ajax, response, status); ajax.commands.insert(ajax, response, status);
// Move the buttons to the jQuery UI dialog buttons area.
if (!response.dialogOptions.buttons) {
response.dialogOptions.drupalAutoButtons = true;
response.dialogOptions.buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
}
// Open the dialog itself. // Open the dialog itself.
response.dialogOptions = response.dialogOptions || {}; response.dialogOptions = response.dialogOptions || {};
var dialog = Drupal.dialog($dialog, response.dialogOptions); var dialog = Drupal.dialog($dialog, response.dialogOptions);
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
drupalSettings.dialog = { drupalSettings.dialog = {
autoOpen: true, autoOpen: true,
autoResize: true,
dialogClass: '', dialogClass: '',
close: function (e) { close: function (e) {
Drupal.detachBehaviors(e.target, null, 'unload'); Drupal.detachBehaviors(e.target, null, 'unload');
...@@ -23,6 +24,10 @@ Drupal.dialog = function (element, options) { ...@@ -23,6 +24,10 @@ Drupal.dialog = function (element, options) {
// Trigger a global event to allow scripts to bind events to the dialog. // Trigger a global event to allow scripts to bind events to the dialog.
$(window).trigger('dialog:beforecreate', [dialog, $element, settings]); $(window).trigger('dialog:beforecreate', [dialog, $element, settings]);
$element.dialog(settings); $element.dialog(settings);
if (settings.autoResize !== 'false' && settings.autoResize !== false) {
$(window).on('resize.dialogResize scroll.dialogResize', autoResize);
resetPosition();
}
dialog.open = true; dialog.open = true;
$(window).trigger('dialog:aftercreate', [dialog, $element, settings]); $(window).trigger('dialog:aftercreate', [dialog, $element, settings]);
} }
...@@ -32,11 +37,43 @@ Drupal.dialog = function (element, options) { ...@@ -32,11 +37,43 @@ Drupal.dialog = function (element, options) {
$element.dialog('close'); $element.dialog('close');
dialog.returnValue = value; dialog.returnValue = value;
dialog.open = false; dialog.open = false;
$(window).off('.dialogResize');
$(window).trigger('dialog:afterclose', [dialog, $element]); $(window).trigger('dialog:afterclose', [dialog, $element]);
} }
/**
* Resets the current options for positioning.
*
* This is used as a window resize and scroll callback to reposition the
* jQuery UI dialog. Although not a built-in jQuery UI option, this can
* be disabled by setting autoResize: false in the options array when creating
* a new Drupal.dialog().
*/
function resetPosition () {
var positionOptions = ['width', 'height', 'minWidth', 'minHeight', 'maxHeight', 'maxWidth', 'position'];
var windowHeight = $(window).height();
var adjustedOptions = $.extend({ position: { my: "center", at: "center", of: window }}, options);
var optionValue, adjustedValue;
for (var n = 0; n < positionOptions.length; n++) {
if (adjustedOptions[positionOptions[n]]) {
optionValue = adjustedOptions[positionOptions[n]];
// jQuery UI does not support percentages on heights, convert to pixels.
if (positionOptions[n].match(/height/i) && typeof optionValue === 'string' && optionValue.match(/%$/)) {
adjustedValue = parseInt(0.01 * parseInt(optionValue, 10) * windowHeight, 10);
// Don't force the dialog to be bigger vertically than needed.
if (positionOptions[n] === 'height' && $element.parent().outerHeight() < adjustedValue) {
adjustedValue = 'auto';
}
adjustedOptions[positionOptions[n]] = adjustedValue;
}
}
}
$element.dialog('option', adjustedOptions);
}
var undef; var undef;
var $element = $(element); var $element = $(element);
var autoResize = Drupal.debounce(resetPosition, 50);
var dialog = { var dialog = {
open: false, open: false,
returnValue: undef, returnValue: undef,
......
/**
* Presentational styles for Drupal dialogs.
*/
.ui-dialog {
position: absolute;
z-index: 1260;
overflow: visible;
color: #000;
background: #fff;
border: solid 1px #ccc;
padding: 0;
}
.ui-dialog .ui-dialog-titlebar {
font-weight: bold;
background: #f3f4ee;
border-style: solid;
border-radius: 0;
border-width: 0 0 1px 0;
border-color: #ccc;
}
.ui-dialog .ui-dialog-titlebar-close {
border: 0;
background: none;
}
.ui-dialog .ui-dialog-buttonpane {
margin-top: 0;
background: #f3f4ee;
padding: .3em 1em;
border-width: 1px 0 0 0;
border-color: #ccc;
}
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
margin: 0;
padding: 0;
}
.ui-dialog .ui-dialog-buttonpane .ui-button-text-only .ui-button-text {
padding: 0;
}
.ui-dialog .ui-dialog-buttonpane button {
background: #fefefe;
background-image: -webkit-linear-gradient(top, #fefefe, #e0e0e0);
background-image: -moz-linear-gradient(top, #fefefe, #e0e0e0);
background-image: -o-linear-gradient(top, #fefefe, #e0e0e0);
background-image: linear-gradient(to bottom, #fefefe, #e0e0e0);
border: 1px solid #c8c8c8;
border-radius: 3px;
text-decoration: none;
padding: 6px 17px 6px 17px;
}
.ui-dialog .ui-dialog-buttonpane button:hover,
.ui-dialog .ui-dialog-buttonpane button:focus {
background: #fefefe;
background-image: -webkit-linear-gradient(top, #fefefe, #eaeaea);
background-image: -moz-linear-gradient(top, #fefefe, #eaeaea);
background-image: -o-linear-gradient(top, #fefefe, #eaeaea);
background-image: linear-gradient(to bottom, #fefefe, #eaeaea);
-webkit-box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
color: #2e2e2e;
text-decoration: none;
}
.ui-dialog .ui-dialog-buttonpane button:active {
border: 1px solid #c8c8c8;
background: #fefefe;
background-image: -webkit-linear-gradient(top, #eaeaea, #fefefe);
background-image: -moz-linear-gradient(top, #eaeaea, #fefefe);
background-image: -o-linear-gradient(top, #eaeaea, #fefefe);
background-image: linear-gradient(to bottom, #eaeaea, #fefefe);
-webkit-box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
color: #2e2e2e;
text-decoration: none;
text-shadow: none;
}
/* Form action buttons are moved in dialogs. Remove empty space. */
.ui-dialog .ui-dialog-content .form-actions {
padding: 0;
margin: 0;
}
.ui-dialog .ajax-progress-throbber {
/* Can't do center:50% middle: 50%, so approximate it for a typical window size. */
left: 49%;
position: fixed;
top: 48.5%;
z-index: 1000;
background-color: #232323;
background-image: url("loading-small.gif");
background-position: center center;
background-repeat: no-repeat;
border-radius: 7px;
height: 24px;
opacity: 0.9;
padding: 4px;
width: 24px;
}
.ui-dialog .ajax-progress-throbber .throbber,
.ui-dialog .ajax-progress-throbber .message {
display: none;
}
...@@ -63,7 +63,7 @@ function theme_ckeditor_settings_toolbar($variables) { ...@@ -63,7 +63,7 @@ function theme_ckeditor_settings_toolbar($variables) {
'#uri' => $button['image' . $rtl], '#uri' => $button['image' . $rtl],
'#title' => $button['label'], '#title' => $button['label'],
); );
$value = drupal_render($image); $value = '<a href="#" class="cke_button" role="button" title="' . $button['label'] . '" aria-label="' . $button['label'] . '"><span class="cke_button_icon">' . drupal_render($image) . '</span></a>';
} }
else { else {
$value = '?'; $value = '?';
......
...@@ -23,6 +23,9 @@ function ckeditor_library_info() { ...@@ -23,6 +23,9 @@ function ckeditor_library_info() {
$module_path . '/js/ckeditor.js' => array(), $module_path . '/js/ckeditor.js' => array(),
array('data' => $settings, 'type' => 'setting'), array('data' => $settings, 'type' => 'setting'),
), ),
'css' => array(
$module_path . '/css/ckeditor.css' => array(),
),
'dependencies' => array( 'dependencies' => array(
array('system', 'drupal'), array('system', 'drupal'),
array('ckeditor', 'ckeditor'), array('ckeditor', 'ckeditor'),
......
...@@ -9,6 +9,13 @@ body { ...@@ -9,6 +9,13 @@ body {
margin: 8px; margin: 8px;
} }
@media screen and (max-width: 600px) {
/* A font-size of 16px prevents iOS from zooming. */
body {
font-size: 16px;
}
}
ol, ul, dl { ol, ul, dl {
/* IE7: reset rtl list margin. (CKEditor issue #7334) */ /* IE7: reset rtl list margin. (CKEditor issue #7334) */
*margin-right: 0px; *margin-right: 0px;
......
...@@ -92,8 +92,10 @@ ul.ckeditor-buttons li .cke-icon-only { ...@@ -92,8 +92,10 @@ ul.ckeditor-buttons li .cke-icon-only {
text-indent: -9999px; text-indent: -9999px;
width: 16px; width: 16px;
} }
ul.ckeditor-buttons li a:focus { ul.ckeditor-buttons li a:focus,
ul.ckeditor-multiple-buttons li a:focus {
z-index: 11; /* Ensure focused buttons show their outline on all sides. */ z-index: 11; /* Ensure focused buttons show their outline on all sides. */
outline: 1px dotted #333;
} }
ul.ckeditor-buttons li:first-child a { ul.ckeditor-buttons li:first-child a {
border-top-left-radius: 2px; /* LTR */ border-top-left-radius: 2px; /* LTR */
......
.ckeditor-dialog-loading {
position: absolute;
top: 0;
width: 100%;
text-align: center;
}
.ckeditor-dialog-loading-link {
border-radius: 0 0 5px 5px;
border: 1px solid #B6B6B6;
border-top: none;
background: white;
padding: 3px 10px;
box-shadow: 0 0 10px -3px #000;
display: inline-block;
font-size: 14px;
position: relative;
top: 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
...@@ -248,6 +248,15 @@ Drupal.behaviors.ckeditorAdmin = { ...@@ -248,6 +248,15 @@ Drupal.behaviors.ckeditorAdmin = {
if (CKEDITOR.instances[hiddenCKEditorID]) { if (CKEDITOR.instances[hiddenCKEditorID]) {
CKEDITOR.instances[hiddenCKEditorID].destroy(true); CKEDITOR.instances[hiddenCKEditorID].destroy(true);
} }
// Load external plugins, if any.
if (hiddenCKEditorConfig.drupalExternalPlugins) {
var externalPlugins = hiddenCKEditorConfig.drupalExternalPlugins;
for (var pluginName in externalPlugins) {
if (externalPlugins.hasOwnProperty(pluginName)) {
CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], '');
}
}
}
CKEDITOR.inline($('#' + hiddenCKEditorID).get(0), CKEditorConfig); CKEDITOR.inline($('#' + hiddenCKEditorID).get(0), CKEditorConfig);
// Once the instance is ready, retrieve the allowedContent filter rules // Once the instance is ready, retrieve the allowedContent filter rules
......
...@@ -7,6 +7,10 @@ Drupal.editors.ckeditor = { ...@@ -7,6 +7,10 @@ Drupal.editors.ckeditor = {
attach: function (element, format) { attach: function (element, format) {
this._loadExternalPlugins(format); this._loadExternalPlugins(format);
this._ACF_HACK_to_support_blacklisted_attributes(element, format); this._ACF_HACK_to_support_blacklisted_attributes(element, format);
// Also pass settings that are Drupal-specific.
format.editorSettings.drupal = {
format: format.format
};
return !!CKEDITOR.replace(element, format.editorSettings); return !!CKEDITOR.replace(element, format.editorSettings);
}, },
...@@ -175,6 +179,99 @@ Drupal.editors.ckeditor = { ...@@ -175,6 +179,99 @@ Drupal.editors.ckeditor = {
} }
}); });
} }
};
Drupal.ckeditor = {
/**
* Variable storing the current dialog's save callback.
*/
saveCallack: null,
/**
* Open a dialog for a Drupal-based plugin.
*
* This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
* framework, then opens a dialog at the specified Drupal path.
*
* @param editor
* The CKEditor instance that is opening the dialog.
* @param string url
* The URL that contains the contents of the dialog.
* @param Object existingValues
* Existing values that will be sent via POST to the url for the dialog
* contents.
* @param Function saveCallback
* A function to be called upon saving the dialog.
* @param Object dialogSettings
* An object containing settings to be passed to the jQuery UI.
*/
openDialog: function (editor, url, existingValues, saveCallback, dialogSettings) {
// Locate a suitable place to display our loading indicator.
var $target = $(editor.container.$);
if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) {
$target = $target.find('.cke_contents');
}
// Remove any previous loading indicator.
$target.css('position', 'relative').find('.ckeditor-dialog-loading').remove();
// Add a consistent dialog class.
var classes = dialogSettings.dialogClass ? dialogSettings.dialogClass.split(' ') : [];
classes.push('editor-dialog');
dialogSettings.dialogClass = classes.join(' ');
dialogSettings.maxHeight = '95%';
dialogSettings.resizable = false;
dialogSettings.autoResize = $(window).width() > 600;
// Add a "Loading…" message, hide it underneath the CKEditor toolbar, create
// a Drupal.ajax instance to load the dialog and trigger it.
var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link"><a>' + Drupal.t('Loading...') + '</a></span></div>');
$content.appendTo($target);
new Drupal.ajax('ckeditor-dialog', $content.find('a').get(0), {
accepts: 'application/vnd.drupal-modal',
dialog: dialogSettings,
selector: '.ckeditor-dialog-loading-link',
url: url,
event: 'ckeditor-internal.ckeditor',
progress: { 'type': 'throbber' },
submit: {
editor_object: existingValues
}
});
$content.find('a')
.on('click', function () { return false; })
.trigger('ckeditor-internal.ckeditor');
// After a short delay, show "Loading…" message.
window.setTimeout(function () {
$content.find('span').animate({ top: '0px' });
}, 1000);
// Store the save callback to be executed when this dialog is closed.
Drupal.ckeditor.saveCallback = saveCallback;
}
}; };
// Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
$(window).on('dialog:beforecreate', function (e, dialog, $element, settings) {
$('.ckeditor-dialog-loading').animate({ top: '-40px' }, function () {
$(this).remove();
});
});
// Respond to dialogs that are saved, sending data back to CKEditor.
$(window).on('editor:dialogsave', function (e, values) {
if (Drupal.ckeditor.saveCallback) {
Drupal.ckeditor.saveCallback(values);
}
});
// Respond to dialogs that are closed, removing the current save handler.
$(window).on('dialog:afterclose', function (e, dialog, $element) {
if (Drupal.ckeditor.saveCallback) {
Drupal.ckeditor.saveCallback = null;
}
});
})(Drupal, CKEDITOR, jQuery); })(Drupal, CKEDITOR, jQuery);
/**
* @file
* Drupal Image plugin.
*/
(function ($, Drupal, drupalSettings, CKEDITOR) {
"use strict";
CKEDITOR.plugins.add('drupalimage', {
init: function (editor) {
// Register the image command.
editor.addCommand('drupalimage', {
allowedContent: 'img[alt,!src,width,height]',
requiredContent: 'img[alt,src,width,height]',
modes: { wysiwyg : 1 },
canUndo: true,
exec: function (editor) {
var imageElement = getSelectedImage(editor);
var imageDOMElement = null;
var existingValues = {};
if (imageElement && imageElement.$) {
imageDOMElement = imageElement.$;
// Width and height are populated by actual dimensions.
existingValues.width = imageDOMElement ? imageDOMElement.width : '';
existingValues.height = imageDOMElement ? imageDOMElement.height : '';
// Populate all other attributes by their specified attribute values.
var attribute = null;
for (var key = 0; key < imageDOMElement.attributes.length; key++) {
attribute = imageDOMElement.attributes.item(key);
existingValues[attribute.nodeName.toLowerCase()] = attribute.nodeValue;
}
}
function saveCallback (returnValues) {
// Save snapshot for undo support.
editor.fire('saveSnapshot');
// Create a new image element if needed.
if (!imageElement && returnValues.attributes.src) {
imageElement = editor.document.createElement('img');
imageElement.setAttribute('alt', '');
editor.insertElement(imageElement);
}
// Delete the image if the src was removed.
if (imageElement && !returnValues.attributes.src) {
imageElement.remove();
}
// Update the image properties.
else {
for (var key in returnValues.attributes) {
if (returnValues.attributes.hasOwnProperty(key)) {
// Update the property if a value is specified.
if (returnValues.attributes[key].length > 0) {
imageElement.setAttribute(key, returnValues.attributes[key]);
}
// Delete the property if set to an empty string.
else {
imageElement.removeAttribute(key);
}
}
}
}
}
// Drupal.t() will not work inside CKEditor plugins because CKEditor
// loads the JavaScript file instead of Drupal. Pull translated strings
// from the plugin settings that are translated server-side.
var dialogSettings = {
title: imageDOMElement ? editor.config.drupalImage_dialogTitleEdit : editor.config.drupalImage_dialogTitleAdd,
dialogClass: 'editor-image-dialog'
};
// Open the dialog for the edit form.
Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/image/' + editor.config.drupal.format), existingValues, saveCallback, dialogSettings);
}
});
// Register the toolbar button.
if (editor.ui.addButton) {
editor.ui.addButton('DrupalImage', {
label: editor.lang.common.image,
command: 'drupalimage',
icon: this.path.replace(/plugin\.js.*/, 'image.png')
});
}
// Double clicking an image opens its properties.
editor.on('doubleclick', function (event) {
var element = event.data.element;
if (element.is('img') && !element.data('cke-realelement') && !element.isReadOnly()) {
editor.getCommand('drupalimage').exec();
}
});
// If the "menu" plugin is loaded, register the menu items.
if (editor.addMenuItems) {
editor.addMenuItems({
image: {
label: editor.lang.image.menu,
command : 'drupalimage',
group: 'image'
}
});
}
// If the "contextmenu" plugin is loaded, register the listeners.
if (editor.contextMenu) {
editor.contextMenu.addListener(function (element, selection) {
if (getSelectedImage(editor, element)) {
return { image: CKEDITOR.TRISTATE_OFF };
}
});
}
}
});
/**
* Finds an img tag anywhere in the current editor selection.
*/
function getSelectedImage (editor, element) {
if (!element) {
var sel = editor.getSelection();
var selectedText = sel.getSelectedText().replace(/^\s\s*/, '').replace(/\s\s*$/, '');
var isElement = sel.getType() === CKEDITOR.SELECTION_ELEMENT;
var isEmptySelection = sel.getType() === CKEDITOR.SELECTION_TEXT && selectedText.length === 0;
element = (isElement || isEmptySelection) && sel.getSelectedElement();
}
if (element && element.is('img') && !element.data('cke-realelement') && !element.isReadOnly()) {
return element;
}
}
})(jQuery, Drupal, drupalSettings, CKEDITOR);
/**
* @file
* Drupal Link plugin.
*/
(function ($, Drupal, drupalSettings, CKEDITOR) {
"use strict";
CKEDITOR.plugins.add('drupallink', {
init: function (editor) {
// Add the commands for link and unlink.
editor.addCommand('drupallink', {
allowedContent: 'a[!href,target]',
requiredContent: 'a[href]',
modes: { wysiwyg : 1 },
canUndo: true,
exec: function (editor) {
var linkElement = getSelectedLink(editor);
var linkDOMElement = null;
// Set existing values based on selected element.
var existingValues = {};
if (linkElement && linkElement.$) {
linkDOMElement = linkElement.$;
// Populate an array with the link's current attributes.
var attribute = null;
for (var key = 0; key < linkDOMElement.attributes.length; key++) {