core/misc/dialog.ajax.js | 50 ++++- core/misc/dialog.js | 38 +++- core/misc/dialog.theme.css | 103 ++++++++++ core/misc/loading-small.gif | 25 +++ core/misc/loading.gif | 43 ++++ core/modules/ckeditor/ckeditor.admin.inc | 2 +- core/modules/ckeditor/ckeditor.module | 3 + core/modules/ckeditor/css/ckeditor-iframe.css | 7 + core/modules/ckeditor/css/ckeditor.admin.css | 4 +- core/modules/ckeditor/css/ckeditor.css | 25 +++ core/modules/ckeditor/js/ckeditor.js | 92 +++++++++ .../ckeditor/js/plugins/drupalimage/image.png | 7 + .../ckeditor/js/plugins/drupalimage/plugin.js | 136 +++++++++++++ .../ckeditor/js/plugins/drupallink/link.png | 3 + .../ckeditor/js/plugins/drupallink/plugin.js | 207 ++++++++++++++++++++ .../ckeditor/js/plugins/drupallink/unlink.png | 7 + .../lib/Drupal/ckeditor/CKEditorPluginBase.php | 15 +- .../Drupal/ckeditor/CKEditorPluginInterface.php | 25 +++ .../lib/Drupal/ckeditor/CKEditorPluginManager.php | 11 ++ .../ckeditor/Plugin/CKEditorPlugin/DrupalImage.php | 65 ++++++ .../ckeditor/Plugin/CKEditorPlugin/DrupalLink.php | 70 +++++++ .../ckeditor/Plugin/CKEditorPlugin/Internal.php | 25 --- .../lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php | 23 ++- .../Drupal/ckeditor/Tests/CKEditorLoadingTest.php | 1 - .../ckeditor/Tests/CKEditorPluginManagerTest.php | 4 +- .../lib/Drupal/ckeditor/Tests/CKEditorTest.php | 25 ++- .../ckeditor_test/Plugin/CKEditorPlugin/Llama.php | 16 +- core/modules/editor/css/editor.css | 15 ++ core/modules/editor/editor.module | 17 ++ core/modules/editor/editor.routing.yml | 12 ++ core/modules/editor/js/editor.dialog.js | 21 ++ .../lib/Drupal/editor/Ajax/EditorDialogSave.php | 47 +++++ .../editor/lib/Drupal/editor/EditorController.php | 5 + .../lib/Drupal/editor/Form/EditorImageDialog.php | 136 +++++++++++++ .../lib/Drupal/editor/Form/EditorLinkDialog.php | 103 ++++++++++ core/modules/system/system.module | 4 + .../standard/config/editor.editor.basic_html.yml | 6 +- .../standard/config/editor.editor.full_html.yml | 6 +- core/themes/bartik/css/style.css | 2 +- core/themes/seven/jquery.ui.theme.css | 49 ----- 40 files changed, 1352 insertions(+), 103 deletions(-) diff --git a/core/misc/dialog.ajax.js b/core/misc/dialog.ajax.js index 8b1f80f..5758b40 100644 --- a/core/misc/dialog.ajax.js +++ b/core/misc/dialog.ajax.js @@ -8,13 +8,55 @@ "use strict"; Drupal.behaviors.dialog = { - attach: function () { + attach: function (context, settings) { // Provide a known 'drupal-modal' DOM element for Drupal-based modal // dialogs. Non-modal dialogs are responsible for creating their own // elements, since there can be multiple non-modal dialogs at a time. if (!$('#drupal-modal').length) { $('
').hide().appendTo('body'); } + + // If a new form is being attached inside a dialog, remove and replace + // the dialog buttons with those from the new form. + var $dialog = $(context).closest('.ui-dialog-content'); + if ($dialog && $dialog.dialog('option', 'drupalAutoButtons')) { + var buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog); + $dialog.dialog('option', 'buttons', buttons); + } + }, + 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], button'); + $buttons.each(function () { + var $originalButton = $(this).hide(); + buttons.push({ + 'text': $originalButton.html() || $originalButton.attr('value'), + 'class': $originalButton.attr('class'), + 'click': function (e) { + $originalButton.trigger('mousedown'); + e.preventDefault(); + } + }); + }); + if ($buttons.length) { + $dialog.find('form').on('submit.dialogSubmit', function (e) { + $buttons.first().trigger('mousedown'); + e.preventDefault(); + }); + } + return buttons; } }; @@ -40,6 +82,12 @@ response.method = 'html'; 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. response.dialogOptions = response.dialogOptions || {}; var dialog = Drupal.dialog($dialog, response.dialogOptions); diff --git a/core/misc/dialog.js b/core/misc/dialog.js index 62cbeb1..2d186e2 100644 --- a/core/misc/dialog.js +++ b/core/misc/dialog.js @@ -19,10 +19,14 @@ drupalSettings.dialog = { Drupal.dialog = function (element, options) { function openDialog (settings) { - settings = $.extend({}, drupalSettings.dialog, options, settings); + settings = $.extend({ autoResize: true }, drupalSettings.dialog, options, settings); // Trigger a global event to allow scripts to bind events to the dialog. $(window).trigger('dialog:beforecreate', [dialog, $element, settings]); $element.dialog(settings); + if (settings.autoResize !== 'false' && settings.autoResize !== false) { + $(window).on('resize.dialogResize scroll.dialogResize', autoResize); + resetPosition(); + } dialog.open = true; $(window).trigger('dialog:aftercreate', [dialog, $element, settings]); } @@ -32,11 +36,43 @@ Drupal.dialog = function (element, options) { $element.dialog('close'); dialog.returnValue = value; dialog.open = false; + $(window).off('.dialogResize'); $(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 $element = $(element); + var autoResize = Drupal.debounce(resetPosition, 50); var dialog = { open: false, returnValue: undef, diff --git a/core/misc/dialog.theme.css b/core/misc/dialog.theme.css new file mode 100644 index 0000000..360e6ae --- /dev/null +++ b/core/misc/dialog.theme.css @@ -0,0 +1,103 @@ +/** + * 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; +} + diff --git a/core/misc/loading-small.gif b/core/misc/loading-small.gif new file mode 100644 index 0000000..5cbf6e7 --- /dev/null +++ b/core/misc/loading-small.gif @@ -0,0 +1,25 @@ +GIF89aŽ{{{ssskkkcccZZZRRRJJJBBB:::111)))!!!! NETSCAPE2.0! +,&6MeK,kG8@qh/MGH:%#Gc4D2a8@@p2HfzH,%x" +WrIix%pqX1*=## +="I#3g~" ">w  kX  (_S(As}RTx  (oBB4xZ %q! +,'GQe04k<\nU6d 'U!`D %XeU(2H `%)2( * PEš b%K`1K:$2$P{({"#)1% %% +#k + {"  ?&*E vl [8w+!! +,Ԡ'WUea > ":)E* )%# +"cI (E +{` ."W! +,۠'w]eҲH뺉*S%Z"Q0DGѨU,5q@0EsJ:}aL G9}+#}%W1 *:"R)L" )Dn˺9R ;ȶ# Ϊ!! +,ߠ'$86pQx%N +y# { + +2 +  H#"#$" 0* *+ % {\" % * NUȷLQ! +,'ee4Ykb&ܴ8 x,lh&8DRQQ4 g5A)q_&dTBP@UF2A1*;$F$ s m1 "J$,+:+%  y% * + + m# +% " " U^ åA!; \ No newline at end of file diff --git a/core/misc/loading.gif b/core/misc/loading.gif new file mode 100644 index 0000000..2dbcd62 --- /dev/null +++ b/core/misc/loading.gif @@ -0,0 +1,43 @@ +GIF89a00###'''(((---111555:::===CCCGGGKKKMMMQQQUUUZZZ^^^aaaggghhhmmmqqqwwwxxx{{{///333444>>>AAAEEEIIIOOORRRXXX```eeekkklllssszzz~~~&&&***,,,222666<<~@ 1;d2;D@[#/nҠ˪uW ٚhxHr·îݫWF?&9yk氅=x8 K j7р.! +,00WWZZqP:z- 8Yܧ$+Cɩ4T(|ѓ`&,pۿOBQGw̡ZL!IWġ{`XA$TP T f1@M("wb}.!c$Rqr1x?DEI2ȤMōR">b%_@Œ2RD:n@r8! &6 +9ys w(GXhLoa`7wr4LXQ @YqG +)Gw*XL뮱A4jiȱ:;$B-}hGr@`ȡJ;Cr]QGA,v0&AN zǭpCWG ms̑@A1uy(G_Jt2j3iuT,U;d u\AZAqlytD! +,00HAh8ȰÆ B!B3j$@ 5HD̀LtKQ9J /s`sOr2``JIS塂[|\8`F@6B!L@B ̘0qdJ7ϖT QRάlbUҥkv,  x‹+Xh!-10`G=.B\BJ% "F%b",~# (00Mb@th& +#E*ڛ`i|$(L> EC! 4$gp"^@iՑ~Fé0p\F4PQF>)W > +*]wƭ&[ڌG ĢHF +ji  +ў:FFQliĀ +3; +jH.{ + +.BU*:>4kA>ȩ,FlQ!7H# Hp7\@qCr'C<6@ !ǐY ʐG@B -҃A=P@! +,00H`AG=z䨡#JttNJwn ȱA9ƈcIǓ#)R$0:`RƘoDsaRdЀwzIIw'$(z,v% 5P#PQ. +IG")>"^6NɖgؐfV0i@ǏG ɲ(崜 ̏4HOrӣ5 +IhF,rD!ʁ9=p"?e Q7E9-CX{$8uX`ZրB5 CjӪs r؋vB4VNv`Ea4Pڴ.$%܅G!40Ivc /~EѲAr{0֘]jH!/S5;| 5(s” FT?ܐ!ƂnJ`H |X`@fPHHX!ǒ%@" TS +2H}H c4jE8Ǝ0 +IFx(%(cif!b%2ƒ`_F!!YxQAHN ȣxK]r g!hPFal(+) .hhAL` 2*F7.@B&C")/ Y:F{ls +B@X0lIP +0D&!D5a + T+n"|r@c~ C + +4JB~L˛v(sÅ#L)M +h*  1XgH@NG_E#aY0Z^r5+) b&5ՆܺDp;ɷ1= +Hp(aƍ>4k';YSFuCG^\MpU$nGk jJT.i<(!K8 <2@}Ztoy;>Cs&d D#CÂ0p9Rl0F4%R0` Z8F*xA@^x8.28~.fc J<Ï@䅉DH=$I4y"W<СS(`he zMe^y7za'Pm|^|F8eI$`g$ŢJ*f@I'P Q(A@@&CpiBŨ )pb"J zĨxqi'g$e"&";Ey,j`4\F-\@"A8 ĽM q@n j 'MpypoQ9tA4̰M8~A*!>4y1o񗸓DC927% C8MBh@C`dH:"S@; \ No newline at end of file diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc index cd043ac..607c6af 100644 --- a/core/modules/ckeditor/ckeditor.admin.inc +++ b/core/modules/ckeditor/ckeditor.admin.inc @@ -63,7 +63,7 @@ function theme_ckeditor_settings_toolbar($variables) { '#uri' => $button['image' . $rtl], '#title' => $button['label'], ); - $value = drupal_render($image); + $value = '' . drupal_render($image) . ''; } else { $value = '?'; diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module index 84c713a..6a1b633 100644 --- a/core/modules/ckeditor/ckeditor.module +++ b/core/modules/ckeditor/ckeditor.module @@ -23,6 +23,9 @@ function ckeditor_library_info() { $module_path . '/js/ckeditor.js' => array(), array('data' => $settings, 'type' => 'setting'), ), + 'css' => array( + $module_path . '/css/ckeditor.css' => array(), + ), 'dependencies' => array( array('system', 'drupal'), array('ckeditor', 'ckeditor'), diff --git a/core/modules/ckeditor/css/ckeditor-iframe.css b/core/modules/ckeditor/css/ckeditor-iframe.css index 54f4b3f..994d1d5 100644 --- a/core/modules/ckeditor/css/ckeditor-iframe.css +++ b/core/modules/ckeditor/css/ckeditor-iframe.css @@ -9,6 +9,13 @@ body { margin: 8px; } +@media screen and (max-width: 600px) { + /* A font-size of 16px prevents iOS from zooming. */ + body { + font-size: 16px; + } +} + ol, ul, dl { /* IE7: reset rtl list margin. (CKEditor issue #7334) */ *margin-right: 0px; diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css index 272e3ea..41af514 100644 --- a/core/modules/ckeditor/css/ckeditor.admin.css +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -85,8 +85,10 @@ ul.ckeditor-buttons li .cke-icon-only { text-indent: -9999px; 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. */ + outline: 1px dotted #333; } ul.ckeditor-buttons li:first-child a { border-top-left-radius: 2px; /* LTR */ diff --git a/core/modules/ckeditor/css/ckeditor.css b/core/modules/ckeditor/css/ckeditor.css new file mode 100644 index 0000000..f45ec26 --- /dev/null +++ b/core/modules/ckeditor/css/ckeditor.css @@ -0,0 +1,25 @@ +.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; +} diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js index 8704aff..3e0866b 100644 --- a/core/modules/ckeditor/js/ckeditor.js +++ b/core/modules/ckeditor/js/ckeditor.js @@ -102,4 +102,96 @@ 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 = $(''); + $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); diff --git a/core/modules/ckeditor/js/plugins/drupalimage/image.png b/core/modules/ckeditor/js/plugins/drupalimage/image.png new file mode 100644 index 0000000..83cd553 --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupalimage/image.png @@ -0,0 +1,7 @@ +PNG + + IHDR(-SPLTEZZZ``` + + + &&& """###%%%)))***---666888999;;;<<<>>>@@@AAABBBDDDGGGIIIJJJKKKLLLNNNOOOPPPQQQRRRUUUYYYw tRNS +!*LPU`tv~=KIDATW`W*2KVG ]yO HjZe3bO xt@+&0RՊX{]Ofbpٻyt 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, editor.config.drupalImage_dialogUrl, existingValues, saveCallback, dialogSettings); + } + }); + + // Register the toolbar button. + if (editor.ui.addButton) { + editor.ui.addButton('DrupalImage', { + label: editor.lang.common.image, + command: 'image', + 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('image').exec(); + } + }); + + // If the "menu" plugin is loaded, register the menu items. + if (editor.addMenuItems) { + editor.addMenuItems({ + image: { + label: editor.lang.image.menu, + command : 'image', + 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); diff --git a/core/modules/ckeditor/js/plugins/drupallink/link.png b/core/modules/ckeditor/js/plugins/drupallink/link.png new file mode 100644 index 0000000..54e506a --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupallink/link.png @@ -0,0 +1,3 @@ +PNG + + IHDR(-SlPLTEXXX]]]PPPPPP#tRNS68abcdghIDATxڭG0 { ZdDkWiILi2uԱ*]x y2lބ{ m6 Ǫ:`ᘦR 2$ :WYzIENDB` \ No newline at end of file diff --git a/core/modules/ckeditor/js/plugins/drupallink/plugin.js b/core/modules/ckeditor/js/plugins/drupallink/plugin.js new file mode 100644 index 0000000..6d3c543 --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupallink/plugin.js @@ -0,0 +1,207 @@ +/** + * @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('link', { + allowedContent: 'a[!href,target,accesskey,charset,dir,id,lang,name,rel,tabindex,title,type]', + 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++) { + attribute = linkDOMElement.attributes.item(key); + existingValues[attribute.nodeName.toLowerCase()] = attribute.nodeValue; + } + } + + // Prepare a save callback to be used upon saving the dialog. + var saveCallback = function (returnValues) { + // Save snapshot for undo support. + editor.fire('saveSnapshot'); + + // Create a new link element if needed. + if (!linkElement && returnValues.attributes.href) { + var selection = editor.getSelection(); + var range = selection.getRanges(1)[0]; + + // Use link URL as text with a collapsed cursor. + if (range.collapsed) { + // Shorten mailto URLs to just the e-mail address. + var text = new CKEDITOR.dom.text(returnValues.attributes.href.replace(/^mailto:/, ''), editor.document); + range.insertNode(text); + range.selectNodeContents(text); + } + + // Create the new link by applying a style to the new text. + var style = new CKEDITOR.style({ element: 'a', attributes: returnValues.attributes }); + style.type = CKEDITOR.STYLE_INLINE; + style.applyToRange(range); + range.select(); + + // Set the link so individual properties may be set below. + linkElement = getSelectedLink(editor); + } + // Update the link properties. + else if (linkElement) { + 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) { + linkElement.setAttribute(key, returnValues.attributes[key]); + } + // Delete the property if set to an empty string. + else { + linkElement.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: linkElement ? editor.config.drupalLink_dialogTitleEdit : editor.config.drupalLink_dialogTitleAdd, + dialogClass: 'editor-link-dialog' + }; + + // Open the dialog for the edit form. + Drupal.ckeditor.openDialog(editor, editor.config.drupalLink_dialogUrl, existingValues, saveCallback, dialogSettings); + } + }); + editor.addCommand('unlink', { + contextSensitive: 1, + startDisabled: 1, + requiredContent: 'a[href]', + exec: function (editor) { + var style = new CKEDITOR.style({ element:'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 }); + editor.removeStyle(style); + }, + refresh: function ( editor, path ) { + var element = path.lastElement && path.lastElement.getAscendant('a', true); + if (element && element.getName() === 'a' && element.getAttribute('href') && element.getChildCount()) { + this.setState(CKEDITOR.TRISTATE_OFF); + } + else { + this.setState(CKEDITOR.TRISTATE_DISABLED); + } + } + }); + + editor.setKeystroke(CKEDITOR.CTRL + 75 /*K*/, 'link'); + + // Add buttons for link and unlink. + if (editor.ui.addButton) { + editor.ui.addButton('DrupalLink', { + label: editor.lang.link.toolbar, + command: 'link', + icon: this.path.replace(/plugin\.js.*/, 'link.png') + }); + editor.ui.addButton('DrupalUnlink', { + label: editor.lang.link.unlink, + command: 'unlink', + icon: this.path.replace(/plugin\.js.*/, 'unlink.png') + }); + } + + editor.on('doubleclick', function (evt) { + var element = getSelectedLink(editor) || evt.data.element; + + if (!element.isReadOnly()) { + if (element.is('a')) { + editor.getSelection().selectElement(element); + editor.getCommand('link').exec(); + } + } + }); + + // If the "menu" plugin is loaded, register the menu items. + if (editor.addMenuItems) { + editor.addMenuItems({ + link: { + label: editor.lang.link.menu, + command: 'link', + group: 'link', + order: 1 + }, + + unlink: { + label: editor.lang.link.unlink, + command: 'unlink', + group: 'link', + order: 5 + } + }); + } + + // If the "contextmenu" plugin is loaded, register the listeners. + if (editor.contextMenu) { + editor.contextMenu.addListener(function (element, selection) { + if (!element || element.isReadOnly()) { + return null; + } + var anchor = getSelectedLink(editor); + if (!anchor) { + return null; + } + + var menu = {}; + if (anchor.getAttribute('href') && anchor.getChildCount()) { + menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF }; + } + return menu; + }); + } + } +}); + + +/** + * Get the surrounding link element of current selection. + * + * The following selection will all return the link element. + * + * li^nk + * [link] + * text[link] + * li[nk] + * [li]nk] + * [li]nk + * + * @param {CKEDITOR.editor} editor + */ +function getSelectedLink(editor) { + var selection = editor.getSelection(); + var selectedElement = selection.getSelectedElement(); + if (selectedElement && selectedElement.is('a')) { + return selectedElement; + } + + var range = selection.getRanges(true)[0]; + + if (range) { + range.shrink(CKEDITOR.SHRINK_TEXT); + return editor.elementPath(range.getCommonAncestor()).contains('a', 1); + } + return null; +} + +})(jQuery, Drupal, drupalSettings, CKEDITOR); diff --git a/core/modules/ckeditor/js/plugins/drupallink/unlink.png b/core/modules/ckeditor/js/plugins/drupallink/unlink.png new file mode 100644 index 0000000..64056ad --- /dev/null +++ b/core/modules/ckeditor/js/plugins/drupallink/unlink.png @@ -0,0 +1,7 @@ +PNG + + IHDR7IDAT(c`!40l`NEY! +/ /韫N@!贈 ïgw! +0Iw/c`9 s{`2getDefinitions()); $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons'])); $enabled_plugins = array(); + $additional_plugins = array(); foreach ($plugins as $plugin_id) { $plugin = $this->createInstance($plugin_id); @@ -74,19 +75,29 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA } $enabled = FALSE; + // Enable this plugin if it provides a button that has been enabled. if ($plugin instanceof CKEditorPluginButtonsInterface) { $plugin_buttons = array_keys($plugin->getButtons()); $enabled = (count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0); } + // Otherwise enable this plugin if it declares itself as enabled. if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) { $enabled = $plugin->isEnabled($editor); } if ($enabled) { $enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile(); + // Check if this plugin has dependencies that also need to be enabled. + $additional_plugins = array_merge($additional_plugins, array_diff($plugin->getDependencies($editor), $additional_plugins)); } } + // Add the list of dependent plugins. + foreach ($additional_plugins as $plugin_id) { + $plugin = $this->createInstance($plugin_id); + $enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile(); + } + // Always return plugins in the same order. asort($enabled_plugins); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php new file mode 100644 index 0000000..77d6b75 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php @@ -0,0 +1,65 @@ + url('editor/dialog/image/' . $editor->format), + 'drupalImage_dialogTitleAdd' => t('Insert Image'), + 'drupalImage_dialogTitleEdit' => t('Edit Image'), + ); + } + + /** + * {@inheritdoc} + */ + public function getButtons() { + return array( + 'DrupalImage' => array( + 'label' => t('Image'), + 'image' => drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/image.png', + ), + ); + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalLink.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalLink.php new file mode 100644 index 0000000..5e08e32 --- /dev/null +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalLink.php @@ -0,0 +1,70 @@ + url('editor/dialog/link/' . $editor->format), + 'drupalLink_dialogTitleAdd' => t('Add Link'), + 'drupalLink_dialogTitleEdit' => t('Edit Link'), + ); + } + + /** + * {@inheritdoc} + */ + public function getButtons() { + $path = drupal_get_path('module', 'ckeditor') . '/js/plugins/drupallink'; + return array( + 'DrupalLink' => array( + 'label' => t('Link'), + 'image' => $path . '/link.png', + ), + 'DrupalUnlink' => array( + 'label' => t('Unlink'), + 'image' => $path . '/unlink.png', + ), + ); + } + +} diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php index e1f07d6..c2f5622 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php @@ -46,13 +46,7 @@ public function getConfig(Editor $editor) { $config = array( 'customConfig' => '', // Don't load CKEditor's config.js file. 'pasteFromWordPromptCleanup' => TRUE, - 'removeDialogTabs' => 'image:Link;image:advanced;link:advanced', 'resize_dir' => 'vertical', - 'keystrokes' => array( - // 0x11000 is CKEDITOR.CTRL, see http://docs.ckeditor.com/#!/api/CKEDITOR-property-CTRL. - array(0x110000 + 75, 'link'), - array(0x110000 + 76, NULL), - ), ); // Next, add the format_tags setting, if its button is enabled. @@ -132,20 +126,6 @@ public function getButtons() { 'image_alternative' => $button('redo'), 'image_alternative_rtl' => $button('redo', 'rtl'), ), - // "link" plugin. - 'Link' => array( - 'label' => t('Link'), - 'image_alternative' => $button('link'), - ), - 'Unlink' => array( - 'label' => t('Unlink'), - 'image_alternative' => $button('unlink'), - ), - 'Anchor' => array( - 'label' => t('Anchor'), - 'image_alternative' => $button('anchor'), - 'image_alternative_rtl' => $button('anchor', 'rtl'), - ), // "blockquote" plugin. 'Blockquote' => array( 'label' => t('Blockquote'), @@ -193,11 +173,6 @@ public function getButtons() { 'label' => t('HTML block format'), 'image_alternative' => '' . t('Format') . '', ), - // "image" plugin. - 'Image' => array( - 'label' => t('Image'), - 'image_alternative' => $button('image'), - ), // "table" plugin. 'Table' => array( 'label' => t('Table'), diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php index 46077a5..3552bc7 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php @@ -33,9 +33,9 @@ public function getDefaultSettings() { 'buttons' => array( array( 'Bold', 'Italic', - '|', 'Link', 'Unlink', + '|', 'DrupalLink', 'DrupalUnlink', '|', 'BulletedList', 'NumberedList', - '|', 'Blockquote', 'Image', + '|', 'Blockquote', 'DrupalImage', '|', 'Source', ), ), @@ -172,6 +172,8 @@ public function getJSSettings(EditorEntity $editor) { 'toolbar' => $this->buildToolbarJSSetting($editor), 'contentsCss' => $this->buildContentsCssJSSetting($editor), 'extraPlugins' => implode(',', array_keys($external_plugins)), + // @todo: Remove image and link plugins from CKEditor build. + 'removePlugins' => 'image,link', 'language' => $language_interface->langcode, // Configure CKEditor to not load styles.js. The StylesCombo plugin will // set stylesSet according to the user's settings, if the "Styles" button @@ -190,12 +192,25 @@ public function getJSSettings(EditorEntity $editor) { } /** - * Implements \Drupal\editor\Plugin\EditPluginInterface::getLibraries(). + * Implements \Drupal\editor\Plugin\EditorPluginInterface::getLibraries(). */ public function getLibraries(EditorEntity $editor) { - return array( + $libraries = array( array('ckeditor', 'drupal.ckeditor'), ); + + // Get the required libraries for any enabled plugins. + $manager = drupal_container()->get('plugin.manager.ckeditor.plugin'); + $enabled_plugins = array_keys($manager->getEnabledPlugins($editor)); + foreach ($enabled_plugins as $plugin_id) { + $plugin = $manager->createInstance($plugin_id); + $additional_libraries = array_udiff($plugin->getLibraries($editor), $libraries, function($a, $b) { + return $a[0] === $b[0] && $a[1] === $b[1] ? 0 : 1; + }); + $libraries = array_merge($libraries, $additional_libraries); + } + + return $libraries; } /** diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php index 63a816f..9eaa08f 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php @@ -121,7 +121,6 @@ function testLoading() { module_enable(array('ckeditor_test')); drupal_container()->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions(); $editor->settings['toolbar']['buttons'][0][] = 'Llama'; - $editor->settings['plugins']['internal']['link_shortcut'] = 'CTRL+K'; $editor->save(); $this->drupalGet('node/add/article'); list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck(); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php index 8cee06e..1c417aa 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php @@ -69,7 +69,7 @@ function testEnabledPlugins() { // Case 1: no CKEditor plugins. $definitions = array_keys($this->manager->getDefinitions()); sort($definitions); - $this->assertIdentical(array('internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.'); + $this->assertIdentical(array('drupalimage', 'drupallink', 'internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.'); $this->assertIdentical(array(), $this->manager->getEnabledPlugins($editor), 'Only built-in plugins are enabled.'); $this->assertIdentical(array('internal' => NULL), $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.'); @@ -82,7 +82,7 @@ function testEnabledPlugins() { // Case 2: CKEditor plugins are available. $plugin_ids = array_keys($this->manager->getDefinitions()); sort($plugin_ids); - $this->assertIdentical(array('internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.'); + $this->assertIdentical(array('drupalimage', 'drupallink', 'internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.'); $this->assertIdentical(array(), $this->manager->getEnabledPlugins($editor), 'Only the internal plugins are enabled.'); $this->assertIdentical(array('internal' => NULL), $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.'); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php index 81c3b80..b8f276a 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php @@ -76,12 +76,22 @@ function testGetJSSettings() { // Default toolbar. $expected_config = $this->getDefaultInternalConfig() + array( + 'drupalImage_dialogUrl' => url('editor/dialog/image/filtered_html'), + 'drupalImage_dialogTitleAdd' => 'Insert Image', + 'drupalImage_dialogTitleEdit' => 'Edit Image', + 'drupalLink_dialogUrl' => url('editor/dialog/link/filtered_html'), + 'drupalLink_dialogTitleAdd' => 'Add Link', + 'drupalLink_dialogTitleEdit' => 'Edit Link', 'toolbar' => $this->getDefaultToolbarConfig(), 'contentsCss' => $this->getDefaultContentsCssConfig(), - 'extraPlugins' => '', + 'extraPlugins' => 'drupalimage,drupallink', + 'removePlugins' => 'image,link', 'language' => 'en', 'stylesSet' => FALSE, - 'drupalExternalPlugins' => array(), + 'drupalExternalPlugins' => array( + 'drupalimage' => file_create_url('core/modules/ckeditor/js/plugins/drupalimage/plugin.js'), + 'drupallink' => file_create_url('core/modules/ckeditor/js/plugins/drupallink/plugin.js'), + ), ); $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for default configuration.'); @@ -91,17 +101,16 @@ function testGetJSSettings() { drupal_container()->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions(); $editor->settings['toolbar']['buttons'][0][] = 'Strike'; $editor->settings['toolbar']['buttons'][1][] = 'Format'; - $editor->settings['plugins']['internal']['link_shortcut'] = 'CTRL+K'; $editor->save(); $expected_config['toolbar'][count($expected_config['toolbar'])-2]['items'][] = 'Strike'; $expected_config['toolbar'][]['items'][] = 'Format'; $expected_config['toolbar'][] = '/'; $expected_config['format_tags'] = 'p;h4;h5;h6'; - $expected_config['extraPlugins'] = 'llama_contextual,llama_contextual_and_button'; + $expected_config['extraPlugins'] .= ',llama_contextual,llama_contextual_and_button'; + $expected_config['removePlugins'] = 'image,link'; $expected_config['drupalExternalPlugins']['llama_contextual'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js'); $expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js'); $expected_config['contentsCss'][] = file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css'); - $expected_config['keystrokes'] = array(array(1114187, 'link'), array(1114188, NULL)); $this->assertEqual($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.'); // Change the allowed HTML tags; the "format_tags" setting for CKEditor @@ -230,18 +239,16 @@ protected function getDefaultInternalConfig() { return array( 'customConfig' => '', 'pasteFromWordPromptCleanup' => TRUE, - 'removeDialogTabs' => 'image:Link;image:advanced;link:advanced', 'resize_dir' => 'vertical', - 'keystrokes' => array(array(0x110000 + 75, 'link'), array(0x110000 + 76, NULL)), ); } protected function getDefaultToolbarConfig() { return array( 0 => array('items' => array('Bold', 'Italic')), - 1 => array('items' => array('Link', 'Unlink')), + 1 => array('items' => array('DrupalLink', 'DrupalUnlink')), 2 => array('items' => array('BulletedList', 'NumberedList')), - 3 => array('items' => array('Blockquote', 'Image')), + 3 => array('items' => array('Blockquote', 'DrupalImage')), 4 => array('items' => array('Source')), 5 => '/' ); diff --git a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/Llama.php b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/Llama.php index 0fd3adf..f8c7443 100644 --- a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/Llama.php +++ b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/Llama.php @@ -33,6 +33,20 @@ class Llama extends PluginBase implements CKEditorPluginInterface { /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getDependencies(). + */ + function getDependencies(Editor $editor) { + return array(); + } + + /** + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getLibraries(). + */ + function getLibraries(Editor $editor) { + return array(); + } + + /** * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal(). */ function isInternal() { @@ -47,7 +61,7 @@ function getFile() { } /** - * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getButtons(). + * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getConfig(). */ public function getConfig(Editor $editor) { return array(); diff --git a/core/modules/editor/css/editor.css b/core/modules/editor/css/editor.css new file mode 100644 index 0000000..e8a9d9e --- /dev/null +++ b/core/modules/editor/css/editor.css @@ -0,0 +1,15 @@ +/** + * @file + * Styles for text editors. + */ +.editor-dialog { + /* This !important is required to override inline CSS of jQuery UI. */ + width: 80% !important; + max-width: 500px; +} + +@media screen and (max-width: 600px) { + .editor-dialog { + width: 95% !important; + } +} diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index a0e78a6..687a47a 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -83,6 +83,9 @@ function editor_library_info() { 'js' => array( $path . '/js/editor.js' => array(), ), + 'css' => array( + $path . '/css/editor.css' => array(), + ), 'dependencies' => array( array('system', 'jquery'), array('system', 'drupal'), @@ -91,6 +94,20 @@ function editor_library_info() { ), ); + $libraries['drupal.editor.dialog'] = array( + 'title' => 'Text Editor Dialog', + 'version' => VERSION, + 'js' => array( + $path . '/js/editor.dialog.js' => array('weight' => 2), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal.dialog'), + array('system', 'drupal.ajax'), + array('system', 'drupalSettings'), + ), + ); + $libraries['edit.formattedTextEditor.editor'] = array( 'title' => 'Formatted text editor', 'version' => VERSION, diff --git a/core/modules/editor/editor.routing.yml b/core/modules/editor/editor.routing.yml index 0bb56cf..f4ae496 100644 --- a/core/modules/editor/editor.routing.yml +++ b/core/modules/editor/editor.routing.yml @@ -5,3 +5,15 @@ editor_field_untransformed_text: requirements: _permission: 'access in-place editing' _access_edit_entity_field: 'TRUE' +editor_image_dialog: + pattern: '/editor/dialog/image/{filter_format}' + defaults: + _form: '\Drupal\editor\Form\EditorImageDialog' + requirements: + _filter_access: 'TRUE' +editor_link_dialog: + pattern: '/editor/dialog/link/{filter_format}' + defaults: + _form: '\Drupal\editor\Form\EditorLinkDialog' + requirements: + _filter_access: 'TRUE' diff --git a/core/modules/editor/js/editor.dialog.js b/core/modules/editor/js/editor.dialog.js new file mode 100644 index 0000000..35dd4be --- /dev/null +++ b/core/modules/editor/js/editor.dialog.js @@ -0,0 +1,21 @@ +/** + * AJAX commands used by Editor module. + */ + +(function ($, Drupal) { + +"use strict"; + +/** + * Command to save the contents of an editor-provided modal. + * + * This command does not close the open modal. It should be followed by a call + * to Drupal.AjaxCommands.prototype.closeDialog. Editors that are integrated + * with dialogs must independently listen for an editor:dialogsave event to save + * the changes into the contents of their interface. + */ +Drupal.AjaxCommands.prototype.editorDialogSave = function (ajax, response, status) { + $(window).trigger('editor:dialogsave', [response.values]); +}; + +})(jQuery, Drupal); diff --git a/core/modules/editor/lib/Drupal/editor/Ajax/EditorDialogSave.php b/core/modules/editor/lib/Drupal/editor/Ajax/EditorDialogSave.php new file mode 100644 index 0000000..a340762 --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Ajax/EditorDialogSave.php @@ -0,0 +1,47 @@ +values = $values; + } + + /** + * {@inheritdoc} + */ + public function render() { + return array( + 'command' => 'editorDialogSave', + 'values' => $this->values, + ); + } + +} diff --git a/core/modules/editor/lib/Drupal/editor/EditorController.php b/core/modules/editor/lib/Drupal/editor/EditorController.php index 2968454..93bbe52 100644 --- a/core/modules/editor/lib/Drupal/editor/EditorController.php +++ b/core/modules/editor/lib/Drupal/editor/EditorController.php @@ -9,8 +9,13 @@ use Symfony\Component\DependencyInjection\ContainerAware; use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\OpenModalDialogCommand; +use Drupal\Core\Ajax\CloseModalDialogCommand; use Drupal\Core\Entity\EntityInterface; use Drupal\editor\Ajax\GetUntransformedTextCommand; +use Drupal\editor\Form\EditorImageDialog; +use Drupal\editor\Form\EditorLinkDialog; +use Drupal\filter\Plugin\Core\Entity\FilterFormat; /** * Returns responses for Editor module routes. diff --git a/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php new file mode 100644 index 0000000..2da9f0b --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php @@ -0,0 +1,136 @@ +'; + $form['#suffix'] = '
'; + + // Everything under the "attributes" key is merged directly into the + // generated img tag's attributes. + $form['attributes']['src'] = array( + '#title' => t('URL'), + '#type' => 'textfield', + '#default_value' => isset($input['src']) ? $input['src'] : '', + '#maxlength' => 2048, + '#required' => TRUE, + ); + + $form['attributes']['alt'] = array( + '#title' => t('Alternative text'), + '#type' => 'textfield', + '#default_value' => isset($input['alt']) ? $input['alt'] : '', + '#maxlength' => 2048, + ); + $form['dimensions'] = array( + '#type' => 'item', + '#title' => t('Image size'), + '#field_prefix' => '
', + '#field_suffix' => '
', + ); + $form['dimensions']['width'] = array( + '#title' => t('Width'), + '#title_display' => 'invisible', + '#type' => 'number', + '#default_value' => isset($input['width']) ? $input['width'] : '', + '#size' => 8, + '#maxlength' => 8, + '#min' => 1, + '#max' => 99999, + '#placeholder' => 'width', + '#field_suffix' => ' x ', + '#parents' => array('attributes', 'width'), + ); + $form['dimensions']['height'] = array( + '#title' => t('Height'), + '#title_display' => 'invisible', + '#type' => 'number', + '#default_value' => isset($input['height']) ? $input['height'] : '', + '#size' => 8, + '#maxlength' => 8, + '#min' => 1, + '#max' => 99999, + '#placeholder' => 'height', + '#field_suffix' => 'pixels', + '#parents' => array('attributes', 'height'), + ); + + $form['actions'] = array( + '#type' => 'actions', + ); + $form['actions']['save_modal'] = array( + '#type' => 'submit', + '#value' => t('Save'), + // No regular submit-handler. This form only works via JavaScript. + '#submit' => array(), + '#ajax' => array( + 'callback' => array($this, 'submitForm'), + ), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $response = new AjaxResponse(); + + if (form_get_errors()) { + unset($form['#prefix'], $form['#suffix']); + $output = drupal_render($form); + $output = '
' . theme('status_messages') . $output . '
'; + $response->addCommand(new HtmlCommand('#editor-image-dialog', $output)); + } + else { + $response->addCommand(new EditorDialogSave($form_state['values'])); + $response->addCommand(new CloseModalDialogCommand()); + } + + return $response; + } + +} diff --git a/core/modules/editor/lib/Drupal/editor/Form/EditorLinkDialog.php b/core/modules/editor/lib/Drupal/editor/Form/EditorLinkDialog.php new file mode 100644 index 0000000..3066392 --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Form/EditorLinkDialog.php @@ -0,0 +1,103 @@ +'; + $form['#suffix'] = ''; + + // Everything under the "attributes" key is merged directly into the + // generated link tag's attributes. + $form['attributes']['href'] = array( + '#title' => t('URL'), + '#type' => 'textfield', + '#default_value' => isset($input['href']) ? $input['href'] : '', + '#maxlength' => 2048, + ); + + $form['attributes']['target'] = array( + '#title' => t('Open in new window'), + '#type' => 'checkbox', + '#default_value' => !empty($input['target']), + '#return_value' => '_blank', + ); + + $form['actions'] = array( + '#type' => 'actions', + ); + $form['actions']['save_modal'] = array( + '#type' => 'submit', + '#value' => t('Save'), + // No regular submit-handler. This form only works via JavaScript. + '#submit' => array(), + '#ajax' => array( + 'callback' => array($this, 'submitForm'), + ), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $response = new AjaxResponse(); + + if (form_get_errors()) { + unset($form['#prefix'], $form['#suffix']); + $output = drupal_render($form); + $output = '
' . theme('status_messages') . $output . '
'; + $response->addCommand(new HtmlCommand('#editor-link-dialog', $output)); + } + else { + $response->addCommand(new EditorDialogSave($form_state['values'])); + $response->addCommand(new CloseModalDialogCommand()); + } + + return $response; + } + +} diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 6a1f766..6ec3a9ed 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1247,9 +1247,13 @@ function system_library_info() { 'js' => array( 'core/misc/dialog.js' => array('group' => JS_LIBRARY), ), + 'css' => array( + 'core/misc/dialog.theme.css' => array('weight' => 1), + ), 'dependencies' => array( array('system', 'jquery'), array('system', 'drupal'), + array('system', 'drupal.debounce'), array('system', 'drupalSettings'), array('system', 'jquery.ui.dialog') ), diff --git a/core/profiles/standard/config/editor.editor.basic_html.yml b/core/profiles/standard/config/editor.editor.basic_html.yml index e66cc2e..cf4a600 100644 --- a/core/profiles/standard/config/editor.editor.basic_html.yml +++ b/core/profiles/standard/config/editor.editor.basic_html.yml @@ -7,14 +7,14 @@ settings: - Bold - Italic - '|' - - Link - - Unlink + - DrupalLink + - DrupalUnlink - '|' - BulletedList - NumberedList - '|' - Blockquote - - Image + - DrupalImage - '|' - Source plugins: diff --git a/core/profiles/standard/config/editor.editor.full_html.yml b/core/profiles/standard/config/editor.editor.full_html.yml index 378cc45..c0111f9 100644 --- a/core/profiles/standard/config/editor.editor.full_html.yml +++ b/core/profiles/standard/config/editor.editor.full_html.yml @@ -12,14 +12,14 @@ settings: - - - RemoveFormat - '|' - - Link - - Unlink + - DrupalLink + - DrupalUnlink - '|' - BulletedList - NumberedList - '|' - Blockquote - - Image + - DrupalImage - Table - HorizontalRule - '|' diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css index 640bca3..796f8f3 100644 --- a/core/themes/bartik/css/style.css +++ b/core/themes/bartik/css/style.css @@ -109,7 +109,7 @@ pre { body, #site-slogan, -.ui-widget, +#page .ui-widget, .comment-form label, .node-form label, .node-form .description { diff --git a/core/themes/seven/jquery.ui.theme.css b/core/themes/seven/jquery.ui.theme.css index ee72497..4a171fb 100644 --- a/core/themes/seven/jquery.ui.theme.css +++ b/core/themes/seven/jquery.ui.theme.css @@ -330,55 +330,6 @@ opacity: .70; filter: Alpha(Opacity=70); } - -/** - * Dialogs - */ -.ui-dialog { - background: #fff; - border: solid 1px #ccc; - padding: 0; -} -.ui-dialog .ui-dialog-titlebar { - font-weight: bold; - background: #e1e2dc; -} -.ui-dialog .ui-dialog-buttonpane { - border-width: 0; -} -.ui-dialog .ui-dialog-buttonpane button { - cursor: pointer; - padding: 4px 17px; - color: #5a5a5a; - text-align: center; - font-family: "Lucida Grande", Verdana, sans-serif; - font-weight: normal; - font-size: 1em; - border: 1px solid #e4e4e4; - border-bottom: 1px solid #b4b4b4; - border-left-color: #D2D2D2; - border-right-color: #D2D2D2; - background: url(images/buttons.png) 0 0 repeat-x; - border-radius: 20px; -} -.ui-dialog .ui-dialog-buttonpane button:hover, -.ui-dialog .ui-dialog-buttonpane button:focus { - background-position: 0 -40px; - border: 1px solid #bebebe; - border-left-color: #afafaf; - border-right-color: #afafaf; - border-bottom-color: #9b9b9b; - color: #2e2e2e; -} -.ui-dialog .ui-dialog-buttonpane button:active { - background-position: 0 -80px; - border: 1px solid #333; - border-left-color: #222; - border-right-color: #222; - border-bottom-color: #111; - color: #fff; - text-shadow: #222 0px -1px 0px; -} .overlay { padding-right: 26px; }